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

### Public API

- **A DOCX export names a block Word may break inside a panel the layout moves it down in.** The
layout moves a block kept together, a layer stack or a shape container whole, or a block kept
with the next with it, to the next page as the page requires. Inside a panel the layout runs on
there, Word and LibreOffice break the panel's row where its lines run out and hold no keeps
across that break.
- Measured in both on Windows, inside a filled section, with nothing named:
- a stack of three lines, or a section kept together, moved whole to the next page, stood
with two of its lines at the foot of the page above;
- a heading kept with the line under it stood there too, the line on the next page.
- The report now names such a block (`APPROXIMATED`), including one opening a panel inside the
panel, as the outer row breaks before it. It is named once, not again for what it holds.
- A block moved whole that Word cannot split is not named: one line, one picture, a drawing, or
a row that may not split. A painted section kept together and a row holding a card were
measured moving whole, as the page sets them. One kept with the next is named all the same.
- None of the corpus's 62 DOCX changes, and none of its 1246 notes.
- **A DOCX export keeps a stack of one layer on the page the layout puts it on.** The layout moves
a layer stack to a new page whole, but Word was told nothing and broke between its paragraphs.
Measured in Word and LibreOffice on Windows, a stack of three lines that the page moved to the
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 @@ -588,8 +588,16 @@ How it lands:
whole right border plus half a point short of that edge, the points taken from its right
margin and no more than it holds, so its text keeps its place and width.
- **Keeping together.** A `keepTogether()` panel the layout placed on one page is a row
Word may not split. Anchors and keeps on the blocks inside a panel carry as they do
anywhere else.
Word may not split. Anchors on the blocks inside a panel carry as they do anywhere else, and
so do their keeps, but Word and LibreOffice do not hold them where the panel's row breaks over
a page: they break it where its lines run out (measured: a stack of three lines, or a section
kept together, that the layout moved whole to the next page inside a panel stood with two of
its lines at the foot of the page above, in both, and a heading kept with the line under it
stood there with that line on the next page). The report names a block kept together, a layer
stack, a shape container or a block kept with the next that the layout moves to a new page
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.
- **Measured.** Cards stacked in a row's columns, panels nested in its table, stand where the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
private String badgeText;
// The cell of the panel being written, painted or framed; null outside a panel.
private XWPFTableCell panelCell;
// The page the last block written straight into panelCell ended on, or into the panel holding
// it, -1 before the first: a block the layout keeps whole and moves to a new page inside the
// panel, Word may break (reportAKeepThePanelBreaks).
private int panelLastEndPage = -1;
// How much of the space above the first paragraph of panelCell its top border takes, in twips
// (writePanelPiece): no room to raise that paragraph's line into (raiseIntoTheSpaceAbove).
private long panelsFirstLineKeeps;
Expand Down Expand Up @@ -1097,6 +1101,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
overTheFlowDepth = 0;
oneLayerDepth = 0;
layerOffsetNotWritten = 0;
panelLastEndPage = -1;
roomHeldAround = 0;
openOverlays.clear();
// An earlier export's lowerings are its own document's.
Expand Down Expand Up @@ -3578,6 +3583,9 @@ private void writeNode(XWPFDocument document, DocumentNode node) throws Exceptio
if (placed != null && inTheBody) {
lastEndPage = Math.max(lastEndPage, placed.endPage());
}
if (placed != null && panelCell != null && currentCell == panelCell) {
panelLastEndPage = Math.max(panelLastEndPage, placed.endPage());
}
}
}

Expand All @@ -3588,9 +3596,10 @@ private void writePlacedNode(XWPFDocument document, DocumentNode node) throws Ex
// In a cell its paragraphs are its table's, which a row keeps or breaks on its own terms.
boolean keepTogether = (node.keepTogether() || currentCell == null && isOneLayer(node))
&& layout.onOnePage(node);
PanelBreak panelBreak = keepThePanelMayBreak(node);
boolean keepWithNext = node.keepWithNext() && layout.onOnePage(node);
String anchor = blockAnchorOf(node, inTheFlow(node, overlayDepth, oneLayerDepth));
if (!keepTogether && !keepWithNext && anchor == null) {
if (!keepTogether && !keepWithNext && anchor == null && panelBreak == null) {
writeNodeContent(document, node);
return;
}
Expand All @@ -3615,6 +3624,118 @@ private void writePlacedNode(XWPFDocument document, DocumentNode node) throws Ex
if (anchor != null) {
bookmarkAround(written, anchor);
}
if (panelBreak != null) {
reportAKeepThePanelBreaks(node, panelBreak, written);
}
}

/** How a block the layout moves to a new page inside a panel may be broken there in Word. */
private enum PanelBreak {
/** Moved whole, which Word may split over the panel's row break. */
WHOLE,
/** Moved with the block after it, which Word may leave behind. */
WITH_NEXT
}

/**
* How Word may break a block the layout moves to a new page inside a panel that runs on
* there, its row breaking with it, or {@code null} where it moves as the page moves it.
*
* <p>Word breaks a table's row where its lines run out and holds no keeps across that break:
* told to, it still set the first two lines of a stack of three, moved whole to the next page
* inside a panel, at the foot of the page above, and a heading kept with the line under it
* there with that line on the next page, in Word and LibreOffice alike (measured). So a block
* the layout keeps whole — kept together, a layer stack, a shape container — may be split, and
* one kept with the next left behind. What Word cannot split moves whole, and is not named
* ({@link #reportAKeepThePanelBreaks}): a painted section kept together, its own row one that
* may not split, as measured. A row in the panel's cell is the row's. A panel whose row Word
* keeps whole is on one page, and nothing in it opens another. A panel inside a panel is in
* the outer one's row, which breaks as well: a block opening it on a new page is a candidate
* too.</p>
*
* <p>A candidate stops what it holds from being one again: the block is named once.</p>
*/
private PanelBreak keepThePanelMayBreak(DocumentNode node) {
if (panelCell == null || currentCell != panelCell || panelLastEndPage < 0 || !layout.onOnePage(node)) {
return null;
}
boolean whole = node.keepTogether()
|| node instanceof com.demcha.compose.document.node.LayerStackNode
|| node instanceof ShapeContainerNode;
if (!whole && !node.keepWithNext()) {
return null;
}
com.demcha.compose.document.layout.PlacedNode placed = layout.placement(node);
if (placed == null || placed.startPage() <= panelLastEndPage) {
return null;
}
panelLastEndPage = placed.startPage();
return whole ? PanelBreak.WHOLE : PanelBreak.WITH_NEXT;
}

/**
* Names a block Word may break inside a panel ({@link #keepThePanelMayBreak}). One moved whole
* is named where it wrote more than one piece Word may set on another page — a paragraph, or a
* row that may split, by its tallest cell — and not where it is one line, one picture, a row
* that may not split, or nothing but a drawing: Word cannot split those. Whether Word keeps
* the lines of a single paragraph there, told to, is not measured, and one is not named.
*/
private void reportAKeepThePanelBreaks(DocumentNode node, PanelBreak panelBreak, List<IBodyElement> written) {
if (panelBreak == PanelBreak.WHOLE && piecesWordMaySplit(written) < 2) {
if (!node.keepWithNext()) {
return;
}
// Whole all the same, and kept with the next: Word may still leave it behind.
panelBreak = PanelBreak.WITH_NEXT;
}
report.add(DocxExportReport.Severity.APPROXIMATED, node.nodeKind(), layout.pathOf(node),
panelBreak == PanelBreak.WHOLE
? "the layout moves it to the next page whole, inside a panel that runs on there; Word breaks "
+ "the panel's row where its lines run out and holds no keeps across that break, so it may "
+ "set its first lines at the foot of the page above"
: "the layout moves it to the next page with the block after it, inside a panel that runs on "
+ "there; Word breaks the panel's row where its lines run out and holds no keeps across that "
+ "break, so it may leave it at the foot of the page above");
}

/**
* How many pieces of what a block wrote Word may set on different pages: a paragraph that
* shows something each — not the empty one closing a table in a cell, or a spacer's — a row
* that may not split one, and a row that may as many as its tallest cell holds paragraphs.
*/
private static int piecesWordMaySplit(List<IBodyElement> written) {
int pieces = 0;
for (IBodyElement element : written) {
if (element instanceof XWPFParagraph paragraph) {
if (showsSomething(paragraph)) {
pieces++;
}
} else if (element instanceof XWPFTable table) {
for (XWPFTableRow row : table.getRows()) {
int tallest = 1;
if (!row.isCantSplitRow()) {
for (XWPFTableCell cell : row.getTableCells()) {
tallest = Math.max(tallest, cell.getParagraphs().size());
}
}
pieces += tallest;
}
}
}
return pieces;
}

/** Whether a paragraph shows text, or a picture or a drawing in one of its runs. */
private static boolean showsSomething(XWPFParagraph paragraph) {
if (!paragraph.getText().isBlank()) {
return true;
}
for (XWPFRun run : paragraph.getRuns()) {
if (run.getCTR().sizeOfDrawingArray() > 0 || !run.getEmbeddedPictures().isEmpty()) {
return true;
}
}
return false;
}

/**
Expand Down Expand Up @@ -6931,6 +7052,14 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container
long outerMarginKept = panelsMarginKeeps;
boolean outerGives = panelsPaddingGives;
long outerRaised = panelRaisedFromItsPadding;
int outerLastEndPage = panelLastEndPage;
if (currentCell != outerPanelCell) {
// In any other cell than the outer panel's what opens its page is that cell's — a
// row's moves whole, and a column's stack is named as one — not the outer panel's. A
// panel kept together straight in the outer one is a candidate itself, which stops
// what it holds from being one (keepThePanelMayBreak).
panelLastEndPage = -1;
}
panelsFirstLineKeeps = Math.max(0, toTwips(topNotTaken) - cellMargin(cell, true));
// What the padding takes of it is no room either (paddingAbove). A shape's layers are
// set inside the outline it holds whole, and its first line rises into nothing of it.
Expand All @@ -6949,6 +7078,7 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container
panelsMarginKeeps = outerMarginKept;
panelsPaddingGives = outerGives;
panelRaisedFromItsPadding = outerRaised;
panelLastEndPage = outerLastEndPage;
}
if (cell.getParagraphs().isEmpty()) {
// A panel with nothing Word can hold inside is its padding tall on the page, not a
Expand Down
Loading
Loading