Repository navigation
DOCX: a body w:framePr paragraph is laid out in the flow instead of as a positioned frame that text wraps around #2231
Description
Problem
A body paragraph with w:pPr/w:framePr is laid out as an ordinary in-flow paragraph. Word lifts it out of the flow into a frame box at the w:x/w:y offset from its w:hAnchor/w:vAnchor, w:w wide (and w:h tall when w:hRule="exact"), and the following body lines wrap around that box. office2pdf 0.9.1 instead (1) gives the framed paragraph a line in the flow, pushing the rest of the page down by one line, (2) draws its text at the flow position instead of the frame position, and (3) wraps nothing around it. This holds even for an empty framed paragraph.
Lead only: frame_property is read solely by extract_hf_frame (crates/office2pdf/src/parser/docx_sections.rs:1404), i.e. the header/footer path; the body paragraph converter never reads it.
Measurements
Public synthetic inputs (Apache-2.0) with Microsoft Word reference PDFs (pdfinfo Creator: Microsoft Word) from docxide-pdf fe7b8bea. Coordinates from mutool draw -F stext, points from the page top; office2pdf 0.9.1 (current main 3e946f31 differs only by #2219, which these cases do not exercise).
empty-frame-control
Empty paragraph, <w:framePr w:w="4659" w:wrap="auto" w:hAnchor="text" w:x="1701"/> (14pt TNR mark), then "Following paragraph". Frame box x = 72 + 85.05 = 157.05 to 390.0.
| Anchor | Word | docxide | office2pdf |
|---|---|---|---|
| "Following" baseline y | 85.03 | 85.13 | 101.17 (+16.14) |
| "paragraph" x (wraps past the frame) | 390.18 | 72 + same line, no wrap | same line, no wrap |
Word puts "Following" left of the empty frame and "paragraph" right of it at x=390.18 = frame right edge.
paragraph-offset-frame-immediate / -exact / -immediate-long
One-cell table ("Recipient"), then <w:framePr w:w="5116" w:h="601" w:hRule="exact" w:wrap="auto" w:vAnchor="text" w:hAnchor="page" w:x="1306" w:y="336"/> with <w:ind w:left="-1418" w:firstLine="1560"/> ("About providing information"), then "Dear recipient". Frame box: x 65.3 to 321.1, top = table bottom 73.3 + 16.8 = 90.1, height 30.05.
| Case / anchor | Word | docxide | office2pdf |
|---|---|---|---|
| immediate: "About" first glyph x / baseline y | 72.28 / 102.53 | 72.40 / 102.73 | 163.05 / 85.87 |
| immediate: "Dear" baseline y | 85.78 | 85.93 | 101.97 (+16.19) |
| exact: "About" x / baseline y | 72.28 / 102.53 | 72.40 / 102.73 | 163.05 / 85.87 |
| immediate-long: wrapped lines 2-3 x | 321.13 | 321.10 | 85.05 (no wrap; line breaks change) |
| immediate-long: line 4 (below the frame) x | 85.03 | 85.05 | 85.05 |
In Word the frame text x is 65.3 (page x) + (-70.9 + 78.0) first-line indent = 72.4, so the frame text also depends on the negative left indent (#2233). The exact case's "Dear" is additionally off from the empty section-mark paragraph rule (#2232).
page-header-frame-marker
Section 1 is a single empty paragraph carrying <w:framePr w:w="4659" w:wrap="auto" w:hAnchor="text" w:x="1701"/> and the section's w:sectPr; section 2 is continuous. Header is one line ending ~40pt, far above the 72pt top margin, so this is not #2056.
| Anchor | Word | docxide | office2pdf |
|---|---|---|---|
| "Following section" line top | 72.03 | 72.33 | 79.67 |
| baseline y | 82.03 | 81.38 | 91.94 |
Factoring out the face (ascent taken from the header line, whose top is 28.35pt in both: Word 9.90, office2pdf 7.94), the body line top is Word 72.1, office2pdf 84.0: Word starts the body at the top margin (frame out of flow), office2pdf lays the empty framed marker as a ~12pt line. (office2pdf's 11pt C059 face instead of 10pt Tahoma is #2234 / #2235.) Compare page-header-ordinary-marker, where the unframed marker does take a line in Word.
Scores (docxide scorer, host fonts)
| Case | office2pdf SSIM | docxide SSIM |
|---|---|---|
| empty-frame-control | 1.7 | 28.6 |
| page-header-frame-marker | 13.4 | 37.6 |
| paragraph-offset-frame-exact | 22.7 | 36.3 |
| paragraph-offset-frame-immediate | 27.3 | 38.3 |
| paragraph-offset-frame-immediate-long | 60.5 | 30.4 |
(immediate-long scores higher for office2pdf only because docxide substitutes a sans face for TNR; the coordinates above are the defect.)
Expected
A body paragraph (or run of adjacent paragraphs with identical w:framePr) with w:framePr is removed from the body flow and laid out in a frame: horizontal position from w:hAnchor (text/margin/page) + w:x (or w:xAlign), vertical from w:vAnchor (text = the paragraph's flow position, margin, page) + w:y (or w:yAlign), width w:w (auto = content), height w:h per w:hRule (auto = content, which for an empty paragraph is its mark's line). With w:wrap auto/around, body lines whose vertical extent intersects the frame box are laid out in the space beside it (both sides when room remains), and resume full width below it. An empty frame still occupies its box for wrapping but adds no flow height.
The Word reference PDFs supply gt.jpg for fix-mode evidence. Reference implementation lead: docxide-pdf src/pdf/mod.rs (paragraph_offset_frame, is_empty_wrapping_frame), which positions and wraps the immediate cases but does not wrap around the empty frame.
Related: #2161 (its marker paragraph also carries w:framePr).
Activity
developer0hye commentedon Oct 18, 2026
#2256 landed the flow-height part of this issue for empty frames. A body paragraph that is alone in a
w:framePrframe Word lifts out of the flow, and has no content, now leaves no line. The rule follows docxide-pdf'sis_empty_wrapping_frame: the frame states more than a barew:wrap, is notyAlign="inline", is notnone/notBeside, and has now:h. Paragraphs, or a table, sharing an identicalw:framePrkeep their empty lines.Results with Word references:
empty-frame-control: the "Following paragraph" baseline moves from 101.17pt to 85.07pt (Word 85.03pt).page-header-frame-marker: the body line is now within 0.65pt of Word, down from 10.85pt.cont-first-first-fits(DOCX: paragraph after a continuous section loses about 8pt of its declared gap #2161): matches Word.Still open here:
w:x/w:y.empty-frame-control's "paragraph" is still not at x=390.18.notBesideornone.empty-margin-frame-*cases are unchanged; for example,empty-margin-frame-15-one-emptyputs "Public greeting" at 224.81pt against Word's 213.83pt.developer0hye commentedon Oct 19, 2026
#2356 (merge
3503c178) landed the second milestone of this issue: positioning frame content atw:x/w:y, for one kind of frame.Lifted now. A body paragraph alone in a
w:framePrframe hungw:ybelow its place in the flow. The predicate follows docxide-pdf's Word-probedparagraph_offset_frame:w:vAnchor="text", a nonzerow:y, now:yAlign, a statedw:wandw:h, and aw:wrapother thannone/notBeside. Such a paragraph becomes aBlock::TextFrame:w:x/w:xAlignresolve againstw:hAnchor, and the frame hangs from the end of the previous block's after-gap.w:hSpace/w:vSpace).Results against the Word references (
mutool draw -F stextbaselines):paragraph-offset-frame-immediate: "Dear recipient" 101.97 → 85.87 (Word 85.78), "About providing information" 85.87 → 102.67 (Word 102.53). Its x stays 71pt right: the negativew:ind w:leftclamp, DOCX: a negative w:ind w:left is clamped to zero, so a paragraph that hangs into the margin starts 71pt right of Word #2233.paragraph-offset-frame-exact: the frame line 85.87 → 102.67 (Word 102.53). "Dear recipient" 182.36 → 166.26 (Word 146.05); the remaining 20.2pt is the empty section-mark line, DOCX: an empty section-mark paragraph's line is kept or dropped by whether its w:pPr is bare, not by its position in the section #2232.paragraph-offset-frame-immediate-long: the frame is in place. The long paragraph starts at the frame's bottom edge (line 1 at 132.72, Word 85.78) instead of narrowing lines 2–3 beside it as Word does. Filed as DOCX: a paragraph that reaches a lifted w:framePr text frame starts below it instead of narrowing only the lines beside it #2355.w:framePrare unchanged. Of the nine bulk documents the rule reaches, three move toward a LibreOffice 24.2 reference, three are mixed (DOCX: a paragraph that reaches a lifted w:framePr text frame starts below it instead of narrowing only the lines beside it #2355), and three are unchanged (details in fix(docx): draw a text-anchored framed paragraph in its frame #2356).Still open here:
empty-frame-control's "paragraph" at x 390.18. The wrap uses only one side; see DOCX: a paragraph that reaches a lifted w:framePr text frame starts below it instead of narrowing only the lines beside it #2355.notBeside/none,w:framePrinherited from a paragraph style, and framed tables (first-cellw:framePr). Example: docxidemassachusetts_community_sanitation, whose letterhead frames push the letter about 167pt down page 1.w:framePr: theempty-margin-frame-*cases, where frames withoutw:vAnchorare margin-anchored per docxide's probe.w:w(auto width), and text-anchored frames withoutw:hsuch ascroatian_grant_guidelines'.w:pBdraround the whole frame box; office2pdf draws it around the paragraph only (bulktdf133702,tdf103544).#2231 stays open for these.
developer0hye commentedon Oct 20, 2026
#2357 (merge
8d25e2d7) landed a third milestone of this issue: a group of empty framed paragraphs leaves no flow lines. CI passed on the PR and on the merge commit.What changed. #2256 dropped the line of an empty framed paragraph only when the paragraph was alone in its frame. Now the rule applies to the whole run of adjacent body children that share an identical
w:framePr:Results against Word's references ("Public greeting" baseline, pt):
empty-margin-frame-14-one-empty,-14-two-empty,-14-two-space-only,-hidden-marks,-off-page: 213.31–236.31 → 201.81 (Word 201.80).empty-margin-frame-15-*,-visible-body-before-frame,-long-prefix(p2): the greeting now sits the right distance below the table. Both are still 12 or 24pt above Word, because Word moves the table itself down; see the first item below.case128/case130: "Below marker" 165.56 → 133.37 (Word 165.30 / 181.30). Word gives these two empty framed paragraphs no lines either. Their spurious lines had been offsetting the misplaced text-anchored floating table (DOCX: a w:tblpPr floating table with w:vertAnchor="text" is positioned from the top margin instead of its anchor paragraph #2236), whosew:clear="all"break therefore clears nothing (DOCX: w:br w:clear="all" is treated as a plain line break, so text after it stays beside a floating table instead of resuming below it #2230).fixtures-v1bulk document and no other docxide document moved.Still open here:
w:vAnchor. A following full-width table moves below the box, 12pt per framed paragraph before the section mark (empty-margin-frame-15-*,visible-body-before-frame,long-prefix). Hidden marks and an off-pagew:xreserve nothing.empty-frame-control's "paragraph" at x 390.18).notBeside/none;w:framePrinherited from a paragraph style; framed tables; groups that hold text. Example: docxidemassachusetts_community_sanitation, page 1 about 167pt low.w:framePris merged attribute by attribute over the style's, as docxide'sparse_frame_propsdoes.behindDocwrapSquaresecretary text box, which leaves no room on its right (DOCX: a wrapSquare/wrapTight anchored picture floats to the page top and the text resumes below it at full width, instead of wrapping beside it #2353 / DOCX: an anchored picture with behindDoc="1" and wp:wrapTight loses its wrap, so text runs at full width under it #2354 territory).w:w(auto width), and text-anchored frames withoutw:h(croatian_grant_guidelines).w:pBdraround the whole frame box (bulktdf133702,tdf103544).developer0hye commentedon Oct 20, 2026
#2361 (merge
ff37c41b) landed a fourth milestone of this issue: in compatibility mode 15, an empty frame's box at the top margin pushes a full-width table below it. CI passed on the PR and on the merge commit.What changed.
compatibilityMode15 or later, a group of empty framed paragraphs still takes no flow lines. Its frame keeps a box at the top margin when the frame:w:vAnchor(ormargin);w:yand now:yAlign;w:w.Results against Word's references (baselines, pt):
empty-margin-frame-15-two-empty-15-one-empty-visible-body-before-frameThe remaining ~1pt is this host's Tahoma substitute, which gives the box's 10pt lines 11.50pt each against Tahoma's 12.07pt. With the Tahoma subset from Word's PDF on
--font-path, "Public left" lands at 129.36. It is recorded under #1874. A scan of all 76 docxide andfixtures-v1documents with a bodyw:framePrchanged only these three.Still open here:
empty-frame-control's "paragraph" at x 390.18 and the compat-14empty-margin-frame-14-*greeting, which Word splits around the box at x 403.17.empty-margin-frame-long-prefixstarts them below the box (page 2, 24pt); here they still start at the top margin.empty-margin-frame-15-two-space-only: its space-only paragraphs collapse (DOCX: space-only paragraphs collapse instead of reserving their line box #2288), so its box has no height.w:y/w:yAlign) have Word references. Narrower tables and offset or aligned margin frames are left alone.notBeside/none;w:framePrinherited from a paragraph style; framed tables; groups that hold text. Example: docxidemassachusetts_community_sanitation, page 1 about 167pt low.w:w(auto width), and text-anchored frames withoutw:h(croatian_grant_guidelines).w:pBdraround the whole frame box (bulktdf133702,tdf103544).developer0hye commentedon Oct 20, 2026
#2362 (merge
6d02d1e8) landed a fifth milestone of this issue: rows a table carries onto the page of an empty frame's top-margin box now start below the box. CI passed on the PR and on the merge commit.What changed.
TODO; no Word reference shows it).Results against Word's reference (
empty-margin-frame-long-prefix, baselines, pt):The remaining ~1pt is the host's Tahoma substitute again: with Word's embedded Tahoma subset on
--font-path, page 2 starts at 129.36 and the greeting sits at 644.52. It is recorded under #1874. A scan of every docxide,fixtures-v1and tracked document with aw:framePrchanged only this case and its tracked copy.Still open here:
empty-frame-control's "paragraph" at x 390.18 and the compat-14empty-margin-frame-14-*greeting, which Word splits around the box at x 403.17.empty-margin-frame-15-two-space-only: its space-only paragraphs collapse (DOCX: space-only paragraphs collapse instead of reserving their line box #2288), so its box has no height.w:y/w:yAlign), have Word references. Narrower tables, offset or aligned margin frames, content between the table and the group, and carried rows the box would push onto the next page are left alone.notBeside/none;w:framePrinherited from a paragraph style; framed tables; groups that hold text. Example: docxidemassachusetts_community_sanitation, page 1 about 167pt low.w:w(auto width), and text-anchored frames withoutw:h(croatian_grant_guidelines).w:pBdraround the whole frame box (bulktdf133702,tdf103544).developer0hye commentedon Oct 20, 2026
#2365 (merge
d041a66e) landed a sixth milestone of this issue: a frame Word keeps at a fixed place on its page and keeps text off the sides of is drawn at that place, and the body text it meets starts below it. CI passed on the PR and on the merge commit.What changed.
w:framePris now its paragraph style's (nearestw:basedOnancestor stating one), with each attribute the paragraph states over it. A table's is its first cell paragraph's. A frame stating onlyw:wrapis no frame. The existing empty-frame, offset-frame and top-margin-box rules use the same effective frame.w:vAnchorpageormargin/none,w:wrapnotBeside/noneand now:yAlignbecome oneTextFrame(text_below), drawn at its page position.Results against Word's reference of docxide-pdf's scraped
massachusetts_community_sanitation(page 1, x and baseline, pt):The date's 5.3pt is two residuals nearly cancelling, each now tracked:
behindDocwrapSquaresecretary text box the letter should start below (DOCX: an anchored picture with behindDoc="1" and wp:wrapTight loses its wrap, so text runs at full width under it #2354). Its own lines are about 8pt high (DOCX: top-aligned caption text crosses its textbox upper border #2314).A corpus scan of 2,366 DOCX moved only that document and six bulk LibreOffice frame documents, all toward a LibreOffice 24.2 reference (labelled non-Word; details in #2365).
Struck from the list: croatian_grant_guidelines' text-anchored frame without
w:h. Word keeps it in the flow, as office2pdf does; only itsw:x="-39"(1.95pt) shift differs, which is sub-material and recorded in #1874.Still open here:
empty-margin-frame-15-two-space-only: DOCX: space-only paragraphs collapse instead of reserving their line box #2288.w:wlays its paragraphs across the text width from its left edge (FIXME). Word sizes it to the widest line, which matters for centred or right-aligned lines.w:yAlignor byw:xAlignwithoutw:w; those inside block-levelw:sdt(bulktdf148671, three of six); and those holding lists or drawings (bulkTdf147485).tdf104394_lostTextbox).auto/around), which still flow.w:pBdraround the whole frame box (bulktdf133702,tdf103544,tdf100075).developer0hye commentedon Oct 20, 2026
Draft #2368: frames inside block-level content controls. Not merged: its only reference is LibreOffice, and this Linux host cannot export from Word. CI passes on the draft, the Visual PR Contract included.
What #2368 does. A paragraph or table inside a block
w:sdtnow has itsw:framePrframe laid out as it would be without the control. Body children are flattened into flow children, each control expanding into its paragraphs and tables. Every existing frame rule groups and places frames by flow index: fixed page frames, offset frames, empty frames and the compat-15 top-margin box. A test pins it for five frame kinds, in single and nested controls. In bulktdf148671, the three controlled page frames move from the flow at the top left (x 35.45) to their frame places (x 434.30), within 1.1pt of a LibreOffice 24.2 export (LibreOffice reference, not Word). The remaining 1.1pt is the two sides' different substitutes for Microsoft Sans Serif. A scan of all 2,366 DOCX packages (docxide-pdf,fixtures-v1, tracked) changed only that file.To merge #2368, a Mac must: export
tests/fixtures/docx/libreoffice/tdf148671.docxfrom Word to PDF and confirm the three controlled frames sit at x 434.3pt with their tops at y 183.05, 198.05 and 212.3pt. Then regenerate the PR'sgt.jpg,layout-audit.jsonandrender-clusters-page-1.jsonfrom that PDF. The steps are in the PR body.Re-audit, so the next session need not repeat it. All 22 framed docxide-pdf cases and the scraped massachusetts letter were measured again on
maind041a66eagainst their Word references. Every remaining deviation already has another open issue: #2355 (both-sides and per-line wrap), #2288 (-15-two-space-only), #2233 (negative indent of the offset frame text), #2232 (paragraph-offset-frame-exactsection mark), #2236/#2230 (case116/128/130), #2253–#2255 (cont-first-*-overflow), #2363, #2354, #2364 and #2314 (massachusetts).Struck from the list: auto width. With Word's embedded Arial Rounded MT Bold subset on
--font-path, massachusetts' auto-width, centred governor frame matches Word within 0.2pt on three of its four lines. The fourth, "Lieutenant Governor", is 2.17pt left because its trailing spaces are centred (sub-material, recorded in #1874). No Word reference shows an auto-width defect, so theFIXME(auto frame width)stays unverified rather than open work.Filed while auditing: the vertical rule in
tdf148671gets arrowheads from its type-lessa:headEnd/a:tailEnd(#2366), and itsschemeClrstroke paints black instead of grey (#2367).Still open here. Every item needs a Word export or a one-factor probe, so this issue is labelled
autofix-blocked:tdf148671described above.tdf104394_lostTextbox, whose last paragraph says it is "Pushed to page 2 by the framed text". That file also holds aw:yAlign="top"frame, which stays in the flow here.Tdf147485.w:pBdrdrawn around the whole frame box: Word PDFs of bulktdf133702,tdf103544andtdf100075.around/autoframes with body text beside them. No corpus document isolates these, so each needs a one-factorscripts/probe_harness.pyprobe exported from Word.empty-margin-frame-15-two-space-only: DOCX: space-only paragraphs collapse instead of reserving their line box #2288.The candidate and base binaries, the evidence PDFs and the timing runs for #2368 are kept on the host that ran this (
/tmp/i2231). The corpus checkouts and build targets were removed.