cd /news/structured-data/a-docx-does-not-store-where-its-page… · home topics structured-data article
[ARTICLE · art-138935] src=dev.to ↗ pub= topic=structured-data verified=true sentiment=· neutral

A .docx does not store where its pages end

A developer hand-wrote an OOXML .docx with A4 page setup, six hard page breaks, a 36-row table, a three-level numbered list, images, footnotes and VML WordArt, then converted it to HTML in-browser to measure what actually changes. The test found the format stores no page-end markers (zero lastRenderedPageBreak entries), so pagination is recomputed at open time, and the conversion produced unevaluated PAGE/NUMPAGES fields (every footer reading "第 1 页 共 10 页"), a 36-row table kept whole on a stretched 794x1733 page with no <th> or <thead>, and WordArt rendered as a 573x61 px SVG with zero text nodes or paths.

by read5 min views1 publishedSep 24, 2026

Someone handed me a Word proposal last week and asked whether converting it to HTML would look exactly like Word. I said no. They thought I was dodging. All I had at the time was "the formats are different," which explains nothing, so I sat down and built a sample to measure what actually changes.

I hand-wrote the OOXML instead of using a template: A4 page setup, six hard page breaks, one 36-row six-column table with w:tblHeader on the first row, a 22-item three-level numbered list, three images, four footnotes, header and footer with PAGE and NUMPAGES fields, and a VML WordArt title on the cover. Company name, order numbers and amounts are all made up — this is not anyone's real document. I ran it through the DOCX-to-HTML tool on ImgIng (https://imging.ai/), which does the work inside the browser. Six runs with the Network panel open produced zero non-GET requests, which matters here: with no server in the loop, every difference I see belongs to parsing and layout rebuilding, and nothing else.

Unzip it, read word/document.xml, and count what is actually there.

body = zipfile.ZipFile('A_proposal.docx').read('word/document.xml').decode()
pw, ph = re.search(r'<w:pgSz w:w="(\d+)" w:h="(\d+)"', body).groups()
print('page:', pw, 'x', ph, 'twips =', round(int(pw)/56.7), 'x', round(int(ph)/56.7), 'mm')
print('cached page-end markers:', body.count('lastRenderedPageBreak'))

That prints page: 11906 x 16838 twips = 210 x 297 mm and cached page-end markers: 0. The section properties know the paper is A4. Six w:type="page" breaks are in there too — the ones I inserted by hand. What is missing is the third thing: lastRenderedPageBreak, the marker Word writes back after it renders — a cache of where the lines fell last time — and my hand-written package has none of them. Even when it is there, it is a record, not a contract. word/settings.xml also carries a compatibilityMode value (15 in my file) that decides which generation of layout rules to apply; change it and the same text breaks elsewhere.

So what a .docx hands you is paper size, margins, font names, spacing, and the few places that must break. Which sentence ends page 3 is computed at open time by whatever program opens it, from glyph metrics that live on that machine — and Word folds printer metrics into the same calculation. Font availability and printer metrics are the two things least likely to match between two computers, which is why the same .docx already paginates differently on your colleague's laptop. HTML conversion just changes who is doing the arithmetic.

The product is one 85,903-byte HTML file with zero external resources and three images inlined as base64 at 31% of the file. Text fidelity is good: 286 of 286 source paragraphs matched, 3,178 characters selectable. But three things are plainly wrong, and none of them needs a Word baseline to spot.

Page numbers first. The output has seven pages, and pages 2 through 7 all print the same footer — "第 1 页 共 10 页", page 1 of 10. The source stores cached values of 1 and 10 next to the PAGE and NUMPAGES fields; the fields were never evaluated, so the cached values were carried straight through — while the viewer's own toolbar reads 01 / 07.

Then the long table. Six of the seven page shells are 794 x 1123 (A4 at 96 dpi); page four is 794 x 1733. The 36-row table was not split across pages — it was kept whole and the page was stretched around it, from 1,123 px to 1,733 px. The w:tblHeader hint is gone too: zero <th>, no <thead>. Print that page and it overflows A4.

Third, the WordArt cover title renders as <svg style="width:430pt;height:46pt" width="0" height="0"> — 573×61 px of layout occupied by zero text nodes and zero paths. A second sample with an embedded Excel OLE object behaved the same way: the object and its 520×300 static preview both vanished, leaving two empty <p> elements.

I built an 8-slide quarterly review with a bar chart, a pie chart, SmartArt, a 7×5 table, four rotated shapes and a two-second embedded video. The source declares p:sldSz as 12191695×6858000 EMU, ratio 1.7777. The output keeps all eight slides at data-width=1280 data-height=720, ratio 1.7778. The four rotations come out at −22°, 15°, −160°, −28° against 338°, 15°, 200°, 332° in the source — each one accounted for. The table is still a real <table>, 7 rows and 35 cells.

Not everything survives there either. Both charts are drawn into a canvas and frozen as PNGs — the axis categories and the data labels appear zero times in the output HTML, and only the HTML legend is selectable. SmartArt does stay vector, eight <svg> elements with all five stage labels selectable. Animations and transitions do not run; @keyframes count is 0.

The difference is not effort, it is what each format stores. PPTX stores canvas size plus absolute coordinates and rotation per object — environment-independent numbers that survive any move. DOCX stores reflow parameters, and the final layout has to be recomputed from metrics that live on the reader's machine.

So the line I use now: fixed-layout formats convert into fixed layout, reflowable formats only ever approximate. Before sending anything out, I check the footer page numbers, the WordArt title, and any embedded spreadsheet objects.

One limit I have to state. There is no Word or LibreOffice on this machine, so I have no authoritative page count to compare against — every page-count claim above is measured against my own six explicit breaks. The three defects do not need a baseline, which is why those are the only ones I am willing to state flatly.

── more in #structured-data 4 stories · sorted by recency
── more on @imging 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-docx-does-not-stor…] indexed:0 read:5min 2026-09-24 ·