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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ PDF `GoTo` actions. External links are unchanged.

### Public API

- **`LineBuilder.lineCap(DocumentLineCap)`** (`@since 1.9.0`). Lines gain the
round / square end-caps `PathBuilder` already exposed. Pairing `ROUND` with a
short dash draws a dotted line — `line.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)`
renders round dots (the standard table-of-contents leader / separator style).
The `BUTT` default emits no cap operator, so existing line output is
byte-identical.

- **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
Expand Down
Binary file added assets/readme/examples/line-cap.pdf
Binary file not shown.
17 changes: 17 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ are with the canonical DSL, then jump to its detailed section below.
|---|---|---|
| [Shape containers](#shape-containers) | Circles, ellipses, rounded cards with `ClipPolicy.CLIP_PATH` | [PDF](../assets/readme/examples/shape-container.pdf) · [Source](src/main/java/com/demcha/examples/features/shapes/ShapeContainerExample.java) |
| [Vector paths (Bézier)](#vector-paths-bézier) | `addPath(...)` + `SvgPath.parse(...)` — design shapes and imported SVG icons as native curves; zero tessellation | [PDF](../assets/readme/examples/vector-path.pdf) · [Source](src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java) |
| [Line caps & dotted lines](#line-caps--dotted-lines) | `line.lineCap(ROUND)` — round / square caps for plain lines; `dashed(0.1, 4).lineCap(ROUND)` draws a dotted leader | [PDF](../assets/readme/examples/line-cap.pdf) · [Source](src/main/java/com/demcha/examples/features/shapes/LineCapExample.java) |
| [SVG icon gallery](#svg-icon-gallery) | 34 real-world multicolour svgrepo icons via `SvgIcon.parse` — up to 19 layers each, the whole set 156 KB of sources | [PDF](../assets/readme/examples/svg-icon-gallery.pdf) · [Source](src/main/java/com/demcha/examples/features/svg/SvgIconGalleryExample.java) |
| [Advanced tables](#advanced-tables) | Row span, zebra rows, totals, repeating header on page break | [PDF](../assets/readme/examples/table-advanced.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java) |
| [Barcodes](#barcodes) | QR, Code 128, Code 39, EAN-13, EAN-8, branded QR with theme colours | [PDF](../assets/readme/examples/barcode-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/barcodes/BarcodeShowcaseExample.java) |
Expand Down Expand Up @@ -385,6 +386,22 @@ flow.addPath(path -> path
[📄 View PDF](../assets/readme/examples/vector-path.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java)

### Line caps & dotted lines

`LineBuilder.lineCap(DocumentLineCap)` brings the round / square end-caps
`PathBuilder` already had to plain lines. The headline use is a dotted line: a
`ROUND` cap on a near-zero dash draws round dots — the classic table-of-contents
leader / separator. The `BUTT` default emits no cap operator, so existing line
output stays byte-identical.

```java
flow.addLine(l -> l.horizontal(w).stroke(stroke)
.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)); // round dots
```

[📄 View PDF](../assets/readme/examples/line-cap.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/shapes/LineCapExample.java)

### SVG icon gallery

A stress-test sheet for the beta SVG reader: 34 real-world multicolour
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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.LineCapExample;
import com.demcha.examples.features.shapes.PhotoClipExample;
import com.demcha.examples.features.shapes.ShapeContainerExample;
import com.demcha.examples.features.shapes.VectorPathExample;
Expand Down Expand Up @@ -143,6 +144,7 @@ public static void main(String[] args) throws Exception {
// v1.5 visual primitives
System.out.println("Generated: " + ShapeContainerExample.generate());
System.out.println("Generated: " + VectorPathExample.generate());
System.out.println("Generated: " + LineCapExample.generate());
System.out.println("Generated: " + PhotoClipExample.generate());
System.out.println("Generated: " + SvgIconGalleryExample.generate());
System.out.println("Generated: " + BlockAlignExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.demcha.examples.features.shapes;

import com.demcha.compose.GraphCompose;
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.DocumentLineCap;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Runnable showcase for v1.9 line caps: {@code LineBuilder.lineCap(...)} brings
* the round / square end-caps {@code PathBuilder} already had to plain lines.
* The headline use is a dotted line — a {@code ROUND} cap on a near-zero dash
* draws round dots, the classic table-of-contents leader / separator.
*
* <pre>{@code
* flow.addLine(l -> l.horizontal(w).stroke(stroke)
* .dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)); // round dots
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class LineCapExample {

private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
private static final DocumentColor MUTED = DocumentColor.rgb(90, 96, 105);
private static final double RULE = 320;

private LineCapExample() {
}

/**
* Renders the line-cap sheet: solid, dashed, and dotted rules, plus a
* thick round-capped vs butt-capped comparison.
*
* @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/shapes", "line-cap.pdf");

DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(10).withColor(MUTED);
DocumentStroke thin = DocumentStroke.of(INK, 1.4);
DocumentStroke thick = DocumentStroke.of(INK, 8);

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(400, 320)
.margin(DocumentInsets.of(36))
.create()) {
document.pageFlow(page -> {
page.addParagraph(p -> p
.text("Line caps & dotted lines")
.textStyle(DocumentTextStyle.DEFAULT.withSize(18)));

page.addParagraph(p -> p.text("solid (default BUTT)").textStyle(caption)
.padding(DocumentInsets.top(10)));
page.addLine(l -> l.horizontal(RULE).stroke(thin));

page.addParagraph(p -> p.text("dashed(4, 3)").textStyle(caption)
.padding(DocumentInsets.top(8)));
page.addLine(l -> l.horizontal(RULE).stroke(thin).dashed(4, 3));

page.addParagraph(p -> p.text("dashed(0.1, 4).lineCap(ROUND) — dotted leader")
.textStyle(caption).padding(DocumentInsets.top(8)));
page.addLine(l -> l.horizontal(RULE).stroke(thin)
.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND));

page.addParagraph(p -> p.text("thick stroke — BUTT vs lineCap(ROUND)")
.textStyle(caption).padding(DocumentInsets.top(12)));
page.addLine(l -> l.horizontal(120).stroke(thick));
page.addLine(l -> l.horizontal(120).stroke(thick)
.lineCap(DocumentLineCap.ROUND).padding(DocumentInsets.top(6)));
});

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 @@ -45,6 +45,7 @@ public void render(PlacedFragment fragment,
stream.setStrokingColor(stroke.strokeColor().color());
stream.setLineWidth((float) stroke.width());
PdfShapeGeometry.applyDashPattern(stream, payload.dashPattern());
PdfShapeGeometry.applyStrokeStyle(stream, payload.lineCap(), null);
stream.moveTo((float) (fragment.x() + payload.startX()), (float) (fragment.y() + payload.startY()));
stream.lineTo((float) (fragment.x() + payload.endX()), (float) (fragment.y() + payload.endY()));
stream.stroke();
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/com/demcha/compose/document/dsl/LineBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public final class LineBuilder implements Transformable<LineBuilder> {
private DocumentInsets margin = DocumentInsets.zero();
private DocumentTransform transform = DocumentTransform.NONE;
private DocumentDashPattern dashPattern = DocumentDashPattern.NONE;
private DocumentLineCap lineCap = DocumentLineCap.BUTT;

/**
* Creates a line builder.
Expand Down Expand Up @@ -242,6 +243,22 @@ public LineBuilder dashed() {
return this;
}

/**
* Sets the line end-cap style. {@code null} keeps the PDF default
* ({@link DocumentLineCap#BUTT}). Pair {@code ROUND} with a short dash to
* draw a dotted line — e.g.
* {@code dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)} renders round dots
* (a zero-length dash is rejected, so use a tiny on-segment).
*
* @param lineCap cap style, or {@code null} for the default
* @return this builder
* @since 1.9.0
*/
public LineBuilder lineCap(DocumentLineCap lineCap) {
this.lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
return this;
}

/**
* Attaches line-level external link metadata.
*
Expand Down Expand Up @@ -364,7 +381,8 @@ public LineNode build() {
margin,
transform,
dashPattern,
anchor);
anchor,
lineCap);
}

private boolean isHorizontalLine() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public List<LayoutFragment> emitFragments(PreparedNode<LineNode> prepared,
node.endY(),
node.linkTarget(),
node.bookmarkOptions(),
node.dashPattern()));
node.dashPattern(),
node.lineCap()));
return withAnchorMarker(
wrapAtomicWithTransform(leaf, placement, node.transform()),
node.anchor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.demcha.compose.document.node.DocumentBookmarkOptions;
import com.demcha.compose.document.node.DocumentLinkTarget;
import com.demcha.compose.document.style.DocumentDashPattern;
import com.demcha.compose.document.style.DocumentLineCap;
import com.demcha.compose.engine.components.content.shape.Stroke;

/**
Expand All @@ -16,6 +17,7 @@
* @param linkTarget optional fragment-level link metadata
* @param bookmarkOptions optional fragment-level bookmark metadata
* @param dashPattern dash pattern for the stroke; {@link DocumentDashPattern#NONE} is solid
* @param lineCap end-cap style; {@link DocumentLineCap#BUTT} is the default
*/
public record LineFragmentPayload(
Stroke stroke,
Expand All @@ -25,14 +27,40 @@ public record LineFragmentPayload(
double endY,
DocumentLinkTarget linkTarget,
DocumentBookmarkOptions bookmarkOptions,
DocumentDashPattern dashPattern
DocumentDashPattern dashPattern,
DocumentLineCap lineCap
) implements PdfSemanticFragmentPayload {

/**
* Normalizes the dash pattern, defaulting to a solid stroke.
* Normalizes the dash pattern and cap, defaulting to a solid butt-capped stroke.
*/
public LineFragmentPayload {
dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern;
lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
}

/**
* Backward-compatible constructor without the cap — defaults to
* {@link DocumentLineCap#BUTT}.
*
* @param stroke line stroke
* @param startX line start x offset inside the fragment
* @param startY line start y offset inside the fragment
* @param endX line end x offset inside the fragment
* @param endY line end y offset inside the fragment
* @param linkTarget optional fragment-level link metadata
* @param bookmarkOptions optional fragment-level bookmark metadata
* @param dashPattern dash pattern for the stroke
*/
public LineFragmentPayload(Stroke stroke,
double startX,
double startY,
double endX,
double endY,
DocumentLinkTarget linkTarget,
DocumentBookmarkOptions bookmarkOptions,
DocumentDashPattern dashPattern) {
this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, dashPattern, DocumentLineCap.BUTT);
}

/**
Expand All @@ -53,6 +81,6 @@ public LineFragmentPayload(Stroke stroke,
double endY,
DocumentLinkTarget linkTarget,
DocumentBookmarkOptions bookmarkOptions) {
this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, DocumentDashPattern.NONE);
this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, DocumentDashPattern.NONE, DocumentLineCap.BUTT);
}
}
47 changes: 46 additions & 1 deletion src/main/java/com/demcha/compose/document/node/LineNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.demcha.compose.document.style.DocumentDashPattern;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentLineCap;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTransform;

Expand All @@ -26,6 +27,9 @@
* {@link DocumentDashPattern#NONE} (solid)
* @param anchor optional in-document navigation anchor name at the line box's
* top-left, or {@code null} for none
* @param lineCap end-cap style for the stroke; defaults to
* {@link DocumentLineCap#BUTT}. {@code ROUND} turns a
* dashed stroke into a dotted line
* @author Artem Demchyshyn
*/
public record LineNode(
Expand All @@ -43,7 +47,8 @@ public record LineNode(
DocumentInsets margin,
DocumentTransform transform,
DocumentDashPattern dashPattern,
String anchor
String anchor,
DocumentLineCap lineCap
) implements DocumentNode {
/**
* Normalizes spacing defaults and validates explicit line geometry.
Expand All @@ -55,6 +60,7 @@ public record LineNode(
transform = transform == null ? DocumentTransform.NONE : transform;
dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern;
anchor = anchor == null || anchor.isBlank() ? null : anchor.trim();
lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
requireNonNegativeFinite(width, "width");
requireNonNegativeFinite(height, "height");
requireFinite(startX, "startX");
Expand All @@ -63,6 +69,45 @@ public record LineNode(
requireFinite(endY, "endY");
}

/**
* Backward-compatible canonical constructor without the line cap — defaults
* to {@link DocumentLineCap#BUTT} (a squared, byte-identical end).
*
* @param name node name used in snapshots and layout graph paths
* @param width resolved line box width
* @param height resolved line box height
* @param startX line start x offset inside the box
* @param startY line start y offset inside the box
* @param endX line end x offset inside the box
* @param endY line end y offset inside the box
* @param stroke line stroke descriptor
* @param linkTarget optional node-level link target
* @param bookmarkOptions optional node-level bookmark metadata
* @param padding inner padding
* @param margin outer margin
* @param transform render-time affine transform
* @param dashPattern dash pattern for the stroke
* @param anchor optional navigation anchor name
*/
public LineNode(String name,
double width,
double height,
double startX,
double startY,
double endX,
double endY,
DocumentStroke stroke,
DocumentLinkTarget linkTarget,
DocumentBookmarkOptions bookmarkOptions,
DocumentInsets padding,
DocumentInsets margin,
DocumentTransform transform,
DocumentDashPattern dashPattern,
String anchor) {
this(name, width, height, startX, startY, endX, endY, stroke, linkTarget, bookmarkOptions,
padding, margin, transform, dashPattern, anchor, DocumentLineCap.BUTT);
}

/**
* Backwards-compatible canonical constructor taking external
* {@link DocumentLinkOptions} (wrapped) and no navigation anchor.
Expand Down
Loading