diff --git a/CHANGELOG.md b/CHANGELOG.md index 4865c793e..d40fec785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ PDF `GoTo` actions. External links are unchanged. ### Public API +- **Content bleed: `DocumentBleed` / `DocumentEdge`** (`@since 1.9.0`). Flow + builders gain `bleed(DocumentBleed)` and `bleedToEdge(DocumentEdge...)`, so a + section's background fill extends to the trimmed physical page edge on the + declared sides — a full-bleed masthead band or an edge-to-edge colour panel — + while the section's children stay inside the content margin (text never runs + off the page). It is the content-side twin of `PageBackgroundFill` and the + intent-revealing replacement for the hand-computed negative-margin idiom, + resolved against the active page margin at layout time. Nodes that do not bleed + render byte-identically to before. + - **In-PDF navigation: anchors + internal links** (`@since 1.9.0`). Every flow and leaf builder gains `anchor(String)`, declaring a named destination at the element's top-left — `section.anchor("intro")`, `paragraph.anchor("fn-1")`, and diff --git a/assets/readme/examples/content-bleed.pdf b/assets/readme/examples/content-bleed.pdf new file mode 100644 index 000000000..c13769579 Binary files /dev/null and b/assets/readme/examples/content-bleed.pdf differ diff --git a/examples/README.md b/examples/README.md index 9065b50bb..5f66f26e9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -75,6 +75,7 @@ are with the canonical DSL, then jump to its detailed section below. | [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) | +| [Content bleed](#content-bleed) | `band.bleedToEdge(TOP, LEFT, RIGHT)` / `bleed(DocumentBleed.of(...))` — a section's fill reaches the trimmed page edge while its children stay in the content margin | [PDF](../assets/readme/examples/content-bleed.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BleedExample.java) | ### 📋 Templates recommended @@ -415,6 +416,26 @@ 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) +### Content bleed + +A section's background fill normally stops at the page content margin. +`bleed(DocumentBleed)` / `bleedToEdge(DocumentEdge...)` extends the fill to the +trimmed physical page edge on the declared sides — a full-bleed masthead band or +an edge-to-edge accent strip — while the section's children stay inside the +content margin, so a heading never runs off the page. It is the content-side twin +of `pageBackground(...)` and the intent-revealing replacement for the +hand-computed negative-margin idiom. + +```java +page.addSection(band -> band + .fillColor(ink) + .bleedToEdge(DocumentEdge.TOP, DocumentEdge.LEFT, DocumentEdge.RIGHT) + .addParagraph("Title")); // title stays in the safe area +``` + +[📄 View PDF](../assets/readme/examples/content-bleed.pdf) · +[📜 Full source](src/main/java/com/demcha/examples/features/layout/BleedExample.java) + ### Advanced tables `DocumentTableCell.rowSpan(int)` mirrors `colSpan(int)`. diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index c546c72e8..bfc3adf4c 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -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.BleedExample; import com.demcha.examples.features.layout.BlockAlignExample; import com.demcha.examples.features.lists.NestedListExample; import com.demcha.examples.features.shapes.PhotoClipExample; @@ -145,6 +146,7 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + PhotoClipExample.generate()); System.out.println("Generated: " + SvgIconGalleryExample.generate()); System.out.println("Generated: " + BlockAlignExample.generate()); + System.out.println("Generated: " + BleedExample.generate()); System.out.println("Generated: " + TransformsExample.generate()); System.out.println("Generated: " + TableAdvancedExample.generate()); diff --git a/examples/src/main/java/com/demcha/examples/features/layout/BleedExample.java b/examples/src/main/java/com/demcha/examples/features/layout/BleedExample.java new file mode 100644 index 000000000..f4e785534 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/features/layout/BleedExample.java @@ -0,0 +1,110 @@ +package com.demcha.examples.features.layout; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.style.DocumentBleed; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentEdge; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Runnable showcase for v1.9 content bleed: a section's background fill extends + * to the trimmed physical page edge on the declared sides, while its children + * stay inside the content margin — a full-bleed masthead band whose title never + * runs off the page. The intent-revealing replacement for the hand-computed + * negative-margin idiom. + * + *
{@code
+ * page.addSection(band -> band
+ * .fillColor(ink)
+ * .bleedToEdge(DocumentEdge.TOP, DocumentEdge.LEFT, DocumentEdge.RIGHT)
+ * .addParagraph("Title")); // title stays in the safe area
+ *
+ * page.addSection(rule -> rule
+ * .fillColor(accent)
+ * .bleed(DocumentBleed.of(DocumentEdge.LEFT, DocumentEdge.RIGHT)));
+ * }
+ *
+ * @author Artem Demchyshyn
+ */
+public final class BleedExample {
+
+ private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
+ private static final DocumentColor ACCENT = DocumentColor.rgb(196, 30, 58);
+ private static final DocumentColor MUTED = DocumentColor.rgb(90, 96, 105);
+
+ private BleedExample() {
+ }
+
+ /**
+ * Renders the bleed sheet: a full-bleed masthead, a body paragraph, and a
+ * side-to-side accent band.
+ *
+ * @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", "content-bleed.pdf");
+
+ DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(10).withColor(MUTED);
+
+ try (DocumentSession document = GraphCompose.document(pdfFile)
+ .pageSize(420, 480)
+ .margin(DocumentInsets.of(36))
+ .create()) {
+ document.pageFlow(page -> {
+ // Full-bleed masthead: fill reaches the top + both side edges,
+ // the heading text stays seated in the content margin.
+ page.addSection(band -> band
+ .fillColor(INK)
+ .padding(DocumentInsets.of(16))
+ .bleedToEdge(DocumentEdge.TOP, DocumentEdge.LEFT, DocumentEdge.RIGHT)
+ .addParagraph(p -> p
+ .text("Content bleed")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(22)
+ .withColor(DocumentColor.rgb(255, 255, 255)))));
+
+ page.addParagraph(p -> p
+ .text("band.bleedToEdge(TOP, LEFT, RIGHT) — the fill runs to the page "
+ + "edge while the title stays inside the content margin, so text "
+ + "never clips. The intent-revealing replacement for a hand-tuned "
+ + "negative margin.")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(9.5).withColor(MUTED))
+ .padding(new DocumentInsets(12, 0, 6, 0)));
+
+ page.addParagraph(p -> p
+ .text("rule.bleed(DocumentBleed.of(LEFT, RIGHT))")
+ .textStyle(caption)
+ .padding(DocumentInsets.top(14)));
+
+ // Edge-to-edge accent band: horizontal bleed only.
+ page.addSection(rule -> rule
+ .fillColor(ACCENT)
+ .padding(new DocumentInsets(5, 0, 5, 0))
+ .bleed(DocumentBleed.of(DocumentEdge.LEFT, DocumentEdge.RIGHT))
+ .addParagraph(p -> p
+ .text("edge to edge")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(9)
+ .withColor(DocumentColor.rgb(255, 255, 255)))));
+
+ page.addParagraph(p -> p
+ .text("Without bleed, the same band would stop at the content margin "
+ + "on every side.")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(9.5).withColor(MUTED))
+ .padding(DocumentInsets.top(12)));
+ });
+
+ document.buildPdf();
+ }
+
+ return pdfFile;
+ }
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("Generated: " + generate());
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java b/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
index 1da637d36..db5c7f84b 100644
--- a/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
+++ b/src/main/java/com/demcha/compose/document/dsl/AbstractFlowBuilder.java
@@ -33,6 +33,7 @@ public abstract class AbstractFlowBuilderOnly the flow's background fill/border extends to the edge — its + * children stay inside the content margin, so text never runs off the page. + * Horizontal bleed widens the flow to the side edges; vertical bleed extends + * it toward the top/bottom edge and is meaningful for a flow already seated + * against that edge (e.g. a masthead band at the top of the page).
+ * + * @param bleed edges to bleed, or {@code null}/{@link DocumentBleed#none()} to clear + * @return this builder + * @see #bleedToEdge(DocumentEdge...) + * @since 1.9.0 + */ + public T bleed(DocumentBleed bleed) { + this.bleed = bleed == null ? DocumentBleed.none() : bleed; + return self(); + } + + /** + * Shorthand for {@link #bleed(DocumentBleed)} with the given edges — e.g. + * {@code bleedToEdge(DocumentEdge.LEFT, DocumentEdge.RIGHT)} for a band that + * reaches both side edges. + * + * @param edges edges to bleed; no edges clears the bleed + * @return this builder + * @since 1.9.0 + */ + public T bleedToEdge(DocumentEdge... edges) { + return bleed(DocumentBleed.of(edges)); + } + /** * Sets the flow background fill. * @@ -1082,6 +1118,10 @@ protected DocumentCornerRadius cornerRadius() { protected DocumentBorders borders() { return borders; } + + protected DocumentBleed bleed() { + return bleed; + } } /** diff --git a/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java b/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java index 89d6a31f3..76a39f787 100644 --- a/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/SectionBuilder.java @@ -51,7 +51,7 @@ public SectionBuilder keepTogether(boolean value) { @Override protected SectionNode buildNode() { return new SectionNode(name(), children(), spacing(), padding(), margin(), fillColor(), - stroke(), cornerRadius(), borders(), keepTogether, anchor()); + stroke(), cornerRadius(), borders(), keepTogether, anchor(), bleed()); } /** diff --git a/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java b/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java index d775f7dee..d817cbdd4 100644 --- a/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java +++ b/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java @@ -5,6 +5,8 @@ import com.demcha.compose.document.node.DocumentNode; import com.demcha.compose.document.node.LayerStackNode; import com.demcha.compose.document.node.PageBreakNode; +import com.demcha.compose.document.style.DocumentBleed; +import com.demcha.compose.document.style.DocumentEdge; import com.demcha.compose.engine.components.style.Margin; import com.demcha.compose.engine.components.style.Padding; import org.slf4j.Logger; @@ -391,6 +393,33 @@ private void compileComposite(PreparedNodeThis is the content-side twin of + * {@link com.demcha.compose.document.api.PageBackgroundFill}: where a background + * fill paints a coloured area to the page edge, a bled node draws live content + * (a band, a rule, an image) to the same edge. It replaces the hand-computed + * negative-margin idiom ({@code margin(new DocumentInsets(-pageMargin, ...))}) + * with an intent-revealing declaration the engine resolves against the active + * page margin.
+ * + *Horizontal bleed ({@link DocumentEdge#LEFT}/{@link DocumentEdge#RIGHT}) + * widens the node to reach the side edges. Vertical bleed + * ({@link DocumentEdge#TOP}/{@link DocumentEdge#BOTTOM}) extends the node toward + * the top/bottom edge and is meaningful for a node already seated against that + * edge (e.g. a masthead band at the top of the first page). Instances are + * immutable and thread-safe.
+ * + * @param edges the set of edges to bleed; never {@code null} + * @author Artem Demchyshyn + * @see DocumentEdge + * @since 1.9.0 + */ +public record DocumentBleed(Set