Repository navigation
fix(docx): pass a dropped section mark's w:after on to the next continuous section - #2381
Merged
Merged
Conversation
…nuous section An empty section mark that takes no line before a continuous section still shapes the gap. Word lays the next section's first paragraph out after the previous paragraph's whole w:after, plus only the part of its own w:before beyond the mark's w:after: after + max(0, before - mark after), as docxide-pdf's Word-probed section_break_spacing has it. Typst collapsed the two gaps to their maximum instead. Where the next section opened a #columns or #move frame, the w:before fell at the frame's region start and was lost. The parser now hands the dropped mark's resolved w:after to the next section's first paragraph (ParagraphStyle::section_mark_after). A mark right after a page or column break hands nothing on, because the section then opens at a page or column top. The renderer writes the residual before-gap as fixed space in front of that paragraph, in the first column when the section has columns, and zeroes the paragraph's own before-gap. Typst's weak block spacing looks past fixed space, so the previous block's below-gap survives whole. docxide-pdf scraped/covid_insomnia_mental_health (Word reference, fe7b8bea): "1. Introduction" 642.68 -> 652.68pt (Word 650.45). Of 2,370 corpus DOCX files, only that one and LibreOffice's tdf90697_complexBreaksHeaders change; the latter's three moved starts now equal the rule's gap. Related: #2232 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
Word-referenced evidence for the dropped section mark's spacing on docxide-pdf scraped/covid_insomnia_mental_health page 1: GT, before and after renders, the page-1 layout audit and the strict render-cluster report. It replaces the first milestone's evidence for #2232, as earlier multi-milestone issues did. Also the compare images for the issues filed during the audit: #2372 (the new-page counterpart of this rule, from docxide-pdf case25) and #2373-#2380 (pre-existing deviations on the same document). Related: #2232, #2372, #2373, #2374, #2375, #2376, #2377, #2378, #2379, #2380 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
Second milestone of #2232: the spacing around an empty section mark that takes no line, before a continuous section.
Word lays the next section's first paragraph out after the previous paragraph's whole
w:after, plus only the part of its ownw:beforebeyond the mark'sw:after: gap = previous after + max(0, next before − mark after). This is docxide-pdf'ssection_break_spacing(fe7b8bea,src/pdf/mod.rs~5666), from Word probes. Typst collapsed the two neighbouring gaps to their maximum instead. Where the next section opened a#columnsor#moveframe, the first paragraph'sw:beforewas lost entirely, because it fell at the start of the frame's region.w:aftergoes on the first paragraph of the next section, as the newParagraphStyle::section_mark_after. A plain paragraph, a caption and a list item all carry it. A mark right after a page or column break opens the section at a page or column top, so it hands nothing on.take_section_mark_spacewrites max(0, before − mark after) as fixed space (#v(…, weak: false)) before that paragraph, and sets the paragraph's own before-gap to zero. Typst's weak block spacing looks past fixed space, so the previous block'sbelowsurvives whole and the residual adds to it. All three continuous-section paths get it: the plain flow (and its#moveframe), the path for a section that breaks the page, and multi-column sections. In a multi-column section the space goes in the first column only. Word's export ofcovid_insomnia_mental_healthstarts its second column above the first column's numbered heading.TODO(section mark spacing)is gone. With fix(docx): keep or drop an empty section mark's line by its place in its section #2371's keep/drop rule, this completes what 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 asks for. The new-page counterpart (Word keeps a new-page section's firstw:beforeless the mark'sw:after, and we drop it) is a separate code path, filed as DOCX: a new-page section's first paragraph loses its w:before, which Word keeps less the closing section mark's w:after #2372 from docxide-pdfcase25.Related issue
Related: #2232, #2372, #1874. Also #2371 (first milestone), #2256.
Testing
cargo test --locked --workspace --profile ci: 4,508 passed, 0 failed.cargo fmt --all -- --checkandcargo clippy --locked --workspace --all-targets -- -D warnings: clean.docx_section_mark_tests.rs, both seen against main's code before the fix:a_dropped_section_mark_absorbs_only_its_own_after_of_the_next_beforecompiles the layout and measures the "Before"→"Next" baseline gap against a zero-spacing control. It uses eight (after, mark after, next before) triples: (12, 8, 10) → 14 (docxide's Word probe), (12, 0, 10) → 22 (covid's shape), (4, 0, 10) → 14, (6, 2, 4) → 8, (0, 0, 10) → 10, (10, 20, 15) → 10, (10, 10, 0) → 10 and (0, 4, 0) → 0. Each runs in one column, in two columns and in a section that later breaks the page, as a plain paragraph and as a list item. Main gives 12 for the first.a_dropped_section_mark_after_a_page_break_leaves_the_next_page_top_alonechecks that a 10ptw:beforeafter a page break and a dropped mark still adds nothing at the new page's top. That is the shape of LibreOffice'stdf170119_bottomSpacingtest. It passes on main and fails with the page-break guard removed.scraped/covid_insomnia_mental_health(fe7b8bea,Creator: Microsoft Word), page 1,mutool draw -F stextbaselines, main → this PR (Word):19436ae1(two byte-identical builds) and this branch. Inputs: 2,370 DOCX files (tracked fixtures, docxide-pdffe7b8beacases/andscraped/, thefixtures-v1bulk corpus). No exit status changed and only 2 PDFs differ:covid_insomnia_mental_health, as above. A survey of every dropped mark before a continuous section in the corpus finds it is the only Word-referenced file where the rule differs from Typst's maximum.tdf90697_complexBreaksHeaders(no Word export). Three continuous starts open a#moveor#columnsframe for changed margins. At two of them (after 0, mark 0, before 3.1 / 7.4pt) main lost thew:beforeat the frame's top, and the paragraph now moves down by exactly 3.10 / 7.40pt. At the third (13.2, 13.2, 2.9pt) main added the 2.9pt on top of the 13.2pt, and the paragraph now moves up 2.90pt. All three now equal the rule's answer. The page count is unchanged at 7.Visual impact
Visual audit
tests/fixtures/scraped/covid_insomnia_mental_health/input.docxatfe7b8beafa7612cc89ad62a7d27b2c6141c4d6ed(not committed; SHA-25613bb08d9074be0506444bbf84a7abd26f1b3dbfb79524ced44e6b07e8db407cd; Word reference SHA-2560fa16efc0adca7dc73238ff396af75213ed1714aa83fe1fb5cecaa2e47943d39). Tracked regressions use synthetic packages built indocx_section_mark_tests.rs.pdftoppmviacompare_render.py, 150 DPIfixassets/bugfixes/issue-2232/layout-audit.jsonassets/bugfixes/issue-2232/render-clusters-page-1.jsonassets/bugfixes/issue-2232/gt.jpgassets/bugfixes/issue-2232/before.jpgassets/bugfixes/issue-2232/after.jpgVisual comparison
Required inspection
Deviation audit
Final performance pass
/usr/bin/time -f "%e %M").cargo build --locked --release -p office2pdf-cli) on an Intel i5-10400, 12 threads, 32 GB, Linux 7.0.0, with Liberation, Carlito and Caladea and no Microsoft core fonts. Warm page cache.19436ae1(base,base2), and the candidate. The candidate is byte-identical to a fresh release build of the PR head's sources.covid_insomnia_mental_health(evidence, 5 pages) andfeminist_voice_dissertation(105 pages), both docxidescraped/;tdf90697_complexBreaksHeaders(bulk, 7 pages, the other mover); trackedspaced_section_mark.docxandsection_alignment_continuous.docx.Option<f64>per paragraph style and, per continuous section, a scan to the first paragraph block.Checklist
Signed-off-byline🤖 Generated with Claude Code