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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.demcha.examples.templates.cv.v2;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
import com.demcha.compose.document.templates.cv.v2.presets.MonogramSidebar;
import com.demcha.examples.support.ExampleDataFactory;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Renders the v2 Monogram Sidebar CV preset against the shared
* grouped skills sample data — pale teal-grey sidebar with a dark
* monogram ring badge, centered icon-driven contact stack, education
* and expertise blocks, plus a two-line spaced-caps headline and the
* main career narrative on the right.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/cv/cv-monogram-sidebar-v2.pdf}.</p>
*/
public final class CvMonogramSidebarExample {

private CvMonogramSidebarExample() {
}

public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare(
"templates/cv", "cv-monogram-sidebar-v2.pdf");
CvDocument doc = ExampleDataFactory.sampleCvDocumentV2();
DocumentTemplate<CvDocument> template = MonogramSidebar.create();

float m = (float) MonogramSidebar.RECOMMENDED_MARGIN;
try (DocumentSession document = GraphCompose.document(outputFile)
.pageSize(DocumentPageSize.A4)
.margin(m, m, m, m)
.create()) {
template.compose(document, doc);
document.buildPdf();
}
return outputFile;
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
26 changes: 25 additions & 1 deletion src/main/java/com/demcha/compose/GraphCompose.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public static final class DocumentBuilder {
private boolean markdown = true;
private boolean guideLines;
private com.demcha.compose.document.style.DocumentColor pageBackground;
private java.util.List<com.demcha.compose.document.api.PageBackgroundFill> pageBackgrounds;
private final List<FontFamilyDefinition> customFontFamilies = new ArrayList<>();

private DocumentBuilder(Path outputFile) {
Expand Down Expand Up @@ -243,6 +244,22 @@ public DocumentBuilder pageBackground(java.awt.Color color) {
: com.demcha.compose.document.style.DocumentColor.of(color));
}

/**
* Configures one or more rectangular page background fills applied
* behind every fragment on every page. See
* {@link com.demcha.compose.document.api.PageBackgroundFill} and
* {@link com.demcha.compose.document.api.DocumentSession#pageBackgrounds}
* for the full semantics.
*
* @param fills ordered fills, or {@code null}/empty to clear
* @return this builder
*/
public DocumentBuilder pageBackgrounds(
java.util.List<com.demcha.compose.document.api.PageBackgroundFill> fills) {
this.pageBackgrounds = fills;
return this;
}

/**
* Registers a document-local font family available to text measurement and
* PDF rendering.
Expand Down Expand Up @@ -363,7 +380,14 @@ public DocumentSession create() {
List.copyOf(customFontFamilies),
markdown,
guideLines);
if (pageBackground != null) {
if (pageBackgrounds != null) {
// Explicit pageBackgrounds() call wins — even an empty
// list is an intentional clear that should override any
// earlier pageBackground(color) on the same builder.
if (!pageBackgrounds.isEmpty()) {
session.pageBackgrounds(pageBackgrounds);
}
} else if (pageBackground != null) {
session.pageBackground(pageBackground);
}
return session;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,64 @@
import com.demcha.compose.document.layout.LayoutGraph;
import com.demcha.compose.document.layout.PlacedFragment;
import com.demcha.compose.document.layout.payloads.ShapeFragmentPayload;
import com.demcha.compose.document.style.DocumentColor;

import java.util.ArrayList;
import java.util.List;

/**
* Splices a session-wide page background fill into a compiled
* {@link LayoutGraph}. Extracted from {@link DocumentSession} as part
* of the Phase E.3 slim.
* Splices session-wide page background fills into a compiled
* {@link LayoutGraph}. Supports any number of partial-page rectangular
* fills per page (multi-column backgrounds, accent stripes, etc.).
*
* <p>Pages get one extra {@link PlacedFragment} at the bottom of their
* z-order so every other fragment paints on top of the background fill.
* If the session has no page background or the layout has no pages,
* the original graph is returned unchanged.</p>
* <p>Background fragments are placed at the very bottom of the page
* z-order (z=0), so every other fragment paints on top of them. Within
* a single page, fills are emitted in list order — later entries paint
* over earlier entries where they overlap. If the layout has no pages
* or no fills were configured, the original graph is returned
* unchanged.</p>
*/
final class DocumentPageBackgrounds {
private DocumentPageBackgrounds() {
}

/**
* Returns a copy of {@code base} with a page-background fragment
* spliced into every page, or {@code base} unchanged when there is
* nothing to do.
* Multi-rect form. Emits one fragment per fill per page, with
* coordinates computed from the fill's ratios and the canvas size.
*
* @param base freshly compiled layout graph
* @param color session-wide background color, or {@code null}
* @param base freshly compiled layout graph
* @param fills ordered list of page-background fills (each painted
* on every page); {@code null}/empty leaves {@code base}
* unchanged
* @return a layout graph with background fragments, or {@code base}
*/
static LayoutGraph apply(LayoutGraph base, DocumentColor color) {
if (color == null || base.totalPages() == 0) {
static LayoutGraph apply(LayoutGraph base, List<PageBackgroundFill> fills) {
if (fills == null || fills.isEmpty() || base.totalPages() == 0) {
return base;
}
List<PlacedFragment> combined = new ArrayList<>(base.fragments().size() + base.totalPages());
double pageWidth = base.canvas().width();
double pageHeight = base.canvas().height();
int extra = base.totalPages() * fills.size();
List<PlacedFragment> combined =
new ArrayList<>(base.fragments().size() + extra);
for (int page = 0; page < base.totalPages(); page++) {
combined.add(new PlacedFragment(
"@page-background[" + page + "]",
0,
page,
0.0,
0.0,
base.canvas().width(),
base.canvas().height(),
com.demcha.compose.engine.components.style.Margin.zero(),
com.demcha.compose.engine.components.style.Padding.zero(),
new ShapeFragmentPayload(color.color(), null, 0.0, null, null, null)));
for (int i = 0; i < fills.size(); i++) {
PageBackgroundFill fill = fills.get(i);
combined.add(new PlacedFragment(
"@page-background[" + page + "][" + i + "]",
0,
page,
fill.xRatio() * pageWidth,
fill.yRatio() * pageHeight,
fill.widthRatio() * pageWidth,
fill.heightRatio() * pageHeight,
com.demcha.compose.engine.components.style.Margin.zero(),
com.demcha.compose.engine.components.style.Padding.zero(),
new ShapeFragmentPayload(fill.color().color(),
null, 0.0, null, null, null)));
}
}
combined.addAll(base.fragments());
return new LayoutGraph(base.canvas(), base.totalPages(), base.nodes(), combined);
return new LayoutGraph(base.canvas(), base.totalPages(),
base.nodes(), combined);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public final class DocumentSession implements AutoCloseable {
private LayoutCanvas canvas;
private boolean markdown;
private boolean guideLines;
private DocumentColor pageBackground;
private List<PageBackgroundFill> pageBackgrounds = List.of();

private final DocumentChromeOptions chromeOptions = new DocumentChromeOptions();

Expand Down Expand Up @@ -321,7 +321,9 @@ public DocumentSession guideLines(boolean enabled) {
*/
public DocumentSession pageBackground(DocumentColor color) {
ensureOpen();
this.pageBackground = color;
this.pageBackgrounds = color == null
? List.of()
: List.of(PageBackgroundFill.fullPage(color));
invalidate();
return this;
}
Expand All @@ -336,6 +338,30 @@ public DocumentSession pageBackground(Color color) {
return pageBackground(color == null ? null : DocumentColor.of(color));
}

/**
* Configures one or more rectangular background fills applied behind
* every fragment on every page. Each fill is defined as a fraction of
* the canvas (see {@link PageBackgroundFill}), so the layout works
* across page sizes. Use this for multi-column page chrome — a pale
* sidebar column plus a white main column, accent stripes, etc. —
* that should repeat automatically when content paginates onto a new
* page.
*
* <p>Pass {@code null} or an empty list to clear. Fills paint in list
* order; later entries paint on top of earlier ones where they
* overlap.</p>
*
* @param fills ordered list of fills, or {@code null}/empty to clear
* @return this session
* @throws IllegalStateException if this session has already been closed
*/
public DocumentSession pageBackgrounds(List<PageBackgroundFill> fills) {
ensureOpen();
this.pageBackgrounds = fills == null ? List.of() : List.copyOf(fills);
invalidate();
return this;
}

/**
* Returns a fluent facade for chrome configuration (metadata,
* watermark, protection, header, footer). The facade is a thin
Expand Down Expand Up @@ -617,7 +643,7 @@ public LayoutGraph layoutGraph() {
LIFECYCLE_LOG.debug("document.layout.start sessionId={} revision={} roots={}", sessionId, revision, roots.size());
try {
DocumentLayoutPassContext context = new DocumentLayoutPassContext(registry, canvas, measurementResources.fontLibrary(), measurementResources.textMeasurementSystem(), markdown);
LayoutGraph computed = layoutCache.layout(() -> DocumentPageBackgrounds.apply(compiler.compile(documentGraph(), context, context), pageBackground));
LayoutGraph computed = layoutCache.layout(() -> DocumentPageBackgrounds.apply(compiler.compile(documentGraph(), context, context), pageBackgrounds));
LIFECYCLE_LOG.debug("document.layout.end sessionId={} revision={} roots={} pages={} nodes={} fragments={} durationMs={}", sessionId, revision, roots.size(), computed.totalPages(), computed.nodes().size(), computed.fragments().size(), elapsedMillis(startNanos));
return computed;
} catch (RuntimeException ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.demcha.compose.document.api;

import com.demcha.compose.document.style.DocumentColor;

import java.util.Objects;

/**
* Per-page rectangular background fill, defined as ratios of the
* canvas page size so the same fill scales correctly to any page
* format. Used by {@link DocumentSession#pageBackgrounds(java.util.List)}
* to paint multi-column or partial-page backgrounds that repeat on
* every page automatically.
*
* <p>Use the factory methods for the common cases:</p>
* <ul>
* <li>{@link #fullPage(DocumentColor)} — entire page (same effect as
* the legacy single-color {@link DocumentSession#pageBackground}).</li>
* <li>{@link #leftColumn(double, DocumentColor)} — full-height column
* aligned to the left edge.</li>
* <li>{@link #rightColumn(double, DocumentColor)} — full-height
* column aligned to the right edge.</li>
* <li>{@link #column(double, double, DocumentColor)} — arbitrary
* horizontal slice spanning the full page height.</li>
* </ul>
*
* <p>Fills supplied to a session are painted at z=0 (below every other
* fragment) in list order, so later entries paint on top of earlier
* entries when they overlap. This is the natural way to layer a
* narrow accent column over a full-page tint.</p>
*
* @param xRatio 0.0 = left edge, 1.0 = right edge
* @param yRatio 0.0 = top edge, 1.0 = bottom edge
* @param widthRatio width as a fraction of the canvas width (0..1]
* @param heightRatio height as a fraction of the canvas height (0..1]
* @param color fill color (required)
*/
public record PageBackgroundFill(double xRatio,
double yRatio,
double widthRatio,
double heightRatio,
DocumentColor color) {

public PageBackgroundFill {
Objects.requireNonNull(color, "color");
if (xRatio < 0.0 || xRatio > 1.0) {
throw new IllegalArgumentException(
"xRatio must be in [0,1] but was " + xRatio);
}
if (yRatio < 0.0 || yRatio > 1.0) {
throw new IllegalArgumentException(
"yRatio must be in [0,1] but was " + yRatio);
}
if (widthRatio <= 0.0 || widthRatio > 1.0) {
throw new IllegalArgumentException(
"widthRatio must be in (0,1] but was " + widthRatio);
}
if (heightRatio <= 0.0 || heightRatio > 1.0) {
throw new IllegalArgumentException(
"heightRatio must be in (0,1] but was " + heightRatio);
}
}

/** Full-page fill, equivalent to the legacy single-color page background. */
public static PageBackgroundFill fullPage(DocumentColor color) {
return new PageBackgroundFill(0.0, 0.0, 1.0, 1.0, color);
}

/** Full-height column at the left page edge, with width = ratio of page width. */
public static PageBackgroundFill leftColumn(double widthRatio,
DocumentColor color) {
return new PageBackgroundFill(0.0, 0.0, widthRatio, 1.0, color);
}

/** Full-height column at the right page edge, with width = ratio of page width. */
public static PageBackgroundFill rightColumn(double widthRatio,
DocumentColor color) {
return new PageBackgroundFill(1.0 - widthRatio, 0.0,
widthRatio, 1.0, color);
}

/** Full-height column at an arbitrary horizontal offset. */
public static PageBackgroundFill column(double xRatio,
double widthRatio,
DocumentColor color) {
return new PageBackgroundFill(xRatio, 0.0, widthRatio, 1.0, color);
}
}
Loading