From 3d48a56c9e3143e38c766db6d56f4b90391b5be7 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Mon, 26 Oct 2026 17:59:12 +0000
Subject: [PATCH 1/3] test(qa): hold three body shape containers in the DOCX
fidelity corpus
In the body, the DOCX export draws a shape container's outline as a shape of
its size and writes its layers in the flow, held in from the edges of the
block the container stands in. Three documents built for that join the
synthetic family of the fidelity corpus: a note the page wraps inside a box
220pt wide, a clipped box holding more text than it has room for, and a card
its text fills.
Their baseline rows record the export as Word 16 and LibreOffice set it
today: the note and the clipped text at other words than the page's, past
the outline, and the paragraph under each box 12 and 20pt high; the card in
place. A change that writes the text inside the outline finds it, moves the
paragraph under each box nearer the page and rewrites the rows; the card
holds such a change to keep it where it stands. The gate reads lines of
text, so it does not see whether the text stays inside the outline, or
whether the card grows when text is added to it; the corpus class and the
CHANGELOG say so.
---
CHANGELOG.md | 34 +++++++
CONTRIBUTING.md | 4 +-
docs/recipes/docx-export.md | 25 ++++--
.../fidelity/SyntheticDocxCorpus.java | 89 ++++++++++++++++++-
.../libreoffice-windows-lines.tsv | 14 +++
.../docx-fidelity/libreoffice-windows.tsv | 4 +
.../docx-fidelity/word-windows-lines.tsv | 14 +++
.../resources/docx-fidelity/word-windows.tsv | 4 +
8 files changed, 178 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c0493353..7dca1033f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4269,6 +4269,40 @@ follow semantic versioning; release dates are ISO 8601.
### Tests
+- **The DOCX fidelity corpus holds three shape containers in the body, as the export writes them
+ today.**
+ - Three documents join `SyntheticDocxCorpus`, each a rounded shape container holding text,
+ with a paragraph under it:
+ - `narrow_container`: a note centred in a box 220pt wide, which the page wraps over three
+ lines inside it;
+ - `clipped_container`: a box 60pt tall, clipped to its outline, holding 84pt of text, which
+ the page cuts at the box's foot;
+ - `rounded_card`: a card as wide as the page between its margins, its heading and text
+ filling it.
+ - Measured in Word 16 and in LibreOffice 24.2 and 26.8, which set the three alike. The export
+ draws each outline at its size, where the page draws it, and writes its text in the flow,
+ held in from the edges of the block the container stands in rather than from the outline's:
+ - the narrow box's note runs over two lines instead of three, up to 95pt past the box's right
+ edge, and the paragraph under the box stands 12pt high, its first line reaching 4pt into
+ the box;
+ - the clipped box's text is written whole, none of it cut, up to 124pt past the box, and the
+ paragraph under it stands 20pt high, inside the box;
+ - the card stands where the page sets it, every line within 0.1pt. Text added to it in Word
+ runs past its foot: the outline keeps its height, the card's text runs over seven lines to
+ 22pt below it, and the paragraph under it moves down 36pt.
+ - The baselines record the three as they stand. Of the narrow box and the clipped box, the
+ gate holds the lines the editors set at the page's words — the paragraph above the box, and
+ the paragraph under it, 12 and 20pt high — and none of the text inside the box, which they
+ set at other words. A change that writes a container's text inside its outline finds that
+ text and moves the paragraph under each box nearer the page, and rewrites their rows. The
+ card, which the export sets where the page does today, holds such a change to keep it there.
+ - The gate measures lines of text, not outlines. It does not see whether the text stays inside
+ the outline, or whether the card grows when text is added to it. Word's editing protocol
+ (`edit-protocol-corpus.ps1`) does not either: it passes all three, since the paragraph it
+ lengthens grows and still ends above the paragraph under it, past the outline as it is.
+ - Each baseline takes the three documents' rows alone, from the run of its editor — Linux's
+ from CI's.
+
- **Every page reference a DOCX export writes names a bookmark the file holds.**
- `DocxPageReferenceGuardTest` reads each `PAGEREF` field, bookmark and paragraph from every XML
part of the exported file — the body, its headers and footers, nested tables and text boxes.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 20a926b57..abf39aacf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -357,7 +357,9 @@ Choose the smallest tests that match the change:
`-Dgraphcompose.docxFidelity.update=true`, and commits it with the change: its diff shows
which documents moved. A new preset joins the corpus in its family's `*DocxCorpus` class,
and so does a preset on a longer fixture; a shape no preset has — a page break inside a
- panel, a break in a row's column — goes in `SyntheticDocxCorpus`.
+ panel, a break in a row's column, a shape container narrower than the block it stands in
+ or clipped short of its text — goes in `SyntheticDocxCorpus`. Its baseline holds it as the editors set it, a defect of the export
+ included, and the change fixing that rewrites its rows nearer the page.
CI runs it on Linux (the `DOCX Fidelity` job) against `libreoffice-linux.tsv`, and uploads
what it measured: LibreOffice sets text a little differently per platform, so a change that
moves documents nearer the page takes that artifact's files as the Linux baseline, and its own
diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md
index 925b72e5a..0375ed144 100644
--- a/docs/recipes/docx-export.md
+++ b/docs/recipes/docx-export.md
@@ -308,7 +308,9 @@ paragraph is the picture's block, and a list's edges go to the paragraphs around
`itemSpacing` as the gap above each item after the first. A list's left and right edges —
its margin and padding — indent its items, the level's own indent added on top, so its
markers start where the page starts them. A paragraph's and a list's sides hold under a
-layer stack or a shape container too, inside the box that places the layer. In a cell, two
+layer stack or a shape container too, inside the box that places the layer — but for a
+shape container narrower than the block it stands in, whose layers run past its right
+edge (see "Shape containers → inline layers"). In a cell, two
points of a side stay the editor's, to set a line a little wider than the page: off the
side a left-to-right line does not lean on, so it keeps the edge it is set from — the right
of a left-aligned paragraph and of a list, the left of a right-aligned one, both of a
@@ -1103,9 +1105,13 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
margin opening a band stood 20pt high, and one in a later layer 7pt. A badge — a ring,
drawn, with initials centred
in it, over a spacer that keeps its place in the flow — writes its initials where
- the ring centres them and keeps its height below them. Each layer, here and in a
- shape container, is held in across to its box in the stack, with a few points to
- spare on the side its text does not lean on. A block in `addAligned(...)` is held in
+ the ring centres them and keeps its height below them. Each layer is held in across
+ to its box in the stack, with a few points to spare on the side its text does not
+ lean on. A shape container's layers are held in by as far as their box stands in
+ from the container's edges, but from the edges of the block the container stands
+ in, set in by its margin and padding: in a container narrower than that block they
+ run as much further right, past its outline (see "Shape containers → inline
+ layers"). A block in `addAligned(...)` is held in
the same way, to where the alignment puts it — composed in a table cell, a paragraph
where the lines its table laid out for it stand and a picture where the alignment sets
one of its written width; any other block there, or one inside a panel or another table
@@ -1162,7 +1168,16 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
- **Shape containers → inline layers.** DOCX has no portable equivalent of
a graphics-state path clip, so the container's layers are written
inline, in source order, without clipping — again with one warning per
- export, and a note on each container that writes a layer in the flow. A
+ export, and a note on each container that writes a layer in the flow.
+ The outline is drawn at its size and does not grow with what is written
+ in the flow, and the layers take the room of the flow, not of the
+ outline. Measured in Word and LibreOffice: a note the page wraps over
+ three lines inside a box 220pt wide runs over two, up to 95pt past the
+ box; a clipped box's text is written whole, past its outline; the block
+ under a container stands where the flow puts it, over the outline where
+ its layers are written shorter than the page sets them — 12pt high under
+ the narrow box, 20pt under the clipped one. In Word, text added to a card
+ runs past its foot, the outline keeping its height. A
container whose layers are all drawn — a tile, an icon, a badge holding
its glyph — writes none: each layer is drawn where the page draws it, in
the order the page stacks them, by their z-index. Word stacks drawings by
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java
index 312790222..e22b1ab73 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java
@@ -4,9 +4,12 @@
import com.demcha.compose.document.api.PageMarginRule;
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.dsl.SectionBuilder;
+import com.demcha.compose.document.node.ParagraphNode;
+import com.demcha.compose.document.style.ClipPolicy;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentStroke;
+import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.table.DocumentTableColumn;
import com.demcha.compose.document.table.DocumentTableStyle;
@@ -19,15 +22,28 @@
* Documents of the DOCX fidelity corpus built for a shape no template preset has on its own
* fixture, so that an editor setting one off the page fails the gate as a preset would.
*
- * Each sets a way the page breaks inside a panel or a column: a page break among a panel's
+ *
Three set a way the page breaks inside a panel or a column: a page break among a panel's
* children and inside a panel nested in it, page breaks the page sets as boxes of no height in a
* row's column and in a stack's layer, and a card whose table the layout runs over three pages,
- * with other margins from the second. Their text is set in a face the export embeds, so both
- * editors break its lines at the page's words.
+ * with other margins from the second. Three others set a shape container holding text in the
+ * body: a box narrower than the page, a box clipped to its outline, and a card. Their text is
+ * set in a face the export embeds, so both editors break at the page's words the lines they
+ * set at the page's width.
+ *
+ * The baselines hold the shape containers as the export writes them now, with their text
+ * past the outline: Word and LibreOffice set the narrow box's note and the clipped box's text
+ * wider than the box, at other words, which the gate therefore does not hold, and the paragraph
+ * under each box 12 and 20pt high. A change that writes the text inside the outline finds it,
+ * moves the paragraph under each box nearer the page, and rewrites their rows. The card, which
+ * the export sets where the page does, holds such a change to keep it there. The gate reads
+ * lines of text only: whether the text stays inside the outline, and whether the card grows
+ * when text is added to it in an editor, it does not see.
*/
final class SyntheticDocxCorpus {
private static final DocumentTextStyle TEXT = DocumentTextStyle.builder().fontName(FontName.LATO).size(10).build();
+ private static final DocumentTextStyle HEADING = DocumentTextStyle.builder().fontName(FontName.LATO).size(12)
+ .decoration(DocumentTextDecoration.BOLD).build();
private static final DocumentTableStyle CELLS = DocumentTableStyle.builder().textStyle(TEXT).build();
private static final DocumentColor SURFACE = DocumentColor.rgb(238, 243, 249);
private static final DocumentColor INNER = DocumentColor.rgb(224, 233, 244);
@@ -42,7 +58,68 @@ static List documents() {
return List.of(
synthetic("break_in_panel", SyntheticDocxCorpus::breakInAPanel),
synthetic("break_in_column", SyntheticDocxCorpus::breakInAColumn),
- synthetic("card_over_pages", SyntheticDocxCorpus::cardOverPages));
+ synthetic("card_over_pages", SyntheticDocxCorpus::cardOverPages),
+ synthetic("narrow_container", SyntheticDocxCorpus::narrowContainer),
+ synthetic("clipped_container", SyntheticDocxCorpus::clippedContainer),
+ synthetic("rounded_card", SyntheticDocxCorpus::roundedCard));
+ }
+
+ /**
+ * A rounded box narrower than the page, its note centred in it and wrapped over three lines
+ * inside it, and a paragraph under the box.
+ */
+ private static void narrowContainer(DocumentSession session) {
+ session.pageSize(400, 500).margin(MARGIN);
+ session.pageFlow(page -> page.spacing(8)
+ .addParagraph(p -> text(p, "Before the box, on a line of its own."))
+ .addContainer(box -> box.roundedRect(220, 90, 10).fillColor(SURFACE).stroke(DocumentStroke.of(INK, 1))
+ .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
+ .center(paragraph("A note centred in a box narrower than the page: the page wraps "
+ + "it over three lines inside the box, each line held within "
+ + "its outline.")))
+ .addParagraph(p -> text(p, "After the box: this paragraph stands under it on the page, "
+ + "and none of its lines runs over the box.")));
+ }
+
+ /**
+ * A rounded box clipped to its outline, holding more text than it has room for: the page
+ * centres the text across the box, sets it from the box's top, as it is taller than the box,
+ * and cuts the lines that run past its foot. A paragraph stands under the box.
+ */
+ private static void clippedContainer(DocumentSession session) {
+ session.pageSize(400, 500).margin(MARGIN);
+ session.pageFlow(page -> page.spacing(8)
+ .addParagraph(p -> text(p, "Before the clipped box, on a line of its own."))
+ .addContainer(box -> box.roundedRect(200, 60, 12).fillColor(SURFACE).stroke(DocumentStroke.of(INK, 1))
+ .clipPolicy(ClipPolicy.CLIP_PATH)
+ .center(paragraph("Clipped text in a rounded box too short to hold it. The page "
+ + "sets it from the top of the box and cuts it at the outline, so "
+ + "the lines that run past the foot of the box are not seen. Only "
+ + "the lines the box holds are read, and the box stays as tall as "
+ + "it was made, whatever it is given to hold.")))
+ .addParagraph(p -> text(p, "After the clipped box: this paragraph stands under it on the page, "
+ + "and none of its lines runs over the box.")));
+ }
+
+ /**
+ * A rounded card as wide as the page between its margins, its heading and text set from its
+ * top left corner and filling it, under its default clip, with a paragraph under it. Text
+ * added to the card in an editor needs it taller.
+ */
+ private static void roundedCard(DocumentSession session) {
+ session.pageSize(400, 500).margin(MARGIN);
+ session.pageFlow(page -> page.spacing(8)
+ .addParagraph(p -> text(p, "Before the card, on a line of its own."))
+ .addContainer(card -> card.roundedRect(328, 80, 12).fillColor(SURFACE).stroke(DocumentStroke.of(INK, 1))
+ .topLeft(new SectionBuilder().padding(DocumentInsets.of(12)).spacing(4)
+ .addParagraph(p -> p.text("A rounded card").textStyle(HEADING))
+ .addParagraph(p -> text(p, "Its text runs over three lines from the card's top "
+ + "left corner, and the card is just tall enough to hold "
+ + "them with its padding round them: a line more, and "
+ + "the card needs to be taller."))
+ .build()))
+ .addParagraph(p -> text(p, "After the card: this paragraph stands under it on the page, "
+ + "and none of its lines runs over the card.")));
}
/**
@@ -123,6 +200,10 @@ private static ParagraphBuilder text(ParagraphBuilder paragraph, String text) {
return paragraph.text(text).textStyle(TEXT);
}
+ private static ParagraphNode paragraph(String text) {
+ return text(new ParagraphBuilder(), text).build();
+ }
+
private static DocxCorpusDocument synthetic(String name, Consumer compose) {
return new DocxCorpusDocument("synthetic", name, -1, compose);
}
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
index 5daddd68d..4c367377a 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
@@ -1,5 +1,6 @@
# libreoffice build bf6ef2662401 on windows — document, line, drift below the page (pt), drift right of the page (pt), letters
# rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: libreoffice build bf6ef2662401 on windows
+# rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: libreoffice build bf6ef2662401 on windows
cv-blue_banner 0:8f1010120ed6 0.02 1.95 jordanrivera
cv-blue_banner 0:8d7a2cca3060 0.00 0.24 +442055551000|jordan.rivera@exam
cv-blue_banner 0:b944906ed84c -0.20 -0.38 professionalsummary
@@ -5016,3 +5017,16 @@ synthetic-card_over_pages 2:1c49f22f6de9 1.08 0.60 413
synthetic-card_over_pages 2:f69c59b028ea 1.08 0.60 lineitem60
synthetic-card_over_pages 2:db55da3fc309 1.08 0.60 420
synthetic-card_over_pages 2:8c569b972e01 1.08 0.10 afterthecard,onitslastpage.
+synthetic-clipped_container 0:cbe6ee14976b -0.02 0.10 beforetheclippedbox,onalineofits
+synthetic-clipped_container 0:2633ce73dc24 -20.02 0.10 aftertheclippedbox:thisparagraph
+synthetic-clipped_container 0:667f8b3b3eff -20.02 0.10 noneofitslinesrunsoverthebox.
+synthetic-narrow_container 0:fa13cab3fe73 -0.02 0.10 beforethebox,onalineofitsown.
+synthetic-narrow_container 0:697e6629574d -12.02 0.10 afterthebox:thisparagraphstandsu
+synthetic-narrow_container 0:0efb534ce6c6 -12.02 0.10 linesrunsoverthebox.
+synthetic-rounded_card 0:2a3c79dd9054 -0.02 0.10 beforethecard,onalineofitsown.
+synthetic-rounded_card 0:b8608d77afb0 0.01 0.10 aroundedcard
+synthetic-rounded_card 0:bcb0465dee5d -0.02 0.10 itstextrunsoverthreelinesfromthe
+synthetic-rounded_card 0:73c347391d3b -0.02 0.10 cardisjusttallenoughtoholdthemwi
+synthetic-rounded_card 0:8993a52a9026 -0.02 0.10 linemore,andthecardneedstobetall
+synthetic-rounded_card 0:103d88aab213 -0.02 0.10 afterthecard:thisparagraphstands
+synthetic-rounded_card 0:66b11b97a20e -0.02 0.10 linesrunsoverthecard.
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
index 61faf3cd6..3c6facd2e 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
@@ -1,6 +1,7 @@
# libreoffice build bf6ef2662401 on windows
document engine_pages editor_pages lines matched median p90 over2
# rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: libreoffice build bf6ef2662401 on windows
+# rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: libreoffice build bf6ef2662401 on windows
cv-blue_banner 2 2 54 52 0.16 0.25 0
cv-boxed_sections 2 2 58 58 0.06 0.11 0
cv-centered_headline 2 2 58 56 0.12 0.29 0
@@ -68,3 +69,6 @@ rota-cobalt 1 1 154 151 0.03 0.19 0
synthetic-break_in_column 1 1 7 7 0.02 0.02 0
synthetic-break_in_panel 3 3 8 8 3.02 5.02 5
synthetic-card_over_pages 3 3 114 114 0.98 1.08 0
+synthetic-clipped_container 1 1 10 3 20.02 20.02 2
+synthetic-narrow_container 1 1 6 3 12.02 12.02 2
+synthetic-rounded_card 1 1 7 7 0.02 0.02 0
diff --git a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
index b6aef8192..9309fe866 100644
--- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
@@ -1,5 +1,6 @@
# word 16.0 (16.0.20430) on windows — document, line, drift below the page (pt), drift right of the page (pt), letters
# rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: word 16.0 (16.0.20527) on windows
+# rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: word 16.0 (16.0.20527) on windows
cv-blue_banner 0:8f1010120ed6 -0.02 -0.08 jordanrivera
cv-blue_banner 0:8d7a2cca3060 0.00 -0.07 +442055551000|jordan.rivera@exam
cv-blue_banner 0:b944906ed84c -0.21 -1.21 professionalsummary
@@ -5136,3 +5137,16 @@ synthetic-card_over_pages 2:1c49f22f6de9 1.12 -36.37 413
synthetic-card_over_pages 2:f69c59b028ea 1.16 0.58 lineitem60
synthetic-card_over_pages 2:db55da3fc309 1.16 -36.37 420
synthetic-card_over_pages 2:8c569b972e01 1.11 0.00 afterthecard,onitslastpage.
+synthetic-clipped_container 0:cbe6ee14976b -0.01 0.02 beforetheclippedbox,onalineofits
+synthetic-clipped_container 0:2633ce73dc24 -19.97 0.02 aftertheclippedbox:thisparagraph
+synthetic-clipped_container 0:667f8b3b3eff -19.97 0.02 noneofitslinesrunsoverthebox.
+synthetic-narrow_container 0:fa13cab3fe73 -0.01 0.02 beforethebox,onalineofitsown.
+synthetic-narrow_container 0:697e6629574d -12.02 0.02 afterthebox:thisparagraphstandsu
+synthetic-narrow_container 0:0efb534ce6c6 -12.02 0.02 linesrunsoverthebox.
+synthetic-rounded_card 0:2a3c79dd9054 -0.01 0.02 beforethecard,onalineofitsown.
+synthetic-rounded_card 0:b8608d77afb0 0.10 0.02 aroundedcard
+synthetic-rounded_card 0:bcb0465dee5d -0.01 0.02 itstextrunsoverthreelinesfromthe
+synthetic-rounded_card 0:73c347391d3b -0.01 0.02 cardisjusttallenoughtoholdthemwi
+synthetic-rounded_card 0:8993a52a9026 -0.01 0.02 linemore,andthecardneedstobetall
+synthetic-rounded_card 0:103d88aab213 0.02 0.02 afterthecard:thisparagraphstands
+synthetic-rounded_card 0:66b11b97a20e 0.02 0.02 linesrunsoverthecard.
diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv
index 21232840a..526ad56ed 100644
--- a/qa/src/test/resources/docx-fidelity/word-windows.tsv
+++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv
@@ -1,6 +1,7 @@
# word 16.0 (16.0.20430) on windows
document engine_pages editor_pages lines matched median p90 over2
# rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: word 16.0 (16.0.20527) on windows
+# rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: word 16.0 (16.0.20527) on windows
cv-blue_banner 2 2 54 52 0.10 0.23 0
cv-boxed_sections 2 2 58 58 0.12 0.21 0
cv-centered_headline 2 2 58 56 0.12 0.25 0
@@ -68,3 +69,6 @@ rota-cobalt 1 1 154 154 0.14 0.26 0
synthetic-break_in_column 1 1 7 7 0.03 0.03 0
synthetic-break_in_panel 3 3 8 8 0.95 0.99 0
synthetic-card_over_pages 3 3 114 114 1.03 1.15 0
+synthetic-clipped_container 1 1 10 3 19.97 19.97 2
+synthetic-narrow_container 1 1 6 3 12.02 12.02 2
+synthetic-rounded_card 1 1 7 7 0.01 0.02 0
From e145464fb3ad823b7afc1f177b88dfe456e8da9e Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Mon, 26 Oct 2026 18:13:21 +0000
Subject: [PATCH 2/3] docs(docx): say how a narrow shape container's room runs
past its outline
A shape container's layers are held in from the edges of the block it
stands in, so in a container narrower than that block the right edge of
their room runs past the outline: wrapped text runs past it, and centred or
right-aligned text stands right of where the page sets it. The recipe and
the CHANGELOG now say so, the passage on a paragraph's sides under a layer
names the exception, and the fixtures' Javadoc names the paragraph above
each box and the note's alignment as they are.
---
CHANGELOG.md | 9 +++++----
CONTRIBUTING.md | 5 +++--
docs/recipes/docx-export.md | 13 +++++++------
.../templates/fidelity/SyntheticDocxCorpus.java | 11 ++++++-----
4 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7dca1033f..32975b74c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4272,16 +4272,17 @@ follow semantic versioning; release dates are ISO 8601.
- **The DOCX fidelity corpus holds three shape containers in the body, as the export writes them
today.**
- Three documents join `SyntheticDocxCorpus`, each a rounded shape container holding text,
- with a paragraph under it:
- - `narrow_container`: a note centred in a box 220pt wide, which the page wraps over three
- lines inside it;
+ between a paragraph above it and one under it:
+ - `narrow_container`: a note set left in a block centred in a box 220pt wide, which the page
+ wraps over three lines inside it;
- `clipped_container`: a box 60pt tall, clipped to its outline, holding 84pt of text, which
the page cuts at the box's foot;
- `rounded_card`: a card as wide as the page between its margins, its heading and text
filling it.
- Measured in Word 16 and in LibreOffice 24.2 and 26.8, which set the three alike. The export
draws each outline at its size, where the page draws it, and writes its text in the flow,
- held in from the edges of the block the container stands in rather than from the outline's:
+ held in from the edges of the block the container stands in rather than from the outline's,
+ so the right edge of its room stands past the outline:
- the narrow box's note runs over two lines instead of three, up to 95pt past the box's right
edge, and the paragraph under the box stands 12pt high, its first line reaching 4pt into
the box;
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index abf39aacf..9db84b2da 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -358,8 +358,9 @@ Choose the smallest tests that match the change:
which documents moved. A new preset joins the corpus in its family's `*DocxCorpus` class,
and so does a preset on a longer fixture; a shape no preset has — a page break inside a
panel, a break in a row's column, a shape container narrower than the block it stands in
- or clipped short of its text — goes in `SyntheticDocxCorpus`. Its baseline holds it as the editors set it, a defect of the export
- included, and the change fixing that rewrites its rows nearer the page.
+ or clipped short of its text — goes in `SyntheticDocxCorpus`. Its baseline holds it as
+ the editors set it, a defect of the export included, and the change fixing that rewrites
+ its rows nearer the page.
CI runs it on Linux (the `DOCX Fidelity` job) against `libreoffice-linux.tsv`, and uploads
what it measured: LibreOffice sets text a little differently per platform, so a change that
moves documents nearer the page takes that artifact's files as the Linux baseline, and its own
diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md
index 0375ed144..9d0408a70 100644
--- a/docs/recipes/docx-export.md
+++ b/docs/recipes/docx-export.md
@@ -308,9 +308,9 @@ paragraph is the picture's block, and a list's edges go to the paragraphs around
`itemSpacing` as the gap above each item after the first. A list's left and right edges —
its margin and padding — indent its items, the level's own indent added on top, so its
markers start where the page starts them. A paragraph's and a list's sides hold under a
-layer stack or a shape container too, inside the box that places the layer — but for a
-shape container narrower than the block it stands in, whose layers run past its right
-edge (see "Shape containers → inline layers"). In a cell, two
+layer stack or a shape container too, inside the box that places the layer — but not in a
+shape container narrower than the block it stands in, whose layers' room runs past its
+right edge (see "Shape containers → inline layers"). In a cell, two
points of a side stay the editor's, to set a line a little wider than the page: off the
side a left-to-right line does not lean on, so it keeps the edge it is set from — the right
of a left-aligned paragraph and of a list, the left of a right-aligned one, both of a
@@ -1109,9 +1109,10 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
to its box in the stack, with a few points to spare on the side its text does not
lean on. A shape container's layers are held in by as far as their box stands in
from the container's edges, but from the edges of the block the container stands
- in, set in by its margin and padding: in a container narrower than that block they
- run as much further right, past its outline (see "Shape containers → inline
- layers"). A block in `addAligned(...)` is held in
+ in, set in by its margin and padding: in a container narrower than that block the
+ right edge of their room runs that much further right, past its outline, so text
+ that wraps runs past the outline and centred or right-aligned text stands right of
+ where the page sets it (see "Shape containers → inline layers"). A block in `addAligned(...)` is held in
the same way, to where the alignment puts it — composed in a table cell, a paragraph
where the lines its table laid out for it stand and a picture where the alignment sets
one of its written width; any other block there, or one inside a panel or another table
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java
index e22b1ab73..c40fc9dc7 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/SyntheticDocxCorpus.java
@@ -65,8 +65,8 @@ static List documents() {
}
/**
- * A rounded box narrower than the page, its note centred in it and wrapped over three lines
- * inside it, and a paragraph under the box.
+ * A rounded box narrower than the page between a paragraph above it and one under it, its
+ * note set left in a block centred in the box and wrapped over three lines inside it.
*/
private static void narrowContainer(DocumentSession session) {
session.pageSize(400, 500).margin(MARGIN);
@@ -84,7 +84,8 @@ private static void narrowContainer(DocumentSession session) {
/**
* A rounded box clipped to its outline, holding more text than it has room for: the page
* centres the text across the box, sets it from the box's top, as it is taller than the box,
- * and cuts the lines that run past its foot. A paragraph stands under the box.
+ * and cuts the lines that run past its foot. A paragraph stands above the box and one under
+ * it.
*/
private static void clippedContainer(DocumentSession session) {
session.pageSize(400, 500).margin(MARGIN);
@@ -103,8 +104,8 @@ private static void clippedContainer(DocumentSession session) {
/**
* A rounded card as wide as the page between its margins, its heading and text set from its
- * top left corner and filling it, under its default clip, with a paragraph under it. Text
- * added to the card in an editor needs it taller.
+ * top left corner and filling it, under its default clip, between a paragraph above it and
+ * one under it. Text added to the card in an editor needs it taller.
*/
private static void roundedCard(DocumentSession session) {
session.pageSize(400, 500).margin(MARGIN);
From 7f3a2d6d2b811c8afb6f43676b578b486ea15321 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Mon, 26 Oct 2026 18:13:21 +0000
Subject: [PATCH 3/3] test(qa): take the shape-container documents' Linux
LibreOffice rows from CI
The three documents' rows, as LibreOffice 24.2 on CI's Linux image measured
them in this branch's first run (artifact docx-fidelity-40373769086), where
the DOCX Fidelity job failed on these three alone, each not yet in the
baseline. The other rows stay as recorded.
---
.../docx-fidelity/libreoffice-linux-lines.tsv | 14 ++++++++++++++
.../resources/docx-fidelity/libreoffice-linux.tsv | 4 ++++
2 files changed, 18 insertions(+)
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
index 5d236505f..b9cfbdfb8 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
@@ -1,5 +1,6 @@
# libreoffice build 420(Build:2) on linux — document, line, drift below the page (pt), drift right of the page (pt), letters
# rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: libreoffice build 420(Build:2) on linux
+# rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: libreoffice build 420(Build:2) on linux
cv-blue_banner 0:8f1010120ed6 0.02 1.90 jordanrivera
cv-blue_banner 0:8d7a2cca3060 0.00 0.14 +442055551000|jordan.rivera@exam
cv-blue_banner 0:b944906ed84c -0.20 -0.38 professionalsummary
@@ -5122,3 +5123,16 @@ synthetic-card_over_pages 2:1c49f22f6de9 1.08 7.10 413
synthetic-card_over_pages 2:f69c59b028ea 1.08 7.10 lineitem60
synthetic-card_over_pages 2:db55da3fc309 1.08 7.10 420
synthetic-card_over_pages 2:8c569b972e01 1.08 0.10 afterthecard,onitslastpage.
+synthetic-clipped_container 0:cbe6ee14976b -0.02 0.10 beforetheclippedbox,onalineofits
+synthetic-clipped_container 0:2633ce73dc24 -20.02 0.10 aftertheclippedbox:thisparagraph
+synthetic-clipped_container 0:667f8b3b3eff -20.02 0.10 noneofitslinesrunsoverthebox.
+synthetic-narrow_container 0:fa13cab3fe73 -0.02 0.10 beforethebox,onalineofitsown.
+synthetic-narrow_container 0:697e6629574d -12.02 0.10 afterthebox:thisparagraphstandsu
+synthetic-narrow_container 0:0efb534ce6c6 -12.02 0.10 linesrunsoverthebox.
+synthetic-rounded_card 0:2a3c79dd9054 -0.02 0.10 beforethecard,onalineofitsown.
+synthetic-rounded_card 0:b8608d77afb0 0.01 0.10 aroundedcard
+synthetic-rounded_card 0:bcb0465dee5d -0.02 0.10 itstextrunsoverthreelinesfromthe
+synthetic-rounded_card 0:73c347391d3b -0.02 0.10 cardisjusttallenoughtoholdthemwi
+synthetic-rounded_card 0:8993a52a9026 -0.02 0.10 linemore,andthecardneedstobetall
+synthetic-rounded_card 0:103d88aab213 -0.02 0.10 afterthecard:thisparagraphstands
+synthetic-rounded_card 0:66b11b97a20e -0.02 0.10 linesrunsoverthecard.
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
index 5e7dc704d..7f45a925d 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
@@ -1,6 +1,7 @@
# libreoffice build 420(Build:2) on linux
document engine_pages editor_pages lines matched median p90 over2
# rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: libreoffice build 420(Build:2) on linux
+# rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: libreoffice build 420(Build:2) on linux
cv-blue_banner 2 2 54 52 0.16 0.25 0
cv-boxed_sections 2 2 58 58 0.06 0.11 0
cv-centered_headline 2 2 58 56 0.12 0.29 0
@@ -68,3 +69,6 @@ rota-cobalt 1 1 154 151 0.22 0.47 0
synthetic-break_in_column 1 1 7 7 0.02 0.02 0
synthetic-break_in_panel 3 3 8 8 3.02 5.02 5
synthetic-card_over_pages 3 3 114 114 0.98 1.08 0
+synthetic-clipped_container 1 1 10 3 20.02 20.02 2
+synthetic-narrow_container 1 1 6 3 12.02 12.02 2
+synthetic-rounded_card 1 1 7 7 0.02 0.02 0