Skip to content

Commit c45623c

Browse files
authored
feat(api): LineBuilder.lineCap — round/square caps and dotted lines (#229)
LineBuilder gains lineCap(DocumentLineCap), bringing the round/square end-caps PathBuilder already exposed to plain lines. Pairing ROUND with a short dash draws a dotted line — line.dashed(0.1, 4).lineCap(ROUND) renders round dots, the classic table-of-contents leader/separator. A lineCap component is threaded through LineNode, LineFragmentPayload, LineDefinition and PdfLineFragmentRenderHandler the same way dashPattern was; the BUTT default emits no cap operator (applyStrokeStyle short-circuits), so existing line output is byte-identical. No lineJoin is added — a single line segment has no corners. Verified: ./mvnw test -pl . — 1501 tests, 0 baselines changed. PdfLineStrokeStyleTest asserts ROUND/SQUARE emit the J operator and BUTT emits none; a runnable LineCapExample ships with a committed preview.
1 parent 03db61a commit c45623c

12 files changed

Lines changed: 346 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ PDF `GoTo` actions. External links are unchanged.
1212

1313
### Public API
1414

15+
- **`LineBuilder.lineCap(DocumentLineCap)`** (`@since 1.9.0`). Lines gain the
16+
round / square end-caps `PathBuilder` already exposed. Pairing `ROUND` with a
17+
short dash draws a dotted line — `line.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)`
18+
renders round dots (the standard table-of-contents leader / separator style).
19+
The `BUTT` default emits no cap operator, so existing line output is
20+
byte-identical.
21+
1522
- **Content bleed: `DocumentBleed` / `DocumentEdge`** (`@since 1.9.0`). Flow
1623
builders gain `bleed(DocumentBleed)` and `bleedToEdge(DocumentEdge...)`, so a
1724
section's background fill extends to the trimmed physical page edge on the
1.12 KB
Binary file not shown.

examples/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ are with the canonical DSL, then jump to its detailed section below.
9292
|---|---|---|
9393
| [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) |
9494
| [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) |
95+
| [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) |
9596
| [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) |
9697
| [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) |
9798
| [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) |
@@ -385,6 +386,22 @@ flow.addPath(path -> path
385386
[📄 View PDF](../assets/readme/examples/vector-path.pdf) ·
386387
[📜 Full source](src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java)
387388

389+
### Line caps & dotted lines
390+
391+
`LineBuilder.lineCap(DocumentLineCap)` brings the round / square end-caps
392+
`PathBuilder` already had to plain lines. The headline use is a dotted line: a
393+
`ROUND` cap on a near-zero dash draws round dots — the classic table-of-contents
394+
leader / separator. The `BUTT` default emits no cap operator, so existing line
395+
output stays byte-identical.
396+
397+
```java
398+
flow.addLine(l -> l.horizontal(w).stroke(stroke)
399+
.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)); // round dots
400+
```
401+
402+
[📄 View PDF](../assets/readme/examples/line-cap.pdf) ·
403+
[📜 Full source](src/main/java/com/demcha/examples/features/shapes/LineCapExample.java)
404+
388405
### SVG icon gallery
389406

390407
A stress-test sheet for the beta SVG reader: 34 real-world multicolour

examples/src/main/java/com/demcha/examples/GenerateAllExamples.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.demcha.examples.features.layout.BleedExample;
1010
import com.demcha.examples.features.layout.BlockAlignExample;
1111
import com.demcha.examples.features.lists.NestedListExample;
12+
import com.demcha.examples.features.shapes.LineCapExample;
1213
import com.demcha.examples.features.shapes.PhotoClipExample;
1314
import com.demcha.examples.features.shapes.ShapeContainerExample;
1415
import com.demcha.examples.features.shapes.VectorPathExample;
@@ -143,6 +144,7 @@ public static void main(String[] args) throws Exception {
143144
// v1.5 visual primitives
144145
System.out.println("Generated: " + ShapeContainerExample.generate());
145146
System.out.println("Generated: " + VectorPathExample.generate());
147+
System.out.println("Generated: " + LineCapExample.generate());
146148
System.out.println("Generated: " + PhotoClipExample.generate());
147149
System.out.println("Generated: " + SvgIconGalleryExample.generate());
148150
System.out.println("Generated: " + BlockAlignExample.generate());
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.demcha.examples.features.shapes;
2+
3+
import com.demcha.compose.GraphCompose;
4+
import com.demcha.compose.document.api.DocumentSession;
5+
import com.demcha.compose.document.style.DocumentColor;
6+
import com.demcha.compose.document.style.DocumentInsets;
7+
import com.demcha.compose.document.style.DocumentLineCap;
8+
import com.demcha.compose.document.style.DocumentStroke;
9+
import com.demcha.compose.document.style.DocumentTextStyle;
10+
import com.demcha.examples.support.ExampleOutputPaths;
11+
12+
import java.nio.file.Path;
13+
14+
/**
15+
* Runnable showcase for v1.9 line caps: {@code LineBuilder.lineCap(...)} brings
16+
* the round / square end-caps {@code PathBuilder} already had to plain lines.
17+
* The headline use is a dotted line — a {@code ROUND} cap on a near-zero dash
18+
* draws round dots, the classic table-of-contents leader / separator.
19+
*
20+
* <pre>{@code
21+
* flow.addLine(l -> l.horizontal(w).stroke(stroke)
22+
* .dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)); // round dots
23+
* }</pre>
24+
*
25+
* @author Artem Demchyshyn
26+
*/
27+
public final class LineCapExample {
28+
29+
private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
30+
private static final DocumentColor MUTED = DocumentColor.rgb(90, 96, 105);
31+
private static final double RULE = 320;
32+
33+
private LineCapExample() {
34+
}
35+
36+
/**
37+
* Renders the line-cap sheet: solid, dashed, and dotted rules, plus a
38+
* thick round-capped vs butt-capped comparison.
39+
*
40+
* @return path to the generated PDF
41+
* @throws Exception if rendering or file IO fails
42+
*/
43+
public static Path generate() throws Exception {
44+
Path pdfFile = ExampleOutputPaths.prepare("features/shapes", "line-cap.pdf");
45+
46+
DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(10).withColor(MUTED);
47+
DocumentStroke thin = DocumentStroke.of(INK, 1.4);
48+
DocumentStroke thick = DocumentStroke.of(INK, 8);
49+
50+
try (DocumentSession document = GraphCompose.document(pdfFile)
51+
.pageSize(400, 320)
52+
.margin(DocumentInsets.of(36))
53+
.create()) {
54+
document.pageFlow(page -> {
55+
page.addParagraph(p -> p
56+
.text("Line caps & dotted lines")
57+
.textStyle(DocumentTextStyle.DEFAULT.withSize(18)));
58+
59+
page.addParagraph(p -> p.text("solid (default BUTT)").textStyle(caption)
60+
.padding(DocumentInsets.top(10)));
61+
page.addLine(l -> l.horizontal(RULE).stroke(thin));
62+
63+
page.addParagraph(p -> p.text("dashed(4, 3)").textStyle(caption)
64+
.padding(DocumentInsets.top(8)));
65+
page.addLine(l -> l.horizontal(RULE).stroke(thin).dashed(4, 3));
66+
67+
page.addParagraph(p -> p.text("dashed(0.1, 4).lineCap(ROUND) — dotted leader")
68+
.textStyle(caption).padding(DocumentInsets.top(8)));
69+
page.addLine(l -> l.horizontal(RULE).stroke(thin)
70+
.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND));
71+
72+
page.addParagraph(p -> p.text("thick stroke — BUTT vs lineCap(ROUND)")
73+
.textStyle(caption).padding(DocumentInsets.top(12)));
74+
page.addLine(l -> l.horizontal(120).stroke(thick));
75+
page.addLine(l -> l.horizontal(120).stroke(thick)
76+
.lineCap(DocumentLineCap.ROUND).padding(DocumentInsets.top(6)));
77+
});
78+
79+
document.buildPdf();
80+
}
81+
82+
return pdfFile;
83+
}
84+
85+
public static void main(String[] args) throws Exception {
86+
System.out.println("Generated: " + generate());
87+
}
88+
}

src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfLineFragmentRenderHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public void render(PlacedFragment fragment,
4545
stream.setStrokingColor(stroke.strokeColor().color());
4646
stream.setLineWidth((float) stroke.width());
4747
PdfShapeGeometry.applyDashPattern(stream, payload.dashPattern());
48+
PdfShapeGeometry.applyStrokeStyle(stream, payload.lineCap(), null);
4849
stream.moveTo((float) (fragment.x() + payload.startX()), (float) (fragment.y() + payload.startY()));
4950
stream.lineTo((float) (fragment.x() + payload.endX()), (float) (fragment.y() + payload.endY()));
5051
stream.stroke();

src/main/java/com/demcha/compose/document/dsl/LineBuilder.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public final class LineBuilder implements Transformable<LineBuilder> {
3030
private DocumentInsets margin = DocumentInsets.zero();
3131
private DocumentTransform transform = DocumentTransform.NONE;
3232
private DocumentDashPattern dashPattern = DocumentDashPattern.NONE;
33+
private DocumentLineCap lineCap = DocumentLineCap.BUTT;
3334

3435
/**
3536
* Creates a line builder.
@@ -242,6 +243,22 @@ public LineBuilder dashed() {
242243
return this;
243244
}
244245

246+
/**
247+
* Sets the line end-cap style. {@code null} keeps the PDF default
248+
* ({@link DocumentLineCap#BUTT}). Pair {@code ROUND} with a short dash to
249+
* draw a dotted line — e.g.
250+
* {@code dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)} renders round dots
251+
* (a zero-length dash is rejected, so use a tiny on-segment).
252+
*
253+
* @param lineCap cap style, or {@code null} for the default
254+
* @return this builder
255+
* @since 1.9.0
256+
*/
257+
public LineBuilder lineCap(DocumentLineCap lineCap) {
258+
this.lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
259+
return this;
260+
}
261+
245262
/**
246263
* Attaches line-level external link metadata.
247264
*
@@ -364,7 +381,8 @@ public LineNode build() {
364381
margin,
365382
transform,
366383
dashPattern,
367-
anchor);
384+
anchor,
385+
lineCap);
368386
}
369387

370388
private boolean isHorizontalLine() {

src/main/java/com/demcha/compose/document/layout/definitions/LineDefinition.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public List<LayoutFragment> emitFragments(PreparedNode<LineNode> prepared,
6464
node.endY(),
6565
node.linkTarget(),
6666
node.bookmarkOptions(),
67-
node.dashPattern()));
67+
node.dashPattern(),
68+
node.lineCap()));
6869
return withAnchorMarker(
6970
wrapAtomicWithTransform(leaf, placement, node.transform()),
7071
node.anchor(),

src/main/java/com/demcha/compose/document/layout/payloads/LineFragmentPayload.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.demcha.compose.document.node.DocumentBookmarkOptions;
44
import com.demcha.compose.document.node.DocumentLinkTarget;
55
import com.demcha.compose.document.style.DocumentDashPattern;
6+
import com.demcha.compose.document.style.DocumentLineCap;
67
import com.demcha.compose.engine.components.content.shape.Stroke;
78

89
/**
@@ -16,6 +17,7 @@
1617
* @param linkTarget optional fragment-level link metadata
1718
* @param bookmarkOptions optional fragment-level bookmark metadata
1819
* @param dashPattern dash pattern for the stroke; {@link DocumentDashPattern#NONE} is solid
20+
* @param lineCap end-cap style; {@link DocumentLineCap#BUTT} is the default
1921
*/
2022
public record LineFragmentPayload(
2123
Stroke stroke,
@@ -25,14 +27,40 @@ public record LineFragmentPayload(
2527
double endY,
2628
DocumentLinkTarget linkTarget,
2729
DocumentBookmarkOptions bookmarkOptions,
28-
DocumentDashPattern dashPattern
30+
DocumentDashPattern dashPattern,
31+
DocumentLineCap lineCap
2932
) implements PdfSemanticFragmentPayload {
3033

3134
/**
32-
* Normalizes the dash pattern, defaulting to a solid stroke.
35+
* Normalizes the dash pattern and cap, defaulting to a solid butt-capped stroke.
3336
*/
3437
public LineFragmentPayload {
3538
dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern;
39+
lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
40+
}
41+
42+
/**
43+
* Backward-compatible constructor without the cap — defaults to
44+
* {@link DocumentLineCap#BUTT}.
45+
*
46+
* @param stroke line stroke
47+
* @param startX line start x offset inside the fragment
48+
* @param startY line start y offset inside the fragment
49+
* @param endX line end x offset inside the fragment
50+
* @param endY line end y offset inside the fragment
51+
* @param linkTarget optional fragment-level link metadata
52+
* @param bookmarkOptions optional fragment-level bookmark metadata
53+
* @param dashPattern dash pattern for the stroke
54+
*/
55+
public LineFragmentPayload(Stroke stroke,
56+
double startX,
57+
double startY,
58+
double endX,
59+
double endY,
60+
DocumentLinkTarget linkTarget,
61+
DocumentBookmarkOptions bookmarkOptions,
62+
DocumentDashPattern dashPattern) {
63+
this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, dashPattern, DocumentLineCap.BUTT);
3664
}
3765

3866
/**
@@ -53,6 +81,6 @@ public LineFragmentPayload(Stroke stroke,
5381
double endY,
5482
DocumentLinkTarget linkTarget,
5583
DocumentBookmarkOptions bookmarkOptions) {
56-
this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, DocumentDashPattern.NONE);
84+
this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, DocumentDashPattern.NONE, DocumentLineCap.BUTT);
5785
}
5886
}

src/main/java/com/demcha/compose/document/node/LineNode.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.demcha.compose.document.style.DocumentDashPattern;
44
import com.demcha.compose.document.style.DocumentInsets;
5+
import com.demcha.compose.document.style.DocumentLineCap;
56
import com.demcha.compose.document.style.DocumentStroke;
67
import com.demcha.compose.document.style.DocumentTransform;
78

@@ -26,6 +27,9 @@
2627
* {@link DocumentDashPattern#NONE} (solid)
2728
* @param anchor optional in-document navigation anchor name at the line box's
2829
* top-left, or {@code null} for none
30+
* @param lineCap end-cap style for the stroke; defaults to
31+
* {@link DocumentLineCap#BUTT}. {@code ROUND} turns a
32+
* dashed stroke into a dotted line
2933
* @author Artem Demchyshyn
3034
*/
3135
public record LineNode(
@@ -43,7 +47,8 @@ public record LineNode(
4347
DocumentInsets margin,
4448
DocumentTransform transform,
4549
DocumentDashPattern dashPattern,
46-
String anchor
50+
String anchor,
51+
DocumentLineCap lineCap
4752
) implements DocumentNode {
4853
/**
4954
* Normalizes spacing defaults and validates explicit line geometry.
@@ -55,6 +60,7 @@ public record LineNode(
5560
transform = transform == null ? DocumentTransform.NONE : transform;
5661
dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern;
5762
anchor = anchor == null || anchor.isBlank() ? null : anchor.trim();
63+
lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
5864
requireNonNegativeFinite(width, "width");
5965
requireNonNegativeFinite(height, "height");
6066
requireFinite(startX, "startX");
@@ -63,6 +69,45 @@ public record LineNode(
6369
requireFinite(endY, "endY");
6470
}
6571

72+
/**
73+
* Backward-compatible canonical constructor without the line cap — defaults
74+
* to {@link DocumentLineCap#BUTT} (a squared, byte-identical end).
75+
*
76+
* @param name node name used in snapshots and layout graph paths
77+
* @param width resolved line box width
78+
* @param height resolved line box height
79+
* @param startX line start x offset inside the box
80+
* @param startY line start y offset inside the box
81+
* @param endX line end x offset inside the box
82+
* @param endY line end y offset inside the box
83+
* @param stroke line stroke descriptor
84+
* @param linkTarget optional node-level link target
85+
* @param bookmarkOptions optional node-level bookmark metadata
86+
* @param padding inner padding
87+
* @param margin outer margin
88+
* @param transform render-time affine transform
89+
* @param dashPattern dash pattern for the stroke
90+
* @param anchor optional navigation anchor name
91+
*/
92+
public LineNode(String name,
93+
double width,
94+
double height,
95+
double startX,
96+
double startY,
97+
double endX,
98+
double endY,
99+
DocumentStroke stroke,
100+
DocumentLinkTarget linkTarget,
101+
DocumentBookmarkOptions bookmarkOptions,
102+
DocumentInsets padding,
103+
DocumentInsets margin,
104+
DocumentTransform transform,
105+
DocumentDashPattern dashPattern,
106+
String anchor) {
107+
this(name, width, height, startX, startY, endX, endY, stroke, linkTarget, bookmarkOptions,
108+
padding, margin, transform, dashPattern, anchor, DocumentLineCap.BUTT);
109+
}
110+
66111
/**
67112
* Backwards-compatible canonical constructor taking external
68113
* {@link DocumentLinkOptions} (wrapped) and no navigation anchor.

0 commit comments

Comments
 (0)