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

### Public API

- **`chrome().viewerPreferences(...)` + `DocumentViewerPreferences` /
`DocumentPageMode` / `DocumentPageLayout`** (`@since 1.9.0`). Controls how a PDF
reader presents the document on open — the page mode (`USE_OUTLINES` opens the
bookmark panel, pairing with `bookmark(...)`), the page layout (single / one-column
/ two-column / two-page), and the window flags (`displayDocTitle`, `hideToolbar`,
`hideMenubar`, `fitWindow`, `centerWindow`). Written to the PDF document catalog;
`DocumentViewerPreferences.openOutline()` is a one-line preset. PDF-only — other
backends ignore it. A document that sets none is unchanged.

- **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
Expand Down
Binary file added assets/readme/examples/viewer-preferences.pdf
Binary file not shown.
19 changes: 19 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.
| [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) |
| [Viewer preferences](#viewer-preferences) | `chrome().viewerPreferences(...)` — open with the bookmark panel (`USE_OUTLINES`), set page layout, or show the doc title in the window | [PDF](../assets/readme/examples/viewer-preferences.pdf) · [Source](src/main/java/com/demcha/examples/features/chrome/ViewerPreferencesExample.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) |
Expand Down Expand Up @@ -742,6 +743,24 @@ session.chrome().footer(DocumentHeaderFooter.builder()
[📄 View PDF](../assets/readme/examples/page-numbering.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/chrome/PageNumberingExample.java)

### Viewer preferences

`chrome().viewerPreferences(...)` controls how a reader presents the document when
it opens — the page mode (`USE_OUTLINES` opens the bookmark panel, pairing with
`bookmark(...)` on sections), the page layout (e.g. two-column), and window-chrome
flags (`displayDocTitle`, `hideToolbar`, `fitWindow`, …). Written to the PDF
catalog; readers honour the subset they support. PDF-only — other backends ignore it.

```java
document.chrome().viewerPreferences(DocumentViewerPreferences.builder()
.pageMode(DocumentPageMode.USE_OUTLINES) // open with the bookmark panel
.displayDocTitle(true)
.build());
```

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

### Page references

`addPageReference(anchor)` prints the page a declared `anchor(...)` lands on — a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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.chrome.ViewerPreferencesExample;
import com.demcha.examples.features.layout.BleedExample;
import com.demcha.examples.features.layout.RowColumnsExample;
import com.demcha.examples.features.layout.RowFlexExample;
Expand Down Expand Up @@ -185,6 +186,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + CustomBusinessThemeExample.generate());
System.out.println("Generated: " + PdfChromeExample.generate());
System.out.println("Generated: " + PageNumberingExample.generate());
System.out.println("Generated: " + ViewerPreferencesExample.generate());

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

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.output.DocumentMetadata;
import com.demcha.compose.document.output.DocumentViewerPreferences;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentPageMode;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Runnable showcase for v1.9 {@code chrome().viewerPreferences(...)}: how a PDF
* reader presents the document when it opens. Here the page mode is
* {@code USE_OUTLINES} so the reader opens with the bookmark panel showing — it
* pairs with {@code bookmark(...)} on the sections — and {@code displayDocTitle}
* shows the document title in the window title bar. Viewer preferences are
* advisory; readers honour the ones they support.
*
* <pre>{@code
* document.chrome().viewerPreferences(DocumentViewerPreferences.builder()
* .pageMode(DocumentPageMode.USE_OUTLINES) // open the bookmark panel
* .displayDocTitle(true)
* .build());
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class ViewerPreferencesExample {

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 = {
{"Habitat", "Where the species lives."},
{"Diet", "What it eats through the seasons."},
{"Behaviour", "How it moves, nests, and migrates."},
};

private ViewerPreferencesExample() {
}

/**
* Renders a short guide that opens with the bookmark panel showing.
*
* @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", "viewer-preferences.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.metadata(DocumentMetadata.builder().title("Field Guide").build());
document.chrome().viewerPreferences(DocumentViewerPreferences.builder()
.pageMode(DocumentPageMode.USE_OUTLINES)
.displayDocTitle(true)
.build());

document.pageFlow(page -> {
page.addParagraph(p -> p.text("Field Guide")
.textStyle(DocumentTextStyle.DEFAULT.withSize(18).withColor(INK)));
page.addParagraph(p -> p.text("opens with the bookmark panel — viewerPreferences(USE_OUTLINES)")
.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 @@ -36,6 +36,7 @@ final class DocumentChromeOptions {
private DocumentMetadata metadata;
private DocumentWatermark watermark;
private DocumentProtection protection;
private DocumentViewerPreferences viewerPreferences;

DocumentChromeOptions() {
}
Expand All @@ -44,6 +45,10 @@ void setMetadata(DocumentMetadata metadata) {
this.metadata = metadata;
}

void setViewerPreferences(DocumentViewerPreferences viewerPreferences) {
this.viewerPreferences = viewerPreferences;
}

void setWatermark(DocumentWatermark watermark) {
this.watermark = watermark;
}
Expand Down Expand Up @@ -76,6 +81,7 @@ boolean isEmpty() {
return metadata == null
&& watermark == null
&& protection == null
&& viewerPreferences == null
&& headersAndFooters.isEmpty();
}

Expand All @@ -89,7 +95,8 @@ DocumentOutputOptions snapshot() {
if (isEmpty()) {
return DocumentOutputOptions.EMPTY;
}
return new DocumentOutputOptions(metadata, watermark, protection, List.copyOf(headersAndFooters));
return new DocumentOutputOptions(metadata, watermark, protection, viewerPreferences,
List.copyOf(headersAndFooters));
}

/**
Expand All @@ -110,7 +117,8 @@ PdfFixedLayoutBackend toConveniencePdfBackend(DocumentDebugOptions debug) {
.debug(debug)
.metadata(PdfOutputOptionsTranslator.toPdf(metadata))
.watermark(PdfOutputOptionsTranslator.toPdf(watermark))
.protect(PdfOutputOptionsTranslator.toPdf(protection));
.protect(PdfOutputOptionsTranslator.toPdf(protection))
.viewerPreferences(PdfOutputOptionsTranslator.toPdf(viewerPreferences));
for (DocumentHeaderFooter entry : headersAndFooters) {
PdfHeaderFooterOptions translated = PdfOutputOptionsTranslator.toPdf(entry);
if (entry.getZone() == DocumentHeaderFooterZone.FOOTER) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,22 @@ public DocumentSession metadata(DocumentMetadata metadata) {
return this;
}

/**
* Sets the PDF viewer preferences — how a reader presents the document when it
* opens (page mode, page layout, window flags). PDF-only; other backends
* ignore it.
*
* @param viewerPreferences viewer preferences, or {@code null} to clear
* @return this session
* @throws IllegalStateException if this session has already been closed
* @since 1.9.0
*/
public DocumentSession viewerPreferences(DocumentViewerPreferences viewerPreferences) {
ensureOpen();
chromeOptions.setViewerPreferences(viewerPreferences);
return this;
}

/**
* @param options legacy PDF metadata options, or {@code null} to clear
* @return this session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.demcha.compose.document.output.DocumentHeaderFooter;
import com.demcha.compose.document.output.DocumentMetadata;
import com.demcha.compose.document.output.DocumentProtection;
import com.demcha.compose.document.output.DocumentViewerPreferences;
import com.demcha.compose.document.output.DocumentWatermark;

import java.util.Objects;
Expand Down Expand Up @@ -67,6 +68,22 @@ public SessionChromeApi metadata(DocumentMetadata metadata) {
return this;
}

/**
* Sets the PDF viewer preferences — how a reader presents the document when it
* opens (page mode, page layout, window flags). PDF-only; other backends
* ignore it.
*
* @param viewerPreferences viewer preferences, or {@code null} to clear
* @return this facade for chaining
* @throws IllegalStateException if the owning session has already been closed
* @since 1.9.0
*/
public SessionChromeApi viewerPreferences(DocumentViewerPreferences viewerPreferences) {
ensureOpen();
chromeOptions.setViewerPreferences(viewerPreferences);
return this;
}

/**
* Configures a backend-neutral document-wide watermark. Pass
* {@code null} to clear.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
import com.demcha.compose.document.backend.fixed.pdf.options.PdfHeaderFooterOptions;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfMetadataOptions;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfProtectionOptions;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfViewerPreferencesOptions;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfWatermarkOptions;
import com.demcha.compose.document.layout.LayoutCanvas;
import com.demcha.compose.document.style.DocumentPageLayout;
import com.demcha.compose.document.style.DocumentPageMode;
import com.demcha.compose.engine.components.style.Margin;
import com.demcha.compose.engine.render.pdf.helpers.PdfHeaderFooterRenderer;
import com.demcha.compose.engine.render.pdf.helpers.PdfWatermarkRenderer;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PageLayout;
import org.apache.pdfbox.pdmodel.PageMode;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand Down Expand Up @@ -132,6 +140,78 @@ public static void applyDocumentMetadataAndProtection(PDDocument document,
}
}

/**
* Writes document-global viewer preferences to the PDF catalog: page mode,
* page layout, and the window-chrome flags. Each unset field leaves the
* reader's default in place. A no-op when {@code options} is {@code null}.
*
* @param document target PDFBox document
* @param options viewer-preference options, or {@code null}
* @since 1.9.0
*/
public static void applyViewerPreferences(PDDocument document, PdfViewerPreferencesOptions options) {
if (options == null) {
return;
}
PDDocumentCatalog catalog = document.getDocumentCatalog();
if (options.getPageMode() != null) {
catalog.setPageMode(toPageMode(options.getPageMode()));
}
if (options.getPageLayout() != null) {
catalog.setPageLayout(toPageLayout(options.getPageLayout()));
}

PDViewerPreferences prefs = catalog.getViewerPreferences();
if (prefs == null) {
prefs = new PDViewerPreferences(new COSDictionary());
}
boolean anyFlag = false;
if (options.getDisplayDocTitle() != null) {
prefs.setDisplayDocTitle(options.getDisplayDocTitle());
anyFlag = true;
}
if (options.getHideToolbar() != null) {
prefs.setHideToolbar(options.getHideToolbar());
anyFlag = true;
}
if (options.getHideMenubar() != null) {
prefs.setHideMenubar(options.getHideMenubar());
anyFlag = true;
}
if (options.getFitWindow() != null) {
prefs.setFitWindow(options.getFitWindow());
anyFlag = true;
}
if (options.getCenterWindow() != null) {
prefs.setCenterWindow(options.getCenterWindow());
anyFlag = true;
}
if (anyFlag) {
catalog.setViewerPreferences(prefs);
}
}

private static PageMode toPageMode(DocumentPageMode mode) {
return switch (mode) {
case USE_NONE -> PageMode.USE_NONE;
case USE_OUTLINES -> PageMode.USE_OUTLINES;
case USE_THUMBNAILS -> PageMode.USE_THUMBS;
case FULL_SCREEN -> PageMode.FULL_SCREEN;
case USE_ATTACHMENTS -> PageMode.USE_ATTACHMENTS;
};
}

private static PageLayout toPageLayout(DocumentPageLayout layout) {
return switch (layout) {
case SINGLE_PAGE -> PageLayout.SINGLE_PAGE;
case ONE_COLUMN -> PageLayout.ONE_COLUMN;
case TWO_COLUMN_LEFT -> PageLayout.TWO_COLUMN_LEFT;
case TWO_COLUMN_RIGHT -> PageLayout.TWO_COLUMN_RIGHT;
case TWO_PAGE_LEFT -> PageLayout.TWO_PAGE_LEFT;
case TWO_PAGE_RIGHT -> PageLayout.TWO_PAGE_RIGHT;
};
}

private static void applyMetadata(PDDocument document, PdfMetadataOptions metadataOptions) {
PDDocumentInformation info = document.getDocumentInformation();
if (metadataOptions.getTitle() != null) {
Expand Down
Loading
Loading