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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added assets/readme/examples/block-align.pdf
Binary file not shown.
21 changes: 19 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <pre>{@code
* flow.addSvgIcon(icon, 48, HorizontalAlign.CENTER);
* flow.addAligned(HorizontalAlign.RIGHT, anyFixedNode);
* }</pre>
*
* @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 = """
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="11" fill="#fde9e3"/>
<path fill="#c41e3a" d="M12 2 L22 12 L12 22 L2 12 Z"/>
</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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ record Entry(String title, String description, List<String> 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");
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<AlignNode> {

/**
* Creates the align layout definition.
*/
public AlignDefinition() {
}

@Override
public Class<AlignNode> nodeType() {
return AlignNode.class;
}

@Override
public PreparedNode<AlignNode> prepare(AlignNode node, PrepareContext ctx, BoxConstraints constraints) {
DocumentNode child = node.child();
double childInner = Math.max(0.0, constraints.availableWidth() - child.margin().horizontal());
PreparedNode<DocumentNode> 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<DocumentNode> children(AlignNode node) {
return node.children();
}

@Override
public List<LayoutFragment> emitFragments(PreparedNode<AlignNode> prepared,
FragmentContext ctx,
FragmentPlacement placement) {
return List.of();
}
}
Loading