diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07796361d..0a505cd83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,15 @@ Entries land here as they merge.
### Public API
+- **Block-level horizontal alignment** (`@since 1.8.0`). Fixed-size flow
+ children (paths, images, SVG icons, barcodes, shape containers) left-align
+ by default — there was no built-in way to centre or right-align one without
+ wrapping it in a full-width container and hand-computing the content width.
+ New `AlignNode` + `HorizontalAlign` (LEFT / CENTER / RIGHT) seat any node
+ across the available width: `flow.addAligned(HorizontalAlign.CENTER, node)`
+ and the icon sugar `flow.addSvgIcon(icon, width, HorizontalAlign.CENTER)`.
+ The wrapper fills the width and reuses the stack placement engine (one
+ anchor), so there is no new render handler and no hot-path change.
- **Native vector charts** (`@since 1.8.0`). New `com.demcha.compose.document.chart`
package with a layered, serialization-friendly API: `ChartData` (categories +
series, type/colour-agnostic), sealed `ChartSpec` (`bar()` / `line()` with
diff --git a/assets/readme/examples/block-align.pdf b/assets/readme/examples/block-align.pdf
new file mode 100644
index 000000000..5a3ec4181
Binary files /dev/null and b/assets/readme/examples/block-align.pdf differ
diff --git a/examples/README.md b/examples/README.md
index 35b702b03..11f7dd1f6 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -15,7 +15,7 @@ Install the library artifact once from the repository root:
./mvnw -DskipTests install
```
-Then run all 54 examples in one shot:
+Then run all 55 examples in one shot:
```bash
./mvnw -f examples/pom.xml exec:java \
@@ -32,7 +32,7 @@ Then run all 54 examples in one shot:
Generated PDFs land in `examples/target/generated-pdfs/`. The same
`mvnw.cmd` form works on Windows PowerShell with backslash paths.
-`GenerateAllExamples` runs **54** example programs — 16 CV + 15
+`GenerateAllExamples` runs **55** example programs — 16 CV + 15
cover-letter presets plus invoices, proposals, a schedule, the feature
demos, and the flagships. The showcase site surfaces the full generated
catalogue (~53 PDFs); a curated 28-PDF subset is committed under
@@ -73,6 +73,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [Composed table cells](#composed-table-cells-v16) | `DocumentTableCell.node(DocumentNode)` — paragraphs, lists, sub-tables inside cells with two-pass measurement | [PDF](../assets/readme/examples/composed-table-cell-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java) |
| [Canvas layer (free placement)](#canvas-layer-v16) | `CanvasLayerNode` — pixel-precise `(x, y)` placement of children inside a fixed bounding box, with `ClipPolicy` clipping | [PDF](../assets/readme/examples/canvas-layer-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/canvas/CanvasLayerExample.java) |
| [Transforms](#transforms) | `rotate`, `scale`, and per-layer `zIndex` swap | [PDF](../assets/readme/examples/transforms.pdf) · [Source](src/main/java/com/demcha/examples/features/transforms/TransformsExample.java) |
+| [Block alignment](#block-alignment) | `addAligned(align, node)` / `addSvgIcon(icon, w, align)` — seat any fixed-size node left / centre / right across the content width | [PDF](../assets/readme/examples/block-align.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java) |
### 📋 Templates recommended
@@ -396,6 +397,22 @@ flow.addSvgIcon(SvgIcon.parse(readResource("/icons/apple.svg")), 50);
[📄 View PDF](../assets/readme/examples/svg-icon-gallery.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/svg/SvgIconGalleryExample.java)
+### Block alignment
+
+A fixed-size node (an SVG icon, a vector path, an image) left-aligns in
+the flow by default. `addSvgIcon(icon, width, HorizontalAlign.CENTER)` and
+the general `addAligned(align, node)` seat it left, centre, or right across
+the content width — the `margin: auto` the flow does not give fixed nodes on
+its own, with no manual width maths.
+
+```java
+flow.addSvgIcon(icon, 44, HorizontalAlign.CENTER);
+flow.addAligned(HorizontalAlign.RIGHT, anyFixedNode);
+```
+
+[📄 View PDF](../assets/readme/examples/block-align.pdf) ·
+[📜 Full source](src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java)
+
### Advanced tables
`DocumentTableCell.rowSpan(int)` mirrors `colSpan(int)`.
diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
index 19697f813..abb959b3d 100644
--- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
+++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
@@ -6,6 +6,7 @@
import com.demcha.examples.features.debug.DebugOverlayExample;
import com.demcha.examples.features.docx.WordExportExample;
import com.demcha.examples.features.chrome.PdfChromeExample;
+import com.demcha.examples.features.layout.BlockAlignExample;
import com.demcha.examples.features.lists.NestedListExample;
import com.demcha.examples.features.shapes.ShapeContainerExample;
import com.demcha.examples.features.shapes.VectorPathExample;
@@ -132,6 +133,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + ShapeContainerExample.generate());
System.out.println("Generated: " + VectorPathExample.generate());
System.out.println("Generated: " + SvgIconGalleryExample.generate());
+ System.out.println("Generated: " + BlockAlignExample.generate());
System.out.println("Generated: " + TransformsExample.generate());
System.out.println("Generated: " + TableAdvancedExample.generate());
diff --git a/examples/src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java b/examples/src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java
new file mode 100644
index 000000000..7e1d09ccd
--- /dev/null
+++ b/examples/src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java
@@ -0,0 +1,109 @@
+package com.demcha.examples.features.layout;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.node.HorizontalAlign;
+import com.demcha.compose.document.style.DocumentColor;
+import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentStroke;
+import com.demcha.compose.document.style.DocumentTextStyle;
+import com.demcha.compose.document.svg.SvgIcon;
+import com.demcha.examples.support.ExampleOutputPaths;
+
+import java.nio.file.Path;
+
+/**
+ * Runnable showcase for v1.8 block-level horizontal alignment: a fixed-size
+ * node (an SVG icon, a vector path, …) placed LEFT / CENTER / RIGHT within the
+ * page content width with one call — the {@code margin: auto} the flow does not
+ * give fixed nodes on its own.
+ *
+ *
+ *
+ * @author Artem Demchyshyn
+ */
+public final class BlockAlignExample {
+
+ private static final DocumentColor INK = DocumentColor.rgb(34, 38, 50);
+ private static final DocumentColor TEAL = DocumentColor.rgb(20, 80, 95);
+
+ /** Inline two-tone badge so the example needs no icon resource. */
+ private static final String BADGE_SVG = """
+
+ """;
+
+ private BlockAlignExample() {
+ }
+
+ /**
+ * Renders the alignment sheet: one icon and one path, each shown
+ * left / centre / right aligned.
+ *
+ * @return path to the generated PDF
+ * @throws Exception if rendering or file IO fails
+ */
+ public static Path generate() throws Exception {
+ Path pdfFile = ExampleOutputPaths.prepare("features/layout", "block-align.pdf");
+
+ SvgIcon icon = SvgIcon.parse(BADGE_SVG);
+ DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(10).withColor(INK);
+
+ try (DocumentSession document = GraphCompose.document(pdfFile)
+ .pageSize(420, 400)
+ .margin(DocumentInsets.of(28))
+ .create()) {
+ document.pageFlow(page -> {
+ page.addParagraph(p -> p
+ .text("Block alignment")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(20)));
+ page.addParagraph(p -> p
+ .text("A fixed-size node left-aligns in the flow by default. "
+ + "addSvgIcon(icon, w, align) / addAligned(align, node) seats it "
+ + "left, centre, or right across the content width — no manual maths.")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(9.5)
+ .withColor(DocumentColor.rgb(90, 96, 105)))
+ .padding(DocumentInsets.bottom(8)));
+
+ for (HorizontalAlign align : HorizontalAlign.values()) {
+ page.addParagraph(p -> p
+ .text("addSvgIcon(icon, 44, HorizontalAlign." + align + ")")
+ .textStyle(caption)
+ .padding(DocumentInsets.top(6)));
+ page.addSvgIcon(icon, 44, align);
+ }
+
+ page.addParagraph(p -> p
+ .text("addAligned(CENTER, anyNode) — works for any fixed node, e.g. a path")
+ .textStyle(caption)
+ .padding(DocumentInsets.top(10)));
+ page.addAligned(HorizontalAlign.CENTER, chevron());
+ });
+
+ document.buildPdf();
+ }
+
+ return pdfFile;
+ }
+
+ /** A small stroked chevron path to show alignment is not icon-specific. */
+ private static com.demcha.compose.document.node.DocumentNode chevron() {
+ return new com.demcha.compose.document.dsl.PathBuilder()
+ .name("Chevron")
+ .size(60, 30)
+ .moveTo(0.0, 1.0).lineTo(0.5, 0.0).lineTo(1.0, 1.0)
+ .stroke(DocumentStroke.of(TEAL, 4))
+ .lineCap(com.demcha.compose.document.style.DocumentLineCap.ROUND)
+ .lineJoin(com.demcha.compose.document.style.DocumentLineJoin.ROUND)
+ .build();
+ }
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("Generated: " + generate());
+ }
+}
diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java
index 14ed92b21..f866741f4 100644
--- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java
+++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java
@@ -97,6 +97,7 @@ record Entry(String title, String description, List tags, String codeUrl
feature("shapes", "shape-container", "Shape-as-Container", "Rounded rect, ellipse, circle containers with ClipPolicy and layered children.", "shapes", "clip");
feature("svg", "svg-icon-gallery", "SVG Icon Gallery", "34 real-world multicolour svgrepo icons through SvgIcon.parse — native vector layers, the whole set 156 KB of sources.", "svg", "icons", "v1.8");
feature("shapes", "vector-path", "Vector Paths (Bézier)", "addPath(...) — free-form design shapes with native cubic Bézier curves: stroked waves, filled blobs, mixed line/curve ribbons. No tessellation.", "shapes", "bezier", "v1.8");
+ feature("layout", "block-align", "Block Alignment", "addAligned(align, node) / addSvgIcon(icon, w, align) — seat any fixed-size node left / centre / right across the content width.", "layout", "align", "v1.8");
feature("transforms", "transforms", "Layers + Transforms", "rotate / scale on every leaf builder + LayerStack with explicit z-index.", "transforms", "layers");
feature("text", "rich-text-showcase", "Rich Text", "Inline runs with bold / italic / colour / link options, markdown parsing.", "text", "rich");
feature("text", "section-presets", "Section Presets", "Pre-baked section bands, accent strips, soft panels for templates.", "text", "sections");
@@ -145,6 +146,7 @@ static String groupLabel(String category, String group) {
case "features/streaming" -> "Streaming & I/O";
case "features/snapshots" -> "Snapshot Testing";
case "features/svg" -> "SVG Import";
+ case "features/layout" -> "Layout & Alignment";
case "features/debug" -> "Debug & Diagnostics";
case "flagships/default" -> "Flagship Demos";
default -> capitalize(group);
diff --git a/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java b/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
index 8f7a23bd9..71dd47b56 100644
--- a/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
@@ -559,6 +559,42 @@ public T addSvgIcon(com.demcha.compose.document.svg.SvgIcon icon, double width)
return add(icon.node(width));
}
+ /**
+ * Adds a multi-layer SVG icon at the given width, horizontally aligned
+ * within the available content width — the one-call way to centre or
+ * right-align an icon on the page.
+ *
+ * @param icon parsed SVG icon
+ * @param width target icon width in points
+ * @param align horizontal placement within the available width
+ * @return this builder
+ * @since 1.8.0
+ */
+ @com.demcha.compose.document.api.Beta
+ public T addSvgIcon(com.demcha.compose.document.svg.SvgIcon icon, double width,
+ com.demcha.compose.document.node.HorizontalAlign align) {
+ Objects.requireNonNull(icon, "icon");
+ return addAligned(align, icon.node(width));
+ }
+
+ /**
+ * Adds a node positioned horizontally within the full available content
+ * width — the block-level {@code margin: auto} / {@code align(center)} for
+ * any fixed-size node (path, image, icon, barcode, shape container) that
+ * would otherwise left-align in the flow.
+ *
+ * @param align horizontal placement
+ * @param node the node to position
+ * @return this builder
+ * @since 1.8.0
+ */
+ public T addAligned(com.demcha.compose.document.node.HorizontalAlign align,
+ com.demcha.compose.document.node.DocumentNode node) {
+ Objects.requireNonNull(align, "align");
+ Objects.requireNonNull(node, "node");
+ return add(new com.demcha.compose.document.node.AlignNode(node, align));
+ }
+
/**
* Adds a filled circle ellipse — shortcut for
* {@code addEllipse(e -> e.circle(diameter).fillColor(fillColor))}.
diff --git a/src/main/java/com/demcha/compose/document/layout/BuiltInNodeDefinitions.java b/src/main/java/com/demcha/compose/document/layout/BuiltInNodeDefinitions.java
index f1a8ac475..7afe5dcc2 100644
--- a/src/main/java/com/demcha/compose/document/layout/BuiltInNodeDefinitions.java
+++ b/src/main/java/com/demcha/compose/document/layout/BuiltInNodeDefinitions.java
@@ -44,6 +44,7 @@ public static NodeRegistry registerDefaults(NodeRegistry registry) {
.register(new CanvasLayerDefinition())
.register(new PolygonDefinition())
.register(new PathDefinition())
+ .register(new AlignDefinition())
.register(new ChartDefinition());
}
}
diff --git a/src/main/java/com/demcha/compose/document/layout/definitions/AlignDefinition.java b/src/main/java/com/demcha/compose/document/layout/definitions/AlignDefinition.java
new file mode 100644
index 000000000..29cfd5a47
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/layout/definitions/AlignDefinition.java
@@ -0,0 +1,84 @@
+package com.demcha.compose.document.layout.definitions;
+
+import com.demcha.compose.document.layout.*;
+import com.demcha.compose.document.layout.payloads.PreparedStackLayout;
+import com.demcha.compose.document.node.AlignNode;
+import com.demcha.compose.document.node.DocumentNode;
+import com.demcha.compose.document.node.HorizontalAlign;
+import com.demcha.compose.document.node.LayerAlign;
+
+import java.util.List;
+
+/**
+ * Layout definition for {@link AlignNode}: a wrapper that fills the available
+ * content width and seats its single child left / centre / right. It reuses
+ * the stack placement machinery (one layer, one anchor) — the only difference
+ * from a {@code LayerStackNode} is that the box is measured to the full
+ * available width instead of shrink-wrapping to the child, which is exactly
+ * what makes horizontal alignment visible against the page.
+ *
+ * @author Artem Demchyshyn
+ * @since 1.8.0
+ */
+public final class AlignDefinition implements NodeDefinition {
+
+ /**
+ * Creates the align layout definition.
+ */
+ public AlignDefinition() {
+ }
+
+ @Override
+ public Class nodeType() {
+ return AlignNode.class;
+ }
+
+ @Override
+ public PreparedNode prepare(AlignNode node, PrepareContext ctx, BoxConstraints constraints) {
+ DocumentNode child = node.child();
+ double childInner = Math.max(0.0, constraints.availableWidth() - child.margin().horizontal());
+ PreparedNode childPrepared = ctx.prepare(child, BoxConstraints.natural(childInner));
+ double height = childPrepared.measureResult().height() + child.margin().vertical();
+ // Fill the width (so the anchor has room to centre / right-align);
+ // height tracks the child so the wrapper adds no vertical space.
+ return PreparedNode.composite(
+ node,
+ new MeasureResult(constraints.availableWidth(), height),
+ new PreparedStackLayout(
+ List.of(toAnchor(node.align())),
+ List.of(0.0),
+ List.of(0.0),
+ List.of(0)),
+ new CompositeLayoutSpec(0.0, CompositeLayoutSpec.Axis.STACK));
+ }
+
+ /**
+ * Maps a block alignment to the top-seated stack anchor. The wrapper's box
+ * height equals the child's, so the vertical band of the anchor is moot;
+ * only the horizontal placement matters.
+ */
+ private static LayerAlign toAnchor(HorizontalAlign align) {
+ return switch (align) {
+ case LEFT -> LayerAlign.TOP_LEFT;
+ case CENTER -> LayerAlign.TOP_CENTER;
+ case RIGHT -> LayerAlign.TOP_RIGHT;
+ };
+ }
+
+ @Override
+ public PaginationPolicy paginationPolicy(AlignNode node) {
+ return PaginationPolicy.ATOMIC;
+ }
+
+ @Override
+ public List children(AlignNode node) {
+ return node.children();
+ }
+
+ @Override
+ public List emitFragments(PreparedNode prepared,
+ FragmentContext ctx,
+ FragmentPlacement placement) {
+ return List.of();
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/node/AlignNode.java b/src/main/java/com/demcha/compose/document/node/AlignNode.java
new file mode 100644
index 000000000..e7178afed
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/node/AlignNode.java
@@ -0,0 +1,58 @@
+package com.demcha.compose.document.node;
+
+import com.demcha.compose.document.style.DocumentInsets;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Positions a single fixed-size child horizontally within the full available
+ * content width — the block-level {@code margin: auto} / {@code align(center)}
+ * the flow does not provide for non-width-filling nodes. The wrapper fills the
+ * width and its height equals the child's; the child seats {@link
+ * HorizontalAlign#LEFT left}, {@link HorizontalAlign#CENTER centre}, or
+ * {@link HorizontalAlign#RIGHT right} inside it.
+ *
+ *
Use it to centre an SVG icon, a path, an image, or a barcode on the page
+ * without hand-computing the content width:
+ * {@code flow.addAligned(HorizontalAlign.CENTER, icon.node(48))}.
+ *
+ * @param name node name used in snapshots and layout graph paths
+ * @param child the node to position
+ * @param align horizontal placement within the available width
+ * @param margin outer margin around the wrapper
+ * @author Artem Demchyshyn
+ * @since 1.8.0
+ */
+public record AlignNode(String name, DocumentNode child, HorizontalAlign align, DocumentInsets margin)
+ implements DocumentNode {
+ /**
+ * Validates the child and alignment; normalizes name and margin.
+ */
+ public AlignNode {
+ name = name == null ? "" : name;
+ Objects.requireNonNull(child, "child");
+ Objects.requireNonNull(align, "align");
+ margin = margin == null ? DocumentInsets.zero() : margin;
+ }
+
+ /**
+ * Creates an align wrapper with no margin.
+ *
+ * @param child the node to position
+ * @param align horizontal placement
+ */
+ public AlignNode(DocumentNode child, HorizontalAlign align) {
+ this("", child, align, DocumentInsets.zero());
+ }
+
+ @Override
+ public List children() {
+ return List.of(child);
+ }
+
+ @Override
+ public String nodeKind() {
+ return "Align";
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/node/HorizontalAlign.java b/src/main/java/com/demcha/compose/document/node/HorizontalAlign.java
new file mode 100644
index 000000000..f42f0e891
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/node/HorizontalAlign.java
@@ -0,0 +1,19 @@
+package com.demcha.compose.document.node;
+
+/**
+ * Horizontal placement of a fixed-size block within the available content
+ * width of a flow — the {@code margin: auto} / {@code align(center)} analogue
+ * for nodes that do not fill the width on their own (paths, images, icons,
+ * barcodes, shape containers).
+ *
+ * @author Artem Demchyshyn
+ * @since 1.8.0
+ */
+public enum HorizontalAlign {
+ /** Flush with the left edge (the flow default). */
+ LEFT,
+ /** Centred in the available width. */
+ CENTER,
+ /** Flush with the right edge. */
+ RIGHT
+}
diff --git a/src/test/java/com/demcha/compose/document/dsl/BlockAlignTest.java b/src/test/java/com/demcha/compose/document/dsl/BlockAlignTest.java
new file mode 100644
index 000000000..232a6a6d1
--- /dev/null
+++ b/src/test/java/com/demcha/compose/document/dsl/BlockAlignTest.java
@@ -0,0 +1,110 @@
+package com.demcha.compose.document.dsl;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.layout.LayoutGraph;
+import com.demcha.compose.document.layout.PlacedNode;
+import com.demcha.compose.document.node.HorizontalAlign;
+import com.demcha.compose.document.style.DocumentColor;
+import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentStroke;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+
+/**
+ * Block-level horizontal alignment: a fixed-size node wrapped in
+ * {@link com.demcha.compose.document.node.AlignNode} seats LEFT / CENTER /
+ * RIGHT within the full content width, where the bare flow would always
+ * left-align it.
+ */
+class BlockAlignTest {
+
+ private static final double EPS = 0.5;
+ private static final double PAGE = 300;
+ private static final double MARGIN = 20;
+ private static final double CONTENT = PAGE - 2 * MARGIN; // 260
+ private static final double ICON = 40;
+
+ /** Places one 40pt square path under the given alignment, returns its x. */
+ private double placedX(HorizontalAlign align) {
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(PAGE, PAGE)
+ .margin(DocumentInsets.of(MARGIN))
+ .create()) {
+ session.dsl().pageFlow().name("Flow")
+ .addAligned(align, square())
+ .build();
+ LayoutGraph graph = session.layoutGraph();
+ PlacedNode square = graph.nodes().stream()
+ .filter(n -> n.path().contains("Square"))
+ .findFirst().orElseThrow();
+ return square.placementX();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ void leftKeepsTheNodeAtTheContentLeftEdge() {
+ assertThat(placedX(HorizontalAlign.LEFT)).isCloseTo(MARGIN, within(EPS));
+ }
+
+ @Test
+ void centerPlacesTheNodeInTheMiddleOfTheContentWidth() {
+ double expected = MARGIN + (CONTENT - ICON) / 2.0; // 20 + 110 = 130
+ assertThat(placedX(HorizontalAlign.CENTER)).isCloseTo(expected, within(EPS));
+ }
+
+ @Test
+ void rightPushesTheNodeToTheContentRightEdge() {
+ double expected = MARGIN + (CONTENT - ICON); // 20 + 220 = 240
+ assertThat(placedX(HorizontalAlign.RIGHT)).isCloseTo(expected, within(EPS));
+ }
+
+ @Test
+ void alignedDocumentRendersToPdf() throws Exception {
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(PAGE, PAGE)
+ .margin(DocumentInsets.of(MARGIN))
+ .create()) {
+ session.dsl().pageFlow().name("Flow")
+ .addAligned(HorizontalAlign.CENTER, square())
+ .build();
+ byte[] pdf = session.toPdfBytes();
+ assertThat(new String(pdf, 0, 5, StandardCharsets.US_ASCII)).isEqualTo("%PDF-");
+ }
+ }
+
+ @Test
+ void svgIconAlignOverloadCentresTheIcon() throws Exception {
+ var icon = com.demcha.compose.document.svg.SvgIcon.parse(
+ "");
+ try (DocumentSession session = GraphCompose.document()
+ .pageSize(PAGE, PAGE)
+ .margin(DocumentInsets.of(MARGIN))
+ .create()) {
+ session.dsl().pageFlow(page -> page.addSvgIcon(icon, ICON, HorizontalAlign.CENTER));
+ LayoutGraph graph = session.layoutGraph();
+ PlacedNode iconNode = graph.nodes().stream()
+ .filter(n -> n.path().contains("SvgIcon"))
+ .findFirst().orElseThrow();
+ assertThat(iconNode.placementX())
+ .isCloseTo(MARGIN + (CONTENT - ICON) / 2.0, within(EPS));
+ }
+ }
+
+ private static com.demcha.compose.document.node.DocumentNode square() {
+ return new PathBuilder()
+ .name("Square")
+ .size(ICON, ICON)
+ .moveTo(0, 0).lineTo(1, 0).lineTo(1, 1).lineTo(0, 1).closePath()
+ .fillColor(DocumentColor.rgb(20, 80, 95))
+ .stroke(DocumentStroke.of(DocumentColor.rgb(0, 0, 0), 1))
+ .build();
+ }
+}