Skip to content

fix(docx): say what an export with no layout leaves to Word, and count an anchor's nodes off the document - #912

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-no-layout-ledger-docs
Oct 16, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-no-layout-ledger-docs

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Why

An export with no layout behind it — a document the engine refused to lay out, whose export
falls back to what the document states, or a bare export(graph, context) — told the caller
less than it lost, and in part the wrong thing:

  • A drawing was dropped as having "its geometry has no semantic Word analogue". A drawing stands
    where the layout puts it, and with a layout the same shape is drawn; the reason was the missing
    layout. An unpainted shape (addCircle(60)) carried that reason with a layout too, though the
    page draws nothing of it.
  • The section's note named line heights, the space between lines and auto column widths, and not
    the rest only a layout places or keeps (measured on the merged tree): a centred alignment and a
    stack's centred layer are written at the left, a SPACE_BETWEEN row as columns Word sizes, and
    no keep is written — keepNext 0 and keepLines 0 against 2 and 3 with the layout, since each
    waits for the layout to show the page held the block on one page.

Three more things went unsaid or were missing, with a layout as well:

  • With metadata set, docProps/app.xml still read <Application>Apache POI</Application>; the
    metadata's creator and producer were not written.
  • A table POI creates has no w:tblGrid, and one whose columns were left to Word kept none: a row
    with no layout, top-level and nested, a composed row the layout does not place, a chart's data
    table. CT_Tbl requires the element.
  • An anchor declared in two composed cells of one table was counted as one node: the layout
    registers both markers under the table's path. The file kept one bookmark and the report said
    nothing. An anchor in a table header row the page repeats is registered on every page the header
    is drawn on; the page's link and page reference go to the last repeat (measured: a reference
    reading "5" on a five-page table), Word's bookmark sits in the one row the file holds — on page
    1, and the field reads 1 once Word updates it (measured with Word COM). That went unsaid too.

What changed

  • Why a drawing is dropped (droppedBecause, now an instance method): a drawing that paints
    nothing — no fill, no fill paint, no stroke wider than zero (paintsNothing) — "paints nothing
    a shape could show"; with no layout, a drawing has "no layout behind the export, nothing says
    where the page draws it". The gradient and unknown-kind reasons stay first. A horizontal rule is
    still written as a rule without a layout.
  • The no-layout note (DocxWithoutLayout.note, new): the old sentence stays as it was for a
    section that holds none of the rest, so existing exports read the same. Each clause is added
    only where the section holds one: a row its arrangement or a growing spacer divides (an empty
    row writes nothing and is not counted); an alignment other than LEFT, or a stack's or a shape
    container's layer set in by its alignment or its offset, "written from the left"; and "Word is
    not told to keep what the page keeps from a page break" with the kinds the section has — a block
    kept together or with the next, a table's row in one piece, a row of columns. A block or a row
    composed in a table cell is kept by its table row, laid out or not, and is not counted; a
    repeated header row is still kept with the row under it, which needs no layout. The keeps stay
    unwritten, as before (withoutALayoutNothingIsKnownToBeOnOnePage): only the layout knows a
    block fits on a page, and a document it refused may hold a row taller than one, which no page
    holds whole however Word is told to keep it.
  • Metadata (applyMetadata): the creator is the extended Application (the producer where no
    creator is set), as the PPTX backend writes it. A Word file has no producer field; a producer
    the caller set, other than the application written, is named (DROPPED, producer). The
    producer every DocumentMetadata starts with is not: a session setting only its creator would
    have been told on every export.
  • Grid (withAGrid, at the two places tables are made): every table starts with an empty
    w:tblGrid, which the column writers fill as before, between w:tblPr and the rows. Measured in
    Word and LibreOffice, word by word: a top-level and a nested SPACE_BETWEEN row, and a table of
    auto columns with and without a width, each with no grid and with an empty one, stand the same in
    both editors (equal gridCols moved LibreOffice's by 0.05pt); so do the committed
    WordExportCompanion file and its re-render.
  • Anchor counting (DocxAnchorDeclarations, new): nodes are counted off the document — each
    node declaring an anchor, read off a table of the kinds that declare one (DECLARING, exact
    classes: node records are final), through the content a table's cells are built from — and the count is
    the larger of that and the paths the layout registers the anchor under
    (DocxLayoutMetrics.anchorPaths), which a node of the caller's own kind declares through and
    the document cannot tell. DocxLayoutMetrics.anchorsDrawnAgain gives the anchors one path
    registers on more than one page. An anchor one node declares, in a repeated header row, drawn on
    two pages or more and bookmarked, is named.

Verification

  • ./mvnw -B -ntp test -f render-docx/pom.xml — BUILD SUCCESS, render-docx 1720 tests (+14):
    DocxWithoutLayoutTest (new, 8: the note unchanged for paragraphs; an arranged and a grown row;
    an alignment, a stack's centred layer, an offset one, a shape container's, and left ones not
    named; each keep kind, all three together, an empty row; a row and a block composed in a cell
    not named; each drawing's reason without a layout, a filled path and a paint fill included, a
    rule not dropped; an unpainted circle, path and line and a zero-width stroke; an empty grid
    top-level and nested, in schema order), DocxSemanticBackendTest +2 (the creator as
    Application, a producer named; the default, a producer standing alone and a creator set alone
    not named), DocxLinkTest +2 (two cells of one table, two paragraphs and a paragraph with a
    drawn circle; a repeated header — direct, inside a block, on one page, a drawn circle),
    DocxReportedLossesTest +1 (an anchor a node of the caller's own kind declares),
    DocxAnchorDeclarationsTest (new: DECLARING holds every node kind GraphCompose ships with an
    anchor component but the page reference, read off the class scan the field ledger uses).
  • 36 sabotages, each undoing one part — the cell walk, the header rows and the flag carried into
    them, a header drawn once, the bookmark condition, the repeat note, the document count, the
    layout's paths, each drop reason, the zero-width stroke, each paint field, the old note, each
    clause and its conditions (stacks, shape containers, offsets, left layers, cells, empty rows),
    the Application, the producer's fallback, its note both ways and the default producer, the
    grid in the body and in a cell, a kind left out of DECLARING (two) — all caught.
  • Corpus: 62/62 DOCX byte-identical, docProps included; report notes 1244 → 1244 (no corpus
    document is exported without a layout, sets metadata or declares an anchor twice).
  • WordExportCompanion's committed DOCX is re-rendered: its Application now reads GraphCompose,
    and its chart's data table has an empty grid. Converted with Word and LibreOffice, the old and
    the new file put every word (186 and 183) at the same place.
  • ./mvnw -B -ntp clean verify — BUILD SUCCESS (core 821, render-docx 1720, examples 93, qa 1820);
    the knowledge pack's --check, stability-doc, claims and routes guards pass.

Lane: render-docx (semantic backend) — report and package metadata; no layout change.

… creating application, and count an anchor's nodes off the document

Without the layout behind the export a drawing was dropped as having
"no semantic Word analogue"; a drawing stands where the layout puts it,
and the reason is now the layout it lacks. A drawing that paints
nothing, with a layout or without, is said to paint nothing.

The section's note named line heights, the space between lines and auto
columns, and none of the rest only a layout places or keeps. It names,
where the section holds them, a row its arrangement or a growing spacer
divides, what an alignment or a stack's layer is set at across the
width, written from the left, and the keeps Word is not told: a block
kept together or with the next, a table's row in one piece, a row of
columns.

Where the session states metadata, its creator is written as the
package's Application, which read "Apache POI"; a Word file has no
producer, and one of the caller's own other than the application is
named.

A table POI makes has no w:tblGrid, and one whose columns were left to
Word kept none, which a Word table may not be without. Every table now
starts with an empty grid; Word and LibreOffice lay a table with an
empty grid out as one without.

An anchor two cells of one table declared was counted as one node, the
layout registering both under the table: nodes are counted off the
document now, and a node of the caller's own kind, which only the
layout tells of, still counts. An anchor in a table's header row the
page repeats goes, on the page, to the last repeat, and in Word to the
one row the file holds, where the table starts; the report names it.
@DemchaAV
DemchaAV marked this pull request as draft October 16, 2026 23:14
… one

The kinds a node's anchor is read from were a chain of nine type tests.
They are a table of exact classes now, node records being final, and a
test holds it to every node kind GraphCompose ships with an anchor but
the page reference, which names one and declares none: a kind added
with an anchor of its own fails the test rather than going uncounted.

The changelog and the recipe say where a node of the caller's own kind
is counted.
@DemchaAV
DemchaAV marked this pull request as ready for review October 16, 2026 23:25
@DemchaAV
DemchaAV merged commit fd0c635 into 2.5-dev Oct 16, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-no-layout-ledger-docs branch October 16, 2026 23:47
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