Skip to content

fix(docx): stack drawings in Word as the page paints them - #922

Merged
DemchaAV merged 1 commit into
2.5-devfrom
fix/docx-layers-and-pictures
Oct 19, 2026
Merged

DemchaAV merged 1 commit into
2.5-devfrom
fix/docx-layers-and-pictures

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

Word stacks a document's drawings by the place each is given (wp:anchor/@relativeHeight), a later
one over an earlier one. The export gave each drawing its place in the order it happened to draw it
(DocxDrawingAnchors.ordered: a running counter). The page paints a layer stack's and a shape
container's layers by zIndex, while the export draws them in the order given, and draws columns'
marks after the columns' own content. A layer the page lays under another, but given after it, came
out on top.

Measured in Word 16 and LibreOffice on Windows: a white dot (zIndex 0) given after a blue disc
(zIndex 1). The page hides the dot.

Where Before, in Word / LibreOffice After, in Word / LibreOffice
a stack that only draws dot over the disc / over dot under the disc / under
a shape container that only draws over / over under / under
a band of layers with text (two boxes, z 1 then 0) white over blue / over under / under
a stack that only draws, nested in such a band over / over under / under

Only the shape container was named, with "its layers are written inline"; the other three went
unsaid.

What changed

  • DocxDrawingAnchors.ordered(shapes, painted): a shape's place is the index of the fragment it
    is drawn from in the page's paint order, times PLACES_PER_FRAGMENT (8; a fragment is drawn as five
    shapes at most), plus its index among that fragment's shapes (capped at 7).
    • The page paints in the order the layout lists its fragments, a stack's layers by zIndex.
    • A shape drawn from no fragment the layout holds is drawn as the fragment of the latest place given,
      after its shapes so far, so it ties with none.
    • The counts restart with each section, whose layout counts its fragments afresh.
    • A drawing's name (docPr, "Drawing N") keeps the running number (Ordered.drawn), so names
      are as before.
  • DocxLayoutMetrics.paintIndexOf(fragment | node): a fragment's index in that order; a node's
    is its first own fragment's.
  • DocxSemanticBackend: every place a drawing is queued passes its fragment's index:
    • shapes drawn from their fragments, and a badge's glyph;
    • a table's cell drawings;
    • a text box laid over the flow, and a picture drawn where the page puts it, by their node.
  • The text is written in the order it was. Only the places change.
  • The shape container is no longer named for layers the page stacks in another order:
    paintsOutOfSourceOrder is gone.
  • DocxDrawings.stackHeight holds the value to a signed 32-bit count, which LibreOffice reads. Past
    about 230,000 fragments in one section, drawings would share the top place.
  • Docs:
    • the DOCX recipe's shape-container paragraph;
    • the capability matrix's layer-stack row;
    • the layers entries of DocxNodeFieldLedgerTest for the stack and the container;
    • CHANGELOG v2.5.0.
  • Example: assets/readme/examples/word-export-companion.docx is re-rendered. Its one drawing's
    relativeHeight changes and nothing else.

Verification

  • Corpus, 62 DOCX before and after, compared part by part:
    • 28 are byte-identical;
    • 34 differ, in relativeHeight values only;
    • 26 of those 34 keep every drawing's rank;
    • the 8 that re-rank (CVs: charcoal_gold, midnight_navy, navy_sidebar, professional_sidebar,
      serif_headline, slate_orange, terracotta_rail, violet_grid) render pixel-identical at 96 dpi,
      before and after, in Word and in LibreOffice;
    • report notes 1246 → 1246.
  • DocxLayerPaintOrderTest (new, 9 tests, read off each anchor's relativeHeight):
    • a stack and a shape container that only draw;
    • one z-index keeping the order given;
    • a band, with its text still in the order given;
    • a stack nested in a band;
    • a stack composed in a table cell;
    • a table cell's drawing under a bar the page lays over the table, the bar given first;
    • a title and its dates over two bars;
    • columns: a backdrop under a column's circle, and two marks over the columns, stacked by zIndex.
  • DocxDrawingAnchorsTest (+3):
    • places by fragment, among a fragment's shapes, and from no fragment;
    • the cap of 8;
    • a new section counting afresh.
  • DocxDrawingsTest: the container stacked out of source order is now stacked so, and not named.
  • 8 sabotages, all caught:
    • the order drawn;
    • a fragment's shapes at one place;
    • no cap;
    • sections not afresh;
    • drawn fragments unplaced;
    • no place for any fragment;
    • the fallback at the first fragment;
    • cell drawings unplaced.
  • ./mvnw -B -ntp clean verify: BUILD SUCCESS (core 821, render-docx 1805, examples 93, qa 1820); the knowledge pack's --check passes.

The places order the drawings behind the text among themselves, and those in front of it. Word draws
every one behind under every one in front: on a painted panel, a layer framing text stays behind while
one that does not stands in front, whatever their zIndex. That split is unchanged.

Lane: render-docx (semantic backend). DOCX output changes for every document with drawings, in
relativeHeight values: where the page stacks overlapping drawings in another order than the export
drew them, they now stack as on the page.

Word stacks drawings by the place each is given (relativeHeight), a
later one over an earlier one, and the export gave them their places
in the order it happened to draw them: a stack's or a shape
container's layers in the order given, columns' marks after the
columns' content. A layer the page stacks under another by its zIndex,
given after it, then stood over it. Measured in Word and LibreOffice
on Windows, a white dot at zIndex 0 given after a disc at zIndex 1
stood over the disc in both, where the page hides it, in a stack that
only draws, a shape container, a band of layers with text and a stack
nested in such a band. Only the shape container was named for it.

Each drawing now takes its place from where the page paints the
fragment it is drawn from: the fragment's index in the layout's paint
order, eight places a fragment, its shapes in the order they come
(DocxDrawingAnchors.ordered, DocxLayoutMetrics.paintIndexOf). Both
editors draw the dot under the disc in all four. The text keeps the
order it is written in, a drawing's name its old number, and the
shape container is no longer named for the order of its layers. The
places order the drawings behind the text among themselves, and those
in front of it.

In the fidelity corpus 34 of the 62 files change, in those places
only: 26 stack as before, and the 8 whose order changes render pixel
for pixel as before at 96 dpi in Word and in LibreOffice. The Word
export example's DOCX is re-rendered for it.
@DemchaAV
DemchaAV merged commit 579d525 into 2.5-dev Oct 19, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-layers-and-pictures branch October 19, 2026 12:45
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