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

### Public API

- **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
bill-to heading stood beside a 20pt badge, and the address under it stood 10pt high. Such a row
is now held at least as tall as the page makes it, as a row inside a painted panel already was;
a drawing no taller than its neighbours' text, a timeline's rail beside its entry, leaves the
row to Word. A section in a cell (a row's, a table's or a panel's) that pulls its first line up
with a negative top edge — `WorkspaceInvoice`'s masthead title, set 4pt above its row — had that
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
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
gap above it that runs about 16pt long in Word.
- **A bordered panel in a table cell shows its right border in Word.** Word draws a table's right
border outside its right edge — measured in its PDF, a table ending at 566.0pt had its right
border from 566.2 to 566.9 — and on screen cuts off what passes its cell's edge and draws the
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ creation date is real metadata.
| Inline chips (`inlineCode(...)`, `inlineChip(...)`, `highlight(...)`) | The chip's fill becomes the run's own `w:shd`, in a paragraph and in a list item alike. Its shape does not travel — see "What a chip keeps and loses" below |
| Images | Embedded pictures at the node's declared size |
| Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is |
| Rows | A one-row table spanning the content width, so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. The row is kept whole across a page break, as the layout keeps it |
| Rows | A one-row table spanning the content width, so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph 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 |
| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel) |
| Spacers | An empty paragraph a tenth of a point tall; the spacer's height is the space above the next block, or below this paragraph when a table follows |
| Page breaks | Explicit Word page breaks |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
// see holdStackedLines.
private final java.util.Map<ParagraphNode, DocxStackedLines.Line> stackedLineHeights =
new java.util.IdentityHashMap<>();
// 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<>();
// 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 @@ -604,6 +607,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
writingInAStandIn.clear();
stackedLineHeights.clear();
lineTopsTakenIn.clear();
risenLines.clear();
picturesDrawnBeside.clear();
listNumbering.clear();
report = new DocxExportReport.Builder();
Expand Down Expand Up @@ -3107,6 +3111,7 @@ private void writeContainerBody(XWPFDocument document, DocumentNode node) throws
double carriedFromOutside = carriedSpacingBefore;
long blocksBefore = blocksWritten;
carriedSpacingBefore += node.margin().top() + node.padding().top();
riseIntoItsLine(node);
// The sides are carried the same way, as the indent of every paragraph inside: a
// container's content starts inside its margin and its padding on the page, and was
// written flush with the page margin, the card's text touching the card's edge.
Expand Down Expand Up @@ -3137,6 +3142,60 @@ private void writeContainerBody(XWPFDocument document, DocumentNode node) throws
carriedSpacingBefore = blocksWritten == blocksBefore ? carriedFromOutside : 0;
}

/**
* Lets a container in a cell that pulls its first line up above the cell — a negative top
* edge — do so inside that line.
*
* <p>A Word paragraph starts no higher than its cell. {@code WorkspaceInvoice}'s title is set
* 4pt above its masthead row by the cell's padding, and in Word stood 4pt low with the whole
* page under it; the row opens the page, and there is no space above to lift it into (see
* {@link #standsAboveItsCell}). A first child of one line is written that much shorter
* instead, its text seated where the page sets it (see {@link DocxStackedLines.Line}), so
* the row is as tall as the page's and the title stands where the page puts it. That holds
* wherever the container stands in its cell: after other blocks, the space owed above it is
* written, and the line rises from there as the page's does.</p>
*
* <p>Word draws an exact line's text on screen only inside the line, so the line gives up no
* more than the room above its letters (see {@link DocxInk}): an accent on a capital pulled
* up further would be cut. What it cannot give stays where it did, the line starting at the
* cell's top. A line whose letters cannot be read — a picture in it — is left as it was.</p>
*/
private void riseIntoItsLine(DocumentNode node) {
if (currentCell == null || node.children().isEmpty() || !(node.children().get(0) instanceof ParagraphNode first)) {
return;
}
Double risen = risenLines.get(first);
if (risen != null) {
// Written again — a header on every page class: the line is already cut, and the
// edge it rises by is taken as it was the first time.
carriedSpacingBefore += risen;
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.
double rise = -(carriedSpacingBefore + pendingSpacingAfter - borderBelow);
if (!(rise > 0.01) || layout.lineCount(first) != 1 || stackedLineHeights.containsKey(first)) {
return;
}
java.util.OptionalDouble own = layout.lineHeight(first);
java.util.Optional<com.demcha.compose.document.layout.payloads.ParagraphLine> line = layout.firstLine(first);
double[] ink = inkOf(first);
if (own.isEmpty() || line.isEmpty() || ink == null) {
return;
}
double aboveTheLetters = line.get().lineHeight() - line.get().baselineOffsetFromBottom() - ink[0]
- DocxStackedLines.INK_MARGIN;
double taken = Math.min(rise, aboveTheLetters);
if (!(taken > 0.01) || taken >= own.getAsDouble()) {
return;
}
stackedLineHeights.put(first, new DocxStackedLines.Line(own.getAsDouble() - taken, -taken, 0));
risenLines.put(first, taken);
carriedSpacingBefore += taken;
}

private static boolean hasRadius(com.demcha.compose.document.style.DocumentCornerRadius radius) {
return radius != null && !radius.isZero();
}
Expand Down Expand Up @@ -6087,6 +6146,41 @@ private void holdRowHeight(XWPFTableRow row, TableNode node, int rowIdx) {
}
}

/**
* Whether a row's tallest child on the page, margins included as the row is sized by them,
* is one whose cell Word holds nothing in, taller by more than half a point than every child
* it writes.
*/
private boolean aDrawingMakesTheRow(RowNode node, XWPFTableRow row) {
double drawn = 0;
double written = 0;
for (int i = 0; i < node.children().size() && i < row.getTableCells().size(); i++) {
DocumentNode child = node.children().get(i);
com.demcha.compose.document.layout.PlacedNode placed = layout.placement(child);
if (placed == null) {
continue;
}
double height = placed.placementHeight() + child.margin().top() + child.margin().bottom();
if (holdsNothing(row.getCell(i))) {
drawn = Math.max(drawn, height);
} else {
written = Math.max(written, height);
}
}
return drawn > written + 0.5;
}

/**
* Whether Word holds nothing in a cell that gives it height: no table, and no paragraph with
* a run. A rule's or a spacer's paragraph counts as nothing; the row it stands in is held no
* taller than the page makes it, which that child already sets.
*/
private static boolean holdsNothing(XWPFTableCell cell) {
return cell.getTables().isEmpty()
&& cell.getParagraphs().stream().allMatch(paragraph -> paragraph.getCTP().sizeOfRArray() == 0
&& paragraph.getCTP().sizeOfHyperlinkArray() == 0);
}

/**
* Writes a row at least a height, less what its cells take above and below their content
* (see {@link #holdRowHeight}).
Expand Down Expand Up @@ -6746,8 +6840,8 @@ private void writeRow(XWPFDocument document, RowNode node) throws Exception {
// Inside a painted panel the row is held as tall as the page made it: its padding and a
// drawing standing in it are what make it taller than its text, as a panel's are, and
// MerchantInvoice's due-date text stood against its card's top without it.
com.demcha.compose.document.layout.PlacedNode placedRow =
surfaceBehind != null && panelCell != null ? layout.placement(node) : null;
boolean inAPanel = surfaceBehind != null && panelCell != null;
com.demcha.compose.document.layout.PlacedNode placedRow = layout.placement(node);
// A row has no fill of its own: inside a panel it is a table nested in the panel's
// cell, and a cell with no shading shows the panel's through it.
for (int i = 0; i < node.children().size(); i++) {
Expand All @@ -6769,7 +6863,14 @@ private void writeRow(XWPFDocument document, RowNode node) throws Exception {
}
applyRowVerticalAlign(cell, node.verticalAlign());
}
if (placedRow != null && placedRow.startPage() == placedRow.endPage()) {
// Anywhere, a row whose tallest child is one Word holds nothing of in its cell — a
// picture drawn where the page puts it, a badge beside a heading — is only as tall there
// as its text: WorkspaceInvoice's bill-to heading stood beside a 20pt badge, and the
// address under it stood 10pt high in Word. Held to the page's height, whose margins are
// written around the table. A drawing no taller than its neighbours' text — a timeline's rail
// beside its entry — changes nothing and is left to Word.
if (placedRow != null && placedRow.startPage() == placedRow.endPage()
&& (inAPanel || aDrawingMakesTheRow(node, row))) {
holdRowAtLeast(row, placedRow.placementHeight() - node.padding().top() - node.padding().bottom());
}
indentTable(table);
Expand Down
Loading
Loading