Skip to content

fix(docx): set a later layer where the page does when its edge reaches above - #934

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-band-layer-resume
Oct 21, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-band-layer-resume

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Why

A stack's layers that overlap are written as a band, one after the other; side-by-side ones as
columns, where the layers sharing a column follow one another in its cell. Each later layer resumes
at the page's gap below the layers before it, less what its first block writes above its content
itself (DocxLayerColumns.flow).

The gap was measured to the block's box and kept at zero or more. Where the block held more above
its content than the gap, the difference was dropped and the block wrote all of it. Measured in
Word 16 and LibreOffice on Windows, against the page's distance from the line above:

  • a caption with a 20pt margin starting 7pt under that line stood 12.7pt low, with nothing named,
    and so did one centred by an alignment wrapper;
  • a table with a 20pt margin stood 12.6pt low;
  • a caption with a 20pt padding stood 12.7pt low, and a picture contained in a 100pt box level with
    that line (25pt of room above it) 12.9pt. The report named each as written under the line, though
    the page sets it below;
  • the same caption in a column's later layer stood 12.7pt low.

A list lost its own margin in a band. Its margin is carried until its first item, where the layer's
resume replaced it. A list with a 20pt margin opening a band stood 20pt high, one with 10pt 10pt
high, and one in a later layer 7pt high.

What changed

  • DocxLayerColumns.resume measures the gap to the block's content: its box's top, its padding
    and a contained picture's room. It subtracts what the block writes above that content and may
    fall below zero, never below minus that edge.
    • This holds where the block givesItsEdgeBack: a paragraph, a picture, a list or a table written
      in its layer's own flow, under plain containers alone (inThePlainFlowOf): unpainted sections
      and containers, and the alignment and anchor wrappers the backend writes as their content. Its
      margin and padding above must be at zero or more.
    • Any other first block keeps the old gap less its margin, at zero or more. A block in a row, a
      panel or a nested stack writes its edge inside them; a spacer writes neither its margin nor its
      padding; and a block with an edge below zero takes it out where it is written.
  • contentInset (backend) is the content's offset in the box, as writeImage writes it:
    padding plus roomRound. It is threaded through DocxLayerColumns.of, band and flow.
  • highestContent replaces highestLeaf for the overlap the report names (Band#over). For a
    block that gives its edge back, the overlap is measured to its content too, so a box whose padding
    or room alone reaches above the line is no longer named. Any other block is measured from its box,
    as before: it is written lower by that edge, and stays named.
  • resumeLayer (backend), at both the band and the column site, turns a resume below zero into
    pullBelow. That is the existing pull out of the block above, which newBodyParagraph and
    newTable take out of the space owed, then out of the block's own top edge.
  • writeList resumes before adding its own margin and padding, as
    writeTableWithItsOwnSpacing already does.
  • Docs:
    • the recipe's "Overlapping layers → one band" bullet;
    • the capability matrix's layer-stack row;
    • CHANGELOG v2.5.0.

Verification

  • Probe, 18 cases, Word and LibreOffice alike:

    • margin, padding, contained picture, column, table, list in a later layer, list opening a band,
      a centred caption;
    • a section's or a row's margin;
    • true overlaps: a caption over a photo's foot, with 0, 6pt and −4pt margins;
    • a row as the first block, with a margin or a padding; a padded caption in a painted panel;
    • spacers.

    Every measured defect now stands within 0.4pt of the page's distance from the line above, and the
    picture exactly where the page draws it. True overlaps are still written under and named: the
    6pt-margin caption, which stood 5.7pt lower again, now stands where the 0pt one does, as does
    the −4pt one. A row, a section, the spacers, the −4pt caption and the padded panel write byte for
    byte as on the base, and the panel's caption is named as there.

  • Bands at a page's foot, re-run from the probe of the band keep: the bands the page keeps there
    with a later layer's margin reaching above now stay there in Word, rather than moving to the next
    page whole.

  • Corpus: all 62 DOCX are byte-identical to the base, and the report's 1246 notes are identical.

  • Example: word-export-companion.docx was regenerated. It differs from the committed one only
    in docProps/core.xml, the export's time.

  • Tests:

    • DocxLayerResumeTest (new, 14 tests):
      • given back: margin, padding, contained picture (not named), a centred caption, column cell,
        table, list in a later layer, list opening a band (no position raise);
      • kept: a gap wider than the margin, a caption over a picture (written under, named), an edge
        below zero (the old gap), a block in a row (named, no spurious space above note), a padded
        caption in a painted panel (named from its box), a spacer (its height kept).
    • Two list tests pinned the lost margin:
      • DocxListItemLowerTest.anItemInAStackOverADrawingKeepsTheSpaceAboveIt now expects the
        list's 10pt margin, nothing grown;
      • DocxListItemSeatTest.anItemOpeningABandWithNoSpaceWrittenAboveItIsRaisedByItsPosition
        drops the list's margin, so its premise of no space above holds again.
  • 13 sabotages, all caught:

    • resume kept at zero;
    • no inset in the resume;
    • overlap measured from the box;
    • overlap measured to every leaf's content;
    • any block gives its edge back;
    • a block in a row gives it back;
    • a painted panel gives it back;
    • an alignment wrapper not counted as plain flow;
    • an edge below zero gives it back;
    • no pull given back;
    • the list's edge before the resume;
    • no picture room;
    • no padding inset.
  • ./mvnw -B -ntp clean verify: BUILD SUCCESS.

  • Knowledge pack: the API surfaces, stability doc, claims and routes checks pass, and core javadoc
    passes with no warnings.

Notes for review

  • A spacer opening a later layer keeps the old count. Its margin comes off the gap, though it
    writes it nowhere, and the report names it with the spacer. A spacer with a 20pt margin 7pt under
    the line above stands 7pt high, as before.
  • A first block in a row keeps the old gap and is named as overlapping where its content reaches
    above. A caption with a 20pt margin in a row's cell stands 12.7pt low, as before.
  • contentInset resolves a contained picture's data twice more while the stack is planned, for
    the resume and for the overlap, on top of writeImage's own resolution: a byte copy and a hash
    each, per such picture in a later layer. A failure to resolve surfaces either way, only earlier.

Lane: render-docx (semantic backend).

…s above

A layer written after another in a band or a column starts at the
page's gap below the layers before it, less what its first block
writes above its content itself. Where the block held more than that
gap, the difference was dropped and the block wrote all of it. Measured
in Word and LibreOffice, a caption with a 20pt margin or padding 7pt
under the line above stood 12.7pt low, a table with a 20pt margin
12.6pt, and a picture contained in a box level with that line 12.9pt,
the report saying it was written under the line.

The resume is now measured to the block's content, its padding and a
contained picture's room included, and may fall below zero, which the
block's own edge gives back as it gives back a pull out of the block
above. That holds for a paragraph, a picture, a list or a table written
in its layer's own flow; any other first block keeps the old gap. The
overlap the report names is measured to the content too.

A list keeps its own margin and padding after the layer's gap, as a
table does: the gap took their place, and a list with a 20pt margin
opening a band stood 20pt high.
@DemchaAV
DemchaAV marked this pull request as draft October 21, 2026 09:08
…s back

The overlap the report names was measured to every leaf's content,
past its padding, while only a paragraph, a picture, a list or a table
in its layer's own flow gives that padding back. A caption padded 20pt
in a painted panel, its box over the line above, is written that much
lower and was named before; it was no longer. Such a block is measured
from its box again, as it is written.

A block with an edge below zero keeps the old gap too: it takes that
edge out where it is written, by its own writer's rules. An alignment
or anchor wrapper, written as its content, counts as plain flow: a
centred caption with a 20pt margin stood 12.7pt low in Word and
LibreOffice, with nothing named, and now stands where the page sets it.
@DemchaAV
DemchaAV marked this pull request as ready for review October 21, 2026 09:45
@DemchaAV
DemchaAV merged commit cd38c79 into 2.5-dev Oct 21, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-band-layer-resume branch October 21, 2026 09:56
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.

1 participant