Skip to content

Configuration Fonts Colors and Viewers

Ignacio Ferro Picón edited this page Oct 16, 2026 · 7 revisions

Configuration: units, fonts, colours, viewers and debugging

Part of the Configuration reference.

Generated from docs/configuration-fonts-colors-viewers-en.mdx (updated 2026-10-10). Read it on postext.dev, where it is also available in Spanish, Catalan, Portuguese, French, German, Italian, Chinese, Japanese and Arabic. To change this page, edit the MDX and run pnpm wiki in apps/web.

Units and colors

Dimensions

All physical measurements in Postext use the Dimension type — a value paired with a unit:

interface Dimension {
  value: number;
  unit: DimensionUnit; // 'cm' | 'mm' | 'in' | 'pt' | 'px' | 'em' | 'rem'
}

Absolute units — cm, mm, in, pt, px — are converted to pixels using the configured DPI. At 300 DPI, 1 cm equals approximately 118 px.

Relative units — em, rem — scale with the current font size. An em is relative to the element's own font size; rem is relative to the body text font size.

Colors

Colors are stored with both a hex representation and a target color model:

interface ColorValue {
  hex: string;         // '#ff0000', 'transparent', etc.
  model: ColorModel;   // 'hex' | 'rgb' | 'cmyk' | 'hsl'
  cmyk?: CmykPercent;  // The exact process values of a colour authored in CMYK.
}

The model field indicates the intended color space. For web rendering, 'hex' or 'rgb' are typical. For print workflows, 'cmyk' says the colour was specified in CMYK, and cmyk keeps its values: a CMYK print render sets them as they are, with hex as their screen rendering (see Colours authored in CMYK).

Because Postext targets publication-grade output, the default body text color ships with model: 'cmyk' (#000000). Heading, bold, italic, and list colors default to the palette-linked Main Color (#295AA3, model: 'hex'). Page background and UI overlays (baseline grid, cut marks, debug indicators) default to model: 'hex'. Override color.model on any field if you need different export semantics.

Transparency

A colour can be translucent. hex accepts an alpha channel, as #rgba or #rrggbbaa. It also accepts an rgb() / rgba() colour, in the comma or the space syntax, with the alpha as a number or a percentage. transparent is fully clear:

const config: PostextConfig = {
  header: {
    elements: [{
      kind: 'box',
      id: 'veil',
      placement: {
        anchor: { to: 'bleed', edge: 'top-left' },
        size: { width: 'fill', height: { value: 40, unit: 'mm' } },
      },
      style: { backgroundColor: { hex: '#ffffffb3', model: 'hex' } }, // white at 70 %
    }],
  },
  bodyText: { color: { hex: 'rgba(0, 0, 0, 0.85)', model: 'rgb' } },
};

The three backends paint it the same way. The canvas and the HTML viewer take the value as a CSS colour. The PDF backend sets the colour's opacity as a constant alpha, an ExtGState with ca for fills and CA for strokes. This covers text, rules, boxes, table fills and borders, chips, swatches and formulas. A translucent colour composes over whatever was painted before it. A box in the header or footer is painted last, so it veils the text under it; a box in an opener band is painted first, so it tints the page under the text. When the PDF is forced into another colour space (pdfGeneration.forceColorSpace with colorSpace: 'cmyk' or 'grayscale'), the colour is converted and its alpha kept. In the Sandbox, the opacity slider of the colour picker writes these values as #rrggbbaa, and the picker reads the other forms too.

Custom fonts

Postext resolves every fontFamily string against both the Google Fonts catalogue and the document's customFonts list. Custom fonts take precedence on name collision — if you declare customFonts: [{ name: 'Roboto', … }], Postext uses your uploaded file instead of the Google Fonts "Roboto".

Use custom fonts when:

  • The document needs a brand or licensed typeface that is not on Google Fonts.
  • The environment can't reach the font CDNs, jsDelivr (where the Sandbox takes Fontsource's files from) or Google Fonts (offline, intranet, privacy-sensitive).
  • You must keep the font binary private and not upload it to a third party.

Configuration schema

type CustomFontFormat = 'woff2' | 'woff' | 'ttf' | 'otf';
type CustomFontStyle = 'normal' | 'italic';

interface CustomFontVariant {
  weight: number;           // CSS font-weight, 100..900
  style: CustomFontStyle;
  fileId: string;           // opaque id of the binary in out-of-band storage
  format: CustomFontFormat;
  fileName?: string;        // original upload filename (optional, shown in UI)
}

interface CustomFontFamily {
  name: string;             // used anywhere a Google Font family name fits
  variants: CustomFontVariant[];
}

interface PostextConfig {
  // ...
  customFonts?: CustomFontFamily[];
}

Each variant's binary is not embedded in the config itself. The config only holds fileId pointers; the bytes live out-of-band. In the sandbox, that means IndexedDB (key-value store, browser-only, private to the document). An integrator embedding Postext in another host is free to resolve fileId however they like — a server endpoint, a service worker cache, anything — as long as the bytes reach the main thread before buildDocument runs.

Managing custom fonts in the sandbox

Open the Fonts panel from the left activity bar (between Resources and Design). Its Typefaces in this book list shows every family the design uses, with its role and where it comes from: Google Fonts, Fontsource (for a family Google Fonts does not have), your file, or nowhere (Not found). Under Your font files, for each family:

  1. Add font family — creates an empty family; rename it inline.
  2. Upload variant(s) — pick a weight (100–900) and style (normal / italic), then choose one or many .woff2, .woff, .ttf, or .otf files. Each file becomes its own variant bound to the currently-selected (weight, style); the uploaded filename is remembered and shown in the row so you can tell variants apart. Re-tune a variant's weight or style from its dropdowns at any time.
  3. Duplicate variants are allowed. If two files land on the same (weight, style) slot, both are kept and a Duplicate font variant warning appears so you know to disambiguate the settings of the extras.
  4. Delete variant or Delete family — removes the entry from the config and the stored bytes from IndexedDB.

Once a family is declared, every font picker groups it under Custom, above the Google Fonts list. Selecting it wires the family into every font-family field you apply it to.

Rendering behaviour

Under the hood:

  • When customFonts changes, every declared family is automatically registered as FontFace entries on document.fonts — so the HTML viewer, the Canvas viewport (which measures through document.fonts), and any direct CSS reference all pick up the custom face without requiring the user to first open the Font Picker.
  • The layout worker receives the same ArrayBuffers through the existing font payload transfer path, so measurement (buildFontString, pretext) works as it does for a family of the catalogue.
  • Changing or removing a variant drops the worker's cached face for that family and re-registers on the next build, so previews stay in sync with the current variant set.
  • PDF export: uploaded binaries flow through the same PdfFontProvider pipeline. .woff2 files are decompressed; .ttf and .otf are passed through directly. .woff is rejected with a clear error (pdf-lib can't embed raw WOFF — re-upload as .woff2/.ttf/.otf). CFF-flavored OpenType (.otf with OTTO magic) is embedded without subsetting, because pdf-lib's CFF subsetter walks every glyph at save() time and can hang for minutes on real fonts; skipping subset trades a somewhat larger PDF for consistent render times.

Missing-font warnings

The Sandbox's Checks panel lists three failure modes specific to custom fonts under its Fonts group (all enabled by the same debug.warnings.missingFont toggle that already guards the generic "not loaded" warning):

  • Unknown font family — a fontFamily references a name that is neither a known Google Font nor a currently-declared custom family. This also fires immediately when you delete a custom family that some fontFamily field still references, instead of waiting for the DOM to notice.
  • Missing font variant — the family exists but at least one of the standard weight/style slots (400 / 700, normal / italic) has no uploaded file. The warning lists the specific combinations that are missing.
  • Duplicate font variant — two or more uploaded files share the same (weight, style) slot within one family. Only one file is actually used at render time; the warning nudges you to retune the remaining entries.

Clicking any of these warnings opens the Fonts panel so you can upload the missing variant, re-add the family, or disambiguate the duplicates.

Once a layout exists, the panel also lists the engine's Font fallback in the layout (fontFallback): a face the pages were measured without, either missing or drawn by the browser from another weight or slant. A family already named as unknown or short of a variant is not listed twice. Before the first layout, a check against document.fonts stands in for it.

Color Palette

The colorPalette property on PostextConfig lets you define a reusable set of named colors and reference them from any ColorValue in the configuration. It is the Postext equivalent of CSS custom properties or an InDesign swatches panel: change the palette entry once, and every color that points to it updates across the document.

interface ColorPaletteEntry {
  id: string;       // stable identifier — referenced by ColorValue.paletteId
  name: string;     // human label shown in sandbox UIs
  value: ColorValue;
}

The default palette

Postext ships with a single-entry default palette called Main Color (id: 'main-color', hex #295AA3). Several defaults — heading color, bold/italic body color, :ref color, bullet and number-marker colors — reference this entry via paletteId: 'main-color', so changing that one swatch retints every part of the document that uses it.

You can inspect, clone, or compare against the default palette via three exports:

import {
  DEFAULT_COLOR_PALETTE,
  cloneDefaultColorPalette,
  isDefaultColorPalette,
} from 'postext';

// Read-only snapshot of the shipped palette.
DEFAULT_COLOR_PALETTE;
// => [{ id: 'main-color', name: 'Main Color', value: { hex: '#295AA3', model: 'hex' } }]

// Independent copy — mutate this, not DEFAULT_COLOR_PALETTE.
const palette = cloneDefaultColorPalette();

// Detect whether a user has customised the palette at all.
isDefaultColorPalette(palette); // true

A palette lives at the top level of the config:

const config: PostextConfig = {
  colorPalette: [
    { id: 'ink',    name: 'Ink',    value: { hex: '#0a0a0a', model: 'cmyk' } },
    { id: 'accent', name: 'Accent', value: { hex: '#b8860b', model: 'hex' } },
  ],
  bodyText: { color: { hex: '#000000', model: 'cmyk', paletteId: 'ink' } },
  headings: { color: { hex: '#000000', model: 'hex', paletteId: 'accent' } },
};

Referencing a palette entry

Any ColorValue in the configuration can carry an optional paletteId field pointing at an entry in colorPalette: page background, body text colors (the :ref colour included), heading colors, column rules, list colors, table, caption, chip and callout colors (box, stripe, icon, marker, label, title, body), cut-mark and baseline-grid colors, debug indicators, and every colour of a design — the running heads, the heading openers and in-column designs, the heading styles' designs and running heads, the part pages and the contents' part rows (text, rule, box fill and border, outline, drop cap). When present, the palette entry's hex / model win over the fallback hex / model stored alongside. The inline fallback is only used if the palette is missing, empty, or doesn't contain that id — useful when shipping a config that will be read by a tool that doesn't understand palettes.

Changed in postext 1.5. Up to postext 1.4 the palette reached only a fixed list of settings: the colours of the designs (running heads, openers, heading styles, parts, contents rows), bodyText.referenceColor, the callout label colours and the bold / italic colours of callout bodies kept the hex stored beside their paletteId. A document whose stored value differs from the palette entry — every one edited in the Sandbox after the entry changed, and every :ref when the Main Color is not #295AA3 — now prints the palette colour, as the link says. To keep a colour as it was, remove its paletteId.

How palettes are applied

buildDocument runs the palette in two places so referenced colors work both for overrides you spelled out and for defaults that are filled in later:

  1. applyPaletteToConfig(config) — resolves every ColorValue in the raw user config that carries a paletteId. Useful when you want to inspect what the engine will actually see.
  2. applyPaletteToResolvedConfig(resolved, palette) — runs after defaults are resolved and rewrites the palette-linked defaults (heading color, bold/italic body color, :ref color, list colors, the colours of the default designs) to match the active palette.

Both walk the whole configuration, so no palette-linked colour is left behind. Most colours of the text flow (body text, headings, lists, tables, captions, chips, the callout box, title and body) come out as plain values. Every other one — the colours of the designs, the :ref colour, the callout labels — takes the palette's hex / model and keeps its paletteId. That link is what a part's palette attribute and a heading style's palette override on their pages (see Parts), so it has to survive. htmlViewer.overrides is left as written: the HTML viewer merges it first, and a palette it carries then applies to everything, the designs included.

You rarely need to call these yourself, but both are exported so you can inspect or reuse them:

import {
  applyPaletteToConfig,
  applyPaletteToResolvedConfig,
  resolveColorValue,
} from 'postext';

const flat = applyPaletteToConfig(config);
// Every ColorValue with a paletteId in the raw config now carries the
// palette entry's hex/model (a design colour keeps its paletteId).

// `applyPaletteToResolvedConfig` is typically handled by buildDocument; use it
// directly if you build a ResolvedConfig yourself and want the palette applied.

resolveColorValue(value, palette, fallback) is the single-value variant, handy when you are composing configs imperatively and need to resolve one color at a time.

Editing the palette

A colour whose paletteId names no entry prints its stored hex / model, which may be older than the colour the entry gave it. So before removing an entry, rewrite every ColorValue linked to it as a plain colour holding the entry's current value. The Sandbox's Palette section (Design → Colours) does this when you delete an entry, wherever the colour sits (designs and callout labels included), and its confirmation lists every setting that uses the entry: by name, or by its path in the configuration (header.elements[2].color).

HTML Viewer

The htmlViewer property controls how the HTML backend lays out pages on screen. It only applies when you render with renderToHtml / renderToHtmlIndexed; the canvas and PDF paths ignore it entirely — they consume the configured page.width, page.height, and page.dpi directly.

interface HtmlViewerConfig {
  maxCharsPerLine?: number;     // Target column width, in characters of the body font.
  columnGap?: number;            // Horizontal gap between columns in multi-column mode (px).
  optimalLineBreaking?: boolean; // Use Knuth–Plass inside the HTML viewer instead of greedy.
  overrides?: HtmlViewerOverrides; // Screen-only partial config merged over the document config.
}

type HtmlViewerOverrides = Omit<PostextConfig, 'htmlViewer'>;
Property Type Default Description
maxCharsPerLine number 70 Target measure for each rendered column, expressed in characters of the body font. The viewport samples a representative prose string at that length to derive the actual pixel width — so the result adapts to any proportional font and font-size combination.
columnGap number 50 Horizontal gap, in CSS pixels, between columns when the viewer is in multi-column mode. Ignored in single-column mode.
optimalLineBreaking boolean false Enable Knuth–Plass line breaking in the HTML viewer. Off by default because the viewer reruns layout on every resize and font-size change — the greedy first-fit algorithm is fast enough to feel instantaneous. Turn it on when you want the same optimal breaks the canvas backend uses.
overrides HtmlViewerOverrides — A partial document config that applies on screen only. The HTML viewer merges it over the document config before laying out (applyHtmlViewerOverrides); canvas and PDF ignore it. Objects merge recursively; a levels array (headings, lists, contents) merges entry by entry on level; every other array — a design slot's elements, calloutStyles, colorPalette… — replaces the base array wholesale. Typical use: a chapter opener without the print bands, or a part page whose title wraps against the number instead of a fixed trim-box width. The sandbox edits it as JSON.
const config: PostextConfig = {
  headings: { levels: [{ level: 1, span: 'page', breakBefore: { enabled: true } }] },
  htmlViewer: {
    // On screen, chapters flow on without the page-span opener.
    overrides: { headings: { levels: [{ level: 1, span: 'column', breakBefore: { enabled: false } }] } },
  },
};

Resolver and stripper follow the same pattern as the other sections:

import {
  DEFAULT_HTML_VIEWER_CONFIG,
  resolveHtmlViewerConfig,
  stripHtmlViewerDefaults,
} from 'postext';

const resolved = resolveHtmlViewerConfig(config.htmlViewer);
// => { maxCharsPerLine: 70, columnGap: 50, optimalLineBreaking: false }

const minimal = stripHtmlViewerDefaults(config.htmlViewer);
// => undefined when everything matches the defaults

See Integrating the HTML viewer below for an end-to-end example.

PDF generation (config)

The pdfGeneration property controls how the PDF backend emits the final document. These settings are consumed by the postext-pdf package at export time; the canvas and HTML viewers ignore them.

buildDocument carries them in the VDT, as doc.config.pdfGeneration, and renderToPdf takes each setting from the first place that gives it:

  1. its own options (outlines, accessible, colorSpace);
  2. the pdfGeneration of the first document it renders (in a book, the first chapter's settings apply to the whole file);
  3. the defaults: bookmarks and tagging on, RGB colour.

So renderToPdf(doc, { fontProvider }) follows the config, and an option passed to renderToPdf wins for that setting only. forceColorSpace and colorSpace together stand for the colorSpace option: the config's colorSpace applies while forceColorSpace is on, and the PDF is RGB while it is off. Earlier releases of postext-pdf read the options only; a config that sets pdfGeneration now changes the PDF of a caller that passes no options.

type PdfColorSpace = 'rgb' | 'cmyk' | 'grayscale';

interface PdfGenerationConfig {
  outlines?: boolean;          // Emit PDF bookmarks from the heading tree.
  forceColorSpace?: boolean;   // Convert every colour to `colorSpace`.
  colorSpace?: PdfColorSpace;  // Target space used when `forceColorSpace` is true.
  accessible?: boolean;        // Tagged, PDF/UA-oriented output (structure tree, alt text, language).
}
Property Type Default Description
outlines boolean true Emit PDF outlines (bookmarks) from the heading hierarchy so readers can jump directly to any heading from the sidebar of a PDF viewer. Turn off for documents where the heading tree is meaningless (e.g. single-page posters).
forceColorSpace boolean false When true, every colour in the rendered PDF is converted to colorSpace at export time. Leave off for screen-first PDFs where input colours are already in the desired space; turn on to guarantee a single colour space across mixed sources.
colorSpace 'rgb' | 'cmyk' | 'grayscale' 'cmyk' Target colour space used when forceColorSpace is on. Use 'cmyk' for offset printing, 'rgb' for screen-only PDFs, and 'grayscale' for black-and-white print proofs. Has no effect when forceColorSpace is false. CMYK is separated through the output profile of print (FOGRA39 by default) with its black handling, and RGB pictures are converted too; a PDF/X standard there writes CMYK whatever this says.
accessible boolean true Emit an accessible, tagged PDF oriented to PDF/UA-1: a logical structure tree in reading order (headings that never skip a level, paragraphs, lists, block quotes, callouts, tables with header cells, figures with their alt text and captions, formulas, clickable refs as links, the contents of a :::toc as one TOC with a TOCI per row: the row's number a Lbl, its title and page a Reference holding the link), the document title and language (the top-level locale), the PDF/UA identification in the XMP metadata, and every decorative mark (page background, rules, baseline grid, running headers and footers, cut marks, repeated table headers, the repeated title and continuation marker of a split callout) flagged as an artifact so screen readers skip it. A figure without altText falls back to its caption, then to its label. A floated figure or table is read right after the text that first cites it, or the text before its ::resource line, and a floated box after the text before its fence, even when the float is placed on a later page; a list or the contents that go on past a float stay one element. Turn off only for print masters where the extra structure is unwanted.
pdfGeneration: {
  outlines: true,
  accessible: true,
  forceColorSpace: true,
  colorSpace: 'cmyk',
}

Resolver and stripper match the other sections:

import {
  DEFAULT_PDF_GENERATION_CONFIG,
  resolvePdfGenerationConfig,
  stripPdfGenerationDefaults,
} from 'postext';

const resolved = resolvePdfGenerationConfig(config.pdfGeneration);
// => { outlines: true, forceColorSpace: false, colorSpace: 'cmyk', accessible: true }

const minimal  = stripPdfGenerationDefaults(config.pdfGeneration);
// => undefined when everything matches the defaults

See Generating PDFs below for the end-to-end export recipe.

Print production (config)

The print property says how a book goes to press: the PDF/X standard of the file, the output profile its CMYK is separated with, how black prints, and the thresholds of the preflight. Layout ignores it, so changing it never moves a line. Three things read it: postext-pdf when it writes the file, preflightDocument when it checks a laid-out document, and the print preview of the canvas and Folio viewers.

type PdfXStandard = 'none' | 'pdfx1a' | 'pdfx4';

interface PrintConfig {
  standard?: PdfXStandard;                 // 'none': an ordinary PDF.
  outputProfile?: string;                  // A catalogue id ('fogra39', 'fogra51'…) or 'custom'.
  customProfile?: CustomOutputProfile;     // An uploaded .icc file.
  renderingIntent?: 'relative' | 'perceptual';
  blackPointCompensation?: boolean;
  convertImages?: boolean;                 // Separate RGB pictures (PDF/X-1a always does).
  inkLimit?: number;                       // Total area coverage, percent.
  black?: PrintBlackConfig;
  preflight?: PrintPreflightConfig;
}

interface CustomOutputProfile {
  name: string;           // Its description, or the file name.
  fileId: string;         // The stored .icc file.
  registryName?: string;  // The condition's ICC registry name (FOGRA51…), else 'Custom'.
  inkLimit?: number;
}
Property Type Default Description
standard 'none' | 'pdfx1a' | 'pdfx4' 'none' The PDF/X flavour of the file. 'pdfx1a' writes PDF/X-1a:2003: CMYK and grey only, no transparency, accepted by every printer. 'pdfx4' writes PDF/X-4: transparency and colour management kept, for current workflows. Either one separates every colour through the output profile, whatever pdfGeneration.colorSpace says.
outputProfile string 'fogra39' The printing condition CMYK is separated for: an id of the profile catalogue, or 'custom' for customProfile. An id the catalogue lacks, or 'custom' without a file, falls back to the default and gets a configuration warning.
customProfile CustomOutputProfile none A CMYK output profile you supply, the one your printer gives you (ECI's PSOcoated_v3.icc, for instance). Its bytes are stored out of band like a font; renderToPdf takes them as its outputProfile option. registryName is written as the output intent's condition identifier.
renderingIntent 'relative' | 'perceptual' 'relative' Relative colorimetric keeps the colours the press can print exact and clips the rest to the nearest printable one; perceptual compresses the whole range so out-of-gamut colours keep their relations.
blackPointCompensation boolean true With the relative intent, map the screen's black onto the darkest black the press prints, so the darkest tones keep their detail instead of filling in.
convertImages boolean true Separate RGB pictures into CMYK with the profile. PDF/X-1a always does. In PDF/X-4, false keeps them RGB, tagged sRGB through the pages' /DefaultRGB, for the printer's RIP to convert. CMYK and grey JPEGs are always embedded as they are.
inkLimit number the profile's The highest total of C+M+Y+K, in percent, the preflight accepts. Defaults to the limit the profile separates to (300 % for most offset conditions, 230 % for IFRA26 newsprint).
black PrintBlackConfig see Black K-only greys, overprint and rich black.
preflight PrintPreflightConfig see Preflight What the preflight checks and its thresholds.
print: {
  standard: 'pdfx4',
  outputProfile: 'fogra51',
  black: { richBlackColor: { c: 60, m: 40, y: 40, k: 100 } },
  preflight: { minImageResolution: 300, safeZone: { value: 5, unit: 'mm' } },
}

Output profiles

postext ships these CMYK output profiles in its icc/ folder (postext/icc/<id>.icc on any npm CDN, and /icc/<id>.icc on postext.dev). All of them are free of known copyright restrictions (CC0): the FOGRA, GRACoL, SWOP and newspaper profiles of colord, generated from the characterization data of each condition, and FOGRA51 and FOGRA52, built by postext with ArgyllCMS from Fogra's own data. ECI's profiles (ISO Coated v2, PSO Coated v3, PSO Uncoated v3) describe the same conditions but may not be redistributed; upload them as a custom profile if your printer asks for them.

Id Condition Registry name Ink limit
fogra39 Offset, coated paper (ISO Coated v2 condition) FOGRA39 300 %
fogra51 Offset, premium coated (PSO Coated v3 condition) FOGRA51 300 %
fogra52 Offset, wood-free uncoated (PSO Uncoated v3 condition) FOGRA52 300 %
fogra47 Offset, uncoated white (PSO Uncoated ISO 12647) FOGRA47 300 %
fogra29 Offset, uncoated white FOGRA29 300 %
fogra30 Offset, uncoated yellowish FOGRA30 340 %
fogra27 Offset, coated (ISO 12647-2:1996) FOGRA27 300 %
fogra28 Heatset web offset, glossy LWC FOGRA28 300 %
fogra45 Heatset web offset, improved LWC FOGRA45 300 %
fogra40 Heatset web offset, SC paper FOGRA40 340 %
gracol2006 GRACoL 2006, grade 1 coated CGATS TR 006 300 %
swop3 SWOP 2006, grade 3 coated CGATS TR 003 300 %
swop5 SWOP 2006, grade 5 coated CGATS TR 005 300 %
ifra26 Coldset newsprint (ISO 12647-3) IFRA26 230 %
snap2007 SNAP 2007 newsprint CGATS TR 002 320 %

renderToPdf reads the profile's bytes from its outputProfile option; without them it fetches the catalogue file from profileBaseUrl (by default https://cdn.jsdelivr.net/npm/postext/icc/). A PDF/X render whose profile cannot be loaded fails; a plain CMYK render falls back to the textbook formula with an outputProfileUnavailable warning.

import { readFile } from 'node:fs/promises';
import { renderToPdf } from 'postext-pdf';

const pdf = await renderToPdf(doc, {
  fontProvider,
  print: { standard: 'pdfx1a', outputProfile: 'fogra39' },
  outputProfile: await readFile('node_modules/postext/icc/fogra39.icc'),
});

PDF/X-1a and PDF/X-4

Both standards write:

  • the output intent (GTS_PDFX) naming the printing condition and embedding the destination profile;
  • the identification in the Info dictionary (GTS_PDFXVersion, /Trapped /False, the title and dates) and in the XMP metadata (pdfxid:GTSPDFXVersion, the document and version ids), merged with the PDF/UA identification when the file is tagged;
  • a TrimBox and a BleedBox on every page (the whole page when there are no cut lines);
  • the trailer /ID;
  • every colour in DeviceCMYK (or grey) through the profile, and the crop marks in registration colour;
  • no link annotations: a file for the press carries none inside its bleed box, so the links of the screen PDF are left out (bookmarks stay).

PDF/X-1a:2003 is PDF 1.4 without object streams and has no transparency: a translucent colour is set as it would print over the paper, a picture's alpha is flattened over white, and the debug page negative is left out (with a pageNegativeIgnored warning). PDF/X-4 is PDF 1.6: transparency stays, each page gets a transparency group blending in CMYK, and RGB pictures kept by convertImages: false are tagged sRGB through /DefaultRGB.

A PDF print master (svg.pdfFileId) is embedded as it is, so its colours, fonts and transparency are its own; the preflight reports what it brings in.

Black

interface PrintBlackConfig {
  kOnlyNeutrals?: boolean;      // Greys and black in black ink only.
  overprint?: boolean;          // 100 % K overprints.
  richBlack?: boolean;          // Large black areas in rich black.
  richBlackColor?: CmykPercent; // { c, m, y, k } in percent.
  richBlackMinSize?: Dimension; // The smaller side an area needs.
}
Property Type Default Description
kOnlyNeutrals boolean true A neutral colour (#000000, #808080…) prints with black ink alone, its K picked so the lightness matches, never as a four-colour grey that shifts with registration. Pictures keep the profile's own black generation.
overprint boolean true Whatever paints in 100 % K alone (black text, rules, strokes, small black shapes) overprints (op/OP with OPM 1), so a plate shifting on press never opens a white edge round it. Everything else knocks out; pictures and shadings never overprint.
richBlack boolean true A black fill whose smaller side reaches richBlackMinSize (a background, a band, a box) prints in richBlackColor and knocks out, so it looks deep instead of dark grey. Text never turns rich.
richBlackColor CmykPercent {�0�} The rich-black recipe, in percent. Keep its total under the ink limit; the preflight checks it.
richBlackMinSize Dimension 6mm The smaller side a black area needs to print in rich black.

Colours authored in CMYK

A colour written in CMYK keeps its exact values: ColorValue.cmyk (percent) is set as it is in a print render, and hex is its screen rendering. A palette entry authored in CMYK covers every colour linked to it.

colorPalette: [
  { id: 'brand', name: 'Brand', value: { hex: '#00a0e3', model: 'cmyk', cmyk: { c: 100, m: 0, y: 0, k: 0 } } },
],

Preflight

interface PrintPreflightConfig {
  enabled?: boolean;
  minImageResolution?: number;       // ppi at the printed size.
  criticalImageResolution?: number;
  minRuleWidth?: Dimension;
  smallTextSize?: Dimension;
  safeZone?: Dimension;
  bleedSnap?: Dimension;
  checkFonts?: boolean;
}
Property Type Default Description
enabled boolean true Run the checks.
minImageResolution number 300 A placed bitmap under this many pixels per inch at its printed size, crop included, gets a warning: figures, table-cell pictures, design images, comic panels. A bitmap with no resolution of its own prints at page.dpi at its natural size, so a page laid out at 150 dpi prints every such picture at 150 ppi; one with a resolution (bitmap.resolution, layout.bitmapResolution) prints at that resolution at its natural size.
criticalImageResolution number 150 Under this the warning is critical. Never above minImageResolution.
minRuleWidth Dimension 0.25pt Rules, borders, column rules, table rules and comic panel borders thinner than this.
smallTextSize Dimension 9pt Text under this size set in more than one ink (a process colour, rich black): it blurs when the plates shift. Black text, K only, never counts.
safeZone Dimension 5mm Text closer than this to the trim, where the guillotine may cut it.
bleedSnap Dimension 3mm A box or picture that stops this close to the trim without reaching the bleed: run it into the bleed or pull it in.
checkFonts boolean true Report fonts a placed PDF does not embed (the Sandbox inspects each print master with inspectPrintMaster). Postext embeds every font it sets.

preflightDocument(doc, options) runs the checks on a laid-out document and returns a list of issues, each with a kind, a severity ('critical', 'warning' or 'info'), the book-absolute pageIndex, the offending rect (page px) and, when the element has one, its source range. The kinds are lowImageResolution, declaredPixelsMismatch, rgbImage, thinRule, smallProcessText, inkLimit, safeZone and nearTrim. Without a transform a neutral counts one ink and any other colour three, and coverage is not checked; with one, the counts and the coverage are exact. The resolution of a picture is worked out from the pixels its resource declares, or from the file's own when imageSize(fileId) gives them (bitmapInfo on the bytes, or the decoded image): a declaration more than a pixel away from the file is reported once as declaredPixelsMismatch, a warning when it claims more pixels than the file has. placedImageResolutions(doc, { resources, imageSize }) lists every placed bitmap with its effective ppi, whether the preflight is on or not.

import { bitmapInfo, outputTransform, parseIccProfile, preflightDocument, resolvePrintConfig } from 'postext';
import { inspectPrintMaster } from 'postext-pdf';

const print = resolvePrintConfig(config.print);
const transform = outputTransform(parseIccProfile(fogra51Bytes), { intent: print.renderingIntent });
const issues = preflightDocument(doc, {
  print,
  transform,
  resources,                                    // pixel sizes of the bitmaps
  imageSize: (fileId) => bitmapInfo(bytesOf(fileId)),  // the files' real pixels
  imageColor: (fileId) => colourOf(fileId),     // 'rgb' | 'cmyk' | 'gray', from the file
});
if (issues.some((i) => i.severity === 'critical')) process.exit(1);

const master = await inspectPrintMaster(masterBytes);
// => { nonEmbeddedFonts: ['Helvetica'], rgb: true, transparency: false }

Print preview

A canvas page can be painted as it will print. createPrintPreview(transform, print, { paper, dpi, grayscale }) builds the soft proof of a setup: every pixel is separated through the profile (K-only neutrals, as in the PDF) and shown back on screen, on the paper's own white when paper is true; black areas large enough for rich black show as rich black. With grayscale, the proof is a grayscale print — a PDF forced to grayscale prints each colour's luma in black ink alone, without rich black. Hand it to renderPageToCanvas as printPreview. For a working view, not a proof, guides adds the trim, bleed and safe-zone lines and marksFor outlines areas of a page (the preflight's rects); the Sandbox leaves both out, so its preview shows only the print. postext-folio takes the same object as printPreview; with paper: true, give the book a neutral paper shade meanwhile (folio.paper.shade), or the paper is tinted twice.

import { createPrintPreview, renderPageToCanvas } from 'postext';

const preview = createPrintPreview(transform, print, { paper: true, dpi: doc.config.page.dpi });
renderPageToCanvas(page, doc, canvas, {
  printPreview: { ...preview, guides: { safeZonePx: 59 }, marksFor: () => issues.map((i) => i.rect!).filter(Boolean) },
});

Colour engine

The colour management postext uses is exported for your own tools. It reads ICC v2 and v4 profiles (matrix/TRC and the mft1, mft2, mAB, mBA lookup tables) in plain TypeScript, with no WebAssembly.

  • parseIccProfile(bytes) reads a profile; deviceChannels(profile) gives its channel count.
  • outputTransform(profile, { intent, blackPointCompensation, preserveNeutrals }) returns fromRgb(r, g, b) (sRGB 0..1 → CMYK 0..1), toLab(cmyk, paper?) and proof(cmyk, paper?) (CMYK → screen sRGB).
  • cmykToLab, labToCmyk, srgbToLab, labToSrgb, deltaE and totalAreaCoverage are the single conversions; buildRgbLut / sampleRgbLut make and read dense lookups for pixel work.
  • OUTPUT_PROFILES, outputProfileInfo(id) and loadOutputProfile(id, baseUrl?) give the catalogue; srgbProfileBytes() writes the sRGB profile PDF/X-4 tags RGB with; authoredCmykColors(config) lists the colours a config authored in CMYK.

The resolver and stripper match the other sections: resolvePrintConfig, stripPrintDefaults and profileInkLimit(config) (the limit of the profile a config names, before any inkLimit override), with DEFAULT_PRINT_CONFIG, DEFAULT_PRINT_BLACK_CONFIG, DEFAULT_PRINT_PREFLIGHT_CONFIG and DEFAULT_RICH_BLACK.

Contrast check (config)

The contrast property sets the check that says whether the text of a book can be read: every text colour against the ground it is set on. Layout ignores it, so changing it never moves a line. contrastDocument reads it when it checks a laid-out document, the Sandbox lists the findings in Checks and shows the ratio beside each colour field, and postext check prints them.

interface ContrastConfig {
  enabled?: boolean;
  level?: 'AA' | 'AAA';
  mode?: 'auto' | 'screen' | 'print' | 'both';
  reversedText?: {
    enabled?: boolean;
    minSize?: Dimension;
    minFineSize?: Dimension;
    lightWeight?: number;
  };
}
Property Type Default Description
enabled boolean true Run the check.
level 'AA' | 'AAA' 'AA' The WCAG 2 level the ratios are held to. See Thresholds.
mode string 'auto' The rendition read: 'screen', 'print', 'both', or 'auto', which reads the screen always and print as well for a book set up for print (a PDF/X standard in print.standard, a CMYK PDF in pdfGeneration, or crop marks in page.cutLines).
reversedText.enabled boolean true Report reversed text in the print check.
reversedText.minSize Dimension 8pt Reversed text under this size is reported.
reversedText.minFineSize Dimension 12pt The same for italics and light weights, whose strokes are thinner.
reversedText.lightWeight number 300 A weight at or under this one counts as light.

A level or a mode written any other way reads as the default and is reported as an unknown setting value (see Configuration warnings).

Thresholds

The ratio a text must reach follows its size and weight, as in WCAG 2.

Level Text Large text
AA 4.5:1 3:1
AAA 7:1 4.5:1

Large text is 18 pt and over, or 14 pt and over at a weight of 700 or more. The sizes are the printed ones, in points. A superscript or a subscript is judged at the size of the text it belongs to.

Screen and print

Postext sets the same pages for a screen and for a press, and a colour does not read the same on both.

  • Screen is the WCAG 2 contrast ratio of the sRGB colours, (L1 + 0.05) / (L2 + 0.05) over their relative luminances: what the canvas, the HTML viewer, an EPUB and an RGB PDF show.
  • Print is the same formula over the luminance each colour has on paper. The colour is separated through the job's output profile (one authored in CMYK keeps its values), the separation is read back through the profile, and its luminance is taken relative to the bare paper, which is the white of a printed page as the display's white is the white of a screen. Ink is never a perfect black: solid black on white comes to about 14:1 on coated paper (FOGRA39, FOGRA51), 8:1 on uncoated paper (FOGRA47, FOGRA52) and 5:1 on newsprint (IFRA26, SNAP), where a screen gives 21:1. A grey that passes on screen can fail on paper: #767676 on white is 4.54:1 on screen and about 3.3:1 on coated paper.

The print ratio needs the transform of the output profile (options.transform, built with outputTransform). Without it only the reversed text is checked for print.

Reversed text

Light text knocked out of an inked ground fills in on press when it is small or its strokes are thin: the ink of the ground spreads into the letters, and a ground built of several inks also depends on the plates staying in register. The ratio does not show it, since white on dark blue is over 10:1. The print check reports such text on its own (reversedText) when it is under minSize, or under minFineSize while italic or at a weight of lightWeight or less. Each finding says how many of the four inks the ground is built of.

What is read

The check reads the lines of every block with their inline colours (bold, italic, references, chips), list markers, drop caps, captions, notes and table cells, and the text of design slots: running heads, openers, heading designs, box titles. The ground is the topmost fill under the text: a design box, a callout, a caption bar, a table cell, a chip, or else the page colour (page.backgroundColor, or the paper a part's palette gives its pages). A translucent colour is blended with what is under it. Text over a picture is skipped, since its ground is not one colour, and so are comic lettering, ruby readings and formulas.

Running the check

contrastDocument(doc, options) returns the findings of a laid-out document, lowest ratio first. Each one has the book-absolute pageIndex, the rect on the sheet (page px) and the source range of its first occurrence, the two colours as painted (color, background), the smallest sizePt it is set at with its weight and italic, a role ('body', 'heading', 'caption', 'table', 'runningHead', 'design', 'chip' or 'marker'), the first run's text and the count of runs set so. A pair is reported once per threshold, however many lines use it.

  • lowContrast adds level, large, required, the ratio in each rendition checked (screen, print) and failing, the renditions under required.
  • reversedText adds reasons ('small', 'italic', 'light'), minimumPt and inks.

The options are contrast (the resolved settings, when they are not the document's), media (the renditions, whatever mode says) and transform.

import { contrastDocument, outputTransform, parseIccProfile } from 'postext';

const transform = outputTransform(parseIccProfile(fogra51Bytes));
for (const issue of contrastDocument(doc, { transform })) {
  if (issue.kind === 'lowContrast') {
    console.log(`${issue.color} on ${issue.background}: ${issue.screen}:1, ${issue.level} asks for ${issue.required}:1`);
  }
}

contrastPairs(doc, options) returns every text and ground pair the document sets, passing or not, with its screen ratio, its print ratio when the transform is given, the ratio required and the colours as the configuration writes them (textColor, groundColor): what a design tool shows beside a colour field. It runs whatever enabled says.

The measures are exported for your own tools:

  • contrastRatio(a, b) and relativeLuminance(color) give the WCAG 2 ratio and luminance of sRGB colours.
  • isLargeText(sizePt, weight) and contrastThreshold(level, large) give the threshold of a size and weight.
  • blendColors(top, under) gives a translucent colour over another, as #rrggbb.
  • printContrastRatio(a, b, context), printLuminance(color, context) and printSeparation(color, context) give the same on paper. The context holds the transform and, as authored, the result of authoredCmykColors(config).
  • contrastMedia(doc.config, contrast) gives the renditions mode resolves to for a document.

The resolver and stripper match the other sections: resolveContrastConfig and stripContrastDefaults, with DEFAULT_CONTRAST_CONFIG, DEFAULT_CONTRAST_REVERSED_TEXT_CONFIG, CONTRAST_LEVELS and CONTRAST_MODES.

Content check (config)

The checks property holds the checks that read the configuration itself. It has one today, contentInConfig, which warns when a design keeps the book's text. A running head, a heading design, a part page and a contents part row are form: they hold placeholders ({chapterTitle}, {titleText}, {number}) and at most a word or two of their own, such as Chapter before {chapterNumber}. Text that is part of the book belongs in the Markdown, where the contents, the search, translations, the tagged PDF and the EPUB see it. Layout ignores the check, so changing it never moves a line.

interface ChecksConfig {
  contentInConfig?: {
    enabled?: boolean;
    allow?: string[];
    maxWords?: number;
    maxChars?: number;
  };
}
Property Type Default Description
contentInConfig.enabled boolean true Run the check.
contentInConfig.allow string[] [] Phrases a design may repeat at any length: the book's title, the publisher's name. They are compared without case, spacing or the punctuation around words, and a phrase found inside a longer text is taken out before the rest is counted.
contentInConfig.maxWords number 2 Most words a design text may hold beyond its placeholders.
contentInConfig.maxChars number 24 Most letters and digits a design text may hold beyond its placeholders. A Chinese, Japanese, Korean or Thai character counts as three, so about eight of them pass.

How a design text is read

  • Its literal text is its content with every placeholder taken out ({name}, {attr.key}, {firstMark.key}), the {{ and }} escapes read as braces and \n as a line break.
  • A text with no letter or digit (a row of leader dots, an ornament, a ·) is never content.
  • A word is what stands between spaces and holds a letter or a digit: a lone · or | is no word, and an address such as postext.dev is one.
  • The phrases of allow are taken out first, and so are the title, subtitle, author and publisher of the document's metadata (its front matter), which a running head often repeats.
  • What is left is content when it has more than maxWords words or more than maxChars letters and digits.

Every design slot is read: header, footer, each heading level's advancedDesign.slot, each heading style's advancedDesign.slot, header and footer, parts.design, parts.versoDesign, toc.parts.design, and the same under htmlViewer.overrides. configDesignSlots(config) lists them with their paths, and designTextLiteral(content) returns the literal text of a template.

The warning

collectConfigWarnings (and VDTDocument.configWarnings) gives one contentInConfig warning per setting that holds content: a heading style (its id in ownerId), a heading level, the header, a part design. path names the setting, count the design texts at fault, chars their letters and digits, elements the path of each (the first 20) and value the start of the first text (40 characters at most). Pass the document's metadata as the second argument to allow its title and author; a build does it on its own.

import { collectConfigWarnings } from 'postext';

collectConfigWarnings(config, { metadata: { title: 'The Third-Floor Detectives' } });
// [{ kind: 'contentInConfig', path: 'headingStyles[3]', ownerId: 'lesson-4',
//    value: 'Look at the picture and answer the questi…', used: '',
//    count: 30, chars: 1017, elements: ['headingStyles[3].header.elements[2]', …] }]

Fix: move the text into the Markdown, as a heading, a callout or a paragraph of the chapter it belongs to, and keep only placeholders in the design. A value that changes per chapter can travel on the heading line and be read with {attr.key}: # Maps {kicker="Lesson 4"} and {attr.kicker} in the design. A phrase that is form, such as the publisher's name in a running head, goes in allow.

The Sandbox lists the warning in Checks and sets the check under Design → Advanced → Content in the configuration; postext check prints it.

Folio viewer (config)

The folio property sets how the Folio viewer (postext-folio) presents the printed book in 3D: the angle of the view, the paper, the binding, the surface the book lies on and the light. Layout ignores it, and so do the canvas, HTML and PDF output. buildDocument carries the resolved settings in the VDT as doc.config.folio when the config sets any, so a document without them keeps its layout hash.

interface FolioConfig {
  tilt?: number;                  // Degrees from straight above, 0–70.
  yaw?: number;                   // Degrees round the book, −180–180.
  paper?: {
    type?: 'uncoated' | 'bookWove' | 'coatedMatte' | 'coatedSilk' | 'coatedGloss'
         | 'bible' | 'newsprint' | 'cardStock' | 'board';
    grammage?: number;            // g/m²
    bulk?: number;                // cm³/g; caliper µm = grammage × bulk
    finish?: 'auto' | 'uncoated' | 'matte' | 'silk' | 'gloss';
    texture?: 'auto' | 'smooth' | 'vellum' | 'wove' | 'laid' | 'linen' | 'felt';
    textureStrength?: number;     // 0–2
    shade?: ColorValue;
    showThrough?: boolean;
  };
  binding?: {
    type?: 'hardcover' | 'paperback' | 'sewn' | 'layflat' | 'saddleStitch' | 'folded';
    cover?: 'case' | 'pages';
    coverMaterial?: 'auto' | 'cloth' | 'paper' | 'leather';
    coverColor?: ColorValue;
    spineImage?: string;          // resource id
  };
  surface?: {
    type?: 'oak' | 'walnut' | 'linen' | 'felt' | 'leather' | 'marble' | 'plain' | 'none';
    color?: ColorValue;
  };
  lighting?: {
    environment?: 'studio' | 'daylight' | 'lamp' | 'overcast' | 'night';
    intensity?: number;           // 0.25–2
    shadows?: boolean;
  };
}
Property Type Default Description
tilt number 22 Angle of the view away from straight above, in degrees, clamped to 0–70. At 0 the open book is seen flat from overhead; a larger angle brings the foot of the pages closer and shows the depth of the book block.
yaw number 0 How far the view is turned round the book, in degrees, brought into −180–180. At 0 the book is seen from the foot of its pages; a positive angle brings the eye round to its right, a negative one to its left. With tilt it is the view the viewer opens on and the one resetView() eases back to.
paper.type FolioPaperType 'uncoated'; 'newsprint' on a newspaper trim The paper stock. It supplies the defaults of the five fields below (see the table of stocks). cardStock is cover card; board is rigid board, as in a board book, and its leaves turn without bending.
paper.grammage number the stock's Weight in grams per square meter, 20–2500. Heavier paper is thicker, stiffer and more opaque: a leaf bends in a wider curve and shows less of the reverse.
paper.bulk number the stock's Thickness per unit of weight, in cm³/g, 0.5–3. The caliper of a sheet in micrometers is grammage × bulk, and the thickness of the book block follows from it and the page count.
paper.finish FolioPaperFinish 'auto' Uncoated (fibre, no sheen), or coated and calendered to matte, silk (a soft sheen) or gloss. A gloss page mirrors the leaf turning over it in Folio. 'auto' takes the stock's.
paper.texture FolioPaperTexture 'auto' The relief of the surface: smooth (calendered), vellum (a fine tooth), wove (the even texture of most book papers, formed on a woven wire), laid (close laid lines crossed by wider chain lines, left by a dandy roll), linen (an embossed cross-weave), felt (irregular felt marks). 'auto' takes the stock's.
paper.textureStrength number 1 How strongly the texture shows in the light, 0–2.
paper.shade ColorValue the stock's The colour of the paper before printing (white, natural, cream). The pages are printed on it.
paper.showThrough boolean true The reverse of a page shows faintly through thin paper. After bible paper, newsprint shows it most: its ink soaks into the sheet.
binding.type FolioBindingType 'hardcover'; 'folded' on a newspaper trim hardcover: case bound, boards slightly larger than the pages. paperback: perfect bound (milled and glued), opens less flat. sewn: a softcover with sewn sections. layflat: opens flat, with no dip at the gutter. saddleStitch: folded sheets stapled through the fold, as a magazine or a booklet; no flat spine. folded (since postext 1.18): a newspaper, sheets folded once and laid one inside the other with nothing holding them; no staples, no spine and no cover board, and the first page is the front. A :::paper run with a shade prints a section, the business pages say, on salmon newsprint.
binding.cover FolioCoverSource 'case' The covers. 'case' draws a case round the pages. 'pages' takes the book's first page as the front board and its last page, when it is a verso, as the back board: the book lies closed until the cover is turned, the boards turn stiff (on a saddle stitch the cover is a sheet a little heavier than the pages and turns as they do), and no case is drawn.
binding.coverMaterial FolioCoverMaterial 'auto' 'auto' is cloth on a hardcover and card ('paper') on the other bindings.
binding.coverColor ColorValue dark blue (#2c3e57) The colour of the cover material.
binding.spineImage string none The id of a bitmap or SVG resource printed on the spine: the spine as seen with the book standing, head up and the front cover to the right. It is fitted to cover the spine, centred. Ignored on a saddle stitch and on a folded binding.
surface.type FolioSurfaceType 'oak' What the book lies on. 'none' leaves the host's background.
surface.color ColorValue none Tints the surface; on 'plain' it is the surface's colour.
lighting.environment FolioEnvironment 'studio' The surroundings reflected by coated and gloss paper, paired with the key light that casts the shadows.
lighting.intensity number 1 Exposure, 0.25–2.
lighting.shadows boolean true Shadows cast by the key light.

The stocks and the values they supply (FOLIO_PAPER_STOCKS), typical of mill data sheets:

Stock Grammage Bulk Caliper Finish Texture Shade
uncoated (woodfree offset) 90 g/m² 1.25 113 µm uncoated wove #fcfbf8
bookWove (cream, high bulk) 80 g/m² 1.6 128 µm uncoated wove #f6efdc
coatedMatte 115 g/m² 1.0 115 µm matte smooth #fdfdfc
coatedSilk 115 g/m² 0.9 104 µm silk smooth #ffffff
coatedGloss 115 g/m² 0.8 92 µm gloss smooth #ffffff
bible 40 g/m² 1.1 44 µm uncoated vellum #f9f6ee
newsprint 48 g/m² 1.5 72 µm uncoated wove #ebe7dc
cardStock 250 g/m² 1.2 300 µm uncoated vellum #fbfaf6
board 1250 g/m² 1.6 2000 µm silk smooth #ffffff

A novel on cream book paper, bound as a paperback, on a walnut desk under a reading lamp:

folio: {
  paper: { type: 'bookWove' },
  binding: { type: 'paperback', coverColor: { hex: '#8a2b1f', model: 'hex' } },
  surface: { type: 'walnut' },
  lighting: { environment: 'lamp' },
}

A page on a newspaper trim (page.sizePreset 'broadsheet', 'berliner', 'tabloid' or 'compact') is shown as a newspaper when the config names no stock and no binding: newsprint paper and a folded binding (since postext 1.18). A stock or a binding the config names is kept, so paper: { type: 'uncoated' } prints a tabloid on offset paper. Paper fields set without a stock (a grammage, a shade) apply to the newsprint. The resolver and the stripper take the trim as a second argument, and folioForTrim(folio, sizePreset) writes the two defaults into the config:

resolveFolioConfig({ tilt: 30 }, 'tabloid');
// => { tilt: 30, paper: { type: 'newsprint', grammage: 48, bulk: 1.5, … }, binding: { type: 'folded', coverMaterial: 'paper', … }, … }

stripFolioDefaults({ paper: { type: 'newsprint' }, binding: { type: 'folded' } }, 'tabloid');
// => undefined

The colours follow palette links like any other colour of the config (paletteId). Resolver and stripper match the other sections; the stripper drops paper values equal to the chosen stock's:

import { FOLIO_PAPER_STOCKS, DEFAULT_FOLIO_CONFIG, resolveFolioConfig, stripFolioDefaults } from 'postext';

resolveFolioConfig({ paper: { type: 'bible' } }).paper;
// => { type: 'bible', grammage: 40, bulk: 1.1, finish: 'uncoated', texture: 'vellum', textureStrength: 1, shade: { hex: '#f9f6ee', … }, showThrough: true }

stripFolioDefaults({ paper: { type: 'bible', grammage: 40 } });
// => { paper: { type: 'bible' } }

In the sandbox these settings are the Folio group of the Design panel (Design → Folio → Folio viewer (3D)), and the Folio tab shows them as you change them, without laying the book out again. See A 3D book for the viewer itself, and Document format › :::paper for a run of pages on another stock.

Debug

The debug property groups two kinds of authoring aids: visual overlays that keep the source text and the rendered layout in sync, and a set of warnings that surface typographic or structural problems in the Sandbox's Checks panel. Neither affects exported output.

Property Type Description
cursorSync SyncIndicatorConfig Caret mirrored into the rendered layout — see Visual overlays.
selectionSync SyncIndicatorConfig Source selection highlighted on the page — see Visual overlays.
looseLineHighlight LooseLineHighlightConfig Overlay on loose justified lines — see Visual overlays.
pageNegative { enabled: boolean } High-contrast negative of the page — see Visual overlays.
warnings WarningsToggleConfig One boolean per kind of authoring warning shown in the editor — see Warnings.

Visual overlays

Property Type Default Description
cursorSync.enabled boolean true Shows a caret in the rendered layout mirroring the source cursor position.
cursorSync.color ColorValue #2563eb Colour of that caret.
selectionSync.enabled boolean true Highlights the rendered range matching the source selection.
selectionSync.color ColorValue #fde04780 Colour of the highlight — a translucent yellow by default.
looseLineHighlight.enabled boolean false Paints an overlay on justified lines whose word spacing exceeds threshold times the normal space width.
looseLineHighlight.color ColorValue #ff000040 Colour of that overlay.
looseLineHighlight.threshold number 3 Multiplier of the normal space width above which a justified line counts as loose. The looseLines warning uses the same threshold. A justified line whose spaces would stretch past 3× is set ragged, so at the default the overlay and the warning find almost nothing in running text; lower it (1.5 or 2) to see lines that are loose but still justified.
pageNegative.enabled boolean false Renders a high-contrast negative overlay over the page — useful for visually auditing the overall shape of a spread (text density, column balance, whitespace) at a glance, without being distracted by glyph detail.

Each SyncIndicatorConfig is { enabled: boolean; color?: ColorValue }. LooseLineHighlightConfig is { enabled: boolean; color?: ColorValue; threshold?: number }. pageNegative is a minimal { enabled: boolean } toggle.

debug: {
  cursorSync: { enabled: true, color: { hex: '#ff0066', model: 'hex' } },
  selectionSync: { enabled: false, color: { hex: '#fde04780', model: 'hex' } },
  looseLineHighlight: { enabled: true, color: { hex: '#ff000040', model: 'hex' }, threshold: 3 },
  pageNegative: { enabled: true },
}

These overlays are drawn by the Sandbox over its Canvas preview. They are not part of the page: renderPage, the HTML output and the PDF never paint them.

Loose lines in your own canvas

The engine exports the loose-line highlight as two helpers, for a page you paint yourself:

import { buildDocument, renderPageToCanvas, drawLooseLines, findLooseLines } from 'postext';

const doc = buildDocument(content, config);
const canvas = document.querySelector('canvas')!;
renderPageToCanvas(doc.pages[0], doc, canvas, { scale: 0.5 });
drawLooseLines(canvas.getContext('2d')!, doc.pages[0], doc, { threshold: 2.5 });

// The same lines as data: a report, an SVG overlay, a count per page.
for (const { ratio, line, block } of findLooseLines(doc, { threshold: 2.5 })) {
  console.log(`page ${block.pageIndex + 1}: ${ratio.toFixed(2)}× — ${line.text}`);
}
  • findLooseLines(doc, { threshold?, pageIndex? }) returns every justified line whose word spacing exceeds threshold, in reading order: { block, line, ratio, x, y, width, height }. ratio is lineWordSpacing(line): the spacing as minWordSpacing and maxWordSpacing bound it (justifiedSpaceLimitRatio when the line holds spaces of another face, else justifiedSpaceRatio), so a line set at the limit with a bold space in it is not reported at a threshold equal to maxWordSpacing. The rectangle, in page pixels, is the band the highlight covers: the block's full width across the line. These are the lines the Sandbox highlights and reports as looseLine in its Checks panel.
  • drawLooseLines(ctx, page, doc, { threshold?, color? }) fills those bands for one page and returns the lines it painted. It draws in page pixels under the context's current transform, so call it right after renderPage or renderPageToCanvas on the same canvas: both leave the context scaled to the page. color is any canvas fill style.
  • Defaults. Both helpers use the default threshold (3) and colour (#ff000040), not the document's debug.looseLineHighlight: that setting belongs to the Sandbox. To follow a configuration, pass resolveDebugConfig(config.debug).looseLineHighlight.threshold and .color.hex.

Warnings

debug.warnings controls which authoring issues appear in the Sandbox's Checks panel (edited in Design → Advanced → Warnings). Each key is an independent boolean toggle; set one to false to silence that specific warning without disabling the others.

These toggles filter the Sandbox's panel only. The warnings the engine itself records — boxes that overflow their column in doc.warnings; unknown resource ids, directives, embeds and style ids, and irregular table grids in doc.contentWarnings — are there whatever the toggles say, and the renderers report images they paint as placeholders; see Warnings in the document.

interface WarningsToggleConfig {
  missingFont?: boolean;
  looseLines?: boolean;
  headingHierarchy?: boolean;
  consecutiveHeadings?: boolean;
  listAfterHeading?: boolean;
  designIssues?: boolean;
}
Property Type Default Description
missingFont boolean true Report when a font referenced by the configuration failed to load in the browser. Catches typos in fontFamily and missing @fontsource/... packages early, before they show up as silent fallback-font substitutions in the rendered output.
looseLines boolean true Report justified lines whose word spacing exceeds debug.looseLineHighlight.threshold. Pairs with the overlay: the warning enumerates them in the panel, the overlay shows them in place.
headingHierarchy boolean true Report heading levels that skip a rank — e.g. an H1 followed directly by an H3. Structural heading gaps usually indicate either a typo in the heading depth or a misunderstanding of the document's outline.
consecutiveHeadings boolean false Report when a heading is immediately followed by another heading with no paragraph or list between them. Off by default because stacked headings are legitimate in many templates (title + subtitle, chapter + epigraph); turn on for manuscripts where every heading is supposed to introduce prose.
listAfterHeading boolean false Report when a list starts immediately after a heading without an introductory paragraph. Off by default because reference material routinely does this; turn on for narrative writing where every list should be framed by prose.
designIssues boolean true Report integrity problems in design slots — page headers, footers, the part opener and the blank verso after it, the part rows of the contents, heading advanced-design slots, and each heading style's design and section running heads. Covers cyclic anchor chains and dangling anchor references (an element anchored to a #id that no longer exists), a page-span heading whose breakBefore is disabled, and an enabled advanced design whose elements never render {titleText}.
debug: {
  warnings: {
    missingFont: true,
    looseLines: true,
    headingHierarchy: true,
    consecutiveHeadings: true,
    listAfterHeading: false,
    designIssues: true,
  },
}

Besides these, the panel always lists the warnings the layout itself raises (VDTDocument.warnings), such as a callout that overflows its column (calloutOverflow), and the configuration values the engine replaced (VDTDocument.configWarnings, or collectConfigWarnings(config); see Configuration warnings below).

Configuration warnings

Eight mistakes in the configuration itself are never silent, and no toggle hides them. The engine does not fail on any of them; it substitutes a value, or leaves the setting out, and says so:

  • Unknown number format — an ordered-list numberFormat, a page.pageNumbering.format or a resource type's counterFormat that is none of the numbering format spellings. It numbers in decimal.
  • Font stack in a font family — a fontFamily (or any …FontFamily) that holds a CSS font stack. The text is set in the stack's first family (see One family per fontFamily).
  • Side column with no room — a 'oneAndHalf' layout's sideColumnPercent (the document's, or a heading style's own layout) that would leave one of the columns under 1% of the content width, or that is not a number. The columns are cut at the nearest value both can take, and used names it (sideColumnPercentClamped; see the 'oneAndHalf' layout).
  • Column count out of range — a 'multiple' layout's columnCount (the document's, or a heading style's own layout) that is not a whole number from 3 to 8. The page is cut into the nearest count that is (3 for a value that is not a number), and used names it (columnCountClamped; see the 'multiple' layout).
  • Character grid too large — a cjk.grid with more characters per line or lines per page than the margins leave room for. The grid is set with the most that fit, and used names the number (cjkGridClamped; see Character grid).
  • Unknown heading setting — a key that headings, headings.balancing, a heading level, a heading style or a paragraph style does not have: a misspelt letterSpacng, a tracking borrowed from another tool, a level on a heading style, a fontStyle: 'italic' on a paragraph style (which takes italic: true). A tab stop is checked the same way (a leaders for leader). The engine ignores it (up to postext 1.4 it did so without a word). value is the key, used is empty, and suggestion names the setting it is closest to, when one is a letter or two away or differs only in case (unknownConfigKey).
  • Unknown setting value — a setting that takes one of a few words holding another, such as direction: 'right' (it takes auto, ltr or rtl). The engine reads the default instead, and used names what that came to: for direction, the direction of the document language (unknownConfigValue). A tab stop's align that is none of its four words is read as 'start', and a position that is no length, 'end' or percentage leaves the stop out (used is 'none'). The words of the comics settings are checked too (Comics › Comic warnings): used is the value the setting resolved to (a balloon style's own default, for a built-in style), and suggestion names the word the value is closest to, when one is close. The level and mode of the contrast check are checked the same way: used is 'AA' or 'auto'.
  • Line numbers in vertical text — lineNumbers.enabled: true in a document set vertically (layout.writingMode: 'vertical-rl'). Vertical pages get no line numbers, and used is false (lineNumbersUnsupported; see Line numbers).
  • Text wrap in vertical text — a resource type's defaultPlacement.wrap in a document set vertically. Vertical pages set no text beside a figure, and used is none (wrapUnsupported; see Document format › Text wrap). A wrap that names no side is an unknown setting value.
  • Page templates and label styles — a problem in pageTemplates or labelStyles: an id or a name used twice, extends, anchors or chained frames that loop, a style, frame or template that does not exist, a minimum above its maximum, an anchor's label style that labelStyles lacks (checked when the resources are given: collectConfigWarnings(config, { resources })). Their path is a JSON pointer (/pageTemplates/3/frames/2/style); each kind is listed with its cause and fix in Page template warnings.
  • Content in a design — a design text element whose text, beyond its placeholders, is longer than a short label: the book's text kept in the configuration. One warning per setting (a heading style, the header, a part design), with the path of each text (contentInConfig; see Content check). Unlike the others, it can be turned off: checks.contentInConfig.enabled: false.

The Sandbox lists them in the Checks panel with the path of the setting. In code, buildDocument puts them on the document as configWarnings (absent when the configuration is clean), and collectConfigWarnings(config) returns them without laying anything out:

import { buildDocument, collectConfigWarnings } from 'postext';

// Plain JavaScript: in TypeScript, 'roman' does not type-check to begin with.
const config = { bodyText: { fontFamily: 'EB Garamond, serif' }, orderedLists: { numberFormat: 'roman' } };
const doc = buildDocument({ markdown }, config);
doc.configWarnings;
// [{ kind: 'fontFamilyStack', path: 'bodyText.fontFamily', value: 'EB Garamond, serif', used: 'EB Garamond' },
//  { kind: 'unknownNumberFormat', path: 'orderedLists.numberFormat', value: 'roman', used: 'arabic' }]
collectConfigWarnings(config); // the same list

Every nested partial configuration is checked too: heading styles, the lists inside parts, htmlViewer.overrides, design elements.

formatWarning (see Warnings in the document) describes them too, with the path of the setting first — bodyText.fontFamily: font stack "EB Garamond, serif" — set in "EB Garamond", headingStyles[0].letterSpacng: unknown setting "letterSpacng" — ignored (did you mean "letterSpacing"?) — so a host can log all three lists a build returns in one loop.


← Configuration: composed pages · Programmatic usage: buildDocument, warnings, fonts and workers →

Clone this wiki locally