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
14 changes: 14 additions & 0 deletions 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 DOCX export sets a layer across where its offset moves it.** Measured in Word and
LibreOffice on Windows, a stack's one layer moved 40pt in stood at the stack's left edge in
both. A band's layer the page sets 20pt left of the stack stood at the stack's edge, the page
margin, in both. Neither was named.
- A layer is now held to its box on both sides, past the stack's edge where the page sets it
there, by an indent below zero. Both editors set the two layers where the page does, and a
stack's one layer of three lines moved 40pt in breaks at the page's words in both.
- A table cell's text starts no further left than the cell's own. There a layer stands left only
as far as the indents round it give, and the rest is named (`layer offset`). A stack's one
layer moved across in a table cell is written where the stack starts, and named. So is one
composed in a table's cell, where the stack has no place of its own in the layout.
- An offset down or up moves a stack's one layer without making room for it, which Word's flow
cannot do. It is written where the stack starts, as before, and now named (`layer offset`),
except where a band sets it at the page's distance below what is above it.
- **A DOCX export stacks its drawings as the page paints them.** Word stacks drawings by the
place each is given (`wp:anchor/@relativeHeight`), a later one over an earlier one. The export
gave them their places in the order it drew them. In the flow it draws a stack's or a shape
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ honour an option ignores it (documented contract).
| Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) |
| Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a |
| Keep a block on one page (`keepTogether()`, `keepWithNext()`) | ✅ resolved by `LayoutCompiler` before any backend runs | ✅ same — the slides are the laid-out pages | ✅ `DocxSemanticBackend.keepOnOnePage` — Word re-paginates, so a block the layout placed on one page is told to stay there: `w:keepLines` on each of its paragraphs and `w:keepNext` on every one but the last (on the last too for `keepWithNext`), a table inside it chained row by row. A block that ran over a page break in the layout is taller than a page and is left to flow, as the layout left it. A body table of one row kept with what follows, its cells holding paragraphs alone, is written with a row that may break (`letTheOneRowTablesKeptWithTheNextBreak`): LibreOffice reads a one-row table whose row may not break as one that may not split, and keeps such a table with nothing |
| Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it. A layer the page starts above where those before it end — a caption over its photo, a label in a card's corner — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured |
| Layer stack — layers drawn over one another (`addLayerStack`) | ✅ each layer's fragments at the place the layout gave it | ✅ same | ⚠️ `DocxSemanticBackend` — Word has no layers, so a stack's children are written one after the other, without their positions. The exception is a stack whose layers are side-by-side columns (`DocxLayerColumns`): every layer a plain container at the stack's top-left corner, the bands their padding leaves either the same or apart. That is written as one table row, a cell per band, the way a two-column CV lays its columns out as layers to draw the name first. Layers sharing a band follow one another in its cell. A spacer the layout placed level with content of another layer of the same band only keeps that content's place, and is not written. The space above a later layer's first block is the gap the page shows below the content before it. Each layer is held to its box across, past the stack's edge where the page sets it past it, by an indent below zero; in a table cell no further left than the indents round it give, the rest named. The one layer of a stack of one layer is held the same way, its offset across kept and its measure with it; in a table cell, composed in a table's cell with no place of its own, and for an offset down or up, which moves it without making room for it, it is written where the stack starts and named — not where a band or a column's layer sets it at the page's distance below what is above it, which holds the offset. A layer the page starts above where those before it end — a caption over its photo, a label in a card's corner — is written under them, in a column's cell as in a band, and the report names it; a stack composed in a table cell, or exported with no layout, has no place of its own, and one writing more than one of its layers is named with them not measured. What the page draws inside another layer's fill comes after that fill. Drawings stack as the page paints them, by their z-index, each given its place (`wp:anchor/@relativeHeight`) from where the page paints its fragment, whatever order the layers are written in — among the drawings behind the text, and among those in front of it, as Word draws every one behind under every one in front. Measured in LibreOffice: `CharcoalGold`, `SidebarPortrait` and `SlateOrange` fit on one page, as on the page; `NavySidebar` runs one line onto a second page. A stack or shape container the page gives no room — its margins taking back its whole height, as `LumaStudioInvoice`'s sidebar over the page's top margin — is laid over the flow (`laidOverTheFlow`): its drawings where the page draws them, each paragraph in a text box in front of the text where the page sets it, nothing in the flow. Not in a table cell or a panel, and not when it holds anything but drawing and plain paragraphs — a link, an anchor, a picture, a list or a table keeps it in the flow as above. A canvas is written as its contents — what it writes one block after another, its drawings where it places them — and holds its height: the room under what it writes is the space below it, and one that only draws holds its whole box where nothing round it — a band before its layer's first block, drawings held whole, an overlay it is a layer of — measures that room already. The report names the places and the width it loses, as it does a column's fixed width narrower than its band, and what it writes running past its height, a drawing in what it writes that takes no room in Word, or a height not measured |

## Output surface and lifecycle

Expand Down
19 changes: 18 additions & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,12 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
where the lines its table laid out for it stand and a picture where the alignment sets
one of its written width; any other block there, or one inside a panel or another table
in its cell, is written from the cell's left and named, a drawing aside, which its table
draws where the page draws it. Drawing is drawn as a shape where the
draws where the page draws it. A layer the page sets past the stack's edge stands out
past it as far, by an indent below zero, its box's own width and no more: a stack at the
page margin with a layer offset 20pt left of it has that layer in the margin. In a table
cell, whose text starts no further left than the cell's own, it stands left only as far
as the indents round it give, and the report names the rest (`layer offset`); its right
side stays at the edge. Drawing is drawn as a shape where the
page draws it (see "What is skipped") and does not count as content. A later layer
the page starts above where the layers before it end — a caption over the foot of
its photo, a label in a card's corner, text over text — is written under them, as
Expand All @@ -869,6 +874,18 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
them writes in the flow, its note says whether the page lays one over another is not
measured. A layer the page starts under the layers before it, but nearer them than
its own margin above it, is written that margin below them, unnamed.
- **A layer stack of one layer → its layer, where the page sets it across.** It lays
nothing over anything and is written as its contents, in the flow. Its layer is held
to its box in the stack, as a band's layer is: an offset across moves it whole, past
the stack's other edge as far, and it keeps the page's measure, its lines breaking at
the page's words. In a table cell, whose text Word holds inside the cell, it is written
where the stack starts; so it is composed in a table's cell, where the stack has no place
of its own in the layout and its offset is named as given. An offset down or up moves
the layer without making room for it, over what follows the stack or what comes before
it, which Word's flow cannot do: it is written where the stack starts. The report names
each (`layer offset`), but not a layer that only draws, which is drawn where the page
draws it, nor one written where a band or a layer of a column sets the next block at the
page's distance below what is above it: that distance holds the offset.
- **Text at the left and the right of one band → one line split by a tab.** A
container or a layer stack holding exactly two single-line paragraphs, level with
one another and apart across the page — a CV entry's title and its dates — is one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7333,6 +7333,14 @@ private void writeContainerBody(XWPFDocument document, DocumentNode node) throws
// alignment puts it: a portrait centred in a sidebar stood at the sidebar's left edge.
if (node instanceof com.demcha.compose.document.node.AlignNode align) {
placeAcross(align, align.child());
} else if (isOneLayer(node)) {
// So does a stack's one layer, where the page sets it in the stack: a line moved 40pt in
// stood at the stack's left edge. Moved whole, it runs as far past the stack's other
// side, which a table cell's text does not: there it is written where the stack starts.
if (currentCell == null) {
placeAcross(node, node.children().get(0));
}
reportTheLayerMoved(node);
}
try {
writeChildren(document, node.children(), spacingOf(node), followedInFlow.remove(node));
Expand Down Expand Up @@ -16394,6 +16402,57 @@ private void placeAcross(DocumentNode stack, DocumentNode layer) {
holdIn(layer, placed.placementX(), placed.placementWidth(), contentLeft, contentRight);
}

/**
* Names the one layer of a stack whose offset Word does not write, which it writes where the
* stack starts.
*
* <ul>
* <li>Lower or higher than the stack starts: an offset moves a layer without making room for
* it, so the page sets it over what follows the stack or over what comes before it, which
* the flow cannot. Not where a band or a column sets the page's distance down to the next
* block written (resumeSpacing): measured to the layer's first block where the page sets
* it, the offset is written in that distance.</li>
* <li>Left or right of the stack's edge, in a table cell (writeContainerBody).</li>
* <li>Where it has no place of its own in the layout — composed in a table cell — by its
* offset as given. With no layout behind the export, the document's note says so.</li>
* </ul>
*/
private void reportTheLayerMoved(DocumentNode stack) {
DocumentNode layer = stack.children().get(0);
if (onlyDrawn(layer)) {
return;
}
com.demcha.compose.document.layout.PlacedNode box = layout.placement(stack);
com.demcha.compose.document.layout.PlacedNode placed = layout.placement(layer);
if (box == null || placed == null) {
com.demcha.compose.document.node.LayerStackNode.Layer given =
((com.demcha.compose.document.node.LayerStackNode) stack).layers().get(0);
if (!layout.isEmpty() && (Math.abs(given.offsetX()) > 0.5 || Math.abs(given.offsetY()) > 0.5)) {
report.add(DocxExportReport.Severity.APPROXIMATED, "layer offset", layout.pathOf(layer),
"with no place of its own in the layout, it is written where the stack starts, its offset of "
+ pointsOf(given.offsetX()) + "pt across and " + pointsOf(given.offsetY()) + "pt down not "
+ "written");
}
return;
}
List<String> moved = new ArrayList<>();
double across = placed.placementX() - layer.margin().left() - (box.placementX() + stack.padding().left());
if (currentCell != null && Math.abs(across) > 0.5) {
moved.add(pointsOf(Math.abs(across)) + "pt " + (across > 0 ? "right" : "left")
+ " of the stack's edge, in a table cell, whose text Word holds inside it");
}
double lower = box.placementY() + box.placementHeight() - stack.padding().top() - layer.margin().top()
- (placed.placementY() + placed.placementHeight());
if (Double.isNaN(resumeSpacing) && Math.abs(lower) > 0.5) {
moved.add(pointsOf(Math.abs(lower)) + "pt " + (lower > 0 ? "lower" : "higher")
+ " than its stack starts, without making room for it");
}
if (!moved.isEmpty()) {
report.add(DocxExportReport.Severity.APPROXIMATED, "layer offset", layout.pathOf(layer),
"the page sets it " + String.join(", and ", moved) + "; Word writes it where the stack starts");
}
}

/**
* Holds an alignment's child in across where the page puts it in a table's cell built from a
* node, where neither has a place of its own in the layout.
Expand Down Expand Up @@ -16508,7 +16567,8 @@ private boolean standsInTheComposedCell(DocxLayoutMetrics.TextBox lines) {

/**
* Holds a block in across, from the edges it is written between to where the layout placed
* it, with room to spare for a paragraph on the side its text does not lean on.
* it, with room to spare for a paragraph on the side its text does not lean on; out past an
* edge where the layout placed it past it.
*
* @param layer the block
* @param placedX the page position of the left edge of its box, where the layout put it
Expand All @@ -16520,7 +16580,27 @@ private void holdIn(DocumentNode layer, double placedX, double placedWidth,
double contentLeft, double contentRight) {
double left = placedX - layer.margin().left() - contentLeft;
double right = contentRight - (placedX + placedWidth + layer.margin().right());
if (!(left > 0.5) && !(right > 0.5)) {
// A block the page sets past an edge stands out past it as far, where a paragraph's indent
// and a table's take a side below zero: a band's layer set left of the stack stands in the
// margin, and a layer moved whole keeps the page's measure, its lines breaking where the
// page breaks them. A cell's text starts no further left than the cell's own (applyInset):
// there a block stands left only as far as the indents round it give, and what it cannot is
// named. Its right side is held at the edge, as a line the page sets past it is given its
// room by the paragraph itself (letTheLineStandOut).
double pastLeft = left < -0.5 ? left : 0;
double pastRight = right < -0.5 && currentCell == null ? right : 0;
double floor = currentCell != null ? -Math.max(0, insetLeft + cellTextShift) : Double.NEGATIVE_INFINITY;
if (pastLeft - floor < -0.5) {
// A drawing is drawn where the page draws it, whatever the indents.
if (!onlyDrawn(layer)) {
report.add(DocxExportReport.Severity.APPROXIMATED, "layer offset", layout.pathOf(layer),
"the page sets it " + pointsOf(-pastLeft) + "pt left of the edge it is written from; "
+ "in its table cell " + (floor < -0.5 ? "it stands only " + pointsOf(-floor)
+ "pt left of that edge" : "it is written at that edge"));
}
pastLeft = floor;
}
if (!(left > 0.5) && !(right > 0.5) && pastLeft == 0 && pastRight == 0) {
return;
}
// Only a paragraph's own box needs room to spare: a container's paragraphs take theirs
Expand All @@ -16535,8 +16615,10 @@ private void holdIn(DocumentNode layer, double placedX, double placedWidth,
} else {
right -= slack;
}
insetLeft += Math.max(0, left);
insetRight += Math.max(0, right);
// Past an edge it keeps its box's own: room to spare there widened a layer moved 40pt in by
// 12.8pt, and its three lines took two in Word and in LibreOffice.
insetLeft += pastLeft < 0 ? pastLeft : Math.max(0, left);
insetRight += pastRight < 0 ? pastRight : Math.max(0, right);
}

/**
Expand Down
Loading
Loading