Home  ›  Guides  ›  Layout debugging

Layout debugging & inspection

New in v1.5.0. Two complementary tools help you understand where pdfnative places every block on the page: an opt-in visual overlay (layout: { debug: true }) that draws margin / content / cell boxes straight onto the PDF, and a programmatic inspection API (inspectDocumentLayout()) that returns the per-page block geometry as plain data. Both are byte-neutral: when debug is off, output is byte-identical to previous releases.

TL;DR#

import { buildDocumentPDFBytes, inspectDocumentLayout } from 'pdfnative';

const params = {
  title: 'Invoice',
  blocks: [
    { type: 'heading', level: 1, text: 'Invoice #42' },
    { type: 'paragraph', text: 'Thanks for your business.' },
  ],
};

// 1. Visual overlay — margin / content / cell boxes drawn on the PDF
const pdf = buildDocumentPDFBytes(params, { debug: true });

// 2. Programmatic geometry — no rendering, just data
const layout = inspectDocumentLayout(params);
for (const page of layout.pages) {
  for (const block of page.blocks) {
    console.log(page.index, block.type, block.x, block.top, block.width, block.height);
  }
}

Visual overlay#

Pass debug: true (or a granular object) in the layout options:

buildDocumentPDFBytes(params, { debug: true });

// or select exactly what you want to see:
buildDocumentPDFBytes(params, {
  debug: { showMargins: true, showContentBounds: true, showCells: false },
});
Option Colour Draws
showMargins blue the page margin box (content area boundary)
showContentBounds red a rectangle around every rendered block
showCells green per-cell rectangles for tables

debug: true is shorthand for enabling all three. The overlay is drawn last, on top of the content, each shape wrapped in its own graphics state (q … Q) so it never leaks colour or line-width into your document. Turn it off (or omit it) and the bytes are exactly what you'd get without the option.

Programmatic inspection#

inspectDocumentLayout(params, layout?) runs the same pagination engine as the builder but produces data instead of PDF bytes — no rendering, no font embedding:

import { inspectDocumentLayout } from 'pdfnative';
import type { LayoutInspection } from 'pdfnative';

const report: LayoutInspection = inspectDocumentLayout(params);

report.pages.forEach((page) => {
  console.log(`Page ${page.index + 1}: ${page.blocks.length} blocks`);
  page.blocks.forEach((b) => {
    console.log(`  ${b.type} @ (${b.x}, ${b.top}) ${b.width}×${b.height}`);
  });
});

Shape#

Type Fields
LayoutInspection { pageWidth, pageHeight, margins, totalPages, pages: InspectedPage[] }
InspectedPage { index, blocks: InspectedBlock[] }
InspectedBlock { type, page, x, top, width, height } (PDF user-space points, origin bottom-left; top is the block's upper edge, height extends downward)

Because it shares the builder's estimateBlockHeight / planTable logic and constants, the reported geometry matches where the real renderer places each block — including table slicing across page breaks.

When to use which#

Diagnostics and build-time errors (v1.8.0)#

Geometry is one thing to debug; the other is a build that warns or refuses. Every engine diagnostic code and every build-time message a downstream tool must classify is listed in docs/data/errors.json (diagnostics and buildErrors) — read that file first when a message is unfamiliar.

TYPOGRAPHY_FEATURE_INEFFECTIVE — a typography.fontFeatures tag changed nothing: no registered font declares it, or the font declares it but no glyph in the document was substituted. The classic case is tnum or lnum with the bundled Noto Sans, whose figures are tabular and lining by default, so there is nothing to substitute (pnum and onum do change glyphs there). It is a warning — console.warn once per build by default, every occurrence through layout.onDiagnostic, a thrown error under layout.strict — because the same document may be built with different fonts. Drop the tag, or register a font whose GSUB declares it; the effective tags are the keys of the font module's features table.

buildDocumentPDFBytes(params, {
  typography: { fontFeatures: ['tnum'] },
  onDiagnostic: (d) => console.error(d.code, d.message), // TYPOGRAPHY_FEATURE_INEFFECTIVE
});

PDF/X coherence errors — pdfx: 'pdfx4' is checked before any byte is written, and an incoherent layout throws one of seven messages (each with its remedy):

Message Remedy
layout.pdfx: unknown target '${target}' — use one of ${PDF_X_CONFORMANCE_TARGETS} Use 'pdfx4'.
layout.pdfx and layout.tagged cannot be combined — pdfnative writes one conformance claim per file; drop one of them Build the print file and the archival file as two documents.
PDF/X forbids encryption (ISO 15930-7) — drop layout.encryption or layout.pdfx Drop one of the two.
PDF/X-4 requires layout.outputIntent: the ICC profile of the printing condition, e.g. ISO Coated v2 or GRACoL from your printer. pdfnative ships no press profile Pass the output ICC profile your printer names.
PDF/X-4 requires an output (printer) profile as layout.outputIntent — the supplied profile's class is '${deviceClass}' Use a press profile (device class prtr), not a monitor profile such as sRGB.
PDF/X requires the trapping state to be known — set metadata.trapped to 'True' or 'False', or omit it for 'False' State 'True' or 'False'; pdfnative never traps, so 'False' is accurate for its output.
PDF/X pages carry a TrimBox or an ArtBox, not both — drop print.artBox, or print.trimBox and print.bleed Keep one of the two boxes.

The messages start with PDF/X or layout.pdfx; a CLI or server that maps print., chart: and outputIntent. prefixes to an input-error code should add those two.

setDeflateImpl() throws on raw DEFLATE — since v1.8.0 the injected compressor is validated, and a function that returns a raw RFC 1951 payload (fflate's deflateSync, CompressionStream's 'deflate-raw') fails at build time instead of producing pages that render blank (#78). Pass a zlib-wrapping function to setDeflateImpl() — fflate's zlibSync, pako's deflate, node:zlib deflateSync — or hand the raw one to setDeflateRawImpl() and let pdfnative add the envelope. An asynchronous compressor cannot be adapted at all; PDF assembly is synchronous.

punctuationSpacing: 'fr' looks like 'fr-CA' — the 'fr' preset sets a narrow no-break space (U+202F) before ; ! ?. WinAnsi has no such character, so on the base-14 (non-embedded) path it degrades to an ordinary space and the two presets become indistinguishable. Register a Latin font that carries the glyph — Noto Sans does — and pass it in fontEntries; the narrow space is then rendered and extracts as U+202F.

Sample#

layout-debug-overlay.ts renders the same document twice — once clean, once with the overlay — and prints the inspectDocumentLayout() report.