Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4269,6 +4269,41 @@ 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,
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,
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;
- 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.
Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,10 @@ 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
Expand Down
26 changes: 21 additions & 5 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down Expand Up @@ -1103,9 +1105,14 @@ 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 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
Expand Down Expand Up @@ -1162,7 +1169,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
* <p>Each sets a way the page breaks inside a panel or a column: a page break among a panel's
* <p>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.</p>
* 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.</p>
*
* <p>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.</p>
*/
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);
Expand All @@ -42,7 +58,69 @@ static List<DocxCorpusDocument> 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 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);
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 above the box and one under
* it.
*/
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, 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);
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.")));
}

/**
Expand Down Expand Up @@ -123,6 +201,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<DocumentSession> compose) {
return new DocxCorpusDocument("synthetic", name, -1, compose);
}
Expand Down
14 changes: 14 additions & 0 deletions qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
4 changes: 4 additions & 0 deletions qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading
Loading