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

### Public API

- **A block pulled up by a negative top edge, and a row padded down its column, stand where the
page puts them in Word.** Word has no negative space above a paragraph, so a paragraph's, a
page reference's or a rule's negative top `margin` or `padding` was dropped:
`PaymentsInvoice`'s header rule is pulled 16.4pt up out of the space its header stack leaves
below its content. The pull now comes out of the space owed above the block, with any pull of
the containers around it. Word has no space above a table either, and a row opening a row's
column lost the padding above it: `PaymentsInvoice`'s metadata grid, padded 6.2pt down its
column, stood 6.3pt above the issuer beside it, as `MerchantInvoice`'s stood 6pt, and
`PaymentsInvoice`'s rule and the page under it stood 10pt low, the pull less that lost
padding. A paragraph a tenth of a point tall now carries that space, the containers' edges
included, in any cell but a table's — as it already carried the space a row owed above itself
at the top of a panel; a row opening a table's cell is left to the row height the table
holds. In Word `PaymentsInvoice`'s median drift falls from 10.3pt to 0.5, and across the 62
templates lines more than 2pt off fall from 705 to 572; in LibreOffice from 1066 to 933.
- **A row a drawing makes tall, and a title pulled above its row, keep the page's height in
Word.** A row whose tallest child is drawn where the page puts it — a badge beside a heading,
its cell holding nothing in Word — was only as tall as its text there: `WorkspaceInvoice`'s
Expand All @@ -19,7 +33,7 @@ follow semantic versioning; release dates are ISO 8601.
line start at the cell's top in Word, and the page under it 4pt low; a one-line first paragraph
is now written as much shorter, its text seated where the page sets it — no more than the room
above its letters, as Word draws an exact line's text only inside the line. The paragraph's own
top edge is written as before. In Word `WorkspaceInvoice`'s median drift
top edge is not counted in the rise. In Word `WorkspaceInvoice`'s median drift
falls from 4.8pt to 0.4 and `SubscriptionInvoice`'s from 8.8 to 0.5; across the 62 templates,
lines more than 2pt off fall from 858 to 705. `PaymentsInvoice`'s parties now stand level with
each other and its median rises from 3.7 to 10.3: the collapsed heading row had been hiding a
Expand Down
12 changes: 10 additions & 2 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,16 @@ paragraph is held to a tenth of a point, so it adds no line of text to the gap i

A table holds its own space the same way. Word has no space above a table and none below
one, so a table's or a row's `margin` and `padding` travel to the paragraphs around it —
the space above a table is the space below the paragraph before it. A table with no
paragraph above it loses that edge, which is the one gap Word has nowhere to put.
the space above a table is the space below the paragraph before it. Where no paragraph
stands above it — a table opening the body, or a row opening any cell but a table's, padded
down it — a paragraph a tenth of a point tall carries the space instead. A row opening a
table's cell is left to the row height the table holds, at least as tall as the page makes it;
a table (not a row) opening a cell still loses that edge.

An edge pulling a block up — a paragraph's, a page reference's or a rule's negative top
`margin` or `padding` — comes out of the space owed above it, as the page takes it from
there: Word has no negative space above a paragraph. What that space cannot give is not
written. Text laid over the flow in text boxes owes no space, so its edges move nothing.

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 @@ -236,6 +236,10 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
// How far each paragraph a container pulled above its cell rises inside its own line: see
// riseIntoItsLine.
private final java.util.Map<ParagraphNode, Double> risenLines = new java.util.IdentityHashMap<>();
// The cells of a table's grid that hold a composed node, whose row the table holds at the
// page's height: see writeTableWithItsOwnSpacing.
private final java.util.Set<org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc> tablesCells =
java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
// How far above the page's first line a paragraph's Word line starts, in points, where its
// lines took the space between them from the space above it: see applyLineGap.
private final java.util.Map<org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP, Double> lineTopsTakenIn =
Expand Down Expand Up @@ -608,6 +612,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
stackedLineHeights.clear();
lineTopsTakenIn.clear();
risenLines.clear();
tablesCells.clear();
picturesDrawnBeside.clear();
listNumbering.clear();
report = new DocxExportReport.Builder();
Expand Down Expand Up @@ -2040,6 +2045,10 @@ private DocxRules.Rule ruleOf(DocumentNode node) {

private void dispatchNode(XWPFDocument document, DocumentNode node) throws Exception {
DocxRules.Rule rule = ruleOf(node);
if (overTheFlowDepth == 0 && (node instanceof ParagraphNode
|| node instanceof com.demcha.compose.document.node.PageReferenceNode || rule != null)) {
standsIntoTheSpaceAbove(node);
}
if (node instanceof ParagraphNode paragraph) {
writeParagraph(document, paragraph);
} else if (node instanceof com.demcha.compose.document.node.PageReferenceNode reference) {
Expand Down Expand Up @@ -2979,7 +2988,9 @@ private void holdTheSpaceAboveATable(XWPFDocument document) {
boolean tableAbove = currentCell == null
? endsWithATable(document.getBodyElements())
: cellEndsWithItsTableCloser();
if (pendingSpacingAfter > 0 && lastBodyParagraph == null && !tableAbove) {
// The space owed is the block's own edge and the edges of the containers opened
// around it, which are carried down to it rather than owed.
if (pendingSpacingAfter + carriedSpacingBefore - borderBelow > 0 && lastBodyParagraph == null && !tableAbove) {
holdToHairline(newBodyParagraph(document));
}
}
Expand Down Expand Up @@ -3172,9 +3183,10 @@ private void riseIntoItsLine(DocumentNode node) {
return;
}
// What the paragraph would have above it before its own edge, which is written apart
// (applyVerticalSpacing): the edges carried down to it and the space the block before it
// owes, less a border standing below that block. Only what that comes short of zero is
// a rise; the rest is written as space.
// (applyVerticalSpacing, or standsIntoTheSpaceAbove for a negative one, after this): the
// edges carried down to it and the space the block before it owes, less a border
// standing below that block. Only what that comes short of zero is a rise; the rest is
// written as space.
double rise = -(carriedSpacingBefore + pendingSpacingAfter - borderBelow);
if (!(rise > 0.01) || layout.lineCount(first) != 1 || stackedLineHeights.containsKey(first)) {
return;
Expand Down Expand Up @@ -3719,6 +3731,28 @@ private static boolean drawn(CTBorder border) {
return border != null && border.getVal() != STBorder.NONE && border.getVal() != STBorder.NIL;
}

/**
* Takes a paragraph's, page reference's or rule's negative top edge out of the space owed
* above it.
*
* <p>Word has no negative space above a paragraph, and {@link #applyVerticalSpacing}
* writes an edge only where it is positive, so an edge pulling the block up was dropped
* and the block stood that much low, with everything after it. {@code PaymentsInvoice}'s
* header rule is pulled 16.4pt up from the foot of the stack above it, whose band runs
* past its content: the pull was lost, and the rule and the page under it stood 10pt low,
* the 16.4pt less the 6.3pt its metadata grid had lost above it. The pull is added
* to the edges carried down to the block, as a container's negative edge already is, so
* {@link #newBodyParagraph} nets it against everything owed above, as the page sums it;
* what that cannot give stays unwritten, as before. Text laid over the flow owes no
* space ({@link #writeOverTheFlow}), so its edges move nothing.</p>
*/
private void standsIntoTheSpaceAbove(DocumentNode node) {
double edge = node.margin().top() + node.padding().top();
if (edge < 0) {
carriedSpacingBefore += edge;
}
}

/**
* Gives back space already owed above the next block, most recent first: text standing
* above its band took it on the page.
Expand Down Expand Up @@ -5982,18 +6016,21 @@ private static boolean startsWith(byte[] bytes, int... signature) {
* below the paragraph before it, and the space below one is the space above the
* paragraph after. Both go through the debt every other gap goes through
* ({@link #owePendingSpacingAfter}), so a table between two paragraphs reads the same as
* two paragraphs with a gap between them — and a table with nothing above it loses that
* edge, which is the one thing Word genuinely cannot hold.</p>
* two paragraphs with a gap between them. With nothing above it — opening the body, or a
* row opening any cell but a table's — a paragraph a tenth of a point tall holds that edge
* ({@link #holdTheSpaceAboveATable}); a table opening a cell still loses it.</p>
*/
private void writeTableWithItsOwnSpacing(XWPFDocument document, DocumentNode node)
throws Exception {
owePendingSpacingAfter(node.margin().top() + node.padding().top());
if (node instanceof RowNode && currentCell != null && currentCell == panelCell) {
// At the top of a panel nothing above holds that space: MerchantInvoice's due-date
if (node instanceof RowNode && currentCell != null && !tablesCells.contains(currentCell.getCTTc())) {
// At the top of a cell nothing above holds that space: MerchantInvoice's due-date
// row lost its 16.7pt of top padding and stood against the card's top edge, once the
// card held the page's height. A row in a table's cell keeps to the row height the
// table holds (holdRowHeight). A table is left as it was: holding its space moved
// ObsidianInvoice's line items 6pt below the page's.
// card held the page's height, and PaymentsInvoice's metadata grid the 6.2pt its
// column is padded down by, standing 6.3pt above the issuer beside it. A row in a
// table's cell keeps to the row height the table holds (holdRowHeight), and a table
// is left as it was: holding the space of either moved ObsidianInvoice's line items
// 6pt below the page's.
holdTheSpaceAboveATable(document);
}
if (node instanceof RowNode row) {
Expand Down Expand Up @@ -6592,6 +6629,7 @@ private void writeCellContent(XWPFTableCell cell, TableGrid.Placement placement,
// exported it as an empty cell.
double previous = currentCellWidth;
currentCellWidth = usableWidthOf(cell, placement);
tablesCells.add(cell.getCTTc());
try {
writeCellBody(cell, source.content());
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,20 @@ void aTableAfterAPageBreakKeepsTheSpaceAboveIt() throws Exception {
}

@Test
void aTableOpeningACellIsWrittenAsBefore() throws Exception {
// Only the body takes the paragraph: in a cell it was measured to set content lower
// than the page does — each of ObsidianInvoice's line items 8.5pt lower.
void aRowOpeningATablesCellIsWrittenAsBefore() throws Exception {
// A table's cell takes no paragraph: the table holds its row at least as tall as the
// page makes it, and one there was measured to set content lower than the page does —
// each of ObsidianInvoice's line items 6pt lower. A row's column does (DocxSpaceAboveTest).
com.demcha.compose.document.dsl.SectionBuilder composed = new com.demcha.compose.document.dsl.SectionBuilder();
composed.padding(new DocumentInsets(12, 0, 0, 0))
.addRow(inner -> inner.addParagraph("Qty").addParagraph("1"));
com.demcha.compose.document.node.DocumentNode item = composed.build();
try (XWPFDocument document = DocxExports.withLayout(400, 600, 0, page -> page
.addRow(outer -> outer
.addSection(cell -> cell.padding(new DocumentInsets(12, 0, 0, 0))
.addRow(inner -> inner.addParagraph("Qty").addParagraph("1")))
.addParagraph("Notes")))) {
.addTable(t -> t
.columns(com.demcha.compose.document.table.DocumentTableColumn.auto(),
com.demcha.compose.document.table.DocumentTableColumn.auto())
.rowCells(com.demcha.compose.document.table.DocumentTableCell.node(item),
com.demcha.compose.document.table.DocumentTableCell.text("Notes"))))) {
XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0);

assertThat(cell.getBodyElements().get(0)).as("the nested table opens the cell")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ void theFlowAfterItStandsAsIfItWereNotThere() throws Exception {
}
}

@Test
void aLinePulledUpInItTakesNothingFromTheFlowAfterIt() throws Exception {
// A negative top edge takes its pull out of the space owed above a block in the flow;
// a line in a text box owes none, and the masthead keeps the 6pt its section holds
// above it.
DocumentNode pulled = new ShapeContainerBuilder().name("Sidebar")
.rectangle(100, STUB).clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
.margin(new DocumentInsets(-40, 0, 40 - STUB, -40))
.position(new ParagraphBuilder().name("Monogram").text("L")
.margin(DocumentInsets.top(-5)).build(), 10, 10, LayerAlign.TOP_LEFT, 0)
.build();
try (XWPFDocument document = DocxExports.withLayout(400, 600, 40, page -> page
.add(pulled)
.addSection("Head", head -> head.padding(DocumentInsets.top(6)).addParagraph("Masthead")))) {
assertThat(document.getDocument().getBody().xmlText()).as("the line is in a text box")
.contains("<w:txbxContent>");
assertThat(before(masthead(document))).as("the section's space above it").isEqualTo(6 * 20L);
}
}

@Test
void aLineIsGivenRoomOnTheSideItsTextDoesNotLeanOn() throws Exception {
// A right-to-left line is set against the page's right side as a right-aligned one is,
Expand Down
Loading
Loading