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 @@ -104,6 +104,21 @@ Entries land here as they merge.
Consumers who relied on the helper can copy the former ~100-line class into
their own codebase or load configs directly with Jackson
(`new ObjectMapper(new YAMLFactory()).readValue(...)`).
- **PDF debug node labels** (`@since 1.8.0`). The debug overlay grew a second
layer: `PdfDebugOptions` (guides + node labels + label-text mode) configures
the canonical PDF backend via `GraphCompose.document(...).debug(...)`,
`DocumentSession.debug(...)`, or `PdfFixedLayoutBackend.builder().debug(...)`.
With `nodeLabels()` enabled, every rendered node prints its stable semantic
path — the same path `layoutSnapshot()` reports — once per node and page at
the node's top-left corner (5pt Helvetica on a pale halo), so a misplaced
block on the sheet reads straight back to the builder call that authored it.
`LabelText.NAME` (default) prints the compact own segment
(`PriceSummaryTitle[0]`); `FULL_PATH` prints the whole ancestry. The overlay
uses the base-14 Helvetica font (non-WinAnsi name characters degrade to
`?`), draws strictly on top of content, and never touches measurement or
pagination. `guideLines(boolean)` everywhere became sugar over the new
options — node-label settings survive the toggle — and disabled debug
output stays byte-identical.

### Bug fixes

Expand Down
Binary file added assets/readme/examples/debug-overlay.pdf
Binary file not shown.
28 changes: 28 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [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) |
| [Debug overlay](#debug-overlay) | `PdfDebugOptions` — guide lines + semantic node-path labels on the sheet; trace any misplaced block back to the builder call that authored it | [PDF](../assets/readme/examples/debug-overlay.pdf) · [Source](src/main/java/com/demcha/examples/features/debug/DebugOverlayExample.java) |
| [Business report cover](#business-report-cover) | Single-page Q1 investor brief — hero image, KPI cards, bar chart, metrics table | [PDF](../assets/readme/examples/business-report.pdf) · [Source](src/main/java/com/demcha/examples/flagships/BusinessReportExample.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) |
Expand Down Expand Up @@ -642,6 +643,33 @@ document.buildPdf();
[📄 View PDF](../assets/readme/examples/invoice-snapshot-regression.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/snapshots/LayoutSnapshotRegressionExample.java)

### Debug overlay

One switch turns the rendered sheet into a self-describing layout map:
fragment boxes, dashed margin / padding guides, and a small purple label
with each node's stable semantic path — the same path
`layoutSnapshot()` reports. Spot a misplaced block on paper, read its
label, then search that name in your builder code.

```java
try (DocumentSession document = GraphCompose.document(outputFile)
.debug(PdfDebugOptions.guidesAndNodeLabels())
.create()) {
document.pageFlow(page -> page
.module("InvoiceHeader", m -> m.paragraph("ACME Corp — Invoice 2026-104")));
document.buildPdf();
}
```

Labels default to the compact own segment (`InvoiceHeaderTitle[0]`);
`PdfDebugOptions.LabelText.FULL_PATH` prints the whole ancestor chain
instead. Debug overlays draw strictly on top of content and never
affect measurement or pagination — disabling them returns the exact
production bytes.

[📄 View PDF](../assets/readme/examples/debug-overlay.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/debug/DebugOverlayExample.java)

---

## Operational documents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.demcha.examples.features.barcodes.BarcodeShowcaseExample;
import com.demcha.examples.features.charts.ChartShowcaseExample;
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.PdfChromeExample;
import com.demcha.examples.features.lists.NestedListExample;
Expand Down Expand Up @@ -151,6 +152,7 @@ public static void main(String[] args) throws Exception {
// Pipelines + tooling
System.out.println("Generated: " + HttpStreamingExample.generate());
System.out.println("Generated: " + LayoutSnapshotRegressionExample.generate());
System.out.println("Generated: " + DebugOverlayExample.generate());

// === Flagships ===
System.out.println("Generated: " + ModuleFirstFileExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.demcha.examples.features.debug;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfDebugOptions;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Runnable showcase for the PDF debug overlay (v1.8): guide lines plus
* semantic node labels.
*
* <p>One switch turns the rendered sheet into a self-describing layout
* map:</p>
*
* <pre>{@code
* GraphCompose.document(out)
* .debug(PdfDebugOptions.guidesAndNodeLabels())
* .create()
* }</pre>
*
* <p>Every rendered node then prints its stable semantic path — the same
* path {@code DocumentSession.layoutSnapshot()} reports — once per node
* and page at the node's top-left corner, next to the familiar fragment
* boxes and dashed margin/padding guides. Spot a misplaced block on
* paper, read its label, and grep that name straight in your builder
* code. {@code PdfDebugOptions.LabelText.FULL_PATH} switches the labels
* from the compact own segment to the whole ancestor chain.</p>
*
* <p>Debug overlays draw strictly on top of regular content and never
* affect measurement or pagination — disabling them returns the exact
* production bytes.</p>
*
* @author Artem Demchyshyn
*/
public final class DebugOverlayExample {

private DebugOverlayExample() {
}

/**
* Renders a small annotated sheet with guides and node labels enabled.
*
* @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/debug", "debug-overlay.pdf");

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(DocumentPageSize.A4)
.margin(DocumentInsets.of(28))
.debug(PdfDebugOptions.guidesAndNodeLabels())
.create()) {
document.pageFlow(page -> page
.module("HowToReadThisSheet", module -> module
.paragraph("Every block carries its debug overlay: gray fragment boxes, "
+ "dashed margin (blue) and padding (orange) guides, and a small "
+ "purple label with the owning node's semantic path.")
.paragraph("Labels print the same stable path that layoutSnapshot() reports — "
+ "spot a misplaced block on paper, then search the label text "
+ "in your builder code."))
.module("InvoiceHeader", module -> module
.paragraph("ACME Corp — Invoice 2026-104")
.paragraph("Issued 2026-06-11, due in 14 days"))
.module("PriceSummary", module -> module
.paragraph("Subtotal 1,180.00 · VAT 236.00 · Total 1,416.00")));

document.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 @@ -104,6 +104,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
feature("streaming", "invoice-http-stream", "HTTP Streaming", "Stream PDF directly to a Servlet response with no buffering.", "streaming", "http");
feature("snapshots", "invoice-snapshot-regression", "Layout Snapshots", "How LayoutSnapshotAssertions captures the resolved layout graph for regression testing.", "snapshots", "testing");
feature("docx", "word-export-companion", "Word Export (DOCX)", "DocxSemanticBackend — the same document as a fixed-layout PDF and an editable Word file; charts fall back to their data table.", "docx", "word", "export");
feature("debug", "debug-overlay", "Debug Overlay", "PdfDebugOptions — guide lines plus semantic node-path labels on the rendered sheet; trace any misplaced block back to the builder call that authored it.", "debug", "labels", "v1.8");

// ===== Flagships =====
flagship("master-showcase", "Master Showcase", "Kitchen-sink demo combining every primitive into a single document — the full GraphCompose surface.", "showcase");
Expand Down Expand Up @@ -141,6 +142,7 @@ static String groupLabel(String category, String group) {
case "features/chrome" -> "PDF Chrome (header / footer / watermark)";
case "features/streaming" -> "Streaming & I/O";
case "features/snapshots" -> "Snapshot Testing";
case "features/debug" -> "Debug & Diagnostics";
case "flagships/default" -> "Flagship Demos";
default -> capitalize(group);
};
Expand Down
23 changes: 23 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,7 @@
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.backend.fixed.pdf.options.PdfDebugOptions;
import com.demcha.compose.document.style.DocumentInsets;

import java.nio.file.Path;
Expand Down Expand Up @@ -140,6 +141,7 @@ public static final class DocumentBuilder {
private DocumentInsets margin = DocumentInsets.zero();
private boolean markdown = true;
private boolean guideLines;
private PdfDebugOptions debug;
private com.demcha.compose.document.style.DocumentColor pageBackground;
private java.util.List<com.demcha.compose.document.api.PageBackgroundFill> pageBackgrounds;
private final List<FontFamilyDefinition> customFontFamilies = new ArrayList<>();
Expand Down Expand Up @@ -226,6 +228,24 @@ public DocumentBuilder guideLines(boolean enabled) {
return this;
}

/**
* Configures PDF debug overlays (guide lines and semantic node labels)
* for the session's convenience PDF output.
*
* <p>Combines with {@link #guideLines(boolean)}: when both switches are
* used, the guide overlay is enabled if either of them requests it.
* Like guide lines, debug overlays draw on top of regular content and
* never alter semantic layout geometry or layout snapshots.</p>
*
* @param options debug overlay options, or {@code null} for none
* @return this builder
* @since 1.8.0
*/
public DocumentBuilder debug(PdfDebugOptions options) {
this.debug = options;
return this;
}

/**
* Configures a document-wide page background fill applied behind every
* fragment on every page.
Expand Down Expand Up @@ -391,6 +411,9 @@ public DocumentSession create() {
List.copyOf(customFontFamilies),
markdown,
guideLines);
if (debug != null) {
session.debug(debug.withGuides(debug.showGuides() || guideLines));
}
if (pageBackgrounds != null) {
// Explicit pageBackgrounds() call wins over a prior
// pageBackground(color). Empty list = clear; see builder Javadoc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,20 @@ DocumentOutputOptions snapshot() {

/**
* Builds a configured {@link PdfFixedLayoutBackend} for the session's
* convenience PDF methods. When {@code guideLines} is {@code false} and
* no chrome is attached, returns the bare default backend so callers do
* convenience PDF methods. When no debug overlay is enabled and no
* chrome is attached, returns the bare default backend so callers do
* not pay for empty option arrays.
*
* @param guideLines whether the convenience PDF backend should draw
* guide-line overlays
* @param debug debug overlay options for the convenience PDF backend;
* never {@code null}
* @return ready-to-use PDF backend
*/
PdfFixedLayoutBackend toConveniencePdfBackend(boolean guideLines) {
if (!guideLines && isEmpty()) {
PdfFixedLayoutBackend toConveniencePdfBackend(PdfDebugOptions debug) {
if (!debug.enabled() && isEmpty()) {
return new PdfFixedLayoutBackend();
}
PdfFixedLayoutBackend.Builder builder = PdfFixedLayoutBackend.builder()
.guideLines(guideLines)
.debug(debug)
.metadata(PdfOutputOptionsTranslator.toPdf(metadata))
.watermark(PdfOutputOptionsTranslator.toPdf(watermark))
.protect(PdfOutputOptionsTranslator.toPdf(protection));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.demcha.compose.document.backend.fixed.FixedLayoutBackend;
import com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend;
import com.demcha.compose.document.backend.fixed.pdf.PdfMeasurementResources;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfDebugOptions;
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;
Expand Down Expand Up @@ -73,7 +74,7 @@ public final class DocumentSession implements AutoCloseable {
private DocumentInsets margin;
private LayoutCanvas canvas;
private boolean markdown;
private boolean guideLines;
private PdfDebugOptions debug = PdfDebugOptions.none();
private List<PageBackgroundFill> pageBackgrounds = List.of();
private PdfMeasurementResources measurementResources;
private boolean closed;
Expand All @@ -100,7 +101,7 @@ public DocumentSession(Path defaultOutputFile,
this.margin = margin == null ? DocumentInsets.zero() : margin;
this.canvas = LayoutCanvas.from(pageSize.width(), pageSize.height(), toEngineMargin(this.margin));
this.markdown = markdown;
this.guideLines = guideLines;
this.debug = PdfDebugOptions.none().withGuides(guideLines);
this.registry = BuiltInNodeDefinitions.registerDefaults(new InvalidatingNodeRegistry());
this.compiler = new LayoutCompiler(registry);
this.customFontFamilies.addAll(List.copyOf(customFontFamilies));
Expand Down Expand Up @@ -308,12 +309,40 @@ public DocumentSession markdown(boolean enabled) {
* and {@link #toPdfBytes()}. It does not change the semantic layout graph,
* so existing layout cache entries remain valid.</p>
*
* <p>Shorthand for toggling only the guide overlay on the current
* {@link #debug(PdfDebugOptions) debug} configuration; node-label
* settings are preserved.</p>
*
* @param enabled {@code true} to draw debug guide-line overlays
* @return this session
*/
public DocumentSession guideLines(boolean enabled) {
ensureOpen();
this.guideLines = enabled;
this.debug = this.debug.withGuides(enabled);
return this;
}

/**
* Configures PDF debug overlays (guide lines and semantic node labels)
* for convenience PDF output.
*
* <p>This option affects {@link #buildPdf()}, {@link #writePdf(OutputStream)},
* and {@link #toPdfBytes()}. Debug overlays draw on top of regular content
* and never participate in measurement or pagination, so the semantic
* layout graph and existing layout cache entries remain valid.</p>
*
* <p>Node labels print each node's stable semantic path — the same path
* reported by {@link #layoutSnapshot()} — so a misplaced block on the
* sheet can be traced straight back to the builder call that authored
* it.</p>
*
* @param options debug overlay options; {@code null} disables all overlays
* @return this session
* @since 1.8.0
*/
public DocumentSession debug(PdfDebugOptions options) {
ensureOpen();
this.debug = options == null ? PdfDebugOptions.none() : options;
return this;
}

Expand Down Expand Up @@ -1008,7 +1037,7 @@ public DocumentOutputOptions outputOptions() {

@Override
public PdfFixedLayoutBackend conveniencePdfBackend() {
return chromeOptions.toConveniencePdfBackend(guideLines);
return chromeOptions.toConveniencePdfBackend(debug);
}
}
}
Loading