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 @@ -52,6 +52,15 @@ Entries land here as they merge.
into the new general-purpose `PolygonNode` (arc-tessellated ring polygons at
a fixed 3° step — deterministic vertices, no new render handlers), which also
lays the groundwork for SVG icon-path import.
- **Vector path primitive** (`@since 1.8.0`). New `PathNode` — the open-path,
curve-capable sibling of `PolygonNode`: normalized `DocumentPathSegment`s
(`moveTo` / `lineTo` / cubic `cubicTo` / `close`; Bézier control points are
free to overshoot the unit box) are scaled to the node's box and rendered
with native PDF curve operators, so curves stay perfectly smooth at any
zoom level instead of being tessellated into straight pieces. Atomic
pagination, deterministic layout snapshots, fill (non-zero winding rule)
and/or stroke. This is the leaf vehicle for smooth chart lines, decorative
design shapes, and future SVG path import.
- **Inline sparklines** (`@since 1.8.0`). `RichText.sparkline(w, h, color,
values...)` draws a filled mini-area silhouette on the text baseline, and
`sparklineLine(w, h, thickness, color, values...)` a constant-thickness line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ private static List<PdfFragmentRenderHandler<?>> defaultHandlers() {
new PdfLineFragmentRenderHandler(),
new PdfEllipseFragmentRenderHandler(),
new PdfPolygonFragmentRenderHandler(),
new PdfPathFragmentRenderHandler(),
new PdfImageFragmentRenderHandler(),
new PdfTableRowFragmentRenderHandler(),
new PdfShapeClipBeginRenderHandler(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.demcha.compose.document.backend.fixed.pdf.handlers;

import com.demcha.compose.document.backend.fixed.pdf.PdfFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pdf.PdfRenderEnvironment;
import com.demcha.compose.document.layout.PlacedFragment;
import com.demcha.compose.document.layout.payloads.PathFragmentPayload;
import org.apache.pdfbox.pdmodel.PDPageContentStream;

import java.io.IOException;

/**
* Renders fixed vector-path fragments with native PDF line and cubic-Bézier
* operators — curves stay smooth at any zoom level.
*
* @author Artem Demchyshyn
*/
public final class PdfPathFragmentRenderHandler
implements PdfFragmentRenderHandler<PathFragmentPayload> {

/**
* Creates the path fragment renderer.
*/
public PdfPathFragmentRenderHandler() {
}

@Override
public Class<PathFragmentPayload> payloadType() {
return PathFragmentPayload.class;
}

@Override
public void render(PlacedFragment fragment,
PathFragmentPayload payload,
PdfRenderEnvironment environment) throws IOException {
if (fragment.width() <= 0 || fragment.height() <= 0) {
return;
}
PDPageContentStream stream = environment.pageSurface(fragment.pageIndex());
float x = (float) fragment.x();
float y = (float) fragment.y();
float width = (float) fragment.width();
float height = (float) fragment.height();
PdfShapeGeometry.fillAndStrokePath(stream, payload.fillColor(), payload.stroke(),
s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments()));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.demcha.compose.document.backend.fixed.pdf.handlers;

import com.demcha.compose.document.style.DocumentPathSegment;
import com.demcha.compose.document.style.ShapePoint;
import com.demcha.compose.engine.components.content.shape.Stroke;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
Expand Down Expand Up @@ -79,6 +80,35 @@ static void addPolygonPath(PDPageContentStream stream,
stream.closePath();
}

/**
* Appends a normalized segment path scaled to the fragment box, emitting
* native line and cubic-Bézier operators. Segments follow the
* {@link DocumentPathSegment} contract: normalized unit-box coordinates,
* PDF y-up orientation, {@code MoveTo} first, control points free to
* overshoot the box.
*/
static void addPathSegments(PDPageContentStream stream,
float x,
float y,
float width,
float height,
List<DocumentPathSegment> segments) throws IOException {
for (DocumentPathSegment segment : segments) {
if (segment instanceof DocumentPathSegment.MoveTo move) {
stream.moveTo(x + (float) (move.x() * width), y + (float) (move.y() * height));
} else if (segment instanceof DocumentPathSegment.LineTo line) {
stream.lineTo(x + (float) (line.x() * width), y + (float) (line.y() * height));
} else if (segment instanceof DocumentPathSegment.CubicTo cubic) {
stream.curveTo(
x + (float) (cubic.control1X() * width), y + (float) (cubic.control1Y() * height),
x + (float) (cubic.control2X() * width), y + (float) (cubic.control2Y() * height),
x + (float) (cubic.x() * width), y + (float) (cubic.y() * height));
} else if (segment instanceof DocumentPathSegment.Close) {
stream.closePath();
}
}
}

/**
* Appends a closed rounded-rectangle path whose four corners may have
* independent radii. Each radius gets its own Bezier arc; a zero radius
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static NodeRegistry registerDefaults(NodeRegistry registry) {
.register(new TableDefinition())
.register(new CanvasLayerDefinition())
.register(new PolygonDefinition())
.register(new PathDefinition())
.register(new ChartDefinition());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.demcha.compose.document.layout.definitions;

import com.demcha.compose.document.layout.*;
import com.demcha.compose.document.layout.payloads.PathFragmentPayload;
import com.demcha.compose.document.node.PathNode;

import java.util.List;

import static com.demcha.compose.document.layout.NodeDefinitionSupport.EPS;
import static com.demcha.compose.document.layout.NodeDefinitionSupport.toStroke;

/**
* Layout definition for {@link PathNode}: a fixed-size atomic vector-path
* fragment rendered through the path fragment pipeline with native curve
* operators.
*
* @author Artem Demchyshyn
* @since 1.8.0
*/
public final class PathDefinition implements NodeDefinition<PathNode> {

/**
* Creates the path layout definition.
*/
public PathDefinition() {
}

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

@Override
public PreparedNode<PathNode> prepare(PathNode node, PrepareContext ctx, BoxConstraints constraints) {
return PreparedNode.leaf(node, new MeasureResult(
node.width() + node.padding().horizontal(),
node.height() + node.padding().vertical()));
}

@Override
public PaginationPolicy paginationPolicy(PathNode node) {
return PaginationPolicy.ATOMIC;
}

@Override
public List<LayoutFragment> emitFragments(PreparedNode<PathNode> prepared,
FragmentContext ctx,
FragmentPlacement placement) {
PathNode node = prepared.node();
double width = Math.max(0.0, placement.width() - node.padding().horizontal());
double height = Math.max(0.0, placement.height() - node.padding().vertical());
if (width <= EPS || height <= EPS) {
return List.of();
}
return List.of(new LayoutFragment(
placement.path(),
0,
node.padding().left(),
node.padding().bottom(),
width,
height,
new PathFragmentPayload(
node.segments(),
node.fillColor() == null ? null : node.fillColor().color(),
toStroke(node.stroke()),
null,
null)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.demcha.compose.document.layout.payloads;

import com.demcha.compose.document.node.DocumentBookmarkOptions;
import com.demcha.compose.document.node.DocumentLinkOptions;
import com.demcha.compose.document.style.DocumentPathSegment;
import com.demcha.compose.engine.components.content.shape.Stroke;

import java.awt.*;
import java.util.List;
import java.util.Objects;

/**
* PDF payload for a resolved vector-path fragment (curved chart strokes,
* decorative design shapes, imported icon paths). The normalized segments are
* scaled to the placed fragment's size by the render handler, which emits
* native PDF line and cubic-Bézier operators.
*
* @param segments normalized path segments, starting with a move-to
* @param fillColor optional fill color (non-zero winding rule)
* @param stroke optional stroke
* @param linkOptions optional fragment-level link metadata
* @param bookmarkOptions optional fragment-level bookmark metadata
* @author Artem Demchyshyn
* @since 1.8.0
*/
public record PathFragmentPayload(
List<DocumentPathSegment> segments,
Color fillColor,
Stroke stroke,
DocumentLinkOptions linkOptions,
DocumentBookmarkOptions bookmarkOptions
) implements PdfSemanticFragmentPayload {
/**
* Copies the segment list defensively.
*/
public PathFragmentPayload {
Objects.requireNonNull(segments, "segments");
segments = List.copyOf(segments);
}
}
76 changes: 76 additions & 0 deletions src/main/java/com/demcha/compose/document/node/PathNode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.demcha.compose.document.node;

import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentPathSegment;
import com.demcha.compose.document.style.DocumentStroke;

import java.util.List;
import java.util.Objects;

/**
* Atomic filled/stroked vector path inside a fixed-size box. Segments are
* normalized {@link DocumentPathSegment}s scaled to the node's
* {@code width × height} at render time — the open-path, curve-capable
* sibling of {@link PolygonNode}.
*
* <p>This is the leaf vehicle for arbitrary vector geometry with real cubic
* Bézier curves: smooth chart lines compile into it, decorative design
* shapes can be authored against it, and imported SVG paths land here
* tomorrow. The PDF backend emits native {@code curveTo} operators, so
* curves stay perfectly smooth at any zoom level instead of being
* tessellated into straight pieces.</p>
*
* @param name node name used in snapshots and layout graph paths
* @param width resolved box width
* @param height resolved box height
* @param segments normalized path segments; must start with a
* {@link DocumentPathSegment.MoveTo}
* @param fillColor optional fill colour (non-zero winding rule)
* @param stroke optional outline stroke
* @param padding inner padding
* @param margin outer margin
* @author Artem Demchyshyn
* @since 1.8.0
*/
public record PathNode(
String name,
double width,
double height,
List<DocumentPathSegment> segments,
DocumentColor fillColor,
DocumentStroke stroke,
DocumentInsets padding,
DocumentInsets margin
) implements DocumentNode {
/**
* Validates dimensions and the segment list; copy-protects the segments.
*/
public PathNode {
name = name == null ? "" : name;
Objects.requireNonNull(segments, "segments");
segments = List.copyOf(segments);
if (segments.size() < 2) {
throw new IllegalArgumentException(
"path needs at least a MoveTo and one drawing segment: " + segments.size());
}
if (!(segments.get(0) instanceof DocumentPathSegment.MoveTo)) {
throw new IllegalArgumentException(
"path must start with a MoveTo segment, found: "
+ segments.get(0).getClass().getSimpleName());
}
padding = padding == null ? DocumentInsets.zero() : padding;
margin = margin == null ? DocumentInsets.zero() : margin;
if (width <= 0 || Double.isNaN(width) || Double.isInfinite(width)) {
throw new IllegalArgumentException("width must be finite and positive: " + width);
}
if (height <= 0 || Double.isNaN(height) || Double.isInfinite(height)) {
throw new IllegalArgumentException("height must be finite and positive: " + height);
}
}

@Override
public String nodeKind() {
return "Path";
}
}
Loading