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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **Text hanging past a block takes its place out of the space above a stack that follows in
DOCX.** A stack written as a band opens at the space the page measures above its first block,
and that block forgot what the text above it already hung into that space: a band's or a
shape container's last line run past its foot, a row's icon line. `ConsultingInvoice`'s
address runs 2.3pt past its band, and the contact bands under it stood 2.9pt low in Word, 0.5pt
now. Lines more
than 2pt off fall from 3 to none for `ConsultingInvoice` and its long version in both editors,
and from 11 to 5 across the corpus in Word.
- **A ruled table's first and last rows keep the page's height in DOCX.** A row the page holds
at a height is written that height less what its cells take around their content, since
Word, as LibreOffice, reads a row's written height as its content. Word gives a rule between
Expand Down
5 changes: 4 additions & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ pulls the next paragraph up into it the same way: it comes out of the space abov
paragraph, its own top edge included, even where the two stand in different containers. What
that space cannot give is not written. Before a table, a page break or the end of a cell it
comes out of the space owed below instead. Layers that overlap on the page take none of it
from one another, as the space between them is measured from their boxes.
from one another, as the space between them is measured from their boxes. Text that runs past
the foot of the block it stands in — a stack's or a shape container's last line, a row cell's
icon line — comes out of the space above what follows the same way, a stack written as a band
included.

A paragraph's or a list's `lineSpacing` — the gap the page puts between two wrapped lines —
goes into the line: Word has one line height for a paragraph and no gap between its lines,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2012,9 +2012,9 @@ invoice-classic_long 1:8822ea375315 0.22 remittancedetails.
invoice-classic_long 1:e3337beaa171 0.20 thankyouforchoosinggraphcomposef
invoice-consulting 0:d44ceaea97d1 0.27 level8,1collinsstreet
invoice-consulting 0:78a0bb33347f 0.29 melbournevic3000australia
invoice-consulting 0:3b45ab243edb 2.81 +61398765432
invoice-consulting 0:a17a9fc6d4a8 2.81 hello@northpoint.com.au
invoice-consulting 0:ef0664c1d352 2.81 northpoint.com.au
invoice-consulting 0:3b45ab243edb 0.51 +61398765432
invoice-consulting 0:a17a9fc6d4a8 0.51 hello@northpoint.com.au
invoice-consulting 0:ef0664c1d352 0.51 northpoint.com.au
invoice-consulting 0:52d6e3de4fa0 -0.44 invoice
invoice-consulting 0:6282391a4cc1 -0.63 invoicenumber:
invoice-consulting 0:7d66eb3c1070 -0.19 inv-2025-0478
Expand Down Expand Up @@ -2104,9 +2104,9 @@ invoice-consulting 0:55ee5fe62c33 0.01 northpointconsultingptyltd|abn12
invoice-consulting 0:bdbada284e80 0.01 page1/1
invoice-consulting_long 0:d44ceaea97d1 0.27 level8,1collinsstreet
invoice-consulting_long 0:78a0bb33347f 0.29 melbournevic3000australia
invoice-consulting_long 0:3b45ab243edb 2.81 +61398765432
invoice-consulting_long 0:a17a9fc6d4a8 2.81 hello@northpoint.com.au
invoice-consulting_long 0:ef0664c1d352 2.81 northpoint.com.au
invoice-consulting_long 0:3b45ab243edb 0.51 +61398765432
invoice-consulting_long 0:a17a9fc6d4a8 0.51 hello@northpoint.com.au
invoice-consulting_long 0:ef0664c1d352 0.51 northpoint.com.au
invoice-consulting_long 0:52d6e3de4fa0 -0.44 invoice
invoice-consulting_long 0:6282391a4cc1 -0.63 invoicenumber:
invoice-consulting_long 0:7d66eb3c1070 -0.19 inv-2025-0478
Expand Down
4 changes: 2 additions & 2 deletions qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ cv-timeline_minimal 2 2 89 86 0.45 0.50 0
cv-violet_grid 1 2 80 76 5.21 21.31 39
invoice-classic 1 1 56 56 0.12 0.30 0
invoice-classic_long 2 2 166 166 0.20 0.27 0
invoice-consulting 1 1 92 92 0.67 0.96 3
invoice-consulting_long 2 2 236 236 0.71 1.06 3
invoice-consulting 1 1 92 92 0.67 0.96 0
invoice-consulting_long 2 2 236 236 0.69 1.04 0
invoice-luma_studio 1 1 97 97 0.12 0.52 2
invoice-luma_studio_long 3 3 192 192 0.20 0.41 2
invoice-merchant 1 1 94 94 0.73 1.91 5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1918,9 +1918,9 @@ invoice-classic_long 1:8822ea375315 0.22 remittancedetails.
invoice-classic_long 1:e3337beaa171 0.20 thankyouforchoosinggraphcomposef
invoice-consulting 0:d44ceaea97d1 0.27 level8,1collinsstreet
invoice-consulting 0:78a0bb33347f 0.29 melbournevic3000australia
invoice-consulting 0:3b45ab243edb 2.81 +61398765432
invoice-consulting 0:a17a9fc6d4a8 2.81 hello@northpoint.com.au
invoice-consulting 0:ef0664c1d352 2.81 northpoint.com.au
invoice-consulting 0:3b45ab243edb 0.51 +61398765432
invoice-consulting 0:a17a9fc6d4a8 0.51 hello@northpoint.com.au
invoice-consulting 0:ef0664c1d352 0.51 northpoint.com.au
invoice-consulting 0:52d6e3de4fa0 -0.44 invoice
invoice-consulting 0:6282391a4cc1 -0.63 invoicenumber:
invoice-consulting 0:7d66eb3c1070 -0.19 inv-2025-0478
Expand Down Expand Up @@ -2010,9 +2010,9 @@ invoice-consulting 0:55ee5fe62c33 0.01 northpointconsultingptyltd|abn12
invoice-consulting 0:bdbada284e80 0.01 page1/1
invoice-consulting_long 0:d44ceaea97d1 0.27 level8,1collinsstreet
invoice-consulting_long 0:78a0bb33347f 0.29 melbournevic3000australia
invoice-consulting_long 0:3b45ab243edb 2.81 +61398765432
invoice-consulting_long 0:a17a9fc6d4a8 2.81 hello@northpoint.com.au
invoice-consulting_long 0:ef0664c1d352 2.81 northpoint.com.au
invoice-consulting_long 0:3b45ab243edb 0.51 +61398765432
invoice-consulting_long 0:a17a9fc6d4a8 0.51 hello@northpoint.com.au
invoice-consulting_long 0:ef0664c1d352 0.51 northpoint.com.au
invoice-consulting_long 0:52d6e3de4fa0 -0.44 invoice
invoice-consulting_long 0:6282391a4cc1 -0.63 invoicenumber:
invoice-consulting_long 0:7d66eb3c1070 -0.19 inv-2025-0478
Expand Down
4 changes: 2 additions & 2 deletions qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ cv-timeline_minimal 2 2 89 86 0.45 0.50 0
cv-violet_grid 1 2 80 76 5.21 21.31 39
invoice-classic 1 1 56 56 0.17 0.32 0
invoice-classic_long 2 2 166 166 0.22 0.27 0
invoice-consulting 1 1 92 92 0.68 1.01 3
invoice-consulting_long 2 2 236 236 0.71 1.06 3
invoice-consulting 1 1 92 92 0.67 1.01 0
invoice-consulting_long 2 2 236 236 0.69 1.04 0
invoice-luma_studio 1 1 97 97 0.12 0.52 2
invoice-luma_studio_long 3 3 192 192 0.20 0.41 2
invoice-merchant 1 2 94 90 0.12 0.23 0
Expand Down
12 changes: 6 additions & 6 deletions qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2012,9 +2012,9 @@ invoice-classic_long 1:8822ea375315 0.33 remittancedetails.
invoice-classic_long 1:e3337beaa171 0.29 thankyouforchoosinggraphcomposef
invoice-consulting 0:d44ceaea97d1 0.26 level8,1collinsstreet
invoice-consulting 0:78a0bb33347f 0.33 melbournevic3000australia
invoice-consulting 0:3b45ab243edb 2.91 +61398765432
invoice-consulting 0:a17a9fc6d4a8 2.91 hello@northpoint.com.au
invoice-consulting 0:ef0664c1d352 2.91 northpoint.com.au
invoice-consulting 0:3b45ab243edb 0.51 +61398765432
invoice-consulting 0:a17a9fc6d4a8 0.51 hello@northpoint.com.au
invoice-consulting 0:ef0664c1d352 0.51 northpoint.com.au
invoice-consulting 0:52d6e3de4fa0 0.08 invoice
invoice-consulting 0:6282391a4cc1 -0.10 invoicenumber:
invoice-consulting 0:7d66eb3c1070 0.12 inv-2025-0478
Expand Down Expand Up @@ -2104,9 +2104,9 @@ invoice-consulting 0:55ee5fe62c33 0.15 northpointconsultingptyltd|abn12
invoice-consulting 0:bdbada284e80 0.15 page1/1
invoice-consulting_long 0:d44ceaea97d1 0.26 level8,1collinsstreet
invoice-consulting_long 0:78a0bb33347f 0.33 melbournevic3000australia
invoice-consulting_long 0:3b45ab243edb 2.91 +61398765432
invoice-consulting_long 0:a17a9fc6d4a8 2.91 hello@northpoint.com.au
invoice-consulting_long 0:ef0664c1d352 2.91 northpoint.com.au
invoice-consulting_long 0:3b45ab243edb 0.51 +61398765432
invoice-consulting_long 0:a17a9fc6d4a8 0.51 hello@northpoint.com.au
invoice-consulting_long 0:ef0664c1d352 0.51 northpoint.com.au
invoice-consulting_long 0:52d6e3de4fa0 0.08 invoice
invoice-consulting_long 0:6282391a4cc1 -0.10 invoicenumber:
invoice-consulting_long 0:7d66eb3c1070 0.12 inv-2025-0478
Expand Down
4 changes: 2 additions & 2 deletions qa/src/test/resources/docx-fidelity/word-windows.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ cv-timeline_minimal 2 2 89 86 0.36 0.48 0
cv-violet_grid 1 1 80 77 0.23 1.30 5
invoice-classic 1 1 56 56 0.22 0.36 0
invoice-classic_long 2 2 166 166 0.27 0.34 0
invoice-consulting 1 1 92 92 0.73 1.05 3
invoice-consulting_long 2 2 236 236 0.74 1.10 3
invoice-consulting 1 1 92 92 0.70 1.02 0
invoice-consulting_long 2 2 236 236 0.74 0.97 0
invoice-luma_studio 1 1 97 97 0.13 0.42 0
invoice-luma_studio_long 3 3 192 192 0.22 0.38 0
invoice-merchant 1 1 94 94 0.14 0.35 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8991,7 +8991,15 @@ private void writeOverlayBand(XWPFDocument document, DocumentNode stack, DocxLay
// A band inside a band that has written nothing yet: the outer band measured its space
// to the first block written, which is this one's first too, so that space stands.
if (Double.isNaN(resumeSpacing)) {
resumeSpacing = carriedSpacingBefore + pendingSpacingAfter + stack.margin().top() + band.above();
double above = carriedSpacingBefore + pendingSpacingAfter + stack.margin().top() + band.above();
// Text hanging below the block before — a band's last line past its foot, a row's
// icon line — takes its place out of that space, as out of the gap above a paragraph
// (newBodyParagraph); the band's first block forgets it (resumeHere). ConsultingInvoice's
// address runs 2.3pt past its band, and the contact bands under it stood 2.9pt low.
// A band the layout starts on a new page leaves the hang on the page above, as a
// paragraph or a table there does.
double hang = currentCell == null && overlayDepth == 0 && startsAPageOfItsOwn(stack) ? 0 : hangingBelow;
resumeSpacing = above - Math.min(Math.max(0, above), hang);
}
carriedSpacingBefore = 0;
pendingSpacingAfter = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,58 @@ void theLastLayersNegativeBottomEdgeIsTakenOnceBelowTheBand() throws Exception {
}
}

@Test
void textHangingBelowABandTakesItsPlaceOutOfTheSpaceAboveTheNextBand() throws Exception {
// ConsultingInvoice's address runs past its band, and the contact band under it opened at
// the gap whole: the band's first block resumes the space the page measures, and the
// text hanging into it from above already stands in it.
DocumentSession session = GraphCompose.document().pageSize(300, 500).margin(DocumentInsets.of(20)).create();
session.pageFlow(page -> page
.spacing(10)
.addLayerStack(stack -> stack.name("First")
.back(new SpacerNode("Space", 200, BADGE, DocumentInsets.zero(), DocumentInsets.zero()))
.layer(new ParagraphBuilder().name("Low").text("Low")
.margin(new DocumentInsets(0, 0, -4, 0)).build(), LayerAlign.BOTTOM_LEFT))
.addLayerStack(stack -> stack.name("Second")
.back(new SpacerNode("Room", 200, 40, DocumentInsets.zero(), DocumentInsets.zero()))
.layer(new ParagraphBuilder().name("Next").text("Next").build(), LayerAlign.TOP_LEFT)));
try (session; XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(
session.export(new DocxSemanticBackend())))) {
XWPFParagraph next = document.getParagraphs().stream()
.filter(paragraph -> "Next".equals(paragraph.getText())).findFirst().orElseThrow();

assertThat(before(next)).as("the 10pt gap, less the 4pt the text above hangs into it")
.isEqualTo((10 - 4) * 20L);
}
}

@Test
void aBandOpeningTheNextPageLeavesTheHangAboveOnItsPage() throws Exception {
// 360pt, the gap and the first stack leave the second no room: it opens the next page,
// and what the first stack's text hangs below it stays on the page above.
DocumentSession session = GraphCompose.document().pageSize(300, 500).margin(DocumentInsets.of(20)).create();
session.pageFlow(page -> page
.spacing(10)
.spacer(0, 360)
.addLayerStack(stack -> stack.name("First")
.back(new SpacerNode("Space", 200, BADGE, DocumentInsets.zero(), DocumentInsets.zero()))
.layer(new ParagraphBuilder().name("Low").text("Low")
.margin(new DocumentInsets(0, 0, -4, 0)).build(), LayerAlign.BOTTOM_LEFT))
.addLayerStack(stack -> stack.name("Second")
.back(new SpacerNode("Room", 200, 40, DocumentInsets.zero(), DocumentInsets.zero()))
.layer(new ParagraphBuilder().name("Next").text("Next").build(), LayerAlign.TOP_LEFT)));
assertThat(session.layoutGraph().nodes().stream()
.filter(node -> "Second".equals(node.semanticName())).findFirst().orElseThrow().startPage())
.as("the second stack opens the next page").isEqualTo(1);
try (session; XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(
session.export(new DocxSemanticBackend())))) {
XWPFParagraph next = document.getParagraphs().stream()
.filter(paragraph -> "Next".equals(paragraph.getText())).findFirst().orElseThrow();

assertThat(before(next)).as("the space above it, written whole: Word drops it at the page top, and the hang is not taken from it").isEqualTo(10 * 20L);
}
}

@Test
void aLayersNegativeBottomEdgeInAShapeContainerMovesNothingBelowIt() throws Exception {
// The outline is its own size; the layer's shorter box is already in the space below it.
Expand Down
Loading