Skip to content

fix(docx): name the drawing a panel painted beside it covers - #989

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-drawing-behind-a-neighbours-fill
Nov 1, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-drawing-behind-a-neighbours-fill

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Why

A panel is written as a shaded cell, and a cell's fill belongs to the text layer. Word and
LibreOffice both paint that layer over everything drawn behind it — including a drawing that
belongs to a neighbour of the panel rather than to the panel itself. So a drawing on a layer
above a painted layer beside it is covered wherever the two boxes meet, and a layer stack's
z-index does not lift it clear. The export said nothing about this at all, while it does name the
case where a drawing stands inside a painted panel.

Measured at 300 dpi in Word 16.0.20527 and LibreOffice 24.2, on the one document of the
71-document fidelity corpus that holds the case — an 18pt icon laid over a card's top right
corner through a layer stack:

engine Word 16.0.20527 LibreOffice 24.2
the icon's ink inside its box 4308 px 0 0
the icon's ink anywhere on the page 4308 px 0 0
the icon's box in the card's colour — 5625 of 5625 5590 of 5625

The icon is not dimmed or shifted; it is not drawn. With the card unpainted — the one difference
— Word draws every pixel the engine does.

What changed

The export names the loss. DocxSemanticBackend records each shape it queues behind the text
with the box it is drawn in (rememberDrawnBehind) and each panel whose cell it gives a fill with
the box the layout gave it (rememberShadedPanel), and at the end of an export names the drawings
a panel standing beside them covers (nameTheDrawingsAPaintedPanelCovers), as APPROXIMATED,
with the path of the drawing and of the panel. A drawing standing inside the panel is skipped:
that is the export's own case, which already puts such a drawing in front wherever it frames no
text, and has a note of its own.

Three decisions in that are worth their lines:

  • The panels are taken where the fill is written, not read off the tree. Whether a section, a
    container or a row that paints becomes a cell with shading is decided as it is written — a
    row's paint goes unwritten where its panel cannot hold it, a container's where it is a band's
    layer or a stack's column — so the tree cannot be asked. Reading it would also have named a
    panel bearing borders alone, which writes no w:shd at all and covers nothing, and missed a
    painted row, which reaches the same cell through writePaintedRow → writePanelPiece.
  • A drawing and a panel are judged only within one section. layout and canvasHeight are
    per-section fields, a section's pages are counted from its own first, and the pass runs once at
    the end of the export, so each record carries its section — as PanelOverPages already does for
    the panels it cuts. Without it the pass would judge every section's drawings against the last
    section's panels, in the last section's page frame. One note per drawing per section, too:
    each section's paths are built from its own roots, so two sections of one preset give their
    drawing a single path, and a note per path would name the first section's loss and hide the
    rest. The note says which section it is in.
  • A shape is asked for its own place in the stack, not for the flag the call was made with. A
    badge holding its initials is handed to the queue already in front and queued with that flag
    saying otherwise, so trusting the flag would have named a badge over a painted neighbour as
    hidden under it when the reader sees it plainly. One record per shape, too, rather than one box
    round a node's shapes: a box drawn as its top border alone is a line, and a box round it would
    be the whole empty rectangle.

A guard over the corpus. DocxNeighbourFillGuardTest holds the corpus to the one drawing that
is known. It judges by the boxes the layout gave the nodes and not by behindDoc: that attribute
is set on drawings the reader sees perfectly well, and the same fixture with its panel unpainted
keeps its anchor's behindDoc="1" while showing all 3237 pixels of its shape in Word. Geometry
is what decides, so geometry is what it computes.

It fails closed, on positive signals:

  • it checks every drawing the export names, one by one, against its own geometry: the drawing
    must come back lying under some painted panel it does not stand inside. Where it cannot place
    the drawing at all — a timeline's rail is drawn under the layout pass's path, which no node of
    the graph carries — it says so in a list of its own rather than calling it a disagreement. The
    other direction is a real difference rather than an identity, a node's box being able to exceed
    its ink, so what the guard finds and the export does not is held in a list too. Both empty
    today;
  • a fixture built for the case must come back named, so a detector that stops seeing its target
    fails here rather than reporting a clean corpus;
  • a note that says a shape was drawn and that the guard cannot classify is a failure, not a
    silence. Four notes in the export begin that way and only two are a drawing's; an unknown one
    would narrow the scan, and a drawing the scan stops seeing is one it can no longer find under
    a panel;
  • the four painted panels the layout breaks across pages are named, not counted — no overlap can
    be judged against a box that belongs to one page of several, and a change that breaks a fifth
    panel across pages is seen here instead of quietly shrinking what is judged;
  • the guard reads the tree more broadly than the export records, counting any filled section,
    container, row or shape container as a candidate panel. Reading it narrowly would make the
    guard blind where the export sees; reading it broadly can only add candidates, and each lands
    in the held list rather than passing unnoticed.

What neither the export nor the guard judges is written out in both, and in the recipe, rather
than left to be discovered — these are the edges of what is checked, not invariants: a panel
composed in an authored table cell, which the layout gives no box of its own; a table cell's own
shading, which belongs to a cell and not to a node; a cell's paint the table draws itself, which
never passes the export's queue; and the second panel over a drawing lying over two, one note
naming one panel.

Verification

./mvnw -B -ntp test -pl :graph-compose-render-docx and ./mvnw -B -ntp test -f qa/pom.xml.

Eight unit tests in DocxDrawingUnderANeighboursFillTest: a shape over a painted neighbour is
named; one over an unpainted neighbour is not; one over a neighbour bearing borders alone is not;
one beside a panel it does not reach is not; one inside a painted panel and behind its text is
not — that fixture first asserts the drawing does go behind the text, so that it reaches the test
of which box holds which rather than passing because the panel put it in front; a badge handed
over already in front is not; across a document of three sections each section's drawing is named
against its own section's panel; and two sections of one preset, whose drawings share a path, are
each named with their section.

Ten sabotages, each breaking one rule and requiring the test that claims it to fail: the overlap
geometry, the test of which box holds which, the shape's own place in the stack, the fill at the
recording point, the section a record is matched within, the note-per-section key, the guard's
note classifier, the guard's own geometry, the ledger, and the list of panels broken across
pages. Each caught on a run of exactly one test, every file byte-identical afterwards against the
copy taken in that same run.

The corpus is unchanged — no document and no part differs, so neither editor is asked to draw
anything differently; the report gains the one note. The full reactor gate passes.

A panel is written as a shaded cell, and a cell's fill belongs to the text
layer, which Word and LibreOffice both paint over everything drawn behind
that layer. A drawing standing beside such a panel — on a layer above it, not
inside it — is therefore covered wherever the two boxes meet, and a layer
stack's z-index does not lift it clear. The export said nothing about it.

Measured at 300 dpi in Word 16.0.20527 and LibreOffice 24.2 on the one
document of the fidelity corpus that holds the case, an 18pt icon laid over a
card's corner: the engine's PDF draws 4308 pixels of the icon's own ink,
Word's draws none and LibreOffice's draws none — not one pixel anywhere on
the page, the icon's whole box coming back the card's colour in both. With
the card unpainted, the one difference, Word draws every pixel the engine
does.

The export now records each shape it queues behind the text with the box it
is drawn in, and each panel whose cell it gives a fill with the box the
layout gave it, and names the drawings a panel beside them covers as
APPROXIMATED, with the path of the drawing and of the panel.

The panels are taken where the fill is written rather than read off the node
tree: whether a section, a container or a row that paints becomes a cell with
shading is decided as it is written, so a panel bearing borders alone writes
no shading and is not named, and a row written as a panel is. Each record
carries its section, since a section's pages are counted from its own first
and a page's height is its section's, and the pass runs once at the end of
the export. A shape is asked for its own place in the stack rather than the
flag of the call: a badge holding its initials is handed over already in
front with that flag saying otherwise.

DocxNeighbourFillGuardTest holds the corpus to the one drawing that is known.
It judges by the boxes the layout gave the nodes, not by behindDoc, which is
set on drawings the reader sees perfectly well — the same fixture with its
panel unpainted keeps its anchor's behindDoc="1" while showing all 3237
pixels of its shape in Word. It computes the overlaps itself and requires the
export to name nothing it does not find; what it finds and the export does
not is held in a list of its own. It names the four painted panels the layout
breaks across pages, which no overlap can be judged against, and treats a
note that says a shape was drawn and that it cannot classify as a failure
rather than a silence.

No document of the corpus changes: the note states what the bytes already
were.
…ot named

A drawing was named once per path. Each section's paths are built from its
own roots, so two sections of one preset give their drawing a single path,
and the second section's loss went unnamed. The key is the section and the
path, and the note says which section the loss is in, as a document of
several sections says of its other notes.

Three claims did not hold. A timeline's rail was said to be beyond judging
for want of a path, but a rail is drawn under the path its layout pass
carries and is judged like any other drawing. A panel composed in an
authored table cell is given no box by the layout and so is never recorded,
and a cell's paint the table draws itself never passes the queue the
drawings are recorded in: both are real edges of what the export sees and
neither was written down. The recipe, the capability matrix, the CHANGELOG
and the guard now say what is named and what is not, and the javadoc of the
pass lists all five.

The guard asked that everything the export names be found by its own scan,
and called that structural. It is not: the export records a rail, which its
note calls a line, and a picture drawn over a badge, neither of which the
scan recognises. It now checks each drawing the export names on its own
geometry, and holds the ones it cannot place — a rail has no node of the
graph to take a box from — apart from a real disagreement.
@DemchaAV
DemchaAV marked this pull request as ready for review November 1, 2026 17:01
@DemchaAV
DemchaAV merged commit 999a756 into 2.5-dev Nov 1, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-drawing-behind-a-neighbours-fill branch November 1, 2026 17:09
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