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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added assets/readme/examples/content-bleed.pdf
Binary file not shown.
21 changes: 21 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <pre>{@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)));
* }</pre>
*
* @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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public abstract class AbstractFlowBuilder<T extends AbstractFlowBuilder<T, N>, N
private DocumentStroke stroke;
private DocumentCornerRadius cornerRadius = DocumentCornerRadius.ZERO;
private DocumentBorders borders = DocumentBorders.NONE;
private DocumentBleed bleed = DocumentBleed.none();

/**
* Creates a base flow builder.
Expand Down Expand Up @@ -131,6 +132,41 @@ public T margin(float top, float right, float bottom, float left) {
return margin(new DocumentInsets(top, right, bottom, left));
}

/**
* Declares the page edges on which this flow bleeds past the page content
* margin to the trimmed physical page edge. The intent-revealing replacement
* for the hand-computed negative-margin idiom — the engine resolves the bleed
* against the active page margin at layout time.
*
* <p>Only 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).</p>
*
* @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.
*
Expand Down Expand Up @@ -1082,6 +1118,10 @@ protected DocumentCornerRadius cornerRadius() {
protected DocumentBorders borders() {
return borders;
}

protected DocumentBleed bleed() {
return bleed;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -391,22 +393,50 @@ private void compileComposite(PreparedNode<DocumentNode> prepared,
advanceSpace(padding.bottom() + margin.bottom(), state);
int endPage = state.pageIndex;
double endPageBottomY = state.pageTop() - state.usedHeight + margin.bottom();

// Content bleed: the decoration box (fill/border) extends to the trimmed
// page edge on the declared edges, while children stay in the content
// region (so text never runs off the page). Byte-identical when the node
// does not bleed — every value below collapses to the in-margin geometry.
DocumentBleed bleed = node.bleed();
double decorX = placementX;
double decorWidth = naturalMeasure.width();
double decorTopY = placementTopY;
double decorBottomY = endPageBottomY;
if (bleed.any()) {
double pageWidth = state.canvas.width();
double pageHeight = state.canvas.height();
if (bleed.bleeds(DocumentEdge.LEFT)) {
decorWidth += decorX;
decorX = 0.0;
}
if (bleed.bleeds(DocumentEdge.RIGHT)) {
decorWidth = Math.max(0.0, pageWidth - decorX);
}
if (bleed.bleeds(DocumentEdge.TOP)) {
decorTopY = pageHeight;
}
if (bleed.bleeds(DocumentEdge.BOTTOM)) {
decorBottomY = 0.0;
}
}
List<PlacedFragment> decorationFragments = compositeDecorationFragments(
prepared,
definition,
path,
parentPath,
childIndex,
depth,
placementX,
placementTopY,
endPageBottomY,
naturalMeasure.width(),
decorX,
decorTopY,
decorBottomY,
decorWidth,
startPage,
endPage,
margin,
padding,
state.canvas,
bleed,
fragmentContext);
if (!decorationFragments.isEmpty()) {
fragments.addAll(decorationInsertIndex, decorationFragments);
Expand Down Expand Up @@ -585,6 +615,7 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
margin,
padding,
state.canvas,
DocumentBleed.none(),
fragmentContext);
if (!decorationFragments.isEmpty()) {
fragments.addAll(decorationInsertIndex, decorationFragments);
Expand Down Expand Up @@ -701,6 +732,7 @@ private void compileStackedLayer(PreparedNode<DocumentNode> prepared,
margin,
padding,
state.canvas,
DocumentBleed.none(),
fragmentContext);
if (!decorationFragments.isEmpty()) {
fragments.addAll(decorationInsertIndex, decorationFragments);
Expand Down Expand Up @@ -1212,6 +1244,7 @@ private double compileNodeInFixedSlot(PreparedNode<DocumentNode> prepared,
margin,
padding,
canvas,
DocumentBleed.none(),
fragmentContext);
if (!stackDecorations.isEmpty()) {
fragments.addAll(decorationInsertIndex, stackDecorations);
Expand Down Expand Up @@ -1306,6 +1339,7 @@ private double compileNodeInFixedSlot(PreparedNode<DocumentNode> prepared,
margin,
padding,
canvas,
DocumentBleed.none(),
fragmentContext);
if (!decorationFragments.isEmpty()) {
fragments.addAll(decorationInsertIndex, decorationFragments);
Expand Down Expand Up @@ -1355,10 +1389,17 @@ private List<PlacedFragment> compositeDecorationFragments(PreparedNode<DocumentN
Margin margin,
Padding padding,
LayoutCanvas canvas,
DocumentBleed bleed,
FragmentContext fragmentContext) {
List<PlacedFragment> placed = new ArrayList<>();
double pageTopY = canvas.height() - canvas.margin().top();
double pageBottomY = canvas.margin().bottom();
// On bled edges the clamp bound is the physical page edge rather than the
// content-area edge, so the fill reaches past the top/bottom margin.
double pageTopY = bleed.bleeds(DocumentEdge.TOP)
? canvas.height()
: canvas.height() - canvas.margin().top();
double pageBottomY = bleed.bleeds(DocumentEdge.BOTTOM)
? 0.0
: canvas.margin().bottom();

for (int pageIndex = startPage; pageIndex <= endPage; pageIndex++) {
double segmentTopY = pageIndex == startPage ? startPageTopY : pageTopY;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.demcha.compose.document.node;

import com.demcha.compose.document.style.DocumentBleed;
import com.demcha.compose.document.style.DocumentInsets;

import java.util.List;
Expand Down Expand Up @@ -70,5 +71,18 @@ default String nodeKind() {
default boolean keepTogether() {
return false;
}

/**
* Edges on which this node bleeds past the page content margin to the
* trimmed physical page edge. Default {@link DocumentBleed#none()} (normal
* in-margin placement), so nodes that do not opt in are placed exactly as
* before.
*
* @return the edges to bleed; never {@code null}
* @since 1.9.0
*/
default DocumentBleed bleed() {
return DocumentBleed.none();
}
}

Loading