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
20 changes: 18 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,24 @@ Entries land here as they merge.
transforms (affine maps are exact on Bézier control points), and
`fill` / `stroke` / `stroke-width` styling with SVG inheritance and
defaults — into ordered layers, and `addSvgIcon(icon, width)` stacks them
back-to-front on the page. The XML reader refuses DOCTYPEs (no XXE);
gradients, CSS, text and filters stay deliberately out of scope.
back-to-front on the page. `SvgIcon#node(width)` packages the same layers
as one ready-to-place node whose box is exactly the icon box, so it
anchors true inside `ShapeContainer` / `LayerStack` nine-point grids (and
rows now accept `ShapeContainerNode` children directly — it is the same
atomic overlay composite as the already-allowed `LayerStackNode`).
**Gradients render natively**: `linearGradient` / `radialGradient`
referenced via `url(#id)` — on fills *and strokes* — map to PDF axial /
radial shadings with exact endpoints (`userSpaceOnUse` and
`objectBoundingBox` units, `gradientTransform`, percentage offsets,
multi-stop stitching, one `href` hop for split definitions); gradient
strokes ride a shading-pattern stroking colour. Underneath,
`DocumentPaint` gains endpoint-exact `LinearAxis` / `RadialCircle` forms
and `PathNode` / `PathBuilder` grow `fill(paint)` / `strokePaint(paint)`
with solid paints normalising to the flat-colour path (byte-identical
output for non-gradient documents). The XML reader refuses DOCTYPEs (no
XXE); CSS, text, filters, focal radials, non-pad `spreadMethod` and
translucent stops stay deliberately out of scope — the reader fails
loudly rather than rendering them wrong.
- **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
Binary file modified assets/readme/examples/vector-path.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentPaint;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.svg.SvgIcon;
import com.demcha.compose.document.svg.SvgPath;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;
import java.util.List;

/**
* Runnable showcase for the v1.8 vector-path primitive: free-form design
Expand Down Expand Up @@ -39,6 +41,13 @@ public final class VectorPathExample {
private static final DocumentColor SAND_EDGE = DocumentColor.rgb(140, 90, 30);
private static final DocumentColor MOSS = DocumentColor.rgb(208, 226, 213);
private static final DocumentColor MOSS_EDGE = DocumentColor.rgb(60, 110, 80);
private static final DocumentColor VIOLET = DocumentColor.rgb(167, 139, 250);
private static final DocumentColor DEEP_VIOLET = DocumentColor.rgb(97, 40, 217);

/** Brand gradient along the top-left → bottom-right diagonal. */
private static final DocumentPaint BRAND_AXIS = new DocumentPaint.LinearAxis(List.of(
new DocumentPaint.Stop(0.0, VIOLET),
new DocumentPaint.Stop(1.0, DEEP_VIOLET)), 0.0, 1.0, 1.0, 0.0);

/** Material Icons "favorite" path data (Apache 2.0), viewBox 0 0 24 24. */
private static final String MATERIAL_HEART_D =
Expand Down Expand Up @@ -68,7 +77,7 @@ public static Path generate() throws Exception {
Path pdfFile = ExampleOutputPaths.prepare("features/shapes", "vector-path.pdf");

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(420, 780)
.pageSize(420, 920)
.margin(DocumentInsets.of(28))
.create()) {
document.pageFlow(page -> page
Expand Down Expand Up @@ -111,6 +120,25 @@ public static Path generate() throws Exception {
.margin(DocumentInsets.bottom(16)))
.addParagraph("Whole-file icon — SvgIcon.read/parse stacks every layer")
.addSvgIcon(SvgIcon.parse(TWO_TONE_BADGE_SVG), 64)
.addParagraph("Gradient paints — strokePaint and fill ride native PDF shadings")
.addPath(path -> path
.name("GradientWave")
.size(364, 52)
.moveTo(0.0, 0.5)
.curveTo(0.25, 1.1, 0.25, -0.1, 0.5, 0.5)
.curveTo(0.75, 1.1, 0.75, -0.1, 1.0, 0.5)
.stroke(DocumentStroke.of(VIOLET, 2.6))
.strokePaint(BRAND_AXIS)
.margin(DocumentInsets.bottom(8)))
.addPath(path -> path
.name("GradientBlob")
.size(96, 56)
.moveTo(0.5, 1.0)
.curveTo(1.12, 0.94, 0.96, 0.08, 0.5, 0.0)
.curveTo(0.04, 0.08, -0.12, 0.94, 0.5, 1.0)
.closePath()
.fill(BRAND_AXIS)
.margin(DocumentInsets.bottom(16)))
.addParagraph("Mixed ribbon — lines and curves in one closed, filled subpath")
.addPath(path -> path
.name("Ribbon")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.LayerStackBuilder;
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.dsl.PathBuilder;
import com.demcha.compose.document.dsl.ShapeContainerBuilder;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.LayerAlign;
Expand Down Expand Up @@ -123,9 +121,7 @@ public static Path generate() throws Exception {
page.addRow(row -> {
row.spacing(10).evenWeights().margin(DocumentInsets.bottom(10));
for (String name : chunk) {
// Rows host sections; the fixed-size card rides inside one.
row.addSection("Tile" + name.replace('-', '_'),
s -> s.add(card(name)));
row.add(card(name));
}
// Pad the last row so its cells line up with the full rows.
for (int filler = chunk.size(); filler < COLUMNS; filler++) {
Expand All @@ -149,33 +145,20 @@ private static DocumentNode card(String name) {
.roundedRect(CARD_WIDTH, CARD_HEIGHT, CARD_RADIUS)
.fillColor(CARD_FILL)
.stroke(DocumentStroke.of(CARD_BORDER, 0.8))
.position(iconStack(name, id), 0, -PLAQUE_HEIGHT / 2.0, LayerAlign.CENTER)
.position(iconStack(name), 0, -PLAQUE_HEIGHT / 2.0, LayerAlign.CENTER)
.bottomCenter(plaque(name, id))
.build();
}

/**
* Builds the icon as a standalone layer stack whose box is exactly the
* icon's contain-fit size, so the card's CENTER anchor lands true.
* (The flow-level {@code addSvgIcon(...)} sugar targets flows; a card
* layer needs the node form of the same composition.)
* Builds the icon as a standalone node via {@link SvgIcon#node(double)}
* — its box is exactly the icon's contain-fit size, so the card's
* CENTER anchor lands true.
*/
private static DocumentNode iconStack(String name, String id) {
private static DocumentNode iconStack(String name) {
SvgIcon icon = loadIcon(name);
double width = Math.min(ICON_BOX, ICON_BOX * icon.aspectRatio());
double height = width / icon.aspectRatio();
LayerStackBuilder stack = new LayerStackBuilder().name("Icon" + id);
for (int i = 0; i < icon.layers().size(); i++) {
SvgIcon.Layer layer = icon.layers().get(i);
stack.layer(new PathBuilder()
.name("SvgLayer" + i)
.size(width, height)
.svg(layer.geometry())
.fillColor(layer.fill())
.stroke(layer.stroke())
.build());
}
return stack.build();
return icon.node(width);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,13 @@ public static Path generate() throws Exception {

feature(flow, "SVG icons (beta) — multicolour files centred on tile cards", """
SvgIcon icon = SvgIcon.read(Path.of("icons/apple.svg")); // layers + resolved paints
card.roundedRect(74, 64, 8) // fixed box = the tile
.position(iconNode(icon), 0, -7, LayerAlign.CENTER) // anchor centres the icon's
.bottomCenter(plaque("APPLE")) // own tight box inside the card""",
card.roundedRect(74, 64, 8) // fixed box = the tile
.position(icon.node(34), 0, -7, LayerAlign.CENTER) // icon.node keeps its tight
.bottomCenter(plaque("APPLE")) // box, so CENTER lands true""",
demo -> demo.addRow(r -> {
r.spacing(8).evenWeights();
for (String[] entry : CATALOG_ICONS) {
r.addSection("Tile" + entry[1], s -> s.add(iconCard(entry[0], entry[1])));
r.add(iconCard(entry[0], entry[1]));
}
}));

Expand Down Expand Up @@ -499,34 +499,21 @@ private static SvgIcon catalogIcon(String name) {
}

/**
* Mini tile for the icon row: fixed rounded card, the icon centred in the
* body as a tight-box layer stack, a label plaque across the bottom. The
* stack keeps the icon's exact contain-fit size — that is what makes the
* card's CENTER anchor land true.
* Mini tile for the icon row: fixed rounded card, the icon centred in
* the body via {@link SvgIcon#node(double)} (its tight box is what makes
* the card's CENTER anchor land true), a label plaque across the bottom.
*/
private static com.demcha.compose.document.node.DocumentNode iconCard(String name, String label) {
SvgIcon icon = catalogIcon(name);
double box = 34;
double width = Math.min(box, box * icon.aspectRatio());
double height = width / icon.aspectRatio();
var stack = new com.demcha.compose.document.dsl.LayerStackBuilder().name("Icon" + label);
for (int i = 0; i < icon.layers().size(); i++) {
SvgIcon.Layer layer = icon.layers().get(i);
stack.layer(new com.demcha.compose.document.dsl.PathBuilder()
.name("SvgLayer" + i)
.size(width, height)
.svg(layer.geometry())
.fillColor(layer.fill())
.stroke(layer.stroke())
.build());
}
double plaqueHeight = 14;
return new com.demcha.compose.document.dsl.ShapeContainerBuilder()
.name("IconCard" + label)
.roundedRect(74, 64, 8)
.fillColor(DocumentColor.rgb(248, 249, 251))
.stroke(DocumentStroke.of(DocumentColor.rgb(228, 231, 236), 0.8))
.position(stack.build(), 0, -plaqueHeight / 2.0,
.position(icon.node(width), 0, -plaqueHeight / 2.0,
com.demcha.compose.document.node.LayerAlign.CENTER)
.bottomCenter(new com.demcha.compose.document.dsl.ShapeContainerBuilder()
.name("Plaque" + label)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
import com.demcha.compose.document.layout.PlacedFragment;
import com.demcha.compose.document.layout.payloads.PathFragmentPayload;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;

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.
*
* <p>Gradient fills clip to the path and paint a native shading; gradient
* strokes set a shading-pattern stroking colour (pattern type 2) so the
* outline itself carries the gradient. Flat-colour paths bypass both and
* take the exact pre-gradient code path, byte for byte.</p>
*
* @author Artem Demchyshyn
* @since 1.8.0
*/
Expand Down Expand Up @@ -41,8 +47,51 @@ public void render(PlacedFragment fragment,
float y = (float) fragment.y();
float width = (float) fragment.width();
float height = (float) fragment.height();
PdfShapeGeometry.fillAndStrokePath(stream, payload.fillColor(), payload.stroke(),
payload.dashPattern(),
s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments()));

if (payload.fillPaint() == null && payload.strokePaint() == null) {
PdfShapeGeometry.fillAndStrokePath(stream, payload.fillColor(), payload.stroke(),
payload.dashPattern(),
s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments()));
return;
}

// Gradient route: fill and stroke are separate passes because each
// may independently be a flat colour or a shading.
stream.saveGraphicsState();
try {
if (payload.fillPaint() != null) {
// Clip in a nested state so the clip never leaks into the
// stroke pass (mirrors the shape handler).
stream.saveGraphicsState();
try {
PdfShapeGeometry.addPathSegments(stream, x, y, width, height, payload.segments());
stream.clip();
stream.shadingFill(PdfShadingSupport.build(payload.fillPaint(), x, y, width, height));
} finally {
stream.restoreGraphicsState();
}
} else if (payload.fillColor() != null) {
PdfShapeGeometry.fillAndStrokePath(stream, payload.fillColor(), null, null,
s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments()));
}

boolean hasStrokeWidth = payload.stroke() != null && payload.stroke().width() > 0;
if (payload.strokePaint() != null && hasStrokeWidth) {
PDResources resources = environment.document()
.getPage(fragment.pageIndex()).getResources();
stream.setStrokingColor(PdfShadingSupport.strokePattern(
payload.strokePaint(), resources, x, y, width, height));
stream.setLineWidth((float) payload.stroke().width());
PdfShapeGeometry.applyDashPattern(stream, payload.dashPattern());
PdfShapeGeometry.addPathSegments(stream, x, y, width, height, payload.segments());
stream.stroke();
} else if (hasStrokeWidth && payload.stroke().strokeColor() != null) {
PdfShapeGeometry.fillAndStrokePath(stream, null, payload.stroke(),
payload.dashPattern(),
s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments()));
}
} finally {
stream.restoreGraphicsState();
}
}
}
Loading