diff --git a/CHANGELOG.md b/CHANGELOG.md index 79282760..f5aaaca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -267,6 +267,11 @@ PDF `GoTo` actions. External links are unchanged. ### Documentation +- New runnable flagship example + `examples/src/main/java/com/demcha/examples/features/title/BookTemplateExample.java` + — a full novel front (full-bleed wave cover, clickable dotted-leader table of + contents with live page numbers, chapters) assembled in one `DocumentSession` + using the v1.9 book primitives, with no external PDF merge or two-pass probe. - New runnable example `examples/src/main/java/com/demcha/examples/features/navigation/InPdfNavigationExample.java` — a clickable table of contents plus a bidirectional footnote. diff --git a/assets/readme/examples/book-template.pdf b/assets/readme/examples/book-template.pdf new file mode 100644 index 00000000..492f3e37 Binary files /dev/null and b/assets/readme/examples/book-template.pdf differ diff --git a/examples/README.md b/examples/README.md index 868097d1..0a51f6c6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -117,6 +117,7 @@ are with the canonical DSL, then jump to its detailed section below. | Financial report one-pager | Single-page monthly financial dashboard — three margin gauges, cash & stacked-OPEX charts, a revenue donut, and forecast bars; all v1.8 native vector charts plus inline sparklines and a path-clipped photo masthead | [PDF](../assets/readme/examples/financial-report.pdf) · [Source](src/main/java/com/demcha/examples/flagships/FinancialReportExample.java) | | [Master showcase](#master-showcase) | Kitchen-sink "Q2 sample report" combining the canonical surface end-to-end | [PDF](../assets/readme/examples/master-showcase.pdf) · [Source](src/main/java/com/demcha/examples/flagships/MasterShowcaseExample.java) | | Feature catalog | Browsable reference PDF: every shipped capability as a block — outline-clickable heading, the exact API call, the rendered result right under it | [PDF](../assets/readme/examples/feature-catalog.pdf) · [Source](src/main/java/com/demcha/examples/flagships/FeatureCatalogExample.java) | +| Book template | A full novel front: full-bleed wave cover, a clickable dotted-leader table of contents with live page numbers, and chapters — the v1.9 book primitives (`pageMargins`, `addTableOfContents`, `DocumentPageNumbering`, container `bookmark`, `viewerPreferences`) in **one session**, no external PDF merge | [PDF](../assets/readme/examples/book-template.pdf) · [Source](src/main/java/com/demcha/examples/features/title/BookTemplateExample.java) | ### 🗄️ Legacy diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index 1ce12cf2..2a19aa45 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -40,6 +40,7 @@ import com.demcha.examples.features.text.RichTextShowcaseExample; import com.demcha.examples.features.text.SectionPresetsExample; import com.demcha.examples.features.themes.CustomBusinessThemeExample; +import com.demcha.examples.features.title.BookTemplateExample; import com.demcha.examples.features.transforms.TransformsExample; import com.demcha.examples.flagships.BusinessReportExample; import com.demcha.examples.flagships.EngineDeckExample; @@ -211,5 +212,6 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + BusinessReportExample.generate()); System.out.println("Generated: " + EngineDeckExample.generate()); System.out.println("Generated: " + FinancialReportExample.generate()); + System.out.println("Generated: " + BookTemplateExample.generate()); } } diff --git a/examples/src/main/java/com/demcha/examples/features/title/BookTemplateExample.java b/examples/src/main/java/com/demcha/examples/features/title/BookTemplateExample.java new file mode 100644 index 00000000..35b8e043 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/features/title/BookTemplateExample.java @@ -0,0 +1,234 @@ +package com.demcha.examples.features.title; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.api.PageMarginRule; +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.PathBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.node.DocumentBookmarkOptions; +import com.demcha.compose.document.node.DocumentNode; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.output.DocumentHeaderFooter; +import com.demcha.compose.document.output.DocumentPageNumbering; +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.DocumentLeader; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontName; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; +import java.util.List; + +/** + * Book template — a full-bleed cover, a clickable table of contents with dotted + * leaders, then chapters, all in one {@code DocumentSession}. The reader + * opens with the bookmark panel showing the cover and every chapter. + * + *

This is the flagship that the v1.9 book primitives were built for. Each part + * uses the API that replaced a former workaround:

+ * + * + * @author Artem Demchyshyn + */ +public final class BookTemplateExample { + + private BookTemplateExample() { + } + + private static final FontName SERIF = FontName.PT_SERIF; + private static final DocumentColor INK = DocumentColor.rgb(20, 35, 80); + private static final DocumentColor BODY_INK = DocumentColor.rgb(35, 40, 55); + private static final DocumentColor MUTED = DocumentColor.rgb(120, 135, 175); + private static final DocumentColor LINK = DocumentColor.rgb(20, 90, 170); + + private static final float M_TOP = 96; + private static final float M_SIDE = 78; + private static final float M_BOTTOM = 84; + + private static final DocumentPageSize PAGE = DocumentPageSize.A4; + + private static final List CHAPTERS = List.of( + new Chapter("CHAPTER ONE", "The Sun Had Not Yet Risen", LoremHolder.LONG), + new Chapter("CHAPTER TWO", "The Sun Rose Higher", LoremHolder.SHORT)); + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare("features/title", "book-template.pdf"); + + try (DocumentSession document = GraphCompose.document(outputFile) + .pageSize(PAGE) + .margin(M_TOP, M_SIDE, M_BOTTOM, M_SIDE) + .create()) { + + // Page 1 is a full-bleed cover (zero margin); pages 2+ keep the book margins. + document.pageMargins(List.of(PageMarginRule.page(1, DocumentInsets.zero()))); + + // Open with the bookmark panel; number the body but not the cover. + document.chrome().viewerPreferences(DocumentViewerPreferences.openOutline()); + document.chrome().footer(DocumentHeaderFooter.builder() + .centerText("{page}").fontSize(10f).textColor(MUTED) + .numbering(DocumentPageNumbering.builder().showOnFirstPage(false).build()) + .build()); + + PageFlowBuilder flow = document.pageFlow().name("Book").spacing(0); + cover(flow, "Virginia Woolf", "The Waves", "A Novel", "LONDON", "1931"); + flow.addPageBreak(pb -> pb.name("afterCover")); + contents(flow); + flow.addPageBreak(pb -> pb.name("afterContents")); + for (int i = 0; i < CHAPTERS.size(); i++) { + chapter(flow, CHAPTERS.get(i), i, i > 0); + } + flow.build(); + + document.buildPdf(); + } + return outputFile; + } + + private record Chapter(String kicker, String title, List body) { + } + + /** Clickable contents with dotted leaders; numbers resolve in one pass via chapter anchors. */ + private static void contents(PageFlowBuilder flow) { + DocumentTextStyle head = style(26, DocumentTextDecoration.BOLD, INK); + DocumentTextStyle link = style(13, DocumentTextDecoration.DEFAULT, LINK); + DocumentTextStyle page = style(13, DocumentTextDecoration.BOLD, INK); + + flow.addTableOfContents(toc -> { + toc.title("Contents").titleStyle(head) + .leader(DocumentLeader.DOTS).leaderColor(MUTED) + .entryStyle(link).pageNumberStyle(page); + for (int i = 0; i < CHAPTERS.size(); i++) { + Chapter ch = CHAPTERS.get(i); + toc.entry(titleCase(ch.kicker()) + " · " + ch.title(), "ch" + i); + } + }); + } + + private static void chapter(PageFlowBuilder flow, Chapter ch, int index, boolean newPage) { + DocumentTextStyle kickerStyle = style(12, DocumentTextDecoration.BOLD, MUTED); + DocumentTextStyle headStyle = style(28, DocumentTextDecoration.BOLD_ITALIC, INK); + DocumentTextStyle bodyStyle = style(13, DocumentTextDecoration.DEFAULT, BODY_INK); + + String mark = titleCase(ch.kicker()) + " · " + ch.title(); + if (newPage) { + flow.addPageBreak(pb -> pb.name("break" + index)); + } + // The section is both the link target (anchor) and an outline entry (bookmark). + flow.addSection("ChapterSec" + index, s -> { + s.anchor("ch" + index) + .bookmark(new DocumentBookmarkOptions(mark, 0)) + .spacing(0); + s.addParagraph(p -> p.text(ch.kicker()).textStyle(kickerStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.bottom(10))); + s.addParagraph(p -> p.text(ch.title()).textStyle(headStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.bottom(28))); + for (String para : ch.body()) { + s.addParagraph(p -> p.text(para).textStyle(bodyStyle) + .lineSpacing(5).margin(DocumentInsets.bottom(12))); + } + }); + } + + /** Layered title page; on page 1 (zero margin) the waves bleed to every edge. */ + private static void cover(PageFlowBuilder flow, String author, String title, String subtitle, + String city, String year) { + double pw = PAGE.width(); + double ph = PAGE.height(); + + DocumentTextStyle authorStyle = style(15, DocumentTextDecoration.ITALIC, INK); + DocumentTextStyle titleStyle = style(54, DocumentTextDecoration.BOLD_ITALIC, INK); + DocumentTextStyle subtitleStyle = style(22, DocumentTextDecoration.BOLD_ITALIC, INK); + DocumentTextStyle footerStyle = style(12, DocumentTextDecoration.BOLD, DocumentColor.WHITE); + + DocumentColor pale = DocumentColor.rgb(206, 219, 246); + DocumentColor deep = DocumentColor.rgb(120, 152, 218); + + DocumentNode topBack = new PathBuilder().name("TopBack").size(pw, ph) + .moveTo(0.0, 1.0).lineTo(1.0, 1.0).lineTo(1.0, 0.82) + .curveTo(0.66, 0.72, 0.33, 0.95, 0.0, 0.84).closePath().fillColor(pale).build(); + DocumentNode topFront = new PathBuilder().name("TopFront").size(pw, ph) + .moveTo(0.0, 1.0).lineTo(1.0, 1.0).lineTo(1.0, 0.88) + .curveTo(0.70, 0.97, 0.30, 0.80, 0.0, 0.90).closePath().fillColor(deep).build(); + DocumentNode botBack = new PathBuilder().name("BotBack").size(pw, ph) + .moveTo(0.0, 0.0).lineTo(1.0, 0.0).lineTo(1.0, 0.155) + .curveTo(0.66, 0.205, 0.33, 0.075, 0.0, 0.16).closePath().fillColor(pale).build(); + DocumentNode botFront = new PathBuilder().name("BotFront").size(pw, ph) + .moveTo(0.0, 0.0).lineTo(1.0, 0.0).lineTo(1.0, 0.11) + .curveTo(0.70, 0.05, 0.30, 0.17, 0.0, 0.095).closePath().fillColor(deep).build(); + + DocumentNode titleBlock = new SectionBuilder().spacing(10) + .addParagraph(p -> p.text(title).textStyle(titleStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.zero())) + .addParagraph(p -> p.text(subtitle).textStyle(subtitleStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.zero())) + .build(); + DocumentNode footerBlock = new SectionBuilder().spacing(2) + .addParagraph(p -> p.text(city).textStyle(footerStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.zero())) + .addParagraph(p -> p.text(year).textStyle(footerStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.zero())) + .build(); + DocumentNode authorBlock = new ParagraphBuilder() + .text(author).textStyle(authorStyle) + .align(TextAlign.CENTER).margin(DocumentInsets.zero()) + .bookmark(new DocumentBookmarkOptions("Cover", 0)) // the cover's outline entry + .build(); + + flow.addContainer(c -> c + .name("Cover") + .rectangle(pw, ph) + .back(topBack).back(topFront) + .back(botBack).back(botFront) + .position(authorBlock, 0, 196, LayerAlign.TOP_CENTER) + .center(titleBlock) + .position(footerBlock, 0, -54, LayerAlign.BOTTOM_CENTER)); + } + + private static DocumentTextStyle style(double size, DocumentTextDecoration deco, DocumentColor color) { + return DocumentTextStyle.builder().fontName(SERIF).size(size).decoration(deco).color(color).build(); + } + + private static String titleCase(String upper) { + StringBuilder sb = new StringBuilder(upper.length()); + boolean start = true; + for (char c : upper.toLowerCase().toCharArray()) { + sb.append(start ? Character.toUpperCase(c) : c); + start = c == ' '; + } + return sb.toString(); + } + + private static final class LoremHolder { + private static final String P = + "The sun had not yet risen. The sea was indistinguishable from the sky, " + + "except that the sea was slightly creased as if a cloth had wrinkles in it. " + + "Gradually as the sky whitened a dark line lay on the horizon dividing the " + + "sea from the sky and the grey cloth became barred with thick strokes moving, " + + "one after another, beneath the surface, following each other, pursuing each " + + "other, perpetually."; + private static final List LONG = List.of(P, P, P, P, P, P, P, P, P, P, P, P); + private static final List SHORT = List.of(P, P, P); + } + + public static void main(String[] args) throws Exception { + System.out.println("Wrote " + generate()); + } +}