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

### Public API

- **A DOCX export's report says a shape container's layers are written inline only where they
are.** Every shape container not written as a panel was noted "its layers are written inline,
one after another in source order", whatever it held. One whose layers are all drawing — a
tile, an icon, a badge holding its glyph — writes nothing in the flow: each layer is drawn
where the page draws it, in source order. The note is now written only where a layer is
written in the flow, as text, a picture or a band beside the drawing, or where the page stacks
the layers in another order than they are given, by their z-index. None of the corpus's 62 DOCX
changes; their reports go from 1332 notes to 1246, the 86
gone being 49 drawing-only containers, 22 badges and plates whose icon is drawn over them, and
15 containers composed in table cells their tables draw.
- **A DOCX export's report no longer names a cell's tile dropped while its table draws it, nor
corners drawn alike as unequal.** A table whose composed cells hold a box framing text — a
panel — leaves that box to the panel and draws its other cells' boxes; asked of the whole
Expand Down
6 changes: 5 additions & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,11 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
- **Shape containers → inline layers.** DOCX has no portable equivalent of
a graphics-state path clip, so the container's layers are written
inline, in source order, without clipping — again with one warning per
export. The report names the clip — a shape container's, and a layer
export, and a note on each container that writes a layer in the flow. A
container whose layers are all drawn — a tile, an icon, a badge holding
its glyph — writes none: each layer is drawn where the page draws it, in
source order, and it is not noted unless the page stacks its layers in
another order, by their z-index. The report names the clip — a shape container's, and a layer
stack's that clips to its bounds — wherever it cuts what the layers paint:
a square tile's corners in a disc, a label run past its chip, an ornament
set past a sidebar's side. One composed in a table's cell, which has no
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8839,11 +8839,6 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node,
writePanel(document, node, new ContainerPaint(node.fillColor(), bordersOf(null, node.stroke())));
return;
}
// The container's layers are written inline, in source order; a clip that cuts them is
// named where every container is written (reportClipCut), and a picture clipped to an
// ellipse takes the ellipse's shape.
report.add(DocxExportReport.Severity.APPROXIMATED, "shape container", layout.pathOf(node),
"its layers are written inline, one after another in source order");
// The outline itself is drawing — a badge's circle, a ring round a portrait — and is
// drawn where the page draws it, behind the layers now held in to where it sets them.
drawOutlineOf(node);
Expand All @@ -8866,6 +8861,7 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node,
}
try {
if (band != null) {
reportLayersWrittenInline(node);
writeOverlayBand(document, node, band);
return;
}
Expand All @@ -8891,10 +8887,17 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node,
}
if (blocksWritten == blocksBefore) {
carriedSpacingBefore = carriedFromOutside;
if (holdsMovedContent(node) || paintsOutOfSourceOrder(node)) {
// Its text, moved into an earlier layer's stand-in, is in the flow all the
// same; and drawings are drawn in source order, where the page stacks them
// by their z-index.
reportLayersWrittenInline(node);
}
} else {
carriedSpacingBefore = 0;
owePendingSpacingAfter(setIn[1] + node.padding().bottom() + node.margin().bottom());
hangBelowItsBox(node);
reportLayersWrittenInline(node);
}
} finally {
insetLeft = outerLeft;
Expand All @@ -8903,6 +8906,34 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node,
}
}

/**
* Names a shape container whose layers are written in the flow: one after another in source
* order, held in to where it sets them, rather than laid over its outline. A clip that cuts
* them is named where every container is written (reportClipCut), and a picture clipped to an
* ellipse takes the ellipse's shape. One that writes nothing — its layers all drawn where the
* page puts them, a tile or an icon — is not named, unless the page stacks its layers in
* another order than they are given ({@link #paintsOutOfSourceOrder}).
*/
private void reportLayersWrittenInline(ShapeContainerNode node) {
report.add(DocxExportReport.Severity.APPROXIMATED, "shape container", layout.pathOf(node),
"its layers are written inline, one after another in source order");
}

/**
* Whether the page paints a node's layers in another order than they are given, as their
* z-index sets them: the export writes and draws them in the order given.
*/
private static boolean paintsOutOfSourceOrder(DocumentNode node) {
List<DocumentNode> painted = inPaintOrder(node);
List<DocumentNode> given = node.children();
for (int index = 0; index < painted.size() && index < given.size(); index++) {
if (painted.get(index) != given.get(index)) {
return true;
}
}
return false;
}

/**
* How far a shape container sets its layers in from the top and the bottom of its content,
* where the layout placed them all on its page; {@code {0, 0}} for one the layout did not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,14 @@ void aPictureTheFlowIsWrittenRoundStaysInIt() throws Exception {
}
}

/** A filled square holding a white dot: a tile that only draws. */
private static com.demcha.compose.document.node.DocumentNode tile(String name) {
return new ShapeContainerBuilder().name(name).rectangle(40, 40).fillColor(ACCENT)
.center(new com.demcha.compose.document.dsl.EllipseBuilder().circle(12)
.fillColor(DocumentColor.WHITE).build())
.build();
}

/** A navy disc holding an 11pt glyph, as a section title's badge. */
private static com.demcha.compose.document.node.DocumentNode badge() {
return new ShapeContainerBuilder().name("Badge").circle(22).fillColor(ACCENT)
Expand Down Expand Up @@ -893,6 +901,58 @@ void theReportSaysWhatAShapeLosesInAPanelOrUnderATransform() throws Exception {
}
}

@Test
void aContainerWhoseLayersAreAllDrawnIsNotSaidToWriteThemInline() throws Exception {
// A tile and a badge holding its icon write nothing in the flow: each layer stands drawn
// where the page draws it. Text is written in the flow, one layer after another, or as a
// band beside the drawing it is laid by.
AtomicReference<DocxExportReport> report = new AtomicReference<>();
try (XWPFDocument document = export(report, session -> session.pageFlow(page -> page
.addLayerStack(stack -> stack.name("Stack")
.layer(tile("Tile"), LayerAlign.TOP_LEFT)
.layer(new ParagraphBuilder().text("Beside the tile").build(), LayerAlign.TOP_RIGHT))
.add(new ShapeContainerBuilder().name("Note").rectangle(160, 40).fillColor(ACCENT)
.center(new ParagraphBuilder().text("A line too long for a badge").build()).build())
.addContainer(band -> band.name("Header").rectangle(300, 30)
.clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
.centerLeft(badge())
.position(new ParagraphBuilder().name("Title").text("EXPERIENCE").build(),
34, 0, LayerAlign.CENTER_LEFT))
.addTable(table -> table.columns(com.demcha.compose.document.table.DocumentTableColumn.fixed(100),
com.demcha.compose.document.table.DocumentTableColumn.fixed(100))
.rowCells(com.demcha.compose.document.table.DocumentTableCell.node(tile("CellTile")),
com.demcha.compose.document.table.DocumentTableCell.text("Beside")))))) {

assertThat(anchors(document.getDocument().xmlText()))
.as("each tile's box and dot, the note's fill, the badge and its glyph").hasSize(7);
assertThat(report.get().notes()).filteredOn(note -> note.subject().equals("shape container"))
.extracting(DocxExportReport.Note::path)
.satisfiesExactlyInAnyOrder(path -> assertThat(path).matches(".*/Note\\[\\d+]"),
path -> assertThat(path).matches(".*/Header\\[\\d+]"));
}
}

@Test
void aContainerWhoseLayersThePageStacksInAnotherOrderIsStillNamed() throws Exception {
// The page lays the disc over the dot by their z-index; the export draws them in the
// order given, the dot over the disc.
AtomicReference<DocxExportReport> report = new AtomicReference<>();
try (XWPFDocument ignored = export(report, session -> session.pageFlow(page -> page
.addLayerStack(stack -> stack.name("Stack")
.layer(new ShapeContainerBuilder().name("Stacked").rectangle(40, 40)
.layer(new com.demcha.compose.document.dsl.EllipseBuilder().circle(36)
.fillColor(ACCENT).build(), LayerAlign.CENTER, 1)
.layer(new com.demcha.compose.document.dsl.EllipseBuilder().circle(8)
.fillColor(DocumentColor.WHITE).build(), LayerAlign.CENTER, 0)
.build(), LayerAlign.TOP_LEFT)
.layer(new ParagraphBuilder().text("Beside the stack").build(), LayerAlign.TOP_RIGHT))))) {

assertThat(report.get().notes()).filteredOn(note -> note.subject().equals("shape container"))
.extracting(DocxExportReport.Note::path)
.singleElement().asString().matches(".*/Stacked\\[\\d+]");
}
}

@Test
void aStarOutlineIsDrawnAsCustomGeometry() throws Exception {
AtomicReference<DocxExportReport> report = new AtomicReference<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ private record Entry(Fate fate, String note) {
+ "at the largest of them; a box "
+ "written as a panel in a table cell, its corners square; one no shape draws, or composed in a "
+ "table cell with nowhere to draw it, dropped",
"layers:REPORTED:written inline one after another in source order where it is written as its "
+ "layers; a panel's in its cell",
"layers:REPORTED:written inline one after another in source order where it writes them in the "
+ "flow, or stacked in another order than given; drawn where the page puts them, unreported, where "
+ "they are all drawing in source order; a panel's in its cell",
"clipPolicy:REPORTED:where it cuts what its layers paint; composed in a table cell, on its table",
"fillColor:REPORTED:its translucency, as a panel's, flattened against the colour under it; "
+ "drawn, its alpha is written; any other is written",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,57 @@ void aSubtitleSetInAChipMovesWithItsChip() throws Exception {
}
}

@Test
void aShapeContainerWhoseTextMovesIntoAStandInIsStillSaidToWriteItsLayersInline() throws Exception {
// The chip is taller than the stand-in and stays in its layer, writing nothing there; its
// subtitle lies inside the stand-in and is written in the strip: in the flow all the same.
double[] name = new double[2];
double[] subtitle = new double[2];
try (DocumentSession probe = session()) {
probe.pageFlow(page -> page.addSection("Probe", strip -> strip.padding(DocumentInsets.of(10))
.addParagraph(p -> p.name("Name").text(NAME).textStyle(TEXT))
.addParagraph(p -> p.name("Subtitle").text("Subtitle").textStyle(TEXT))));
for (PlacedNode node : probe.layoutGraph().nodes()) {
double[] box = "Name".equals(node.semanticName()) ? name
: "Subtitle".equals(node.semanticName()) ? subtitle : null;
if (box != null) {
box[0] = node.placementWidth();
box[1] = node.placementHeight();
}
}
}
java.util.concurrent.atomic.AtomicReference<DocxExportReport> report = new java.util.concurrent.atomic.AtomicReference<>();
try (DocumentSession session = session()) {
session.pageFlow(page -> page.addLayerStack(stack -> stack
.layer(column("Sidebar", 0, 260, sidebar -> sidebar
.addParagraph(p -> p.text("Contact").textStyle(TEXT))), LayerAlign.TOP_LEFT)
.layer(column("StripLayer", 100, 0, main -> main.addSection("Strip", strip -> strip
.fillColor(DocumentColor.rgb(240, 240, 236))
.padding(DocumentInsets.of(10))
.addParagraph(p -> p.name("Name").text(NAME).textStyle(TEXT))
.addSpacer(spacer -> spacer.name("SubtitlePlace")
.width(subtitle[0]).height(subtitle[1] + 20)))), LayerAlign.TOP_LEFT)
.layer(column("MainLayer", 100, 0, main -> {
main.addSection("Hero", hero -> hero.padding(new DocumentInsets(10, 10, 30, 10))
.addSpacer(spacer -> spacer.name("NamePlace").width(name[0]).height(name[1]))
.addContainer(chip -> chip.name("Chip").rectangle(subtitle[0] + 40, subtitle[1] + 30)
.fillColor(DocumentColor.rgb(26, 86, 148))
.center(new com.demcha.compose.document.dsl.ParagraphBuilder().name("Subtitle")
.text("Subtitle").textStyle(TEXT).build())));
main.addParagraph(p -> p.name("Body").text("Body").textStyle(TEXT));
}), LayerAlign.TOP_LEFT)));
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(
session.export(new DocxSemanticBackend(report::set))))) {
// The premise: written in the strip's panel, under the name, not in the chip's layer.
assertThat(cellHolding(document, NAME).getText()).contains("Subtitle");
assertThat(allText(document).split("Subtitle", -1)).as("written once").hasSize(2);
}
}

assertThat(report.get().notes()).filteredOn(note -> note.subject().equals("shape container"))
.extracting(DocxExportReport.Note::path).anyMatch(path -> path.contains("/Chip["));
}

private record Export(XWPFDocument document, List<PlacedNode> nodes) {
PlacedNode node(String name) {
return nodes.stream().filter(node -> name.equals(node.semanticName())).findFirst().orElseThrow();
Expand Down
Loading