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

- **Container `bookmark(...)`** (`@since 1.9.0`). `bookmark(DocumentBookmarkOptions)`
on any container flow builder (`Section` / `Container` / page flow) — previously
only the seven leaf builders carried a bookmark — adds a PDF outline entry pointing
at that container's start page, making a structured document navigable through the
reader's bookmark panel. Emitted via its own non-visual marker fragment, so it works
even on an unstyled container, and a container without a bookmark is unaffected.

- **`RowBuilder.flexSpacer()` / `pushRight()` / `arrangement(...)` + `RowArrangement`
+ `SpacerBuilder.grow(...)`** (`@since 1.9.0`). Main-axis (`justify-content`) layout
for a row. A `flexSpacer()` (or `pushRight()`) is an invisible spring that absorbs
Expand Down
Binary file added assets/readme/examples/container-bookmark.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 @@ -105,6 +105,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) |
| [Container bookmarks](#container-bookmarks) | `section.bookmark(new DocumentBookmarkOptions(title))` — make a section / container a PDF outline (bookmark-panel) target | [PDF](../assets/readme/examples/container-bookmark.pdf) · [Source](src/main/java/com/demcha/examples/features/navigation/ContainerBookmarkExample.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) |
Expand Down Expand Up @@ -780,6 +781,22 @@ 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)

### Container bookmarks

`bookmark(...)` on a section or container flow adds a PDF **outline** entry — the
reader's bookmark panel — pointing at that container's start page, making a
structured document navigable. It works on any container, even an unstyled one
(no fill or border), and is independent of the page content.

```java
flow.addSection(s -> s.bookmark(new DocumentBookmarkOptions("2. Methodology"))
.addParagraph(heading)
.addParagraph(body));
```

[📄 View PDF](../assets/readme/examples/container-bookmark.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/navigation/ContainerBookmarkExample.java)

### Multi-section documents

`GraphCompose.documents()` concatenates several independently authored sections —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.demcha.examples.features.text.InlineHighlightExample;
import com.demcha.examples.features.navigation.InPdfNavigationExample;
import com.demcha.examples.features.navigation.PageReferenceExample;
import com.demcha.examples.features.navigation.ContainerBookmarkExample;
import com.demcha.examples.features.navigation.TocExample;
import com.demcha.examples.features.structure.MultiSectionExample;
import com.demcha.examples.features.text.RichTextShowcaseExample;
Expand Down Expand Up @@ -177,6 +178,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: " + ContainerBookmarkExample.generate());
System.out.println("Generated: " + MultiSectionExample.generate());

// Theming + chrome
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.demcha.examples.features.navigation;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.PageFlowBuilder;
import com.demcha.compose.document.node.DocumentBookmarkOptions;
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 container {@code bookmark(...)}: a section (or any
* container flow) becomes a PDF outline target. Each bookmarked section adds an
* entry to the reader's bookmark panel pointing at the section's start page — a
* navigable outline for a structured document, with no manual coordinates. The
* outline is a viewer-panel feature; the page itself shows the report content.
*
* <pre>{@code
* flow.addSection(s -> s.bookmark(new DocumentBookmarkOptions("2. Methodology"))
* .addParagraph(heading)
* .addParagraph(body));
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class ContainerBookmarkExample {

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. Introduction", "Why this report exists and what it covers."},
{"2. Methodology", "How the data was gathered and analysed."},
{"3. Results", "What the analysis found, in brief."},
{"4. Conclusion", "What it means and what to do next."},
};

private ContainerBookmarkExample() {
}

/**
* Renders a short report whose sections are each a PDF outline entry.
*
* @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/navigation", "container-bookmark.pdf");

DocumentTextStyle heading = DocumentTextStyle.DEFAULT.withSize(13).withColor(INK);
DocumentTextStyle body = DocumentTextStyle.DEFAULT.withSize(10).withColor(MUTED);

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(360, 320)
.margin(DocumentInsets.of(34))
.create()) {
document.pageFlow(page -> {
page.addParagraph(p -> p.text("Quarterly Report")
.textStyle(DocumentTextStyle.DEFAULT.withSize(18).withColor(INK)));
page.addParagraph(p -> p.text("each section is a bookmark — open the reader's outline panel")
.textStyle(DocumentTextStyle.DEFAULT.withSize(9).withColor(MUTED))
.padding(DocumentInsets.bottom(10)));

for (String[] chapter : CHAPTERS) {
chapterSection(page, heading, body, chapter[0], chapter[1]);
}
});

document.buildPdf();
}

return pdfFile;
}

private static void chapterSection(PageFlowBuilder page,
DocumentTextStyle heading,
DocumentTextStyle body,
String title,
String summary) {
page.addSection(s -> s.bookmark(new DocumentBookmarkOptions(title))
.spacing(2)
.addParagraph(p -> p.text(title).textStyle(heading))
.addParagraph(p -> p.text(summary).textStyle(body)));
page.addSpacer(s -> s.height(8));
}

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 @@ -114,7 +114,8 @@ private static List<PdfFragmentRenderHandler<?>> defaultHandlers() {
new PdfShapeClipEndRenderHandler(),
new PdfTransformBeginRenderHandler(),
new PdfTransformEndRenderHandler(),
new PdfAnchorMarkerRenderHandler());
new PdfAnchorMarkerRenderHandler(),
new PdfBookmarkMarkerRenderHandler());
}

private static PdfLinkAnnotationWriter.PlacedPdfRect spanLinkRectangle(ParagraphSpan span,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.demcha.compose.document.backend.fixed.pdf.handlers;

import com.demcha.compose.document.backend.fixed.pdf.PdfFragmentRenderHandler;
import com.demcha.compose.document.backend.fixed.pdf.PdfRenderEnvironment;
import com.demcha.compose.document.layout.PlacedFragment;
import com.demcha.compose.document.layout.payloads.BookmarkMarkerPayload;

/**
* No-op render handler for a {@link BookmarkMarkerPayload} fragment. The marker
* draws nothing — the outline entry is registered generically in the backend's
* semantic post-pass (the payload implements {@code PdfSemanticFragmentPayload}),
* so this handler exists only so fragment dispatch finds a handler for the type.
*
* @author Artem Demchyshyn
*/
public final class PdfBookmarkMarkerRenderHandler
implements PdfFragmentRenderHandler<BookmarkMarkerPayload> {

/**
* Creates the bookmark-marker handler.
*/
public PdfBookmarkMarkerRenderHandler() {
}

@Override
public Class<BookmarkMarkerPayload> payloadType() {
return BookmarkMarkerPayload.class;
}

@Override
public void render(PlacedFragment fragment,
BookmarkMarkerPayload payload,
PdfRenderEnvironment environment) {
// Intentionally empty — registration happens in finishRenderedFragment.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.demcha.compose.document.dsl.internal.BuilderSupport;
import com.demcha.compose.document.image.DocumentImageData;
import com.demcha.compose.document.node.ChartNode;
import com.demcha.compose.document.node.DocumentBookmarkOptions;
import com.demcha.compose.document.node.DocumentLinkOptions;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.PageReferenceNode;
Expand All @@ -28,6 +29,7 @@ public abstract class AbstractFlowBuilder<T extends AbstractFlowBuilder<T, N>, N
private final List<DocumentNode> children = new ArrayList<>();
private String name = "";
private String anchor;
private DocumentBookmarkOptions bookmarkOptions;
private double spacing = 0.0;
private DocumentInsets padding = DocumentInsets.zero();
private DocumentInsets margin = DocumentInsets.zero();
Expand Down Expand Up @@ -75,6 +77,21 @@ public T anchor(String anchor) {
return self();
}

/**
* Adds a PDF outline (bookmark) entry pointing at this container's top on its
* start page, making a section or container a navigable outline target (a
* {@code chrome().viewerPreferences(...)} page mode of {@code USE_OUTLINES}
* opens the bookmark panel automatically).
*
* @param bookmarkOptions outline entry (title, level), or {@code null} to clear
* @return this builder
* @since 1.9.0
*/
public T bookmark(DocumentBookmarkOptions bookmarkOptions) {
this.bookmarkOptions = bookmarkOptions;
return self();
}

/**
* Sets vertical spacing between child nodes.
*
Expand Down Expand Up @@ -1136,6 +1153,10 @@ protected String anchor() {
return anchor;
}

protected DocumentBookmarkOptions bookmarkOptions() {
return bookmarkOptions;
}

protected List<DocumentNode> children() {
return List.copyOf(children);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.demcha.compose.document.dsl.internal.SemanticNameNormalizer;
import com.demcha.compose.document.node.*;
import com.demcha.compose.document.style.DocumentBleed;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextStyle;

Expand Down Expand Up @@ -332,8 +333,10 @@ protected SectionNode buildNode() {
.build());
}
moduleChildren.addAll(children());
// A module does not bleed (matching its long-standing behaviour) — thread
// only the bookmark, leaving bleed at none().
return new SectionNode(name(), moduleChildren, spacing(), padding(), margin(), fillColor(),
stroke(), cornerRadius(), borders(), keepTogether, anchor());
stroke(), cornerRadius(), borders(), keepTogether, anchor(), DocumentBleed.none(), bookmarkOptions());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected PageFlowBuilder self() {

@Override
protected ContainerNode buildNode() {
return new ContainerNode(name(), children(), spacing(), padding(), margin(), fillColor(), stroke(), cornerRadius(), borders(), anchor());
return new ContainerNode(name(), children(), spacing(), padding(), margin(), fillColor(), stroke(), cornerRadius(), borders(), anchor(), bookmarkOptions());
}

/**
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(), bleed());
stroke(), cornerRadius(), borders(), keepTogether, anchor(), bleed(), bookmarkOptions());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,47 @@ public static List<LayoutFragment> withAnchorMarker(List<LayoutFragment> base,
return List.copyOf(out);
}

/**
* Builds a non-visual {@link BookmarkMarkerPayload} fragment at the placement's
* top-left, declaring a PDF outline entry for a container.
*
* @param bookmark the outline entry
* @param placement resolved fragment placement
* @return one bookmark marker fragment
*/
public static LayoutFragment bookmarkMarkerFragment(DocumentBookmarkOptions bookmark,
FragmentPlacement placement) {
return new LayoutFragment(
placement.path(),
0,
0.0,
0.0,
placement.width(),
placement.height(),
new BookmarkMarkerPayload(bookmark));
}

/**
* Appends a {@link BookmarkMarkerPayload} fragment to {@code base} when
* {@code bookmark} is non-null, otherwise returns {@code base} unchanged.
*
* @param base already-emitted fragments
* @param bookmark optional outline entry; {@code null} skips the marker
* @param placement resolved fragment placement
* @return {@code base}, optionally with a bookmark marker appended
*/
public static List<LayoutFragment> withBookmarkMarker(List<LayoutFragment> base,
DocumentBookmarkOptions bookmark,
FragmentPlacement placement) {
if (bookmark == null) {
return base;
}
List<LayoutFragment> out = new ArrayList<>(base.size() + 1);
out.addAll(base);
out.add(bookmarkMarkerFragment(bookmark, placement));
return List.copyOf(out);
}

/**
* Emits an optional background/border decoration fragment.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ public List<LayoutFragment> emitFragments(PreparedNode<ContainerNode> prepared,
node.cornerRadius(),
toSideBorders(node.borders()),
placement);
return withAnchorMarker(
decoration,
placement.pageIndex() == placement.startPage() ? node.anchor() : null,
boolean onStartPage = placement.pageIndex() == placement.startPage();
return withBookmarkMarker(
withAnchorMarker(decoration, onStartPage ? node.anchor() : null, placement),
onStartPage ? node.bookmarkOptions() : null,
placement);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ public List<LayoutFragment> emitFragments(PreparedNode<SectionNode> prepared,
node.cornerRadius(),
toSideBorders(node.borders()),
placement);
return withAnchorMarker(
decoration,
placement.pageIndex() == placement.startPage() ? node.anchor() : null,
boolean onStartPage = placement.pageIndex() == placement.startPage();
return withBookmarkMarker(
withAnchorMarker(decoration, onStartPage ? node.anchor() : null, placement),
onStartPage ? node.bookmarkOptions() : null,
placement);
}
}
Loading
Loading