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

### Public API

- **`DocumentPageNumbering` / `DocumentPageNumberStyle`** (`@since 1.9.0`). Header
and footer `{page}` / `{pages}` tokens can now offset, restart, restyle, and
suppress-on-first-page numbering per zone via
`DocumentHeaderFooter.builder().numbering(...)`. `DocumentPageNumbering` carries
`startAt` (printed value on the first counted page), `countFrom` (physical page
where counting begins), `showOnFirstPage`, and a `DocumentPageNumberStyle`
(`DECIMAL`, `LOWER_ROMAN`, `UPPER_ROMAN`, `LOWER_ALPHA`, `UPPER_ALPHA`) — e.g.
lower-roman or alphabetic numbering, an uncounted cover, or an offset/restarted
count (one style per zone; switching style mid-document — roman front matter then
arabic body — is a per-section concern). Under
an offset, `{pages}` expands to the counted total
(`startAt + (totalPages - countFrom)`), not the physical page count. The default
(`DocumentPageNumbering.DEFAULT`) is decimal, no offset, shown on every page, so
existing header/footer output is byte-identical.

- **`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)`
Expand Down
Binary file added assets/readme/examples/page-numbering.pdf
Binary file not shown.
23 changes: 23 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [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) |
| [Charts](#charts) | Native vector bar, line, and pie/donut charts — data/spec/style layers, axis & grid toggles, point markers, value labels, legend | [PDF](../assets/readme/examples/chart-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/charts/ChartShowcaseExample.java) |
| [PDF chrome](#pdf-chrome) | `DocumentMetadata`, `DocumentWatermark`, `DocumentHeaderFooter`, `DocumentBookmarkOptions` | [PDF](../assets/readme/examples/pdf-chrome.pdf) · [Source](src/main/java/com/demcha/examples/features/chrome/PdfChromeExample.java) |
| [Page numbering](#page-numbering) | `DocumentPageNumbering` — offset / restart / roman / suppress-on-first-page for `{page}` / `{pages}` footer tokens | [PDF](../assets/readme/examples/page-numbering.pdf) · [Source](src/main/java/com/demcha/examples/features/chrome/PageNumberingExample.java) |
| [HTTP streaming](#http-streaming) | `writePdf(OutputStream)` for Servlet / S3 / GCS — caller's stream is not closed | [PDF](../assets/readme/examples/invoice-http-stream.pdf) · [Source](src/main/java/com/demcha/examples/features/streaming/HttpStreamingExample.java) |
| [Word export (DOCX)](#word-export-docx) | `DocxSemanticBackend` — the same session renders a fixed-layout PDF and an editable Word file; paragraphs / lists / tables / images map 1:1, charts fall back to their data table | [PDF](../assets/readme/examples/word-export-companion.pdf) · [DOCX](../assets/readme/examples/word-export-companion.docx) · [Source](src/main/java/com/demcha/examples/features/docx/WordExportExample.java) |
| [Layout snapshot regression](#layout-snapshot-regression) | Deterministic `layoutSnapshot()` workflow with baseline + drift report — production regression-testing pattern | [PDF](../assets/readme/examples/invoice-snapshot-regression.pdf) · [Source](src/main/java/com/demcha/examples/features/snapshots/LayoutSnapshotRegressionExample.java) |
Expand Down Expand Up @@ -641,6 +642,28 @@ GraphCompose.document(outputFile)
[📄 View PDF](../assets/readme/examples/pdf-chrome.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/chrome/PdfChromeExample.java)

### Page numbering

`DocumentHeaderFooter.builder().numbering(...)` controls how the `{page}` /
`{pages}` footer tokens count: an offset (`startAt`), a restart point
(`countFrom`), a style (`DECIMAL` / `LOWER_ROMAN` / `UPPER_ROMAN` / `LOWER_ALPHA`
/ `UPPER_ALPHA`), and whether the number shows on the first page
(`showOnFirstPage`). Under an offset, `{pages}` reports the counted total, not the
physical page count. Here a cover is left uncounted and the body is lower-roman.

```java
session.chrome().footer(DocumentHeaderFooter.builder()
.centerText("{page} / {pages}")
.numbering(DocumentPageNumbering.builder()
.style(DocumentPageNumberStyle.LOWER_ROMAN)
.countFrom(2) // physical page 1 (the cover) is uncounted
.build())
.build());
```

[📄 View PDF](../assets/readme/examples/page-numbering.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/chrome/PageNumberingExample.java)

---

## Production patterns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.demcha.examples.features.canvas.CanvasLayerExample;
import com.demcha.examples.features.debug.DebugOverlayExample;
import com.demcha.examples.features.docx.WordExportExample;
import com.demcha.examples.features.chrome.PageNumberingExample;
import com.demcha.examples.features.chrome.PdfChromeExample;
import com.demcha.examples.features.layout.BleedExample;
import com.demcha.examples.features.layout.BlockAlignExample;
Expand Down Expand Up @@ -167,6 +168,7 @@ public static void main(String[] args) throws Exception {
// Theming + chrome
System.out.println("Generated: " + CustomBusinessThemeExample.generate());
System.out.println("Generated: " + PdfChromeExample.generate());
System.out.println("Generated: " + PageNumberingExample.generate());

// DOCX export
System.out.println("Generated: " + WordExportExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.demcha.examples.features.chrome;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.output.DocumentHeaderFooter;
import com.demcha.compose.document.output.DocumentPageNumberStyle;
import com.demcha.compose.document.output.DocumentPageNumbering;
import com.demcha.compose.document.style.DocumentColor;
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 page numbering: a footer whose {@code {page}} /
* {@code {pages}} tokens offset, restart, restyle, and suppress-on-first-page via
* {@code DocumentHeaderFooter.builder().numbering(...)}. Here a cover page is left
* uncounted ({@code countFrom = 2}) and the body is numbered in lower-roman, so
* {@code {pages}} reports the counted total rather than the physical page count.
*
* <pre>{@code
* session.chrome().footer(DocumentHeaderFooter.builder()
* .centerText("{page} / {pages}")
* .numbering(DocumentPageNumbering.builder()
* .style(DocumentPageNumberStyle.LOWER_ROMAN)
* .countFrom(2) // physical page 1 (the cover) is uncounted
* .build())
* .build());
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class PageNumberingExample {

private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
private static final DocumentColor MUTED = DocumentColor.rgb(96, 102, 112);

private PageNumberingExample() {
}

/**
* Renders a 4-page booklet: an uncounted cover, then three lower-roman body
* pages whose footer reads {@code i / iii}, {@code ii / iii}, {@code iii / iii}.
*
* @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/chrome", "page-numbering.pdf");

DocumentTextStyle title = DocumentTextStyle.DEFAULT.withSize(22).withColor(INK);
DocumentTextStyle body = DocumentTextStyle.DEFAULT.withSize(11).withColor(INK);
DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(9.5).withColor(MUTED);

try (DocumentSession session = GraphCompose.document(pdfFile)
.pageSize(300, 230)
.margin(DocumentInsets.of(30))
.create()) {

session.chrome().footer(DocumentHeaderFooter.builder()
.centerText("{page} / {pages}")
.numbering(DocumentPageNumbering.builder()
.style(DocumentPageNumberStyle.LOWER_ROMAN)
.countFrom(2) // the cover (physical page 1) is uncounted
.startAt(1)
.build())
.build());

session.pageFlow(page -> {
// Cover — uncounted, no footer number.
page.addParagraph(p -> p.text("Field Notes").textStyle(title));
page.addParagraph(p -> p.text("countFrom(2) leaves this cover uncounted; "
+ "the body restarts at i and {pages} reports the counted total.")
.textStyle(caption).padding(DocumentInsets.top(8)));
page.addPageBreak(b -> b.name("toBody"));

page.addParagraph(p -> p.text("Chapter I").textStyle(title));
page.addParagraph(p -> p.text("Footer reads i / iii.").textStyle(body)
.padding(DocumentInsets.top(6)));
page.addPageBreak(b -> b.name("toII"));

page.addParagraph(p -> p.text("Chapter II").textStyle(title));
page.addParagraph(p -> p.text("Footer reads ii / iii.").textStyle(body)
.padding(DocumentInsets.top(6)));
page.addPageBreak(b -> b.name("toIII"));

page.addParagraph(p -> p.text("Chapter III").textStyle(title));
page.addParagraph(p -> p.text("Footer reads iii / iii.").textStyle(body)
.padding(DocumentInsets.top(6)));
});

session.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 @@ -3,10 +3,13 @@
import com.demcha.compose.document.backend.fixed.pdf.options.*;
import com.demcha.compose.document.node.DocumentBarcodeOptions;
import com.demcha.compose.document.node.DocumentBarcodeType;
import com.demcha.compose.document.output.DocumentPageNumberStyle;
import com.demcha.compose.document.output.DocumentPageNumbering;
import com.demcha.compose.engine.components.content.barcode.BarcodeData;
import com.demcha.compose.engine.components.content.barcode.BarcodeType;
import com.demcha.compose.engine.components.content.header_footer.HeaderFooterConfig;
import com.demcha.compose.engine.components.content.header_footer.HeaderFooterZone;
import com.demcha.compose.engine.components.content.header_footer.PageNumberStyle;
import com.demcha.compose.engine.components.content.metadata.DocumentMetadata;
import com.demcha.compose.engine.components.content.protection.PdfProtectionConfig;
import com.demcha.compose.engine.components.content.watermark.WatermarkConfig;
Expand Down Expand Up @@ -74,6 +77,9 @@ static HeaderFooterConfig toEngine(PdfHeaderFooterOptions options) {
if (options == null) {
return null;
}
DocumentPageNumbering numbering = options.getNumbering() == null
? DocumentPageNumbering.DEFAULT
: options.getNumbering();
return HeaderFooterConfig.builder()
.zone(map(options.getZone()))
.height(options.getHeight())
Expand All @@ -85,6 +91,10 @@ static HeaderFooterConfig toEngine(PdfHeaderFooterOptions options) {
.showSeparator(options.isShowSeparator())
.separatorColor(options.getSeparatorColor())
.separatorThickness(options.getSeparatorThickness())
.numberStartAt(numbering.getStartAt())
.numberCountFrom(numbering.getCountFrom())
.numberShowOnFirstPage(numbering.isShowOnFirstPage())
.numberStyle(map(numbering.getStyle()))
.build();
}

Expand Down Expand Up @@ -122,6 +132,19 @@ private static HeaderFooterZone map(PdfHeaderFooterZone zone) {
return zone == PdfHeaderFooterZone.FOOTER ? HeaderFooterZone.FOOTER : HeaderFooterZone.HEADER;
}

private static PageNumberStyle map(DocumentPageNumberStyle style) {
if (style == null) {
return PageNumberStyle.DECIMAL;
}
return switch (style) {
case DECIMAL -> PageNumberStyle.DECIMAL;
case LOWER_ROMAN -> PageNumberStyle.LOWER_ROMAN;
case UPPER_ROMAN -> PageNumberStyle.UPPER_ROMAN;
case LOWER_ALPHA -> PageNumberStyle.LOWER_ALPHA;
case UPPER_ALPHA -> PageNumberStyle.UPPER_ALPHA;
};
}

private static BarcodeType map(DocumentBarcodeType type) {
return switch (type) {
case CODE_128 -> BarcodeType.CODE_128;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public static PdfHeaderFooterOptions toPdf(DocumentHeaderFooter entry) {
.showSeparator(entry.isShowSeparator())
.separatorColor(entry.getSeparatorColor() == null ? null : entry.getSeparatorColor().color())
.separatorThickness(entry.getSeparatorThickness())
.numbering(entry.getNumbering())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.demcha.compose.document.backend.fixed.pdf.options;

import com.demcha.compose.document.output.DocumentPageNumbering;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -42,6 +43,9 @@ public final class PdfHeaderFooterOptions {
@Builder.Default
private final float separatorThickness = 0.5f;

@Builder.Default
private final DocumentPageNumbering numbering = DocumentPageNumbering.DEFAULT;

private PdfHeaderFooterOptions() {
this.zone = PdfHeaderFooterZone.HEADER;
this.height = 30f;
Expand All @@ -53,6 +57,7 @@ private PdfHeaderFooterOptions() {
this.showSeparator = false;
this.separatorColor = new Color(200, 200, 200);
this.separatorThickness = 0.5f;
this.numbering = DocumentPageNumbering.DEFAULT;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public final class DocumentHeaderFooter {
@Builder.Default
private final float separatorThickness = 0.5f;

@Builder.Default
private final DocumentPageNumbering numbering = DocumentPageNumbering.DEFAULT;

private DocumentHeaderFooter() {
this.zone = DocumentHeaderFooterZone.HEADER;
this.height = 30f;
Expand All @@ -55,6 +58,7 @@ private DocumentHeaderFooter() {
this.showSeparator = false;
this.separatorColor = DocumentColor.LIGHT_GRAY;
this.separatorThickness = 0.5f;
this.numbering = DocumentPageNumbering.DEFAULT;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.demcha.compose.document.output;

/**
* How a page number is rendered in a header / footer {@code {page}} or
* {@code {pages}} token. A backend-neutral selector; the engine performs the
* actual numeral formatting.
*
* @author Artem Demchyshyn
* @see DocumentPageNumbering
* @since 1.9.0
*/
public enum DocumentPageNumberStyle {
/** Arabic numerals: {@code 1, 2, 3} (the default). */
DECIMAL,
/** Lowercase Roman numerals: {@code i, ii, iii} — common for front matter. */
LOWER_ROMAN,
/** Uppercase Roman numerals: {@code I, II, III}. */
UPPER_ROMAN,
/** Lowercase letters: {@code a, b, c, … z, aa}. */
LOWER_ALPHA,
/** Uppercase letters: {@code A, B, C, … Z, AA}. */
UPPER_ALPHA
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.demcha.compose.document.output;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

/**
* Page-numbering policy for the {@code {page}} / {@code {pages}} tokens in a
* header or footer zone: an offset, a restart point, a style, and whether the
* number shows on the first page. Attaches to a {@link DocumentHeaderFooter} via
* {@code builder().numbering(...)}.
*
* <p>{@code startAt} is the value printed on the first counted page;
* {@code countFrom} is the physical (1-based) page where counting begins —
* pages before it are not counted. So {@code startAt=1, countFrom=3} prints the
* first two pages without numbering and starts the body at {@code 1}. Under an
* offset, {@code {pages}} expands to the <em>counted</em> total
* ({@code startAt + (totalPages - countFrom)}), not the physical page count.</p>
*
* <p>Suppression is whole-zone: on a page where the number is not shown
* ({@code showOnFirstPage=false}, or a physical page before {@code countFrom}),
* the entire header/footer zone is skipped, not just the number — which is what
* a numbered cover / uncounted front matter wants. Put branding that must always
* appear in a separate, always-on zone.</p>
*
* <p>The {@link #DEFAULT} (decimal, no offset, shown on every page) reproduces
* the pre-1.9 behaviour exactly. Instances are immutable and thread-safe.</p>
*
* @author Artem Demchyshyn
* @see DocumentPageNumberStyle
* @since 1.9.0
*/
@Getter
@Builder(toBuilder = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class DocumentPageNumbering {

/** Decimal, no offset, shown on every page — the pre-1.9 default. */
public static final DocumentPageNumbering DEFAULT = builder().build();

/** Value printed on the first counted page. */
@Builder.Default
private final int startAt = 1;

/** Physical 1-based page where counting begins; earlier pages are uncounted. */
@Builder.Default
private final int countFrom = 1;

/** Whether the numbered zone is rendered on the first physical page. */
@Builder.Default
private final boolean showOnFirstPage = true;

/** Numeral style for the rendered number. */
@Builder.Default
private final DocumentPageNumberStyle style = DocumentPageNumberStyle.DECIMAL;

private DocumentPageNumbering() {
this.startAt = 1;
this.countFrom = 1;
this.showOnFirstPage = true;
this.style = DocumentPageNumberStyle.DECIMAL;
}
}
Loading