Repository navigation
fix(docx): keep an empty top-margin frame's box above a full-width table - #2361
Merged
Merged
Conversation
Empty body paragraphs that share a w:framePr frame take no flow lines since #2256 and #2357. Word 2013 and later still keep the frame's box at the top margin when the frame states no w:vAnchor (or "margin"), and start a full-width table on that page below the box. Word's exports of docxide-pdf's empty-margin-frame-15-* cases show it: the table starts one line lower per framed paragraph (12pt or 24pt), whether visible text comes between the table and the frame or not. The compatibility-mode-14 exports leave the table at the top margin. A hidden paragraph mark, the paragraph ending the section, and a box wholly beside the text area add nothing. office2pdf left the table at the top margin in every mode, 12 to 24pt above Word. In compatibility mode 15, such a group's visible lines now form a TextFrame anchored to the top margin. The renderer draws it invisibly from the top of the text area, with labels on its edges. Each full-width table at the root of the same section asks at layout time whether it starts inside the box on its page, and if so a block before it moves it to the box's bottom edge. Body text keeps its place; Word lays it on both sides of the box, which stays with #2355. Related: #2231 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
File submission policy
Before attaching or committing files, read the submission policy.
Complete a security review and obtain your organization's internal approval for
public sharing where applicable. You are responsible for lawful disclosure;
the project and maintainers disclaim related liability to the extent permitted
by applicable law.
Summary
This is the fourth #2231 milestone. In compatibility mode 15, an empty frame's box at the top margin now pushes a full-width table below it.
Problem. Since #2256 and #2357, empty body paragraphs that share a
w:framePrframe take no flow lines. Word 2013 and later still keep that frame's box at the top margin, though, when the frame states now:vAnchor(ormargin), and a full-width table on that page starts below the box. Word's exports of docxide-pdf'sempty-margin-frame-*cases (fe7b8bea,pdfinfoCreator: Microsoft Word) show this:visible-body-before-frame).hidden-marks), the paragraph that ends the section, and a box wholly beside the text area (off-page) add nothing.office2pdf left the table at the top margin in every mode, 12 to 24pt above Word.
Change.
TextFramewith a newvertical_anchor: FrameAnchor::Margin. Dropping a box beside the text area happens during section resolution. Mode 14, and documents that declare no mode, are unchanged.empty-frame-control); that stays with DOCX: a paragraph that reaches a lifted w:framePr text frame starts below it instead of narrowing only the lines beside it #2355.Results (baselines in pt from
mutool draw -F stext):empty-margin-frame-15-two-empty(evidence)-15-one-empty-visible-body-before-frame--font-path, the same build puts "Public left" at 129.36 (Word 129.30) and the greeting at 225.95 (Word 225.83). This is sub-material and recorded under Tracker: sub-materiality visual deviations (revisit in bulk) #1874.-15-two-space-only: its space-only paragraphs collapse to no height (DOCX: space-only paragraphs collapse instead of reserving their line box #2288), so the box is empty.-long-prefix: Word also starts the rows a table carries onto the box's page below the box. Here they still start at the top margin (aTODOin the clearance helper).-hidden-marksand-off-page: already match Word.mutoolstext were compared over all 76 documents with a bodyw:framePr: the docxide cases and scraped documents, and the bulkfixtures-v1corpus. Only the three cases above change.Related issue
Related: #2231
Testing
cargo test --locked --workspace --profile ci: 4488 passed, 0 failed, rerun after the final code change (only doc comments changed after that run).cargo clippy --locked --workspace --all-targets -- -D warningsandcargo fmt --all -- --check: clean.empty_paragraphs_in_a_top_margin_frame_keep_its_box_in_word_2013_layout: the parser rule. Mode 15 keeps one box with two lines and none for the section mark. Explicitmarginalso keeps it. Mode 14, an undeclared mode, hidden marks, a box beside the text area, page anchoring, aw:y, aw:yAlign, and now:wkeep none.a_full_width_table_in_an_empty_frames_box_starts_below_it: compiled layout with exact 12pt frame lines. A table moves exactly 24pt with the frame after it and with the frame before it. Body text in the box does not move, nor does a table that starts below the box.a_full_width_table_starts_below_an_empty_frames_box: the unmodified docxide case as a GT-backed regression.Visual impact
Visual audit
tests/fixtures/docx/docxide/empty_frame_box.docx(docxide-pdfcases/empty-margin-frame-15-two-empty/input.docxatfe7b8bea, Apache-2.0); GT is that case'sreference.pdf(pdfinfoCreator: Microsoft Word, one A4 page;check_gt_integrity.py --source: no structural corruption; it embeds Times New Roman and Tahoma, ours Liberation Serif)fixassets/bugfixes/issue-2231/layout-audit.jsonassets/bugfixes/issue-2231/render-clusters-page-1.jsonassets/bugfixes/issue-2231/gt.jpgassets/bugfixes/issue-2231/before.jpgassets/bugfixes/issue-2231/after.jpgVisual comparison
Required inspection
Deviation audit
Final performance pass
Setup:
/usr/bin/time -f "%e %M". Release CLIs were built with--lockedin separate run-owned targets. Each input got 7 runs per build, builds interleaved, with a warm page cache. Intel i5-10400 (12 threads), Linux 7.0.0. Host fonts: Liberation substitutes, no Microsoft core fonts.Results, median time (min–max) and median peak RSS:
empty_frame_box.docxtdf133924(real frame document, unchanged)tests/fixtures/docx/10-pages.docx(no frame)Small inputs move by under 1 MiB, within the variance between identical builds. Scoped measurements, not product SLAs.
empty_frame_table_bandsscopes its bands. In stress A that drops 300 checks: 0.93→0.90s and 152.3→145.8 MiB.Checklist
Signed-off-byline🤖 Generated with Claude Code