Skip to content

fix(docx): name a layer written under the layers the page lays it over - #904

Merged
DemchaAV merged 2 commits into
2.5-devfrom
test/docx-stack-layers-probe
Oct 15, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
test/docx-stack-layers-probe

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Why

Word writes one block under another. So a layer stack's layers go into the file one after the other: as one band (DocxLayerColumns.band), or one after another in a column's cell (DocxLayerColumns.of). Each later layer resumes the page's distance below the layers before it.

Where the page starts a layer above where those layers end, that distance is below zero, and resume clamped it to nothing. What the layer writes in the flow was written under them, and the report said nothing:

  • a caption set over the foot of its photo came out under the photo;
  • a label in a card's corner came out under the card;
  • text laid over text came out a line lower;
  • a title whose place an earlier layer holds with a spacer came out after the body.

A stack composed in a table cell has no place of its own, and neither has any stack exported with no layout, so neither is a band nor columns. Their layers were written one after another with nothing said either. The ledger called LayerStackNode.layers WRITTEN.

What changed

  • DocxLayerColumns.flow measures where the layers before a layer end (endOf) against the top of the layer's highest written block (highestLeaf).
    • It uses the highest block, not the first: a row centring its label beside a taller column starts at the column.
    • A layer the page starts more than half a point (EDGE) above their end is recorded in over, which Plan and Band both carry. Half a point or less is a layer touching the one above it.
    • The resume is measured as before. spaceAbove returns the gap to the first block's box, and the caller takes the block's margin off. The file does not change.
  • Where the layers before it end is where a panel among them ends, at its box's foot, past the padding below its text.
    • Columns already measured their resumes that way.
    • A band keeps measuring its resumes from text, but measures over with the panel predicate. band now takes it as DocxSemanticBackend::paintsAPanel, which the columns' painted predicate shares.
  • writeOverlayBand and writeLayerColumns name such a layer. The note is overlapping layer, "the page starts its highest block N pt above where the layers before it in its stack|shape container|column end, and Word writes one block under another: what it writes in the flow is written under them".
    • The note gives a page fact, not how much lower the layer lands in Word. That depends on how tall Word sets the layers before it. Measured in Word, a column fixed at 150pt wraps in 2 lines rather than 4, and the layer after it lands 25.7pt lower, not 51.8pt.
    • For a badge drawn over the layers before it, what the band writes in the flow is the line its initials hold. The badge itself is drawn where the page draws it.
  • writeContainerChildren names a stack that writes more than one layer and has no place in the layout. That is a stack composed in a table cell, or one exported with no layout. Its note says its layers are written one after another, and that whether the page lays one over another is not measured. A stack writing a single layer over drawing is not named.
  • Ledger: LayerStackNode.layers is REPORTED.
  • Docs:
    • docs/recipes/docx-export.md: the band bullet names both notes, and the limit below.
    • backend-capability-matrix.md: the layer stack row names both notes. A clause saying SidebarPortrait's subtitle stands under its name strip is gone: since fix(docx): write a strip drawn in two layers as one strip #765 the export writes it in the strip, in the same cell as the name.
    • CHANGELOG.md: an entry for the notes.

Verification

  • Word. The probe documents, converted through Word and measured with PyMuPDF:

    • the caption lands 12.9pt lower;
    • the text over text lands 12.9pt lower;
    • the label in the card's corner lands 62.8pt lower, under the card;
    • the column layer lands 25.7pt lower.

    Each was written under the layers before it.

  • DocxOverlappingLayerTest, 14 tests. It covers:

    • a caption over its photo;
    • a label over a card, measured from the panel's foot;
    • a title whose place an earlier layer holds;
    • a badge over a name, named while its initials stay out of the flow;
    • a title over the badge before it in a shape container, with the initials' line written before the title;
    • a row measured from its highest block, with the premise that its first block starts lower checked;
    • a layer over another in its column, written as a table and saying "column";
    • a layer over a card in a column;
    • a layer with a margin, measured to its box;
    • in a table cell, a stack of two written layers named as not measured, while one that only draws, and one writing a single layer over a drawing, are not named;
    • with no layout, a stack of two written layers named, one writing a single layer not named;
    • a stand-in in a later layer, layers set apart, a line pair and text over a drawing, none named;
    • 0.3pt over not named, 1pt over named.

    Expected numbers are read off the layout.

  • DocxOverlayBandTest, 1 new test. It pins that a later layer's top margin is written once in the gap above it.

  • Sabotage, 14 of 14 caught:

    • O1: nothing recorded;
    • O2: a band measured from text;
    • O3: no EDGE;
    • O4: the band names none;
    • O5: the columns name none;
    • O6: the resume forgets the margin;
    • O7: the columns measured from text;
    • O8: over counts the margin;
    • O9: the first leaf instead of the highest;
    • O10: no not-measured note;
    • O11: columns saying "stack";
    • O12: a stack that only draws named;
    • O13: a shape container saying "stack";
    • O14: a stack writing a single layer named.
  • Corpus. All 62 DOCX are byte-identical to 2.5-dev, and the reports are unchanged at 1246 notes.

  • Build. ./mvnw -B -ntp clean verify → BUILD SUCCESS (render-docx 1599, qa 1820).

Notes for review

  • A layer nearer the layers before it than its own top margin is not named. The page starts it under them, but by less than that margin, so its resume is clamped to 0. Word writes the whole margin, and the layer lands the difference lower. This is a spacing drift rather than an overlap. Counting the margin into the measure gives no note anywhere in the corpus. The recipe states it as a limit.
  • The exact 0.5pt boundary is not pinned. A layer at 0.5pt over sits on a floating-point edge, so > and >= cannot be told apart by a stable test. The test checks 0.3pt and 1pt.

Lane: render-docx (DocxSemanticBackend, DocxLayerColumns) — report and ledger only, the file unchanged.

A layer stack's layers are written one after the other, as a band or in
a column's cell, each resuming the page's distance below the layers
before it. Where the page starts a layer above where those end, that
distance was clamped to nothing and the layer written under them in
silence: a caption over the foot of its photo, a label in a card's
corner, text over text.

DocxLayerColumns.flow now records, in Plan and Band, how far above
where the layers before it end the page starts a layer's highest
block, a panel among them ending at its box's foot; within half a
point is touching. writeOverlayBand and writeLayerColumns name such a
layer (overlapping layer). A stack composed in a table cell, with no
place of its own, is named with its layers not measured. The resume is
measured as before, so the file does not change. The ledger's
LayerStackNode.layers is REPORTED.
The note measures from a layer's highest block, so it says so, and it
names a shape container's band as one. The not-measured note is
written only for a stack writing more than one layer: the docs, the
ledger and the changelog say so, and name an export with no layout
too. The changelog's corpus claim is narrowed to what was measured,
and the recipe names the one clamp left unnamed: a layer nearer the
layers before it than its own top margin.

Tests cover a title over a badge in a shape container, a stack writing
one layer over a drawing in a cell, and an export with no layout; the
number in a note is read without an uncaught NumberFormatException.
@DemchaAV
DemchaAV merged commit 57d0c51 into 2.5-dev Oct 15, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the test/docx-stack-layers-probe branch October 15, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants