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
18 changes: 16 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Entries land here as they merge.
series, type/colour-agnostic), sealed `ChartSpec` (`bar()` / `line()` with
axis, legend, value-label, and sizing knobs), `ChartStyle` (nullable-field
cascade merged over `ChartTheme` tokens, per-series paint overrides), and
`DocumentPaint` (solid today; linear/radial gradient stops reserved for the
gradient work). Charts compile at layout time into existing primitives
`DocumentPaint` (solid, linear, and radial — see the gradient entry below).
Charts compile at layout time into existing primitives
(shapes, lines, paragraphs) via `ChartDefinition` — no new render handlers,
deterministic geometry, covered by the standard snapshot machinery; any
fixed-layout backend renders charts with no chart-specific code, while the
Expand Down Expand Up @@ -68,6 +68,20 @@ Entries land here as they merge.
configurable halo chip (`ChartStyle.valueLabelHalo(...)`, themed white) so
digits stay legible where lines cross them, and deterministically flip below
their point when two series' labels would collide at the same category.
- **Gradient fills** (`@since 1.8.0`). `DocumentPaint` graduates to
`com.demcha.compose.document.style` as the shared paint vocabulary, and
gradients now actually render: `ShapeNode` gains an optional `fillPaint`
(`ShapeBuilder.fill(paint)`) that wins over `fillColor`. The PDF backend
paints `DocumentPaint.linear` as a native axial shading (0° = left→right,
90° = bottom→top; two stops exponential, more stops stitched) and
`DocumentPaint.radial` as a radial shading reaching the farthest corner,
clipped to the shape path — rounded corners included. Chart bars now carry
their full series paint, so a gradient palette renders as gradients instead
of degrading to the first stop. Solid paints normalise to the plain
fill-colour path, keeping existing documents byte-identical; backends
without shading support fall back to `primaryColor()` by contract. The
flagship `BusinessReportExample` hero is now fully vector — gradient-sky
shape plus polygon mountain ranges replace the last Graphics2D raster.
- **Translucent shape colours** (`@since 1.8.0`). `DocumentColor.rgba(r, g, b, a)`
and `withOpacity(0..1)`: the PDF backend honours the alpha channel on shape
fills and strokes (rectangles/panels/bars, chart value-label halos, ellipse
Expand Down
Binary file modified assets/readme/examples/business-report.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import com.demcha.compose.document.chart.ChartSize;
import com.demcha.compose.document.chart.ChartSpec;
import com.demcha.compose.document.chart.ChartStyle;
import com.demcha.compose.document.chart.DocumentPaint;
import com.demcha.compose.document.style.DocumentPaint;
import com.demcha.compose.document.chart.LegendPosition;
import com.demcha.compose.document.chart.NumberFormatSpec;
import com.demcha.compose.document.chart.PointMarker;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.image.DocumentImageData;
import com.demcha.compose.document.image.DocumentImageFitMode;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.LayerAlign;
import com.demcha.compose.document.node.TextAlign;
Expand All @@ -21,13 +19,6 @@
import com.demcha.compose.font.FontName;
import com.demcha.examples.support.ExampleOutputPaths;

import javax.imageio.ImageIO;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.file.Path;

/**
Expand Down Expand Up @@ -86,8 +77,6 @@ private BusinessReportExample() {
public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare("flagships", "business-report.pdf");

DocumentImageData heroImage = DocumentImageData.fromBytes(renderHeroImage(380, 200));

try (DocumentSession document = GraphCompose.document(outputFile)
.pageSize(DocumentPageSize.A4)
.pageBackground(PAPER)
Expand Down Expand Up @@ -155,22 +144,17 @@ public static Path generate() throws Exception {
.margin(new DocumentInsets(4, 0, 0, 0))))
.addSection("HeroImage", section -> section
.padding(DocumentInsets.zero())
// Hero image lives inside a rounded
// shape container so the navy edges
// soften into a frame instead of
// bleeding straight to the page edge.
// The hero scene is fully vector now: a
// gradient-sky shape with two polygon
// mountain ranges, clipped to the rounded
// frame. No raster, no AWT.
.addContainer(frame -> frame
.name("HeroFrame")
.roundedRect(210, 110, 12)
.fillColor(NAVY_DARK)
.stroke(DocumentStroke.of(GOLD, 0.6))
.clipPolicy(ClipPolicy.CLIP_PATH)
.center(new com.demcha.compose.document.dsl.ImageBuilder()
.name("HeroImage")
.source(heroImage)
.size(204, 104)
.fitMode(DocumentImageFitMode.COVER)
.build()))))
.center(buildHeroScene(204, 104)))))

// Three KPI cards
.addRow("KpiRow", row -> row
Expand Down Expand Up @@ -444,8 +428,8 @@ private static DocumentNode buildChart() {
.build();
com.demcha.compose.document.chart.ChartStyle style =
com.demcha.compose.document.chart.ChartStyle.builder()
.seriesPaint(0, com.demcha.compose.document.chart.DocumentPaint.solid(NAVY))
.seriesPaint(1, com.demcha.compose.document.chart.DocumentPaint.solid(GOLD))
.seriesPaint(0, com.demcha.compose.document.style.DocumentPaint.solid(NAVY))
.seriesPaint(1, com.demcha.compose.document.style.DocumentPaint.solid(GOLD))
.build();
return new com.demcha.compose.document.node.ChartNode(
"PerformanceChart", spec, style, null, null);
Expand All @@ -454,53 +438,60 @@ private static DocumentNode buildChart() {
// ─────────────────── Hero image generator ─────────────────────

/**
* Renders a simple gradient hero image (sunset sky + mountain
* silhouette) so the example does not depend on any external image
* asset. The result is encoded as PNG bytes and embedded directly.
*
* <p>This is the one remaining raster block in the example, and it is
* deliberate: the sky requires a smooth linear gradient, which the engine
* does not paint natively yet ({@code DocumentPaint.linear} is reserved
* for the gradient work). Once gradients land, the mountains become
* {@code PolygonNode}s, the glow a translucent fill, and this method goes
* away like the old chart raster did.</p>
* Builds the hero scene fully from vector primitives: a gradient-sky
* shape (warm cream at the horizon rising into slate) with two polygon
* mountain ranges — the distant one translucent, the foreground one
* solid. The same sunset the old Graphics2D raster painted, now native:
* deterministic, crisp at any zoom, and free of the AWT dependency.
*/
private static byte[] renderHeroImage(int width, int height) throws Exception {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Sky gradient: slate at the top, warm cream at the horizon.
g.setPaint(new GradientPaint(
0, 0, new java.awt.Color(58, 70, 100),
0, height * 0.7f, new java.awt.Color(218, 196, 162)));
g.fillRect(0, 0, width, height);
// Distant mountain silhouette.
g.setColor(new java.awt.Color(50, 62, 88, 230));
Polygon farRange = new Polygon(
new int[]{0, (int) (width * 0.18), (int) (width * 0.36), (int) (width * 0.54),
(int) (width * 0.72), (int) (width * 0.88), width, width, 0},
new int[]{(int) (height * 0.55), (int) (height * 0.40), (int) (height * 0.50),
(int) (height * 0.34), (int) (height * 0.46), (int) (height * 0.36),
(int) (height * 0.50), height, height},
9);
g.fill(farRange);
// Foreground mountain wedge.
g.setColor(new java.awt.Color(28, 36, 60));
Polygon foreRange = new Polygon(
new int[]{0, (int) (width * 0.22), (int) (width * 0.40), (int) (width * 0.60),
(int) (width * 0.82), width, width, 0},
new int[]{(int) (height * 0.78), (int) (height * 0.55), (int) (height * 0.68),
(int) (height * 0.50), (int) (height * 0.62), (int) (height * 0.74),
height, height},
8);
g.fill(foreRange);
} finally {
g.dispose();
private static DocumentNode buildHeroScene(double width, double height) {
com.demcha.compose.document.node.ShapeNode sky =
new com.demcha.compose.document.dsl.ShapeBuilder()
.name("HeroSky")
.size(width, height)
.fill(new com.demcha.compose.document.style.DocumentPaint.Linear(
java.util.List.of(
new com.demcha.compose.document.style.DocumentPaint.Stop(
0.0, DocumentColor.rgb(218, 196, 162)),
new com.demcha.compose.document.style.DocumentPaint.Stop(
0.30, DocumentColor.rgb(218, 196, 162)),
new com.demcha.compose.document.style.DocumentPaint.Stop(
1.0, DocumentColor.rgb(58, 70, 100))),
90.0))
.build();
com.demcha.compose.document.node.PolygonNode farRange =
new com.demcha.compose.document.node.PolygonNode(
"HeroFarRange", width, height,
heroPoints(new double[][] {
{0, .45}, {.18, .60}, {.36, .50}, {.54, .66},
{.72, .54}, {.88, .64}, {1, .50}, {1, 0}, {0, 0}}),
DocumentColor.rgba(50, 62, 88, 230), null,
DocumentInsets.zero(), DocumentInsets.zero());
com.demcha.compose.document.node.PolygonNode foreRange =
new com.demcha.compose.document.node.PolygonNode(
"HeroForeRange", width, height,
heroPoints(new double[][] {
{0, .22}, {.22, .45}, {.40, .32}, {.60, .50},
{.82, .38}, {1, .26}, {1, 0}, {0, 0}}),
DocumentColor.rgb(28, 36, 60), null,
DocumentInsets.zero(), DocumentInsets.zero());
return new com.demcha.compose.document.node.CanvasLayerNode(
"HeroScene", width, height,
java.util.List.of(
new com.demcha.compose.document.node.CanvasChild(sky, 0, 0),
new com.demcha.compose.document.node.CanvasChild(farRange, 0, 0),
new com.demcha.compose.document.node.CanvasChild(foreRange, 0, 0)),
ClipPolicy.OVERFLOW_VISIBLE, DocumentInsets.zero(), DocumentInsets.zero());
}

private static java.util.List<com.demcha.compose.document.style.ShapePoint> heroPoints(
double[][] xy) {
java.util.List<com.demcha.compose.document.style.ShapePoint> points =
new java.util.ArrayList<>(xy.length);
for (double[] p : xy) {
points.add(new com.demcha.compose.document.style.ShapePoint(p[0], p[1]));
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(img, "png", out);
return out.toByteArray();
return points;
}

// ─────────────────── Text styles ──────────────────────────────
Expand Down
Loading