diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64784acb1..81cb6794a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/assets/readme/examples/debug-overlay.pdf b/assets/readme/examples/debug-overlay.pdf
new file mode 100644
index 000000000..9872ee1c1
Binary files /dev/null and b/assets/readme/examples/debug-overlay.pdf differ
diff --git a/examples/README.md b/examples/README.md
index a1c08f539..25ecf3628 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -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) |
@@ -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
diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
index 099e78bf5..645189d91 100644
--- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
+++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
@@ -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;
@@ -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());
diff --git a/examples/src/main/java/com/demcha/examples/features/debug/DebugOverlayExample.java b/examples/src/main/java/com/demcha/examples/features/debug/DebugOverlayExample.java
new file mode 100644
index 000000000..18eb6d469
--- /dev/null
+++ b/examples/src/main/java/com/demcha/examples/features/debug/DebugOverlayExample.java
@@ -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.
+ *
+ *
One switch turns the rendered sheet into a self-describing layout
+ * map:
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.
+ *
+ *
Debug overlays draw strictly on top of regular content and never
+ * affect measurement or pagination — disabling them returns the exact
+ * production bytes.
+ *
+ * @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());
+ }
+}
diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java
index 8243582e3..3f23df311 100644
--- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java
+++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java
@@ -104,6 +104,7 @@ record Entry(String title, String description, List 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");
@@ -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);
};
diff --git a/src/main/java/com/demcha/compose/GraphCompose.java b/src/main/java/com/demcha/compose/GraphCompose.java
index f20ba3ca7..d1481c050 100644
--- a/src/main/java/com/demcha/compose/GraphCompose.java
+++ b/src/main/java/com/demcha/compose/GraphCompose.java
@@ -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;
@@ -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 pageBackgrounds;
private final List customFontFamilies = new ArrayList<>();
@@ -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.
+ *
+ *
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.
+ *
+ * @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.
@@ -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.
diff --git a/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java b/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java
index 49f618a2e..24b00198b 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java
@@ -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));
diff --git a/src/main/java/com/demcha/compose/document/api/DocumentSession.java b/src/main/java/com/demcha/compose/document/api/DocumentSession.java
index 548099e85..ee2d2daed 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentSession.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentSession.java
@@ -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;
@@ -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 pageBackgrounds = List.of();
private PdfMeasurementResources measurementResources;
private boolean closed;
@@ -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));
@@ -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.
*
+ *
Shorthand for toggling only the guide overlay on the current
+ * {@link #debug(PdfDebugOptions) debug} configuration; node-label
+ * settings are preserved.
+ *
* @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.
+ *
+ *
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.
+ *
+ *
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.