From 2560bc537feb539a202e8b2edfb1eb06d6968a63 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Tue, 27 Oct 2026 12:26:18 +0000 Subject: [PATCH 1/4] fix(docx): anchor a drawing in a paragraph of the block it belongs to A drawing stood in the paragraph whose text was nearest it, within 48pt and rising above it by 24pt at most, by a stroke's width in a table cell, and otherwise was placed from the page's edges, where it stayed while the text above it was edited: an icon over a card's corner, a sidebar's portrait, a skill's meter in a row, a timeline's dots in a column of their own. A drawing a node paints is now anchored in the first paragraph able to carry it that the nearest block holding it writes on its page, the page's flow left out (DocxLayoutMetrics.holdersOf), and placed from that paragraph's top however far off it stands. In a table cell the cell of that paragraph's row holding it across takes it, else the paragraph's own cell keeps it out of its column, unless it holds text, which LibreOffice 26.8 pulls back into the column. A cell set to the middle or the bottom carries none: LibreOffice 24.2 sets such a cell's text from its top once it carries a drawing. A drawing no holder carries goes to the paragraph nearest it on its page. What a table's composed cell draws keeps the nearest-text rule. The report's notes say how each drawing is anchored; the recipe, the capability matrix and the CHANGELOG describe the rule; the Word export companion preview is re-rendered, its badge now in the paragraph above it. --- CHANGELOG.md | 31 ++ .../examples/word-export-companion.docx | Bin 10247 -> 10268 bytes .../architecture/backend-capability-matrix.md | 4 +- docs/recipes/docx-export.md | 85 ++-- .../semantic/docx/DocxDrawingAnchors.java | 231 +++++++-- .../semantic/docx/DocxLayoutMetrics.java | 30 ++ .../semantic/docx/DocxSemanticBackend.java | 28 +- .../semantic/docx/DocxCellDrawingTest.java | 17 +- .../semantic/docx/DocxDrawingAnchorsTest.java | 90 ++-- .../semantic/docx/DocxDrawingHolderTest.java | 175 +++++++ .../docx/DocxDrawingOwnerAnchorsTest.java | 457 ++++++++++++++++++ .../semantic/docx/DocxDrawingsTest.java | 27 +- 12 files changed, 1041 insertions(+), 134 deletions(-) create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDrawingHolderTest.java create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDrawingOwnerAnchorsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5746c8590..15197e437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,37 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX export anchors a drawing in a paragraph of the block it belongs to, so it moves with + that block when the text above is edited.** + - A shape stood in the paragraph whose text was nearest it, within 48pt, rising above that + paragraph's top by 24pt at most — in a table cell by a stroke's width —, and otherwise was + placed from the page's edges, where it stayed while the text moved away from it: an icon laid + over a card's corner, a sidebar's portrait, a skill's meter beside its label in a row, a + timeline's dots in a column of their own. + - A drawing a node paints is now anchored in the first paragraph able to carry it that the + nearest block holding it writes on its page — the node itself, a layer stack, a panel, a row, + but not the page's flow —, placed from that paragraph's top however far from its text it + stands. In a table cell, the cell of that paragraph's row holding the drawing across takes it, + its own where it stands in a column of its own; another cell keeps it placed out of its + column, unless it holds text — a text box, a badge's letters —, which LibreOffice 26.8 pulls + back into the column. A cell set to the middle or the bottom carries none: the block's next + paragraph able to place it takes the drawing, or else the next block up. A drawing no node + paints — a timeline's rail — and one whose blocks write no such paragraph on its page go to + the paragraph nearest it there. What a table's composed cell draws keeps the nearest-text + rule. The report's note on each drawing says how it is anchored. + - Measured in Word 16.0.20527 and LibreOffice 24.2: a shape anchored in a table cell's + paragraph stands where both say — rising 36pt above its row, hanging below its table, out of + its cell, taller than its row, in an exact row or a nested table, in front of the text or + behind it, and across the fidelity corpus from 155pt above its paragraph to 620pt below —; + neither clips it nor moves a line for it, and both move it with its row when the text above + is edited. LibreOffice 24.2 sets a cell carrying a drawing from its top whatever its vertical + alignment says, 16pt high in a cell set to the middle and 32pt in one set to the bottom. + - Of the 465 drawings the fidelity corpus placed on the page, 270 were drawn by a node, 5 were + timelines' rails and 190 were composed in table cells. All but 14 of the first 275 are now + anchored in a paragraph: the text boxes of `LumaStudio`'s sidebar and closing note, which no + paragraph of their column holds across, and two drawings on pages whose only text is a pair + of lines. + - **A DOCX export writes a painted box holding text in the body as a panel, its text inside its outline.** - A shape container shaped as a rectangle or a rounded rectangle, filled or outlined, holding diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx index 808a3ef6ffbd321c946a580a758636f778c774c9..f804b8074dd6bce66a45b5022b31f4c80d11ae3d 100644 GIT binary patch delta 3616 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 delta 3556 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