Skip to content

fix(docx): hold a canvas's height in Word, and name only what Word cannot hold - #866

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-canvas-room
Oct 7, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-canvas-room

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Why

The page gives a canvas its height whatever it holds: what follows starts below it, and a cell or a layer it ends is that tall. The DOCX export writes a canvas as its contents, one block after another, and dropped the room under them. What followed stood that much higher.

The report named this only where something followed the canvas in its flow. It said nothing in four cases:

  • a canvas that wrote something and was its row's tallest cell;
  • a canvas that ended a layer stack's column, or a timeline entry's body;
  • a canvas that was followed inside a shape container's layer, or ended one;
  • a canvas that only drew, between the blocks of a band's layer.

DocxNodeFieldLedgerTest carried CanvasLayerNode.height as the last node field marked GAP.

What changed

  • writeCanvas writes a canvas and holds its room. writeContainerChildren hands every canvas to it, since a canvas paints nothing of its own.

  • A canvas that writes something owes the room under what it writes as space below it (owePendingSpacingAfter): its height less what it writes, with the blocks stacked and their margins counted. The existing plumbing puts the space in place:

    • the next paragraph takes it as its space above, and the paragraph before a table takes it as its space below;
    • a cell the canvas ends holds it on its last paragraph, so a row is as tall as its tallest canvas and a column as long as the canvas ending it;
    • a band's own below, a resumed layer and the end of a section replace it or drop it, as they do any owed space.
  • The room is owed only where it moves something: where something follows the canvas, where the canvas ends a cell, or inside an overlay's layer, whose foot a shape container measures its space from. At the end of the body, or before a page break there, it is not owed: written below the paragraph before a break, it would stay on that page.

  • A canvas that writes nothing (empty, or only drawing) holds its whole box through holdTheSpaceOf, as a stack of drawings does in the flow. Whether it holds the box or not, nothing it holds owes more: the space owed before its drawings is put back after them. So a section in it that writes nothing owes no edges, and a canvas in it holds no room of its own.

  • It holds no room of its own where something round it measures that room already, since that would count the room twice:

    • before the first block a band's layer writes. The band sets the page's distance down to that block past the drawing (bandDepth with a pending resumeSpacing). Between a layer's blocks, and in a layer stack's column, whose resume is measured to the drawing itself, the canvas holds its box;
    • inside a stack or shape container of drawings held whole (roomHeldAround, counted in the existing drawn-only branch of writeNodeContentOf);
    • where roomMeasuredRound says something round it measures its room. That is the case where the layout lays it over another layer (a layer of a stack of several layers, of a shape container, or of a canvas), or where it stands in a block that the canvas round it writes nothing of, and so counts as taking no room. DocxLayoutMetrics.parentOf reads a node's parent from the layout.
  • writeInBand passes on what follows a timeline entry's wrapped body to the body itself, so a canvas ending the body holds its room above the next entry.

  • CanvasWrites measures what a canvas writes in one pass:

    • its stacked height on the page;
    • whether the layout placed the canvas and every block it writes;
    • whether a block is placed apart;
    • whether it writes nothing;
    • whether it holds wrapping text;
    • whether a block holds a drawing that takes no room in Word (holdsADrawingWithNoRoom, looking through sections, containers, alignments, anchors and stacks of one layer).

    A block the layout did not place makes the canvas unmeasured, rather than counting as none, so no room is owed that the page does not hold. No laid-out document in the tests or in the corpus reaches that case.

  • The report (canvasLosses) names only what Word cannot hold:

    • what the canvas writes, one block under another, running past its height, which Word makes room for and the page does not, where that moves something;
    • a drawing in what it writes, which takes no room in Word inside a canvas, so what stands below the drawing stands higher by its room;
    • a height that is not measured, in a table's composed cell or with no layout, where the canvas writes something, where something follows it, or where it ends a cell with no layout to hold the row.

    The note "its height is not held: what follows starts below what it writes" is gone, because the room is written now.

  • Ledger: CanvasLayerNode.height moves from GAP to REPORTED, naming those cases. No node field is GAP any more.

  • Docs:

    • the canvas paragraph in the DOCX recipe;
    • the layer stack row of the capability matrix;
    • the CHANGELOG, including the earlier entry that listed the old note.

Verification

  • ./mvnw -B -ntp install -pl :graph-compose-render-docx → BUILD SUCCESS, 1146 tests, 0 failures, 1 skipped (the property-gated fidelity probe).

  • DocxCanvasRoomTest (new, 26 tests) reads the Word file and compares it with the layout's placements, or with the difference two layouts make. Each Word distance is read from the space above and below each paragraph and from its exact line, and the test asserts that the two paragraphs are adjacent.

    • In the flow:
      • what follows a canvas starts the page's distance below its caption;
      • a canvas with padding and margins holds its whole box;
      • a canvas that only draws holds its box, and holds it once when what it draws sits in a section with edges of its own;
      • in a stack of one layer, a writing canvas holds its room above what follows, and a drawing canvas holds its room above the caption under it.
    • In cells and layers:
      • a row whose tallest cell is a canvas is as tall in Word as the page sets the row;
      • a column that a canvas ends is as long as the page sets the layer;
      • a drawing canvas opening a later layer of a column holds its room;
      • a painted panel that a canvas ends is as tall as the page sets the panel;
      • in a shape container's layer, a canvas followed by text and a drawing canvas ending the layer both hold their room;
      • in a band's layer, a drawing canvas between two blocks and a writing canvas followed by text both hold theirs;
      • a canvas ending a timeline entry's body moves the next entry as far as the page does: a 60pt taller canvas moves it 60pt in both;
      • a timeline's marker, alone in its cell, holds its room there.
    • Not counted twice:
      • before a band's first block, the title under a drawing canvas stands the page's distance down, once;
      • a seal laid at another canvas's corner holds no room of its own: alone, with padding, or inside a block of drawings; the outer canvas is its height in Word;
      • a drawing canvas laid over a shape container's other layers leaves them where they stand without it;
      • inside a canvas held whole, and inside a stack of drawings held whole, the room is the outer one's.
    • Before a page break, neither a drawing canvas nor a writing one writes space below the paragraph above the break or above the next page.
    • In the report:
      • a canvas whose line overruns its 10pt is named, and nothing is owed under it;
      • a drawing in what a canvas writes is named, in a section and in a stack of one layer;
      • these are named as not measured: a canvas in a composed table cell; one with no layout, writing, or drawing and followed; and a drawing canvas ending a row's cell with no layout;
      • a drawing canvas that nothing follows, with no layout, is not named.
  • DocxFlowContainerReportTest:

    • the canvas notes drop the height phrase;
    • three tests are renamed for what they check;
    • the drawing-only and composed-cell cases move to the new class.
  • The tests fail without the code they cover. 20 sabotages were run, each breaking one thing, and each made the tests that cover it fail:

    • the room under what a canvas writes: none owed; the whole height owed; owed where it moves nothing; an entry body not passing on what follows it;
    • a drawing canvas: not held; held before a band's first block; not held anywhere in a band; not held before a column's first leaf; held inside drawings held whole; a held stack not counted; held as a layer laid over another; held in a block the canvas round it counts as taking no room; what a held canvas holds owing more; one not held owing its edges;
    • the report: overflow not named; the unmeasured case not named; the unmeasured case named for a drawing canvas nothing follows; the unmeasured case not named at a cell's end with no layout; a drawing inside not named; a drawing in a stack of one layer not looked for.

    One guard breaks no test: a block of a placed canvas that the layout did not place makes the canvas unmeasured. A temporary probe across the render-docx suite and the 62 corpus documents found no such canvas, so the guard stays as fail-closed.

  • Corpus bytes: the 62 corpus documents were exported deterministically (DocxFidelityCorpusTest -Dgraphcompose.docxFidelity=export) and compared by SHA-256 against the export before the change. 54 are byte-identical. 8 differ, each only in its timeline's marker cells:

    • cv-charcoal_gold, cv-midnight_navy, cv-navy_sidebar, cv-professional_sidebar, cv-serif_headline, cv-terracotta_rail and receipt-modern: each marker is a canvas that only draws, alone in its cell, and holds its room as space above the hairline paragraph in that cell;
    • cv-violet_grid: each entry marker is a canvas holding a dot paragraph in a box the title line's height, and holds the 1.6pt under the dot.
  • Word and LibreOffice render all eight as before. Word 16.0.20430 and LibreOffice 26.8.0.3 converted the documents from before and after the change. In each editor every word and every drawing of all eight stands at the same coordinates, on the same single page.

  • The report across the corpus is unchanged: 955 notes, with the same subjects and texts.

  • Documentation guards: core -Dtest='com.demcha.documentation.**' → 166 tests, 0 failures; qa documentation guards plus DocxPageZoneTest, DocxTransparentWrapperTest, TimelineRailAcrossBackendsTest and RtlAcrossBackendsTest → 50 tests, 0 failures.

  • The full reactor gate was not run; no public signature, POM or workflow file changed.

Known limits

  • What a canvas writes past its height is named, not taken back. Word cannot make a block shorter than what it holds, and the page draws the overrun over what follows. It is named wherever the canvas's room would move something, a cell included, even one shorter than its row, where the extra room moves nothing.
  • A drawing inside what a canvas writes takes no room in Word, as drawings inside an overlay do. It is named. The space owed under the canvas does not make up for it.
  • A canvas in a table's composed cell is not measured, since nothing in such a cell has a placement. Its row is still held at the layout's height.
  • A canvas that only draws, alone in a cell, holds its room above a hairline paragraph 0.1pt tall, so a row that the marker makes tallest is 0.1pt taller.

Lane: render-docx backend, plus tests and docs.

…nnot hold

The page gives a canvas its height whatever it holds; the export wrote what it
holds one block after another and dropped the room under it. The room under what
it writes is now owed as the space below it, where it moves something, and a
canvas that only draws holds its whole box where nothing round it holds it
already. The report names what Word cannot hold: what it writes running past its
height, a drawing in what it writes that takes no room, and a height not measured.
…t an entry's end, once

A canvas that only draws held no room anywhere in a band, though the band measures
only the space above its layer's first block; it now holds its box between a
layer's blocks, and in a column, whose resume is measured to the drawing. It holds
none of its own in a block another canvas counts as taking no room, nor owes its
edges where it is not held. A timeline entry's wrapped body passes on what follows
it, so a canvas ending the body holds its room above the next entry. The report
names a canvas ending a cell with no layout, and a drawing inside a stack of one
layer.
@DemchaAV
DemchaAV merged commit df488bd into 2.5-dev Oct 7, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-canvas-room branch October 7, 2026 09:49
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