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

### Public API

- **`GraphCompose.documents()` + `MultiSectionDocumentBuilder` / `MultiSectionDocument`**
(`@since 1.9.0`). Concatenates several independently authored `DocumentSession`
sections — each with its own page size, margins, fonts, and footer numbering —
into one PDF inside the engine, with no external PDF merge. Anchors, internal
links, and the bookmark outline resolve across section boundaries against the
combined document, and each section is numbered from its own first page, so a
full-bleed cover of one page size can precede a margined, page-numbered body of
another. Document-level metadata and protection are taken from the first section
that declares them. Single-section output is unchanged. `MultiSectionDocument`
is `AutoCloseable` and owns its sections.

- **`addTableOfContents(...)` + `TocBuilder` / `DocumentLeader`** (`@since 1.9.0`).
A native, clickable table of contents: each `entry(label, anchor)` becomes a row
whose label links to the chapter (`linkTo`), a dotted or dashed leader fills the
Expand Down
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 @@ -103,6 +103,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [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) |
| [Page references](#page-references) | `addPageReference(anchor)` — print the page an `anchor(...)` lands on (a native "see page N" cross-reference), resolved in one authoring pass | [PDF](../assets/readme/examples/page-reference.pdf) · [Source](src/main/java/com/demcha/examples/features/navigation/PageReferenceExample.java) |
| [Table of contents](#table-of-contents) | `addTableOfContents(toc -> toc.entry(label, anchor))` — a native clickable TOC with dot leaders and auto-resolved page numbers | [PDF](../assets/readme/examples/table-of-contents.pdf) · [Source](src/main/java/com/demcha/examples/features/navigation/TocExample.java) |
| [Multi-section documents](#multi-section-documents) | `GraphCompose.documents()` — concatenate sections with different page sizes / margins / numbering into one PDF, with cross-section links and outline | [PDF](../assets/readme/examples/multi-section-document.pdf) · [Source](src/main/java/com/demcha/examples/features/structure/MultiSectionExample.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 @@ -742,6 +743,28 @@ flow.addTableOfContents(toc -> toc.title("Contents")
[📄 View PDF](../assets/readme/examples/table-of-contents.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/navigation/TocExample.java)

### Multi-section documents

`GraphCompose.documents()` concatenates several independently authored sections —
each a full `DocumentSession` with its own page size, margins, fonts, and footer
numbering — into one PDF **inside the engine** (no external merge). Anchors, links,
and the bookmark outline resolve across section boundaries, and each section is
numbered from its own first page, so a full-bleed landscape cover can precede a
portrait, page-numbered body in a single document.

```java
DocumentSession cover = GraphCompose.document().pageSize(440, 300).margin(DocumentInsets.of(0)).create();
DocumentSession body = GraphCompose.document().pageSize(300, 440).margin(DocumentInsets.of(40)).create();
body.footer(DocumentHeaderFooter.builder().centerText("{page} / {pages}").build());

try (MultiSectionDocument doc = GraphCompose.documents(out).section(cover).section(body).create()) {
doc.buildPdf(); // cover keeps its geometry; body is numbered 1..N from its own first page
}
```

[📄 View PDF](../assets/readme/examples/multi-section-document.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/structure/MultiSectionExample.java)

---

## Production patterns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.demcha.examples.features.navigation.InPdfNavigationExample;
import com.demcha.examples.features.navigation.PageReferenceExample;
import com.demcha.examples.features.navigation.TocExample;
import com.demcha.examples.features.structure.MultiSectionExample;
import com.demcha.examples.features.text.RichTextShowcaseExample;
import com.demcha.examples.features.text.SectionPresetsExample;
import com.demcha.examples.features.themes.CustomBusinessThemeExample;
Expand Down Expand Up @@ -172,6 +173,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + InPdfNavigationExample.generate());
System.out.println("Generated: " + PageReferenceExample.generate());
System.out.println("Generated: " + TocExample.generate());
System.out.println("Generated: " + MultiSectionExample.generate());

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

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.api.MultiSectionDocument;
import com.demcha.compose.document.output.DocumentHeaderFooter;
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 {@code GraphCompose.documents()}: several sections,
* each a full {@link DocumentSession} with its own page size and chrome,
* concatenated into one PDF — no external merge. Here a full-bleed landscape
* cover precedes a portrait, page-numbered body, and the cover's call to action
* links straight to a chapter in the body.
*
* <pre>{@code
* DocumentSession cover = GraphCompose.document().pageSize(440, 300).margin(of(0)).create();
* DocumentSession body = GraphCompose.document().pageSize(300, 440).margin(of(40)).create();
* body.footer(DocumentHeaderFooter.builder().centerText("{page}").build());
*
* try (MultiSectionDocument doc = GraphCompose.documents(out).section(cover).section(body).create()) {
* doc.buildPdf();
* }
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class MultiSectionExample {

private static final DocumentColor COVER_BG = DocumentColor.rgb(28, 39, 64);
private static final DocumentColor COVER_INK = DocumentColor.rgb(244, 247, 252);
private static final DocumentColor COVER_ACCENT = DocumentColor.rgb(126, 170, 255);
private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
private static final DocumentColor MUTED = DocumentColor.rgb(120, 126, 135);

private static final String[][] CHAPTERS = {
{"1. The opening chapter", "ch1"},
{"2. The middle chapter", "ch2"},
{"3. The closing chapter", "ch3"},
};

private MultiSectionExample() {
}

/**
* Renders a landscape cover and a portrait body into a single PDF via
* {@code GraphCompose.documents()}.
*
* @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/structure", "multi-section-document.pdf");

// Cover: landscape, full-bleed background, no margin.
DocumentSession cover = GraphCompose.document()
.pageSize(440, 300)
.margin(DocumentInsets.of(0))
.pageBackground(COVER_BG)
.create();
cover.pageFlow(page -> page.addSection(s -> s.padding(DocumentInsets.of(46))
.addParagraph(p -> p.text("The GraphCompose Book")
.textStyle(DocumentTextStyle.DEFAULT.withSize(30).withColor(COVER_INK)))
.addParagraph(p -> p.text("One document, two page geometries")
.textStyle(DocumentTextStyle.DEFAULT.withSize(13).withColor(COVER_INK)))
.addParagraph(p -> p.text("Open the opening chapter")
.textStyle(DocumentTextStyle.DEFAULT.withSize(13).withColor(COVER_ACCENT))
.linkTo("ch1"))));

// Body: portrait, margined, page-numbered from its own first page.
DocumentSession body = GraphCompose.document()
.pageSize(300, 440)
.margin(DocumentInsets.of(40))
.create();
body.footer(DocumentHeaderFooter.builder().centerText("{page} / {pages}").build());
body.pageFlow(page -> {
for (int i = 0; i < CHAPTERS.length; i++) {
String[] chapter = CHAPTERS[i];
if (i > 0) {
page.addPageBreak(b -> b.name("to_" + chapter[1]));
}
page.addSection(s -> s.anchor(chapter[1])
.addParagraph(p -> p.text(chapter[0])
.textStyle(DocumentTextStyle.DEFAULT.withSize(18).withColor(INK)))
.addParagraph(p -> p.text("Body pages carry their own margins and footer page numbers, "
+ "independent of the borderless cover in front of them.")
.textStyle(DocumentTextStyle.DEFAULT.withSize(11).withColor(MUTED))));
}
});

try (MultiSectionDocument document = GraphCompose.documents(pdfFile)
.section(cover)
.section(body)
.create()) {
document.buildPdf();
}

return pdfFile;
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/demcha/compose/GraphCompose.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.demcha.compose.font.DefaultFonts;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.api.MultiSectionDocument;
import com.demcha.compose.document.api.MultiSectionDocumentBuilder;
import com.demcha.compose.document.output.DocumentDebugOptions;
import com.demcha.compose.document.style.DocumentInsets;

Expand Down Expand Up @@ -92,6 +94,31 @@ public static DocumentBuilder document(Path outputFile) {
return new DocumentBuilder(outputFile);
}

/**
* Starts a multi-section document: several independently authored
* {@link DocumentSession} sections — each with its own page size, margins,
* fonts, and page numbering — concatenated into one PDF with cross-section
* anchors, links, and bookmark outline.
*
* @return builder for assembling a multi-section document
* @since 1.9.0
*/
public static MultiSectionDocumentBuilder documents() {
return new MultiSectionDocumentBuilder(null);
}

/**
* Starts a multi-section document with a default output target used by
* {@link MultiSectionDocument#buildPdf()}.
*
* @param outputFile default PDF output path for {@link MultiSectionDocument#buildPdf()}
* @return builder for assembling a multi-section document
* @since 1.9.0
*/
public static MultiSectionDocumentBuilder documents(Path outputFile) {
return new MultiSectionDocumentBuilder(outputFile);
}

/**
* Returns the logical font families bundled with GraphCompose out of the box.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,24 @@ public <E extends DocumentNode> NodeRegistry register(NodeDefinition<E> definiti
}
}

/**
* Captures this session as one section of a {@link MultiSectionDocument}: its
* resolved layout graph, page geometry, custom fonts, and chrome (header /
* footer / watermark / metadata). Package-private — used only by
* {@link MultiSectionDocument} to concatenate sessions into one PDF.
*
* @return a render unit for this section
*/
com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend.Section toSectionRenderUnit() {
ensureOpen();
ensureRenderable();
return new com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend.Section(
layoutGraph(),
canvas,
List.copyOf(customFontFamilies),
chromeOptions.toConveniencePdfBackend(debug));
}

/**
* Inner adapter exposing session-private state to {@link DocumentRenderingFacade}
* without making the corresponding session methods public.
Expand Down
Loading
Loading