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

### Public API

- **A DOCX export breaks a page inside a section or a panel of a panel where the page does.**
- Word breaks no page inside a table cell (measured in Word and LibreOffice 24.2: a run's page
break and a paragraph's break before it, in a table of the body or one nested in a cell, left
what follows on the same page). A panel was cut into two tables at a page break among its own
children only; one in a section or a panel inside it was written after the panel's table, so
the panel's text after the break stood on the page before, in the panel, and the page broke
after it.
- The panel's table is now cut at a page break among the children of any section or panel its
cell writes them in, at any depth, each table holding its part of every one the break runs
through: a section's top edge where it starts and its bottom edge where it ends, a panel's top
padding in its first table. A cut section's anchor marks where it starts, and its notes are
said once.
- The page goes on with a panel a break runs through at the top of the next page, with no
padding above its text there, its own or that of any panel round it: a table after the first
has none now, where it had the panel's top padding, the text 8pt lower in Word for a card
padded 8pt. What follows a break stands its container's spacing below that top.
- No document of the corpus has a page break inside a panel; its export is unchanged.

- **A DOCX export's lines of runs of several depths grow with their text where every line is as
deep as the deepest.**
- A paragraph whose runs reach to different depths below the baseline — a bold title beside
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 @@ -715,8 +715,16 @@ How it lands:
inside a panel that runs on there. One moved whole that Word cannot split — one line, one
picture, a drawing, a row that may not split — is not named: a painted section kept together,
whose own row may not split, and a row in the panel move whole, as the page sets them.
- **Page breaks.** Word breaks no page inside a table cell, so a page break among a
panel's children closes the panel there and opens it again after the break.
- **Page breaks.** Word breaks no page inside a table cell (measured in Word and LibreOffice
24.2: a run's page break and a paragraph's break before it, in a table of the body or one
nested in a cell, left what follows on the same page), so a page break among a panel's
children, or among those of a section or a panel inside it, at any depth, closes the panel
there and opens it again after the break, each table holding its part of every section and
panel the break runs through. The page goes on with them at the top of the next page, with
no padding above, its own or theirs, so the tables after the first have none; what follows
the break stands the spacing of the container it is in below that top, as the page sets it.
A break in a panel composed in a table cell, a row's column or a stack's column is not cut,
nor one in an alignment, an anchor's wrapper or a stack of one layer inside a panel.
- **Measured.** Cards stacked in a row's columns, panels nested in its table, and a card set in
from a filled panel's text stand where the page sets them in Word 16 and LibreOffice on
Windows, as far as the left edge and the right end above say.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.demcha.compose.document.backend.semantic.docx;

import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.PageBreakNode;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

/**
* Where a panel the page breaks over pages is cut into tables: at each page break among its
* children, and among the children of each section or panel its cell writes them in, at any depth.
*
* <p>Word breaks no page inside a table cell: measured in Word and LibreOffice 24.2, a page break
* in a cell — a run's break or a paragraph's break before it, in a table of the body or one
* nested in a cell — left the text after it on the same page. The page breaks a card a break runs
* through, the card going on at the top of the next page, as a section or a card inside it does.
* So the panel's table ends at the break and a new one opens after it, on the next page, each
* holding its part of every container the break runs through.</p>
*
* @author Artem Demchyshyn
*/
final class DocxPanelPieces {

private DocxPanelPieces() {
}

/**
* What one piece of a panel holds of one of a container's children: the child whole, or the
* run of its own children from one break to the next ({@code parts}), the child opening in
* that piece where the run starts its children and closing where it ends them.
*
* @param node the child
* @param parts what the piece holds of the child's children, or null where it holds the child
* whole
* @param opens whether the piece holds the child's start: its top edge
* @param closes whether the piece holds the child's end: its bottom edge
*/
record Part(DocumentNode node, List<Part> parts, boolean opens, boolean closes) {

static Part whole(DocumentNode node) {
return new Part(node, null, true, true);
}

boolean isWhole() {
return parts == null;
}
}

/**
* A container's children cut at the page breaks among them and among the children of those
* of them its cell writes them in, at any depth.
*
* @param container the container
* @param writtenInItsCell whether a child is a container its parent's cell writes in it, its own
* children after one another: a section or a panel there
* @return the pieces, one more than the breaks found, each the parts it holds in order; a
* piece is empty where a break ends or opens the container
*/
static List<List<Part>> of(DocumentNode container, Predicate<DocumentNode> writtenInItsCell) {
List<List<Part>> pieces = new ArrayList<>();
pieces.add(new ArrayList<>());
for (DocumentNode child : container.children()) {
if (child instanceof PageBreakNode) {
pieces.add(new ArrayList<>());
continue;
}
List<List<Part>> inner = writtenInItsCell.test(child) ? of(child, writtenInItsCell) : List.of();
if (inner.size() < 2) {
pieces.get(pieces.size() - 1).add(Part.whole(child));
continue;
}
for (int index = 0; index < inner.size(); index++) {
if (index > 0) {
pieces.add(new ArrayList<>());
}
pieces.get(pieces.size() - 1).add(new Part(child, inner.get(index), index == 0,
index == inner.size() - 1));
}
}
List<List<Part>> written = new ArrayList<>(pieces.size());
for (List<Part> piece : pieces) {
written.add(List.copyOf(piece));
}
return List.copyOf(written);
}

/** The children a run of parts holds, whole or in part, in order. */
static List<DocumentNode> nodesOf(List<Part> parts) {
List<DocumentNode> nodes = new ArrayList<>(parts.size());
for (Part part : parts) {
nodes.add(part.node());
}
return nodes;
}

/**
* Whether the first part of a piece after the first follows a page break among the container's
* own children, so that the container's spacing stands above it: a part holding a child's
* start, not one going on from a break inside that child.
*
* @param piece the piece's parts
* @param firstPiece whether the piece is the container's first
*/
static boolean followsABreak(List<Part> piece, boolean firstPiece) {
return !firstPiece && !piece.isEmpty() && piece.get(0).opens();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
// How tall Word stands each written paragraph's line, and where its text stands in it, by the
// faces the export ships (DocxGrownLines).
private DocxGrownLines grownLines = DocxGrownLines.NONE;
// The part a panel's table being written holds of each container a page break cuts it at
// (DocxPanelPieces), by node, taken by the container's writer.
private final java.util.Map<DocumentNode, DocxPanelPieces.Part> panelWindows = new java.util.IdentityHashMap<>();
// Where the shapes this export draws are anchored — see DocxDrawingAnchors.
private final DocxDrawingAnchors anchors = new DocxDrawingAnchors(() -> nextDrawingId++,
paragraph -> grownLines.lineOf(paragraph));
Expand Down Expand Up @@ -1220,6 +1223,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
markdownWritten.clear();
fittedWritten.clear();
listNumbering.clear();
panelWindows.clear();
measuredColumns.clear();
prefixColumns.clear();
report = new DocxExportReport.Builder();
Expand Down Expand Up @@ -3740,7 +3744,11 @@ private void writePlacedNode(XWPFDocument document, DocumentNode node) throws Ex
&& layout.onOnePage(node);
PanelBreak panelBreak = keepThePanelMayBreak(node);
boolean keepWithNext = node.keepWithNext() && layout.onOnePage(node);
String anchor = blockAnchorOf(node, inTheFlow(node, overlayDepth, oneLayerDepth));
// A container a page break cuts is written once a table of its panel, its anchor marked
// where it starts: a bookmark round the part after the break would be the one kept.
DocxPanelPieces.Part part = panelWindows.get(node);
String anchor = part != null && !part.opens() ? null
: blockAnchorOf(node, inTheFlow(node, overlayDepth, oneLayerDepth));
if (!keepTogether && !keepWithNext && anchor == null && panelBreak == null) {
writeNodeContent(document, node);
return;
Expand Down Expand Up @@ -6996,12 +7004,20 @@ && known && holdsWrappedText(node)
alsoLost.add("its bleed is not in the file, so its fill and borders stop at its box, not at "
+ "the page's edge");
}
reportWrittenWithout(node, paint.isEmpty() ? "written as its contents" : "written as a panel", alsoLost);
// A container a page break cuts is written once a table of its panel (DocxPanelPieces), and
// named once, where it starts.
DocxPanelPieces.Part part = panelWindows.get(node);
boolean itsStart = part == null || part.opens();
if (itsStart) {
reportWrittenWithout(node, paint.isEmpty() ? "written as its contents" : "written as a panel", alsoLost);
}
if (paint.isEmpty()) {
writeContainerBody(document, node);
return;
}
warnContainerRadiusDropped(node);
if (itsStart) {
warnContainerRadiusDropped(node);
}
writePanel(document, node, paint);
}

Expand Down Expand Up @@ -7035,20 +7051,22 @@ && known && holdsWrappedText(node)
* margins are the space around the table, owed like any block's. A container kept
* together that the layout held on one page is a row that may not break.</p>
*
* <p>Word breaks no page inside a table cell, so a page break among the panel's children
* closes the panel there and opens it again after the break, on the next page — the way
* the page draws a card a break runs through. Inside a cell there is no page to break,
* and the panel is written whole.</p>
* <p>Word breaks no page inside a table cell, so a page break among the panel's children, or
* among those of a section or panel its cell writes them in, at any depth, closes the panel
* there and opens it again after the break, on the next page — the way the page draws a card a
* break runs through ({@link DocxPanelPieces}). A panel inside one so cut is written in the
* piece of it each table holds. Inside a cell the break is not cut otherwise, and the panel is
* written whole.</p>
*/
private void writePanel(XWPFDocument document, DocumentNode node, ContainerPaint paint) throws Exception {
List<List<DocumentNode>> pieces = new ArrayList<>();
pieces.add(new ArrayList<>());
for (DocumentNode child : node.children()) {
if (child instanceof PageBreakNode && currentCell == null) {
pieces.add(new ArrayList<>());
} else {
pieces.get(pieces.size() - 1).add(child);
}
DocxPanelPieces.Part window = panelWindows.remove(node);
List<List<DocxPanelPieces.Part>> pieces;
if (window != null) {
pieces = List.of(window.parts());
} else if (currentCell == null) {
pieces = DocxPanelPieces.of(node, DocxSemanticBackend::writtenInItsCell);
} else {
pieces = List.of(node.children().stream().map(DocxPanelPieces.Part::whole).toList());
}
for (int index = 0; index < pieces.size(); index++) {
boolean onANewPage = false;
Expand All @@ -7066,11 +7084,54 @@ private void writePanel(XWPFDocument document, DocumentNode node, ContainerPaint
}
onANewPage = true;
}
List<DocumentNode> piece = pieces.get(index);
List<DocxPanelPieces.Part> piece = pieces.get(index);
ShapeContainerNode shape = node instanceof ShapeContainerNode container ? container : null;
writePanelPiece(document, node, paint, () -> writeChildren(document, piece, spacingOf(node), false, shape),
index == 0, index == pieces.size() - 1, onANewPage);
boolean first = window == null ? index == 0 : window.opens();
boolean last = window == null ? index == pieces.size() - 1 : window.closes();
writePanelPiece(document, node, paint, () -> writeParts(document, piece, spacingOf(node), false, shape,
DocxPanelPieces.followsABreak(piece, first)), first, last, onANewPage);
}
}

/**
* Writes what a piece of a panel holds of a container's children ({@link DocxPanelPieces}),
* each one cut by a break written in the part of it the piece holds ({@link #panelWindows}).
*
* @param lastFollowed whether something is written after them in the flow, as
* {@link #writeChildren(XWPFDocument, List, double, boolean)}
* @param afterABreak whether the first of them follows a page break among the container's own
* children, the container's spacing above it
*/
private void writeParts(XWPFDocument document, List<DocxPanelPieces.Part> parts, double spacing,
boolean lastFollowed, ShapeContainerNode shape, boolean afterABreak) throws Exception {
for (DocxPanelPieces.Part part : parts) {
if (!part.isWhole()) {
panelWindows.put(part.node(), part);
}
}
if (afterABreak && spacing > 0) {
// The page sets what follows a break the container's spacing below the page's top.
owePendingSpacingAfter(spacing);
}
try {
writeChildren(document, DocxPanelPieces.nodesOf(parts), spacing, lastFollowed, shape);
} finally {
// A part not written through its container's writer, one an earlier layer wrote in
// its place (movedIntoAStandIn), leaves no window for anything after it.
for (DocxPanelPieces.Part part : parts) {
panelWindows.remove(part.node());
}
}
}

/**
* Whether a panel's cell writes a node in it as its children after one another, its own top
* and bottom edges around them: a section or a container, painted or not — the containers a
* page break inside is cut through ({@link DocxPanelPieces}). An alignment, an anchor's
* wrapper and a stack of one layer, written so too, are not cut through.
*/
private static boolean writtenInItsCell(DocumentNode node) {
return node instanceof SectionNode || node instanceof ContainerNode;
}

/**
Expand Down Expand Up @@ -7554,7 +7615,11 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container
// Below zero — a padding thinner than what of the half it does not keep — written as none
// (setCellMargin).
double marginLeft = inTheBody ? padding.left() : padding.left() - halfLeft + setIn;
applyCellPadding(cell, new DocumentInsets(margins.top(), Math.max(0, marginHeld - slackTaken),
// The page goes on with a panel a break runs through at the top of the next page, its text
// there with no padding above it, its own or any panel's round it (measured: a card's and a
// card's in it, padded 8 and 6pt, set their text at the page's top edge after the break):
// a piece after the first has none.
applyCellPadding(cell, new DocumentInsets(first ? margins.top() : 0, Math.max(0, marginHeld - slackTaken),
margins.bottom(), marginLeft));
// A row is laid out as one piece, never across a page break (writeRow): so is its panel.
if ((node.keepTogether() || node instanceof RowNode) && layout.onOnePage(node)) {
Expand Down Expand Up @@ -8136,10 +8201,17 @@ private static double spacingOf(DocumentNode node) {
}

private void writeContainerBody(XWPFDocument document, DocumentNode node) throws Exception {
// Cut by a page break its panel's tables are cut at, it writes the part of it this table
// holds, and its top and bottom edges where that part holds them (DocxPanelPieces).
DocxPanelPieces.Part window = panelWindows.remove(node);
boolean opens = window == null || window.opens();
boolean closes = window == null || window.closes();
double carriedFromOutside = carriedSpacingBefore;
long blocksBefore = blocksWritten;
carriedSpacingBefore += node.margin().top() + node.padding().top();
riseIntoItsLine(node);
if (opens) {
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 All @@ -8164,14 +8236,22 @@ private void writeContainerBody(XWPFDocument document, DocumentNode node) throws
if (isOneLayer(node)) {
layerOffsetNotWritten += layerOffsetDown(node);
}
boolean followed = followedInFlow.remove(node);
try {
writeChildren(document, node.children(), spacingOf(node), followedInFlow.remove(node));
if (window == null) {
writeChildren(document, node.children(), spacingOf(node), followed);
} else {
writeParts(document, window.parts(), spacingOf(node), closes && followed, null,
DocxPanelPieces.followsABreak(window.parts(), opens));
}
} finally {
insetLeft = outerLeft;
insetRight = outerRight;
layerOffsetNotWritten = outerOffset;
}
owePendingSpacingAfter(node.margin().bottom() + node.padding().bottom());
if (closes) {
owePendingSpacingAfter(node.margin().bottom() + node.padding().bottom());
}
// A container that wrote something has had its top edge taken — by its first
// paragraph, or owed above its first table (see newTable) — and none of it is left
// waiting. A container that wrote nothing at all — empty, or holding nothing but a
Expand Down
Loading
Loading