Skip to main content

M12 — HTML → PDF engine

State: to do — Depends on: M06, M07, M08, M09, M10, M11 — XL, delivered as M12.1 to M12.7 — Resources per ADR 38; forward references per ADR 39; shaping by HarfBuzz, bidirectional analysis and line breaking ours, and Skia used in .Html only to decode three raster formats, within ADR 43

Goal​

Render an HTML business document and its CSS — an invoice, a multi-page report, a contract, general terms of sale in columns — to a PDF paginated as CSS Paged Media and GCPM say, whose every character reads back as it was written in any script the corpus holds, with nothing loaded that the caller did not allow, in memory that follows the page being laid out rather than the document, and in the same bytes on every run.

This is the library's original promise (ADR 2): no browser, no external process, no native PDF engine. Everything below layout exists when M12 starts — pages, merging and page labels (M06), image containers, outlines and open parameters (M07), fonts, subsetting, the glyph-run path and the content builder (M08), stamps (M09), barcodes (M10), annotations (M11) — so M12 is the CSS engine, the layout engine and the painter, and the seams between them and the rest. The failures it exists to prevent are the ones every user of an HTML-to-PDF service has met: 300 MB of Chromium per render; a template that reads a cloud instance's credentials through an <img>; a footer's "page 3 of 12" that doubles the cost of every invoice; an IBAN that runs out of its cell; an Arabic client name printed backwards on a French invoice; a table header lost on page two; subtotals that do not carry forward; and one unsupported property that fails a run of ten thousand invoices.

Scope​

In, by sub-milestone. Each closes on its own exit criteria and acceptance conditions, in this order:

  • M12.1 — CSS engine. CSS Syntax 3 tokenizing and parsing with its error recovery; Selectors 4 matching — :is(), :where(), :not(), :has(), the structural, linguistic (:lang(), :dir()) and form-state pseudo-classes, ::before, ::after, ::marker, ::first-line, ::first-letter; the cascade of Cascade 5 with origins, importance and layers; inheritance; typed computed values; the default stylesheet of the HTML Standard's rendering section adapted to print, with presentational hints; Values and Units 4 — every length unit, calc() and the other math functions —, custom properties and var(), @property; CSS Nesting 1; @media (Media Queries 4, print), @supports, @import with layers and conditions; logical properties in horizontal writing modes; and a declared CSS level, generated from the property table the engine runs on and published, with a diagnostic for everything outside it. The resolver seam of ADR 38 with its deny-by-default resolver for data:, declared directories and embedded resources, since @import and <link rel=stylesheet> need it before anything else does.
  • M12.2 — Block and inline layout, text and pagination. The box tree; block, inline, float and positioned layout; text itemized by UAX #9 (bidirectional analysis) and UAX #24 (scripts), shaped by HarfBuzz with OpenType features from CSS (tabular-nums, small capitals, real superscripts), broken into lines by UAX #14 and CSS Text 3, hyphenated through an optional satellite whose patterns are cleared language by language, justified; @page with page-margin boxes, page selectors, named pages, page-orientation and page groups; running elements, named strings, counters and @counter-style; fragmentation — break-*, orphans, widows, box-decoration-break; forward references by the strategy of ADR 39.
  • M12.3 — Tables. Automatic and fixed layout, border-collapse, spanning cells, captions; across pages: repeated headers and footers, rows kept whole, rows and row spans split when they must be, carried-forward subtotals, shrink-to-fit.
  • M12.4 — Flexbox and grid. Flexbox 1 whole; a defined grid level — explicit and implicit tracks, fr, minmax(), repeat(), template areas, named lines, auto-placement; both fragmented across pages.
  • M12.5 — Images, SVG, visual CSS and resources. JPEG, PNG, TIFF and JPEG 2000 passed through by M07's container parsers (JPEG in its EXIF orientation); GIF, WebP and BMP decoded by Skia; inline and referenced SVG drawn as vectors with real, extractable text; borders, backgrounds, rounded corners, shadows, opacity and blend modes, gradients as native shadings, 2D transforms, clip-path basic shapes; CSS Color 4 and 5; barcodes from M10; and ADR 38's resolver completed — the network, with its address checks, redirect, size, count and time bounds.
  • M12.6 — Navigation, web fonts, metadata, the public API. Links, bookmarks (bookmark-level, -label, -state), named destinations from ids, cross-references (target-counter(), target-counters(), target-text()) and a table of contents with real page numbers and leader(); @font-face with WOFF2 and unicode-range; metadata from <title>, <meta> and lang; attachments declared in HTML; PDF pages as images and @page letterheads; stamps rendered from an HTML fragment for M09; IPdfRenderer, its options and ASP.NET Core integration (ADR 8); batch generation with compiled templates and shared caches under a throughput budget; the options of Puppeteer's page.pdf() mapped for those who migrate; the command-line tool's html2pdf verb.
  • M12.7 — Footnotes and multi-column layout. GCPM footnotes — float: footnote, ::footnote-call, ::footnote-marker, the @footnote area, footnote-policy —, and Multi-column Layout 1 — counts and widths, gaps and rules, column-span, balancing, columns continued across pages.

Out, explicitly:

  • the logical structure — structure tree, parent tree, alternative text, reading order, PDF/UA-1 — M13. M12 keeps every box and fragment traced to its source node (architecture §4), writes margin-box content, running elements and position: fixed repeats as /Artifact /Pagination and leaders as /Artifact /Layout from the start, and calls a structure sink that M12 implements as a no-op and M13 fills;
  • PDF/A-3 output, output intents and the XMP model — M14; PDF/A-2 — M20; PDF/UA-2 — M28. M12 writes the metadata M03's version policy asks for, and nothing that would later stop a PDF/A-2 or 3 claim (every font embedded by M08, transparency grouped with its color space);
  • form fields from <input>, <select> and <textarea> — M17; M12 prints form controls statically, as a browser prints a form;
  • vertical writing modes, ruby and MathML — M30: writing-mode: vertical-* is laid out horizontally, <ruby> inline with its <rp> parentheses — HTML's own fallback for user agents without ruby layout —, and <math> not rendered; each reported;
  • CMYK and spot colors, overprint, bleed, crop marks, PDF/X and PDF/VT — M29: device-cmyk() is converted to RGB by CSS Color 5's naive formula and reported; marks and bleed are parsed and reported; batch generation keeps each record's boundaries for M29's document parts;
  • downsampling, re-encoding and recompressing images, and a target file size — M23, where every lossy step is opt-in and reported (ADR 42); M12 passes images through or writes them losslessly, and reports an image placed far above a useful resolution;
  • rasterizing our own pages — M25; until then MuPDF, in a container, is the visual referee;
  • DOCX to HTML — M31, which relies on this engine's named pages and running elements for sections, headers and footers;
  • executing JavaScript, <canvas> drawing, animations and transitions, <video> and <audio> playback, <iframe> content — never (ADR 37): the document is parsed with scripting disabled, so <noscript> and <canvas> fallback content print as the HTML Standard says, a <video> prints its poster, and javascript: URLs never become actions;
  • variable fonts, color fonts and emoji — open questions of the roadmap; M08 uses a variable face at its default instance and reports it;
  • container queries, @scope, subgrid, masonry, anchor positioning, CSS filters and backdrop-filter, mask and mask-image in CSS (SVG masks are in), 3D transforms, shape-outside — reported, and degraded as the specifications' fallback rules say; each enters a later slice when a real template asks for it;
  • endnotes, sidenotes and page floats, change bars, line numbering, generating a back-of-book index — not planned;
  • a templating language — the caller's: Razor components (through ASP.NET Core's HtmlRenderer), Scriban, Fluid, Handlebars.Net. M12 compiles what is shared between records — stylesheets, fonts, resources —, not markup;
  • a code-first layout API beside HTML — an open question of the roadmap (ADR 8);
  • fetching web fonts, or any resource, unless the caller allows it — never by default (ADR 11, ADR 38);
  • inserting characters the source did not contain — automatic French spacing before ;, :, ! and ? included: the source's own U+202F is kept and drawn (M08), none is added.

Order and dependencies. The milestones outside M12 are closed before it starts, in the roadmap's order.

SubContentInside M12Outside M12
M12.1CSS engine, the resolver seam—M08 (the first page of text)
M12.2Block and inline layout, text, paginationM12.1M08 (registry, glyph runs, content builder), M06 (page labels, used from M12.6)
M12.3TablesM12.2—
M12.4Flexbox and gridM12.2—
M12.5Images, SVG, visual CSS, the network resolverM12.2M07 (image container parsers, EXIF orientation), M10 (barcodes)
M12.6Navigation, web fonts, metadata, API, batch, Puppeteer options, HTML stamps, html2pdfM12.2 to M12.5M06 (copier, attachments, page labels), M07 (outline model, open parameters), M09 (stamp pipeline), M11 (link annotations)
M12.7Footnotes and multi-column layoutM12.2, M12.3—

Design​

Where it lives​

  • AdCodicem.Pdf.Html depends on the core, on AngleSharp for HTML5 parsing and nothing else of it, on HarfBuzzSharp for shaping, on SkiaSharp for decoding GIF, WebP and BMP and for nothing else (ADR 43), and on AdCodicem.Pdf.Barcodes (M12.5, below). Its layers:
Parsing/ AngleSharp adapter: HtmlSource -> DOM, scripting disabled, source positions kept; chunked sources
Css/ tokenizer, parser, selectors and their index, the cascade, the generated property table, values,
media queries, @supports, @import, @counter-style, @font-face and @page rules
Style/ ComputedStyle (grouped immutable structs, interned), the per-element resolver, style sharing
Boxes/ the box tree, built from the DOM ahead of layout and released behind it; generated content
Text/ itemization (UAX #9, UAX #24, M08's font runs), HarfBuzz shaping and its cache, UAX #14 and CSS Text 3
break opportunities, the IHyphenator seam, justification, reordering per line
Layout/ block, inline, float, positioned, table, flex, grid and multicol formatting contexts; fragmentation
(fragmentainers, break tokens); pagination (page boxes, margin boxes, page selectors, page groups);
references (the anchor map and the pass orchestrator of ADR 39)
Paint/ the fragment tree of one page -> M08: stacking order, backgrounds and borders, glyph runs, images,
SVG, clipping, transforms, transparency; links and destinations; the structure sink M13 fills
Resources/ IResourceResolver, the built-in policy resolver, file confinement, the network loader, caches
Api/ IPdfRenderer, PdfRenderer, PdfRenderOptions, HtmlSource, PdfRenderResult, PdfRenderTemplate,
HtmlStamp, PuppeteerPdfOptions
  • AdCodicem.Pdf.AspNetCore (M12.6): registration, IResult and MVC results, Razor components.
  • AdCodicem.Pdf.Hyphenation (proposed identifier, settled by the ADR of M12.2 slice 5): Liang's algorithm, and pattern tries for the languages whose licenses the review clears; it depends on .Html for the IHyphenator seam and on nothing else. The architecture's package table names it then.
  • In the core, one addition: a form XObject reserved on M08's PdfDocumentBuilder — a number reserved now, its content written at any later point before Finish — which ADR 39's late-filled counters need. M03's writer already reserves numbers ahead; M08's builder exposes the reservation for form XObjects if it does not already.
  • Layout knows nothing of PDF, and painting recomputes nothing (architecture §4): the boundary between Layout/ and Paint/ is the fragment tree, which a later rasterizer or SVG export could consume unchanged.

The pipeline​

HtmlSource ─▶ DOM (AngleSharp, scripting disabled) ─┐
├─▶ cascade per element, lazily ─▶ ComputedStyle
stylesheets (UA, options, <link>, <style>, style="") ┘ │
▼
IResourceResolver ◀── every url(), src, href ── box tree, built ahead of layout ─▶ layout of one fragmentainer
─▶ fragments + a break token
pagination: page box, margin boxes, counters, running content ◀┘
│
▼
paint ─▶ M08 content builder, glyph runs, XObjects, annotations ─▶ M03 writer

The text of one inline formatting context goes through its own chain:

logical text ─▶ UAX #9 levels ─▶ runs by level, script (UAX #24), face (M08's per-cluster fallback), language,
features ─▶ HarfBuzz per run (shape cache) ─▶ UAX #14 and CSS Text 3 opportunities, hyphenation ─▶ lines
(greedy; `balance`) ─▶ reshaping at unsafe-to-break edges ─▶ justification ─▶ reordering per line (UAX #9 L2)
─▶ glyph runs to M08, each glyph mapped to its text by (glyph, text) pairs

A page at a time​

Memory follows the page (invariant 2, architecture §5), which for an HTML engine means four mechanisms:

  • Resumable layout. Laying out a fragmentainer — a page, a column — returns its fragment tree and a break token: the path of positions where layout stopped, one entry per open formatting context (a block's child index, a line index inside a paragraph, a table's row, a flex line, a grid row, a column set). The next page resumes from the token. Once a page is painted and written, its fragment tree is released; the token, the counters in scope and the running content survive.
  • Boxes built ahead, released behind. The box builder walks the DOM a little ahead of layout — to the next break opportunity past the page's end — and the boxes of a written page are dropped.
  • A streamed source. HtmlSource.FromChunks(head, bodyChunks) takes the <head> once and the body as an enumeration of fragments — one per section, per record, per thousand journal lines — each parsed by AngleSharp as a fragment in the body's context, laid out, and released with its DOM once its last page is written. It is the path on which memory is flat whatever the length. A single-string source is parsed whole by AngleSharp, which has no streaming DOM; the engine then detaches each top-level subtree once no pass needs it again, so what is retained falls behind the writer while the peak includes the parsed DOM. The acceptance measures both, and the documentation says which to use when.
  • What is kept to the end is counted. Everything held across pages is listed here, bounded, and measured by the memory checkpoints:
StateHeldReleased
DOMthe whole source (single string), or the current chunk and one chunk of look-ahead (streamed)subtrees behind the writer, once no pass needs them
Boxes and fragmentsthe page being laid out, and boxes built up to the next break opportunitywhen the page is written
Break tokenone position per open formatting contextreplaced page by page
Counters, named strings, running elementsthe values in scope; one laid-out running element per name and policyas their scope closes
Anchor map (ADR 39)in a two-pass document: one entry per id, and the text of the ids a target-text() namesat the end
Late-filled form XObjectsone per distinct forward value — the document's page total, a page group'swhen the value is known: a group's at the group's end
Outlinethe open path of headings, at most one per bookmark leveleach item written as soon as its next sibling or its parent's end is known
Named destinationsone entry per destination writtenthe name tree, sorted, at the end
Fonts, imagesM08's (glyph, text) maps per face; one reference per distinct imageat Finish
Automatic table layoutper-column minimum and maximum widthsat the table's end

Threads, determinism and cancellation​

  • PdfRenderer and PdfRenderTemplate are immutable and thread-safe (invariant 8); one render runs on one thread with a context of its own; caches shared between renders — parsed stylesheets, decoded faces, shaped runs, decoded resources — are immutable or concurrent, bounded, and never change what a render produces. Parallelism is the caller's, across documents (architecture §5).
  • Determinism (invariant 6): no clock — creation and modification dates only when the caller supplies them; invariant culture everywhere, number formatting included; no hash-table order reaches the output; within one render a resource read twice returns the bytes it returned first. HarfBuzzSharp and SkiaSharp are pinned package versions and count as inputs: a new HarfBuzz may shape differently, and the fingerprints are regenerated, reviewed, in the commit that raises it. Flate output is deterministic per runtime (M03).
  • Cancellation and progress follow M03's convention: CancellationToken last, IProgress<PdfProgress> before it, stage LayingOut (added by M03's enumeration for M12) in units of pages, the token checked once per page and once per 4,096 boxes laid out, with no allocation.

Bounds, classified​

Everything a template or a resource can make large is bounded, classified as ADR 34 classifies the reader's bounds: a bound a valid document can exceed is a guard — an option in HtmlRenderLimits, on by default, reported under its own limit.* code naming the property, thrown as PdfLimitExceededException when the caller sets PdfRenderOptions.ThrowOnLimit —; a bound only an invalid input or the specification itself reaches is an internal constant, with its reason written where it is declared. Defaults below are proposals, set by measurement in the slice that introduces each.

BoundKindWhy
Resources per document — 256; bytes per resource — 32 MB; bytes in all — 128 MB; loading time — 30 s in all, 10 s per request; redirects — 3Guards, MaxResourceCount, MaxResourceLength, MaxTotalResourceLength, MaxLoadingTime, RequestTimeout, MaxRedirects; limit.resource-count, limit.resource-length, limit.resource-total-length, limit.resource-time, limit.redirectsADR 38. A catalog with 400 product photographs is valid
Decoded pixels of one raster image — 100 megapixelsGuard, MaxImagePixels, limit.image-pixelsChecked from the header, by our own parser, before Skia is called: a native decoder must never be handed a size an input chose
Box nesting depth — 512Guard, MaxBoxDepth, limit.box-depthLayout recurses by formatting context; valid HTML may nest deeper. Deeper content is laid out as its deepest permitted ancestor's in-flow content, and reported
@import depth — 8, with cycles cutGuard, MaxImportDepth, limit.import-depthA valid chain can be longer; a cycle is cut whatever the depth, and reported
Tokens one custom property may expand to — 65,536Guard, MaxCustomPropertyExpansion, limit.custom-property-expansionNested var() references grow exponentially; CSS Variables 1 lets a user agent cap the expansion, and a property past it is invalid at computed-value time
Selector matching steps per document — 50 millionGuard, MaxSelectorMatchingSteps, limit.selector-cost:has() and nested :is() make matching superlinear. Past the guard, the remaining :has() evaluations are false
Pages per document — 100,000Guard, MaxPages, limit.pagesA valid document can be longer; a monolithic box that never fits must not paginate forever
SVG elements after <use> expansion — 100,000Guard, MaxSvgElements, limit.svg-elementsNested <use> of groups of <use> is SVG's billion laughs
Layout passes — 4Option of ADR 39, MaxLayoutPasses; reaching it is layout.forward-reference-unsettledA document whose references oscillate is valid; the last pass writes, and the references that did not settle are named
Digits reserved for a late-filled counter — 4Option, ReservedCounterDigits; layout.reserved-width-exceededADR 39's price for one pass; a value past it is drawn condensed into the reserved box
colspan 1,000, rowspan 65,534Internal constantsThe HTML Standard's own clamps
Grid lines per axis — 10,000Internal constantCSS Grid 1 lets a user agent clamp the implicit grid; items past it are placed on the last line, reported
Counter values — 32-bit integersInternal constantCSS Lists 3 lets a user agent clamp them
Column balancing — 32 iterationsInternal constantA binary search to the layout unit (1/64 px) over any block size a 32-bit layout unit can express converges within 32
Break backtracking — to the start of the current pageInternal constant, by designCSS Fragmentation 3 asks for the best break, not the globally optimal one

Diagnostics​

In PdfDiagnosticCodes, disjoint from validation rule identifiers (ADR 36). Every diagnostic from the HTML engine carries its source location — the stylesheet's or document's URL, line and column from AngleSharp's source references, or the element's path — so that the offending line of HTML can be found (architecture §4). Each code is reported once per distinct feature and location, then counted.

CodeSeverityMeaning
css.syntax-errorInformationA declaration or rule dropped by CSS error recovery — ordinary in real stylesheets
css.property-unsupported, css.value-unsupported, css.at-rule-unsupported, css.selector-unsupportedWarningOutside the declared level: the declaration, rule or selector has no effect
css.invalid-at-computed-value-timeInformationA var() substitution produced an invalid value; the property took its inherited or initial value
css.custom-property-cycleWarningCustom properties referring to each other; all of them are guaranteed-invalid
css.streamed-selector-approximatedWarningA selector that looks forward across chunks of a streamed source (:last-child of <body>, :has() on an ancestor) was evaluated on what the look-ahead held
html.script-ignoredInformation<script>, event handler attributes or a javascript: URL — never executed (ADR 37)
html.element-unsupportedWarning<iframe>, <audio>, <math>, <ruby> annotations, vertical writing: what was printed instead, if anything
resource.refusedWarningA resource the policy refused, naming the rule: scheme, address class, origin, outside root, redirect off origin
resource.not-found, resource.failed, resource.type-mismatch, resource.undecodableWarningAbsent; a transport or HTTP failure; bytes that are not the kind expected; bytes that do not decode
limit.*WarningThe guards above; the message names the HtmlRenderLimits property that lifts it
layout.overflowInformationContent wider than its containing block and not clipped
layout.monolithic-overflowWarningA monolithic box taller than a page, sliced across pages
layout.break-rule-relaxedInformationAn avoid, orphans or widows rule could not be honored
layout.shrink-appliedInformationA box scaled to fit (M12.3), with its factor
layout.fixed-element-lateWarningOn a streamed source, a position: fixed element found after pages were written appears from its own page onward
layout.reserved-width-exceededWarningA late-filled counter's value is wider than the width reserved for it
layout.forward-reference-unsettledWarningADR 39's passes ran out: the references named here may be wrong by a page
layout.source-changedWarningA streamed source yielded different chunks on its second enumeration
layout.footnote-deferredInformationA footnote, or its call's line, moved to the next page (M12.7)
text.hyphenation-unavailableInformationhyphens: auto for a language with no patterns; manual hyphens only
image.format-unsupported, image.animation-dropped, image.resolution-excessiveWarning, Information, InformationAVIF, HEIC or JPEG XL; an animated image's first frame used; an image placed at over three times the resolution the options call useful
paint.effect-unsupported, paint.svg-feature-unsupported, paint.color-convertedWarning, Warning, InformationA filter, backdrop-filter, blurred text shadow; an SVG feature skipped; device-cmyk() or an out-of-gamut color mapped
compat.option-ignoredWarningA Puppeteer option with no meaning here (M12.6)

M08's text.* codes — text.code-point-not-covered, text.fallback-used, text.style-synthesized — keep their meaning, reported with the element's location.

The visual referee and approved references​

The roadmap's visual acceptance is a comparison with approved reference images, page by page:

  • Rendering: MuPDF (mutool draw) in a pinned container image (ADR 27) at 150 dpi, RGB; poppler's pdftoppm as a second opinion on disagreements. Images are compared inside the container with Pillow and NumPy, as M11 does, so the test process decodes no image.
  • The regression threshold, fixed in M12.1 slice 1 and recorded under that name in docs/status.md, beside M09's cross-engine threshold, which it does not replace; proposed: a pixel differs when a channel differs by more than 24 of 255 (M09's rule, without its one-pixel neighborhood: the same engine draws the same edges), and a page matches when at most 0.2 % of its pixels differ and no 8-connected region of differing pixels exceeds 144 pixels, a 2 mm square at 150 dpi (M09's region clause). It is tighter than the cross-engine threshold because the comparison is with our own approved output, not with another engine's; M25 keeps both and adds a third for scans.
  • Approval. References live in tests/visual/<document>/page-NNN.png. A first approval, and every later one, is a reviewed commit: the harness writes a side-by-side report — our page, Chromium's rendering of the same source (the corpus's chromium-* documents), LibreOffice's where one exists, and the difference image — as a CI artifact; the reviewer records in tests/visual/APPROVALS.md what differs from Chromium and why. Regenerating is explicit (an environment variable on the test run), never automatic.
  • Chromium as a second, looser referee. Where Chromium implements a feature, its rendering of the same fixture — Chromium pinned at the corpus producer's version, 141, run headless through Playwright in a container — is compared under the same threshold for single-line and single-box fixtures (shaping, color, gradients, borders), and by page structure for whole documents (page count, the page each heading lands on), since two engines' line breaking legitimately differs by a pixel here and there.

M12.1 — CSS engine​

Tokens and parsing.

  • The tokenizer follows CSS Syntax 3 over ReadOnlySpan<char>: tokens are structs — kind, offset, length, numeric value and unit, flags — in a pooled list; a string is made only for an identifier the rule index keeps, and interned. Line and column are computed from offsets when a diagnostic needs them.
  • The parser implements Syntax 3's entry points (stylesheet, rule list, declaration list, component values) and its error recovery exactly — a bad declaration dropped to the next ; at its nesting level, a bad rule to the end of its block, unclosed blocks closed at the end of the input. Templates written for browsers rely on it; each drop is a css.syntax-error.

Where styles come from, in cascade order:

  • the user-agent stylesheet: the HTML Standard's rendering section (CC BY 4.0, attributed in NOTICE), adapted to print, plus GCPM's user-agent defaults — h1 to h6 carry bookmark-level 1 to 6;
  • presentational hints from the same section — align, valign, width, height, bgcolor, border, cellpadding, cellspacing, <font>, <center> — which ERP-generated and e-mail-style invoices still use;
  • stylesheets from the options (PdfRenderOptions.Stylesheets), at the author origin, ahead of the document's own;
  • <link rel=stylesheet> and <style> in document order, each with its media; style attributes.

Selectors.

  • Supported: type, universal, class, id, attribute selectors with every operator and the i and s flags; the four combinators; :is(), :where() (forgiving lists), :not() (complex), :has() (relative selectors); :root, :empty, :first-child, :last-child, :only-child, :nth-child(An+B of S), :nth-last-child(), the -of-type family; :lang() with wildcard ranges, :dir(); :link, :any-link; :checked, :disabled, :enabled, :required, :optional, :read-only, :read-write, :placeholder-shown, :default, :indeterminate, :defined from attributes; ::before, ::after, ::marker, ::first-line, ::first-letter, ::placeholder; the page selectors (:first, :left, :right, :blank, :nth()) parsed here and matched in M12.2; ::footnote-call and ::footnote-marker parsed here and matched in M12.7.
  • Never matching in print, silently, since that is their correct meaning: :hover, :focus and its variants, :active, :visited, :target, ::selection, :fullscreen, :modal.
  • Unsupported, reported: the column combinator, shadow-tree selectors (:host, ::part, ::slotted), :state(), :nth-col(). An invalid selector invalidates its whole selector list, except inside the forgiving lists of :is() and :where() — as browsers do.
  • Matching is right to left without allocation: rules are bucketed by the rightmost compound's id, class, tag or attribute name; an ancestor Bloom filter of tags, ids and classes rejects most descendant selectors without walking; :has() results are memoized per element for the render, and every step counts against MaxSelectorMatchingSteps.

Cascade and computed values.

  • Cascade 5 order: origin and importance, then layers (declaration order; unlayered styles last for normal declarations and first for important ones), then specificity, then order of appearance; style attributes above rules of the same origin; initial, inherit, unset, revert, revert-layer. @scope (Cascade 6) is unsupported and reported.
  • The property table is one data file, from which a build step generates the C# property definitions — grammar, initial value, inheritance, what it applies to, its status in the declared level and the sub-milestone that brings it — and the published css-support.md; a test fails when the two drift.
  • ComputedStyle is grouped immutable structs — inherited font, inherited text, box, border, background, paged media, table, flex and grid, generated content, SVG — each group interned by value, so elements with equal styles share the groups; style sharing reuses a preceding sibling's computed style when their tag, classes, attributes and parents' styles match, as browsers do. Memory follows the distinct styles, not the elements. Styles are computed lazily when the box builder reaches an element, each once.
  • Values 4: absolute units (px is 0.75 pt), font-relative units (em, rem, ex, ch, cap, ic, lh, rlh), viewport-percentage units relative to the initial containing block, which CSS Paged Media makes the first page's page area; container units unsupported. calc(), min(), max(), clamp(), round() with its strategies, mod(), rem(), abs(), sign(), the trigonometric and exponential functions, typed and simplified as Values 4 says, evaluated in double precision and rounded to the layout unit (1/64 px) deterministically. attr() is supported in content; typed attr() (Values 5) is reported.
  • Custom properties: cascaded like any property, var() substituted at computed-value time with its fallback, cycles made guaranteed-invalid, the expansion counted against its guard; @property registrations validated against their syntax, with their inheritance and initial value.
  • Color in M12.1 is CSS Color 3's syntax — names, hexadecimal, rgb(), hsl(), currentcolor, transparent; Color 4 and 5 arrive in M12.5.
  • Logical properties (margin-inline-start, inset-block, inline-size…) map to physical ones by direction, in horizontal-tb only (vertical writing is M30's).
  • Legacy aliases: page-break-before, -after and -inside map to break-* (always to page), word-wrap to overflow-wrap; the -webkit- aliases the Compatibility Standard defines, where the property is in the level (-webkit-print-color-adjust, -webkit-line-clamp); other prefixed properties are reported.

Conditional rules, imports and nesting.

  • Media Queries 4 for print: print and all match, screen does not; width, height, aspect-ratio and orientation are answered against the page size the options declare (A4 by default) — never against @page rules, which may themselves sit inside @media; resolution answers the options' value (300 dpi by default); prefers-color-scheme: light, scripting: none, hover: none, pointer: none, update: none, color true; range syntax supported.
  • @supports answers property–value pairs from the property table — true only for the declared level, so a template can provide its own fallback —, selector() from the selector parser, and font-format() and font-tech() from M08's decoders.
  • @import with layer(), supports() and a media list, resolved through IResourceResolver, cycles cut by absolute URL, depth guarded.
  • CSS Nesting 1: nested style rules and group rules, &, the relaxed syntax that lets a nested rule start with a type selector, & with the specificity of :is().
  • @container, @scope, @view-transition, @starting-style, @font-feature-values and @font-palette-values are reported; the rules inside them do not apply.

The declared level. Each property has a status: Supported; Partial, with the values that are; Parsed — accepted and without effect by design in print (transition, animation, cursor, pointer-events, will-change, scroll-*, touch-action), never reported; Unsupported. css-support.md is generated from the table; each later sub-milestone moves rows to Supported as it lands.

The vendor prefix. The engine's own properties and functions — carried-forward subtotals and shrink-to-fit (M12.3), the attachment relationship and stamp fields (M12.6), M13's -adc-pdf-* tagging overrides and M17's -adc-pdf-field family — carry the prefix -adc-, fixed here and entered in the property table like any property, each by the milestone that brings it: a stylesheet written for us stays valid CSS that other engines ignore, and @supports (-adc-sum: x attr(y)) lets a template ask. The data-adc-* attributes follow the same name. The convention is recorded in an ADR in slice 7, since three milestones build on it.

The resolver seam (ADR 38, first half).

IResourceResolver ResolveAsync(ResourceRequest, CancellationToken) -> ResourceResponse
ResourceRequest absolute URL, kind (Stylesheet, Image, Font, Svg, Attachment, PdfPage), the requesting
location for diagnostics
ResourceResponse bytes (pooled, bounded) with a media type, or a refusal naming its rule
ResourcePolicy the built-in resolver: data: (always), AllowDirectory(root), AllowEmbedded(assembly, prefix),
AllowOrigin(uri) (the network, M12.5), Then(resolver) for a caller's own; default: data: only

file: confinement is complete in M12.1: the percent-decoded path is made absolute with Path.GetFullPath and must start with the root and a separator; links are resolved to their final target and checked again; .. after decoding, encoded separators, file://host/, UNC and device paths (\\server\, \\?\, \\.\) and alternate data streams (name:stream) are refused, each with resource.refused naming the rule.

M12.2 — Block and inline layout, text and pagination​

Boxes.

  • Box generation by display — outer (block, inline, run-in treated as block) and inner (flow, flow-root, table, flex, grid, list-item), contents, none —, anonymous block and inline boxes as CSS 2.2 §9.2 makes them, white space processed as CSS Text 3 §4 says (collapse, preserve, pre-line, break-spaces, segment breaks), ::before, ::after and ::marker.
  • Replaced and special elements: <img>, <svg>, <object> (M12.5, M12.6); <br>, <wbr>, <hr>; form controls as inline blocks drawing their value, a check or a radio mark as a path; with scripting disabled, <noscript> content, <canvas> fallback content and a <video>'s poster are rendered, <iframe>, <audio>, <template> and <script> are not; <details> shows its summary unless open; <dialog> is hidden unless open.
  • Every box keeps its source node: diagnostics point at it, and M13 tags from it.

Block layout. Block and inline formatting contexts, margin collapsing (CSS 2.2 §8.3.1), box-sizing, percentages, min-, max-, auto and intrinsic sizes (min-content, max-content, fit-content), aspect-ratio, overflow (hidden and clip clip; auto and scroll behave as hidden, since print has no scrolling), visibility.

Fonts and shaping.

  • font-family lists resolve through M08's registry by the CSS Fonts 4 matching M08 implements; generic families map to the registry's generics (system-ui and ui-* to sans-serif); @font-face faces (M12.6) are layered over the caller's registry for one document; per-cluster fallback is M08's; font-synthesis decides whether M08 may synthesize bold, oblique and small capitals; line-height: normal uses M08's metrics.
  • HarfBuzz shapes each run — one direction, one script, one face, one language, one feature set — in font units, so that shaping is independent of the size: an hb_buffer reused per render context; features from font-kerning, font-variant-ligatures, -caps (real smcp and c2sc, synthesized when the face lacks them, reported), -numeric (lnum, onum, pnum, tnum, frac, ordn, zero), -position (sups and subs when the face covers the run, synthesized otherwise, as CSS Fonts 4 says), -east-asian, font-feature-settings; the element's lang as HarfBuzz's language, so locl applies. A non-zero letter-spacing disables the optional ligatures, as CSS Text 3 asks.
  • The shape cache maps (face, features, script, direction, language, text) to glyphs, advances, offsets, clusters and HarfBuzz's unsafe-to-break flags; bounded, with a clock eviction; a template's is shared by its renders (M12.6).
  • Glyph runs go to M08's glyph-run path, which maps each glyph to its text by (glyph, text) pair, writes ActualText where a cluster's glyphs cannot map one to one, and rounds widths as /W will say (M08's trap: layout advances by the rounded widths).

Line breaking.

  • UAX #14's classes and rules LB1 to LB31 over tables generated as span data from the Unicode Character Database of the version CharUnicodeInfo implements (M08's rule, with the same version test), by M08's generator, the only one (ADR 43): the tables .Html alone reads — line break, grapheme cluster break, bidirectional brackets and mirroring — compiled into .Html, Script and Bidi_Class held by the core, with CSS Text 3's tailorings: word-break (normal, break-all, keep-all, break-word), line-break (auto, loose, normal, strict, anywhere — the CJK strictness classes), overflow-wrap (anywhere counting in min-content, break-word not), white-space and text-wrap-mode, hyphens, <wbr>. U+00A0, U+202F and U+2011 keep their no-break classes; no opportunity falls inside a grapheme cluster (UAX #29).
  • Unsafe to break. When a line ends where HarfBuzz flags a glyph unsafe to break — a ligature across the break, an Arabic joining form —, the end of the line and the start of the next are shaped again from their text, so that a line's glyphs are what shaping its own text gives.
  • Line layout: greedy first fit; text-wrap: balance by a bounded binary search on the width for blocks of at most ten lines; pretty treated as wrap (it is a hint); text-indent with hanging and each-line; text-align (start, end, left, right, center, justify, match-parent), text-align-last, text-justify (auto — between words for spaced scripts, between characters for CJK —, inter-word, inter-character, none); vertical-align, line-height and the strut; inline boxes' padding and borders across lines under box-decoration-break; atomic inlines; text-overflow: ellipsis; line-clamp and -webkit-line-clamp; text decoration lines with text-decoration-thickness, text-underline-offset and -position; text-transform with the language's case mappings (a Turkish dotted capital, French capitals that keep their accents).
  • Justification distributes space on justification opportunities and writes it as TJ adjustments, not Tw, which applies only to the single-byte code 32 and never to M08's two-byte codes; the space glyph is still drawn, so extraction sees each word boundary.

Hyphenation.

  • hyphens: auto asks the IHyphenator registered in the options for the element's language (lang, inherited, matched by BCP 47 prefix); opportunities are used only when a word would not otherwise fit; hyphenate-character (U+2010, or - when the face lacks it), hyphenate-limit-chars (auto meaning 5, 2 and 2), hyphenate-limit-lines; soft hyphens (U+00AD) always count under manual and auto.
  • The inserted hyphen is soft. It is drawn with the (hyphen glyph, U+00AD) pair — a code of its own in ToUnicode — so that extraction tells it from a hard hyphen, as ISO 32000-2's hyphenation clause for tagged PDF asks (cited as §14.8.2.3; the number to verify).
  • The satellite: Liang's algorithm with each language's exceptions and minima, over a compact trie built at package build time from the patterns, loaded lazily per language from embedded resources, shared across renders, walked without allocation (the word lower-cased into a pooled buffer between boundary marks). A caller may register patterns of its own in TeX's format, so that a language the review does not clear can still be used without the package redistributing it.
  • The license review is an ADR of its own: for each language, the upstream file (hyph-utf8, at a pinned commit), its license as the file states it, and the decision — ship (a license compatible with an MIT package, its notice carried in NOTICE), caller-supplied only, or exclude —, with LPPL's conditions on modified versions and the GPL-only files read explicitly. French, English (US and GB), German (1996 orthography), Spanish, Italian and Dutch are reviewed first.

Bidirectional text.

  • UAX #9 in full: the paragraph level (P2, P3) from direction or dir="auto"; explicit embeddings, overrides and isolates from unicode-bidi (normal, embed, isolate, bidi-override, isolate-override, plaintext), <bdi>, <bdo> and dir; W1 to W7; N0 with bracket pairs (BidiBrackets.txt); N1, N2, I1, I2; L1 on trailing white space, L2 reordering per line after line breaking on the logical text, L3 combining marks, L4 mirroring (BidiMirroring.txt, and HarfBuzz's rtlm).
  • direction also sets the start and end of blocks, the side of list markers, table column order (M12.3) and the main-start of flex and grid (M12.4).
  • Extraction stays logical. Glyph runs are drawn in visual order, as every producer draws them; each glyph maps to its own text, so poppler's and PyMuPDF's bidirectional analysis recovers the logical order — as it does on the corpus's InDesign Arabic, whose manifest string reads logically —; ActualText is written only where M08's rule asks for it. M13's structure tree will carry the logical order explicitly.

Pages.

  • @page rules: size (lengths, A5, A4, A3, B5, B4, JIS-B5, JIS-B4, letter, legal, ledger, portrait, landscape, auto), margins, the page's background painted over the whole page box, page-orientation (rotate-left and rotate-right written as /Rotate 270 and 90, the content laid out in the unrotated box), marks and bleed parsed and reported (M29). A4 is 210 × 297 mm exactly.
  • Page selectors: :first, :left and :right by page progression, :blank, named pages (the page property; a change of page name between siblings forces a break), and page groups with :nth(An+B of <name>) — a group starts where a run of pages of that name starts; specificity as CSS Paged Media 3 orders it.
  • Page-margin boxes: the sixteen boxes, generated when content is not none, sized by Paged Media 3's resolution of the variable dimension between a center box and its neighbors, with their default alignments.
  • position: fixed renders on every page (CSS 2.2 §9.6.1), relative to the page area — including the pages before the element in source order: the corpus invoice's fixed footer is the last child of <body>. Fixed elements are therefore found before layout, by matching only the rules that can set position: fixed; each is laid out once and drawn as one form XObject shared by every page, unless its content depends on the page. On a streamed source one found in a later chunk appears from its own page onward, reported.

Fragmentation.

  • Fragmentainers are pages here and columns in M12.7; a formatting context lays out into the block size left and returns a fragment and a break token.
  • Break opportunities of classes A, B and C (CSS Fragmentation 3 §4.1); break-before, -after and -inside (auto, avoid, avoid-page, page, left, right, recto, verso, and column from M12.7); forced breaks, with a blank page inserted for left and right and matched by :blank; orphans and widows.
  • The best break is the last opportunity that violates the fewest rules, searched back to the start of the page at most; when none satisfies them, the rules are relaxed in the order CSS Fragmentation 3 §4.4 gives, each relaxation reported.
  • A margin adjoining an unforced break is truncated to zero; after a forced break it is kept (CSS Fragmentation 3 §5.2). box-decoration-break: slice | clone for blocks and inline boxes.
  • A monolithic box taller than a page — an image, a fixed-height block with overflow: hidden — starts on a fresh page and is sliced across pages, as CSS Fragmentation 3 permits, reported.

Floats and positioning. float (left, right, inline-start, inline-end), clear, line boxes and block formatting context roots avoiding floats, a float that does not fit pushed to the next page with the content that follows it; position: relative, absolute, fixed, and sticky as relative; containing blocks across pages (an absolutely positioned box is placed in the fragment of its containing block where its static position falls); inset properties; z-index stacking contexts and the painting order of CSS 2.2 appendix E.

Counters and generated content. counter-reset, -increment and -set; counter() and counters() ("7.2.a"); @counter-style with every system (cyclic, numeric, alphabetic, symbolic, additive, fixed), extends, pad, negative, range, fallback, symbols(), and the predefined styles of CSS Counter Styles 3; list-style-type, -position and -image; ::marker; quotes by language — French guillemets with their own U+202F where the stylesheet puts them —, open-quote and close-quote; the page context's page and pages counters, reset and incremented per page and per group.

Running content. position: running(name) with content: element(name, first | start | last | first-except); string-set with string(name, …) and content() (text, before, after, first-letter); both evaluated per page as GCPM 3 defines. A running element is laid out once per distinct content and drawn as a shared form XObject; margin-box content is written as /Artifact with /Type /Pagination and /Subtype /Header or /Footer (ISO 32000-2 §14.8.2.2.2).

Forward references (ADR 39).

  • Late-filled counters. counter(pages) and a page group's total in a margin box are drawn as a form XObject reserved when the first page that shows it is written and filled when the value is known: the document's total at the end, a group's at the group's end. One XObject per distinct forward value — every page of a group shows the same total —, so pending XObjects never grow with the page count. The box reserves the width of ReservedCounterDigits of the counter style's widest digit (digit advances, exact with tabular-nums); the value is aligned inside the reserved box by the margin box's text-align, so a right-aligned "Page 3 sur 12" stays right-aligned; a wider value is condensed into the box (Tz) and reported. One layout pass.
  • In-flow references — counter(pages) in running text here; target-counter(), target-counters(), target-text() and the table of contents in M12.6 — go through the pass orchestrator. The first pass lays out and writes nothing: it records every id's page, the ids a target-text() names, and their text when met after the reference (a backward target-text() is resolved in the writing pass itself, which meets its target first). The second pass lays out with the map and writes; it records a map of its own, and if an anchor moved — a longer table of contents pushing everything down a page — the output is discarded and the pass repeated, up to MaxLayoutPasses, the last one writing and reporting the references that did not settle. Discarding is possible on a seekable output, truncated back to where the render started; on a non-seekable one (an HTTP response body) the orchestrator lays out map-only passes until the map settles, then writes once: one more layout, and no buffering of the output.
  • A streamed source used by a two-pass document is enumerated once per pass, and must yield the same chunks; a different chunk count is layout.source-changed, and the output is the last pass's.
  • A document with no in-flow reference is laid out once.

M12.3 — Tables​

  • The table model: the table wrapper and grid built by CSS 2.2 §17.2.1's anonymous object rules, so that display: table-* on non-table elements works; <col> and <colgroup> widths and spans; colspan and rowspan clamped as the HTML Standard clamps them; caption-side; border-spacing; border-collapse with the conflict resolution of CSS 2.2 §17.6.2 (wider wins, then style, then color by origin); empty-cells; vertical-align in cells; direction reversing column order.
  • Fixed layout from the first row and the columns; automatic layout by minimum and maximum content contributions per column, spanning cells distributed, percentages honored, as CSS Tables 3's draft and the browsers do. Automatic layout of a long table is two streaming passes over its rows: the first shapes each cell's text and keeps only per-column minimum and maximum widths; the second lays the rows out page by page. A table of a thousand pages holds the columns' numbers, not its rows.
  • Across pages: rows are break opportunities; thead (table-header-group) repeats at the top of every fragment and tfoot (table-footer-group) at the bottom, their height reserved first; the caption stays with the first fragment; break-inside: avoid on a row keeps it whole; a row taller than a page is split, its cells' contents fragmented together; a cell spanning rows across a break continues in the next fragment; collapsed borders are resolved again at each fragment edge, so a repeated header keeps its own bottom border.
  • Carried-forward subtotals — à reporter at the foot of a page, report at the head of the next, Übertrag in German — have no CSS equivalent (XSL-FO has fo:retrieve-table-marker); they are a vendor extension with the -adc- prefix, documented as ours and reported as unsupported by every other engine. The shape, fixed in its slice:
tbody tr { -adc-sum: net attr(data-net); } /* accumulate per row */
tfoot .carried { content: -adc-sum(net, page-end, "# ##0,00 €"); } /* rows up to this page's end */
thead .brought { content: -adc-sum(net, page-start, "# ##0,00 €"); } /* rows before this page */

Amounts are parsed from the attribute as invariant decimals (digits, one ., a leading -), accumulated as System.Decimal — never a double — and formatted by a pattern whose grouping and decimal characters are the ones written in it, with no culture. The footer's value is known once the page's rows are placed, before it is painted; its height is laid out with the value itself, and a height that changes reflows the page once, a bound of one.

  • Shrink-to-fit, a second vendor extension: -adc-shrink-to-fit: auto | <number> on a table or on a box of fixed size (an envelope's address window) scales it down uniformly, to at most the given minimum, and reports the factor (layout.shrink-applied). Without it, a table wider than its container overflows, stays visible and is reported (layout.overflow).
  • Painting calls the structure sink with the table, its rows, header cells and their scope, for M13.

M12.4 — Flexbox and grid​

  • Flexbox 1 whole: flex-direction, flex-wrap, flex-flow, order, the flexible length resolution of §9.7, justify-content, align-items, align-self, align-content, baseline alignment, gap, auto margins, automatic minimum sizes (min-width: auto), intrinsic sizing of flex containers, direction reversing main-start.
  • Flex fragmentation (Flexbox 1 §10): a multi-line row container breaks between flex lines; a single-line row container fragments its items in parallel; a column container fragments its items in order; break-* on items honored.
  • The defined grid level — CSS Grid 1, and Grid 2's fragmentation: grid-template-columns and -rows with lengths, percentages, fr, auto, min-content, max-content, minmax(), fit-content(), repeat() with auto-fill and auto-fit; named lines; grid-template-areas; grid-area, grid-row, grid-column and their longhands; auto-placement, sparse and dense; grid-auto-rows, -columns and -flow; alignment and gaps; the track sizing algorithm of Grid 1 §12. Subgrid and masonry are reported, and laid out as an ordinary nested grid and as a grid respectively.
  • Grid fragmentation (Grid 2 §13): rows are break opportunities; an item spanning rows across a break is fragmented; track sizes are computed once for the whole grid, from items' measurements kept as numbers, so a long grid holds its tracks and not its items' boxes.

M12.5 — Images, SVG, visual CSS and resources​

The resolver, completed (ADR 38).

  • The network, only for origins the caller allows (ResourcePolicy.AllowOrigin), on an HttpClient the caller supplies or the policy builds. The address check runs in a SocketsHttpHandler.ConnectCallback, on the address the connection actually uses, so that a name resolving to a public address when checked and to a private one when connecting (DNS rebinding) cannot pass. Refused: loopback (127.0.0.0/8, ::1), private (10/8, 172.16/12, 192.168/16, fc00::/7), link-local (169.254/16, fe80::/10), shared address space (100.64/10), the unspecified, broadcast, multicast and reserved ranges, the cloud metadata addresses (169.254.169.254, fd00:ec2::254, 100.100.100.200), and any of these carried inside an IPv6 address (IPv4-mapped, IPv4-compatible, 6to4, NAT64's well-known prefix).
  • Redirects are followed by the policy, not by the handler: at most MaxRedirects, each target checked again and required to stay within an allowed origin. Sizes are counted while the body streams, and a response past its bound is cut and reported; time is bounded per request and for the whole document; content is sniffed against the kind requested — a stylesheet must not be a PNG — whatever the Content-Type says.
  • Time bounds are the one place a clock enters a render, and only where the network is allowed — which is already a caller's decision to give up determinism (ADR 38).
  • CachingResourceResolver: a bounded, thread-safe cache of responses keyed by absolute URL and kind, shared by a batch (M12.6) without the engine holding global state (invariant 8).

Raster images.

  • <img>, <picture> with srcset (density descriptors up to the options' resolution; width descriptors against the laid-out size), background-image, list-style-image, content: url(); intrinsic sizes and ratios; object-fit, object-position; image-orientation: from-image applying the EXIF orientation through the placement matrix (M07's rule: four of the eight orientations are mirror images); image-rendering parsed, and /Interpolate never written, since PDF/A forbids it true (M14) and viewers decide without it.
  • Passed through by M07's container parsers, bytes untouched where M07's table says so: JPEG (DCTDecode), PNG (FlateDecode with the PNG predictor, alpha split losslessly into an /SMask), TIFF (CCITT, LZW, Deflate, PackBits) and JPEG 2000 (JPXDecode, raising the output to PDF 1.5, reported by ADR 40's policy).
  • Decoded by Skia, in .Html only: GIF (first frame; an animated one reported), WebP (lossy and lossless), BMP. Our own parser reads the header first and checks the dimensions against MaxImagePixels; the pixels are written losslessly as FlateDecode, alpha as an /SMask. AVIF, HEIC and JPEG XL are refused with image.format-unsupported.
  • One XObject per distinct image per document, keyed by a SHA-256 of the encoded bytes and the decode parameters: a logo on a thousand pages is written once. An image placed at more than three times the resolution the options call useful (300 dpi by default) is reported, never resampled (M23).

Borders, backgrounds and effects.

  • Every border style (solid, dashed, dotted with round dots, double, groove, ridge, inset, outset), border-radius with elliptical corners clipping backgrounds and, under overflow, content; background layers with -size, -position, -repeat (as tiling patterns), -clip, -origin and -attachment; outline; print-color-adjust. border-image is reported.
  • box-shadow on rectangles and rounded rectangles as vector shadings whose edge is a sampled Gaussian (a type 0 function), deterministic and resolution-independent; inset shadows the same way; a text-shadow without blur drawn as offset text; a blurred one drawn unblurred and reported.
  • opacity as a transparency group (a form XObject with /Group /S /Transparency); alpha colors as /ca and /CA; mix-blend-mode, background-blend-mode and isolation as /BM and isolated groups; a page that uses transparency gets a page group with its color space, which PDF/A-2 and 3 require (M14). Each raises the minimum version where it needs to (ADR 40).
  • transform and transform-origin in 2D as cm, affecting painting and overflow, never layout; link areas transformed alike (M12.6). clip-path with inset(), circle(), ellipse(), polygon(), path() and a reference to an SVG <clipPath>.

Color. CSS Color 4 and 5: rgb(), hsl(), hwb(), lab(), lch(), oklab(), oklch(), color() in every predefined space, color-mix(), relative colors, light-dark() (the light value, print being light), system colors; conversion to sRGB with CSS Color 4's gamut mapping (chroma reduction in OKLCh), each component rounded to four decimals as M08 formats numbers; device-cmyk() converted naively and reported (M29 writes DeviceCMYK).

Gradients. linear-gradient(), radial-gradient(), conic-gradient() and their repeating forms as native shadings: axial and radial (types 2 and 3) with stitching functions for their stops and Extend; conic gradients as a Coons patch mesh (type 6) with a bounded error; interpolation in the gradient's color space approximated in sRGB by intermediate stops until the error is below a ΔE (OK) of 0.02, the number of stops bounded.

SVG.

  • Inline <svg> comes from AngleSharp's DOM; a referenced SVG file (<img>, background-image, <object>) is parsed with XmlReader, DTD processing prohibited, into the same model. Both are drawn by our own renderer onto M08's content builder: shapes and paths, transforms, viewBox and preserveAspectRatio, presentation attributes and CSS from our cascade, fill and stroke with dashes, joins, caps and the miter limit, fill rules, gradients and patterns, clipPath, mask (as a soft mask), opacity, <use> and <symbol> expanded under MaxSvgElements, markers, <image> through the resolver, <a> as links (M12.6).
  • Text is text. <text> and <tspan>, with their x, y, dx, dy and rotate positioning, go through the same itemization, shaping and M08 embedding as HTML text, so a chart's axis labels are extractable in the registry's fonts.
  • Skipped and reported: filters, <foreignObject>, textPath, animation, and <script> (ADR 37).

Barcodes. barcode: URLs, whose grammar M10's BarcodeSpecification owns, in <img src>, content: url() and background-image: resolved in-process — the URL carries its own data, so it is allowed by default like data: and loads nothing — and painted as M10's form XObject at its intrinsic size, with PdfBarcode.AlternativeText kept for M13. The M10 seam, decided: .Html references .Barcodes directly — a managed, dependency-free, AOT-compatible package — rather than resolving the scheme through a resolver that .Barcodes could only implement by referencing .Html and its native dependencies. The architecture's package table records it.

M12.6 — Navigation, web fonts, metadata, the public API​

Links and destinations.

  • <a href>: an internal #id becomes a GoTo to a named destination; http, https, mailto and tel become URI actions; a relative URL is resolved against the document's base; other.pdf#page=3 and #nameddest= become GoToR through M07's open parameters; a javascript: URL becomes nothing, reported (ADR 37). One link annotation per line fragment, with QuadPoints and their union as /Rect, transformed with the box; SVG <a> alike. Link annotations are written through M11's model.
  • Named destinations for every link target, always, and for every id when the options ask (NamedDestinations.AllIds), each pointing at the top of its box's first fragment; written as a sorted name tree at the end.

Bookmarks. bookmark-level, bookmark-label (a content list) and bookmark-state (open, closed); the user-agent stylesheet's h1 to h6 defaults, removable by bookmark-level: none. Written forward through M07's outline model: an item is written once its next sibling or its parent's end is known, so only the open path is held; /Count from the items it closed.

Cross-references and the table of contents. target-counter(), target-counters() and target-text() (CSS Generated Content 3) on ADR 39's orchestrator; leader() — dotted, solid, space or a string — filling the line between an entry and its page number, drawn as an /Artifact /Layout. A table of contents, a list of figures and "see article 7.2, page 18" are the same mechanism; no special element exists.

Web fonts. @font-face with src lists (url() with format() and tech() hints; local() looked up in the caller's registry by full and PostScript name — never in the machine's fonts), WOFF and WOFF2 decoded by M08, unicode-range choosing the face per character, the descriptor ranges of weight, stretch and style, font-feature-settings, size-adjust, ascent-override, descent-override and line-gap-override; font-display ignored, since print waits for every font. Loaded through the resolver — web fonts are off by default (ADR 11) —, decoded faces shared by a template's renders.

Metadata and viewer settings. <title> to /Title with DisplayDocTitle; <meta name> author, description, keywords and generator to /Author, /Subject, /Keywords and /Creator; <html lang> to /Lang; XMP kept in step or authoritative as ADR 40 says for 1.7 and 2.0 output; /Producer names the library; no date unless the caller supplies it (Chromium's corpus output carries the clock's). Page labels follow the page counter when a named page resets it with another counter style — roman front matter, then decimal — through M06's PdfPageLabels, so a generated case-file inventory (M18) is labeled as it is numbered.

Attachments and PDF pages as images.

  • <link rel="attachment" href title> attaches at document level, <a rel="attachment"> as a file attachment annotation, each with its MIME type and an AFRelationship from data-adc-relationship (Data, Source, Alternative, Supplement, Unspecified), through the resolver and M06's attachments API, streamed. M14's Factur-X embedding uses the same path.
  • <img src="letterhead.pdf#page=2">, <object data> and @page { background: url(letterhead.pdf) } place a page of another PDF as a vector form XObject made by M06's copier — resources copied lazily, deduplicated —, one per source page per document; as a page background it is an /Artifact of type /Background. The loss it may carry — a font the source page did not embed, transparency — is reported as M09 reports an underlay's.

Stamps from an HTML fragment. HtmlStamp renders a fragment with its stylesheet into a box of given width (and height, or auto) inside a target document: fonts subset into the target's own M08 font resources, the result a form XObject that M09's PdfStamp accepts as "a form XObject supplied by the caller" and that M11 can use as an annotation appearance. Elements marked data-adc-field="page" (or pages, label, piece, bates, date) are left as reserved boxes whose values M09's variable part draws per page in the span's computed font — the static part written once, as M09 compiles every mark. What the core's simple path cannot set — Arabic, Hebrew, Indic scripts, a mixed-direction line — is set properly here.

The public API (ADR 8):

IPdfRenderer thread-safe: RenderAsync(HtmlSource, Stream output, IProgress<PdfProgress>?, CancellationToken)
-> PdfRenderResult; RenderToPart(HtmlSource) -> an M06 assembly source whose pages are laid
out as the assembly writes them
PdfRenderer Create(PdfRenderOptions); Compile(HtmlSource template) -> PdfRenderTemplate
PdfRenderOptions immutable record: Fonts (M08 registry), Resources (IResourceResolver; data: only by default),
Limits (HtmlRenderLimits), ThrowOnLimit, OutputVersion (ADR 40), DocumentId, CreationDate and
ModificationDate (supplied, never read from a clock), Metadata overrides, DefaultPage (A4,
margins), Stylesheets, Resolution, PrintBackgrounds, NamedDestinations, Outline, Hyphenator,
ReservedCounterDigits, MaxLayoutPasses, Conformance (the target M13 and M14 honor; until
they land, a target is accepted and what it cannot yet mean is reported)
HtmlSource FromString(html, baseUri), FromStream(stream, encoding, baseUri), FromChunks(head, chunks,
baseUri)
PdfRenderResult Diagnostics (PdfDiagnostics), PageCount, LayoutPasses, Resources (each request: URL, kind,
outcome, bytes), Unsupported (features and counts)
PdfRenderTemplate Render(record source, output) and RenderBatch(records, output per record | one output)
HtmlStamp Create(fragment, HtmlStampOptions) -> Render(PdfDocument target) -> the stamp's XObject
PuppeteerPdfOptions a record named as Puppeteer's PDFOptions; ToRenderOptions()

PdfRenderOptions grows with the milestones that extend the engine, each adding its own members to the record: Tagging (M13), the PDF/A-3 targets through Conformance (M14), FormFields, FormActions, FormFieldFonts and SignatureFields (M17), ExternalDestinations for links to destinations an enclosing assembly defines (M18). IPdfRenderer gains MeasureAsync (M18): the map-only pass made callable — the page count and each id's page, nothing written. The declared level gains -adc-tab-stops, and the painter the data-adc-annotation attribute, which it turns into an M11 text annotation (M31), both under M12.1's -adc- convention.

  • Rendering into an assembly (RenderToPart) is the architecture's "HTML page added to an existing PDF": a case file's inventory and its exhibits are one forward-only write (M18).
  • AdCodicem.Pdf.AspNetCore: AddAdCodicemPdf() registering IPdfRenderer as a singleton with its options; Results.Extensions.Pdf(…) for minimal APIs and a PdfResult for MVC, streaming into the response body — a non-seekable output, on which two-pass documents take the map-only pass —; Razor components rendered to HTML through ASP.NET Core's HtmlRenderer, which is how a .NET caller compiles a template; the resolver's HttpClient from IHttpClientFactory, with the checked connect callback as its primary handler.

Batch generation.

  • PdfRenderTemplate is compiled once: the user-agent and template stylesheets parsed and indexed, counter styles and @page rules resolved, @font-face faces decoded, resources cached, the shape cache warm. Each record supplies its HTML — a document or a body fragment —; styles in a record's own <head> are parsed per record but cached by content hash, so the same inline stylesheet repeated by a template engine is parsed once.
  • Output: one file per record, or one document for the run, each record starting a page group — its own page and pages counters, a bookmark and a named destination per record — with the record boundaries kept for M29's PDF/VT document parts.
  • The throughput budget is enforced in CI (below); records rendered in parallel give the bytes sequential renders give.

Puppeteer's page.pdf(). PuppeteerPdfOptions carries the option names of Puppeteer's PDFOptions — format, width, height, landscape, margin, preferCSSPageSize, printBackground, omitBackground, scale, pageRanges, displayHeaderFooter, headerTemplate, footerTemplate, outline, tagged, timeout, waitForFonts, path —, with Puppeteer's defaults (Letter, not A4), and maps them onto the engine:

  • page size and margins as a user-agent-level @page rule that the document's own @page overrides only under preferCSSPageSize; printBackground: false omits backgrounds, as Chromium does; scale lays out at the page area's width divided by the scale and paints scaled; pageRanges lays out every page and writes the selected ones;
  • headerTemplate and footerTemplate rendered in the whole top or bottom margin area, page edge to page edge, as Chromium draws them — not in @top-center, which the corner boxes narrow —, styled by their own <style> only, since Chromium isolates them from the page's stylesheets; the classes pageNumber and totalPages become counter(page) and a late-filled total, title the document's title, url its base URL, and date a value the caller supplies, empty and reported otherwise (invariant 6);
  • outline asks for bookmarks from headings; tagged is accepted and takes effect with M13; timeout becomes a cancellation deadline the caller's token observes; waitForFonts and path have no meaning here and are reported (compat.option-ignored).

Each mapping is established against a pinned Puppeteer version by probe documents that Chromium prints in a container, and published as a table with the migration guide. It is option compatibility, not browser fidelity (CLAUDE.md).

M12.7 — Footnotes and multi-column layout​

Multi-column layout.

  • column-count, column-width, columns, column-gap, column-rule (drawn between columns that have content), column-fill: balance | auto, column-span: all, break-before, -after and -inside: column and avoid-column; multicol containers nested, within MaxBoxDepth.
  • Columns are fragmentainers inside the page: a column set lays out its content column by column; balancing finds, by binary search on the column height, the lowest height at which the content fits the columns with the break rules honored (at most 32 iterations, above); on a page that the content overflows, columns are filled and only the last fragment is balanced; a spanning element ends a column set and starts another after it.
  • Content order is kept: column one is painted before column two, so a PDF reader and M13's structure tree both read in source order. Floats and tables inside columns are laid out in their column.

Footnotes (GCPM 3 §2).

  • float: footnote moves an element to the page's footnote area; ::footnote-call is generated where it was, ::footnote-marker before its body; the footnote counter increments per note and is reset per page when the @page rule says counter-reset: footnote; the @footnote area inside @page takes max-height, a border (the separator) and padding; footnote-display: block | inline (compact reported).
  • Placement: when a line holding a call is placed, its note is laid out into the page's footnote area and the page area shrinks by the note's height; if the note does not fit, footnote-policy decides — auto splits the note across pages, continuing it at the top of the next page's area; line moves the call's line to the next page; block moves its paragraph. The re-fit after each note reuses the page's pooled fragments: no allocation per iteration, and at most one iteration per call on the page.
  • The call and its note are linked both ways by internal GoTo links; M13 tags the note Note, with its /ID, and the call Reference, in 1.7 and 2.0 output alike — FENote and the /Ref entry are PDF 2.0's, left to M28 by ADR 40.
  • Notes are painted after the page's body, so extraction reads each page's text before its notes.

The command-line tool​

html2pdf IN.html -o OUT.pdf [--base DIR|URL]... [--allow-origin URL]... [--stylesheet CSS]...
[--page-size A4] [--options OPTIONS.json] [--puppeteer PDFOPTIONS.json] [--throw-on-limit] [--json]
html2pdf --template T.html --records RECORDS.jsonl -o DIR|OUT.pdf [--one-document] [--json]
  • Nothing is loaded that --base or --allow-origin does not allow; --json prints the render result — page count, passes, every resource request with its outcome, every diagnostic. Exit codes are M06's: 0 rendered, 2 usage, 3 unreadable input, 4 refused by policy (a guard reached under --throw-on-limit).
  • The Native AOT binary ships SkiaSharp's and HarfBuzzSharp's native assets for its runtime identifier beside it; if AngleSharp or either binding raises trimming or AOT warnings that cannot be suppressed where they fire with a justification (ADR 29), html2pdf exists in the dotnet tool only, and the slice records the measurement.

Slices​

Each slice is vertical, ends on a green commit, documents the diagnostics it introduces, records its benchmark in docs/status.md when it has one, and moves the rows of css-support.md it makes true. Slices are numbered within their sub-milestone ("M12.2 slice 11"); a sub-milestone closes when its last slice's commit passes its acceptance conditions.

M12.1 — CSS engine​

  1. A walking skeleton, the API baseline and the visual harness. Delivers the AdCodicem.Pdf.Html project; AngleSharp parsing with scripting disabled and source positions kept; a stub cascade (the user-agent stylesheet's display and font-* only); block boxes of greedy lines through M08's simple path on one A4 page; PdfRenderer.RenderAsync(HtmlSource, Stream); #35's public API baseline, which M03 put in place, extended to .Html and the other packages M12 adds; the visual harness — MuPDF, Pillow and NumPy in a container, the approval switch, the side-by-side report as a CI artifact, the regression threshold recorded in status.md. Proved by pdftotext and PyMuPDF extracting a French paragraph of tests/corpus/sources/contract-fr.html exactly; qpdf --check; the harness approving a page and then failing on a one-line shift and on a removed word; the baseline test failing on a deliberately added public member. Leaves real CSS.
  2. Tokens and parsing (CSS Syntax 3). Delivers the span tokenizer, the parser's entry points, error recovery, @charset, source locations, css.syntax-error. Proved by Simon Sapin's css-parsing-tests (JSON, CC0, pinned and vendored) passing whole; FsCheck — any character sequence tokenizes and parses without an exception, and serializing a parsed stylesheet parses back to the same tree —; CssBenchmarks.Tokenize at 0 B allocated per token; the tokenizer and parser in the fuzzing campaign. Leaves meaning.
  3. Selectors 4. Delivers selector parsing, the rule index, the ancestor Bloom filter, right-to-left matching, specificity, :is(), :where(), :not(), :has() with memoization and MaxSelectorMatchingSteps, the linguistic and form-state pseudo-classes, the pseudo-elements, page selectors parsed. Proved by ChromiumSelectorRefereeTests: over the DOMs of the three corpus sources and a selector fixture set, each selector matches exactly the elements Chromium 141's querySelectorAll returns (Playwright, in a container); a :has() bomb stopping at its guard with limit.selector-cost; CssBenchmarks.Match at 0 B per element. Leaves the cascade.
  4. Cascade 5, the user-agent stylesheet and presentational hints. Delivers origins, importance, layers, order, style attributes, revert and revert-layer, inheritance, the HTML Standard's rendering section adapted to print with its NOTICE, the presentational hints, the legacy aliases. Proved by ChromiumComputedStyleRefereeTests on the three corpus sources for the properties typed so far (display, box model, fonts, color): every element's computed values equal getComputedStyle's under print emulation, normalized to CSS pixels; a layered fixture whose !important reversal matches Chromium's. Leaves values.
  5. Values 4, custom properties and typed computed values. Delivers the property table and its generator, ComputedStyle's interned groups and style sharing, every unit, the math functions, custom properties with var(), cycles and the expansion guard, @property, CSS Color 3 colors, logical properties. Proved by the computed-style referee extended to every declared property, on the corpus sources and on the framework fixtures — Bootstrap 5, normalize.css and a Tailwind CSS v4 build, vendored under their MIT licenses as test fixtures — applied to them; FsCheck on calc() — simplification never changes the value, over random expressions —; an exponential var() chain stopped by its guard. Leaves conditions.
  6. Conditional rules, imports, nesting and the resolver seam. Delivers Media Queries 4 for print, @supports, @import with its conditions, cycles and depth guard, CSS Nesting 1, IResourceResolver, ResourcePolicy with data:, AllowDirectory and AllowEmbedded, and file: confinement complete. Proved by the computed-style referee on nested and conditional fixtures; a table of hostile file: URLs (encoded .., a link out of the root, UNC and device paths, alternate data streams) each refused with the rule it broke; an @import cycle cut and reported. Leaves the declared level.
  7. The declared level and graceful degradation. Delivers the status column, the generated reference/css-support.md, the -adc- vendor prefix entered in the table and its ADR, the css.*-unsupported family, once per feature and location, and the result's Unsupported counts. Proved by CorpusCssTests.Unsupported_css_degrades_and_says_so on the unsupported-CSS fixture (below); a test that fails when the published page and the table part. Leaves the budgets.
  8. Budgets and the hostile set. Delivers CssBenchmarks with MemoryDiagnoser (tokenize, parse, match and cascade Bootstrap over the report's DOM repeated a hundred times), the style sharing hit rate, the HTML and CSS mutation fuzzing seeded with the corpus sources and the framework fixtures, and the hostile set — blocks nested 100,000 deep, a million selectors, exponential var(), :has() chains, @import cycles. Proved by the budgets recorded; no untyped exception, hang or allocation an input chose under the default limits; M12.1's acceptance rows. Leaves layout.

M12.2 — Block and inline layout, text and pagination​

  1. The box tree and block layout. Delivers box generation, anonymous boxes, white space processing, ::before and ::after with strings, attr() and quotes, block formatting contexts, margin collapsing, sizing, overflow clipping, solid borders and background colors, MaxBoxDepth. Proved by ChromiumLayoutRefereeTests: the border boxes of block fixtures equal getBoundingClientRect under print emulation at the page area's width, within 0.5 CSS px; the WPT css/CSS2 block and margin-collapsing reftests of the pinned subset. Leaves text.
  2. Fonts and shaping. Delivers CSS font resolution through M08's registry, font runs, UAX #24 script runs from generated tables, HarfBuzz shaping, the shape cache, OpenType features from CSS, letter-spacing and ligatures, font-synthesis, glyph runs to M08. Proved by a fixture of ligatures, kerning, tabular-nums, small capitals and sups extracting exactly through pdftotext and PyMuPDF; pdffonts showing every face embedded and subset; ShapingBenchmarks at 0 B per cache hit; the corpus invoice's .brand, whose letter-spacing must leave "AD CODICEM" without ligatures. Leaves lines.
  3. Line breaking and inline layout. Delivers UAX #14 with CSS Text 3's tailorings, unsafe-to-break reshaping, line boxes, vertical-align, the strut, inline boxes across lines, atomic inlines, text alignment, text-indent, text-wrap: balance, text-overflow, line-clamp, text decoration, text-transform. Proved by LineBreakConformanceTests passing the whole of LineBreakTest.txt for the pinned Unicode version, with CSS's tailorings listed as expected differences; the invoice footer's IBAN in a 40 mm cell breaking under overflow-wrap: anywhere and never inside a grapheme; CorpusPaginationTests at paragraph level — each paragraph of the three sources set in the number of lines Chromium sets it, or the difference recorded with its cause. Leaves bidirectional text.
  4. Bidirectional text. Delivers UAX #9 whole, direction, unicode-bidi, <bdi>, <bdo>, dir="auto", mirroring, box-level direction for blocks and markers. Proved by BidiConformanceTests passing the whole of BidiTest.txt and BidiCharacterTest.txt; CorpusTextLayoutTests.Right_to_left_text_extracts_in_logical_order on the manifest's Arabic and Hebrew strings (the acceptance rows below); ChromiumRenderingRefereeTests: our rendering of each bidirectional fixture line within the threshold of Chromium's rendering of the same line with the same faces. Leaves justification.
  5. Justification and hyphenation. Delivers text-align: justify through TJ, text-justify, text-align-last, the IHyphenator seam, the soft inserted hyphen; the AdCodicem.Pdf.Hyphenation satellite with Liang's algorithm, the trie, lazy loading, caller-supplied patterns; the license ADR and the first languages it clears. Proved by PyphenRefereeTests: for each shipped language, the hyphenation points of the pattern set's own word list, and of every word of the contract source, equal those Pyphen computes from the same patterns, in a Python container; CorpusHyphenationTests.Hyphenated_text_extracts_whole; a HyphenationBenchmarks walk at 0 B per word. Leaves pages.
  6. Pages and margin boxes. Delivers @page rules, sizes, margins, page backgrounds, page-orientation, page selectors with named pages, groups and blank pages, the sixteen margin boxes and their sizing, fixed elements found before layout and drawn once, the fragmentation driver with break tokens: each page laid out, painted, written and released. Proved by unit tests of margin-box sizing over Paged Media 3's examples; pikepdf reading each page's size and /Rotate; CorpusPaginationTests.Reference_documents_paginate_as_chromium_does for the invoice and the contract (one and two pages; the contract's second page starting at "Article 7 — Confidentialité"); the invoice's fixed footer on every page of a three-page variant; the first memory checkpoints at 10 and 100 pages. Leaves break control.
  7. Fragmentation. Delivers break classes, break-*, forced breaks, orphans, widows, the best-break search and its relaxation, margin truncation, box-decoration-break, monolithic slicing. Proved by the WPT css/css-break print reftests of the pinned subset; the report source paginated as Chromium paginates chromium-report-fr.pdf — sections 1 to 3 starting on page 1, 4 to 6 on page 2, the measures on page 3 and the annex on page 4, each h2 kept with its first paragraph —; the text conservation property (FsCheck): for any document the grammar generates, the text extracted from the output equals the source's rendered text in order — nothing lost, nothing doubled at a page break. Leaves floats.
  8. Floats and positioning. Delivers floats, clear, float avoidance, floats across pages, relative, absolute, fixed and sticky positioning, containing blocks across pages, stacking contexts and painting order. Proved by the Chromium layout referee on single-page float and positioning fixtures; the WPT css/CSS2 float reftests of the subset; an envelope-window fixture whose address block lands within 0.1 mm of its declared position (PyMuPDF text boxes). Leaves generated page content.
  9. Counters, counter styles and generated content. Delivers counters, counters(), @counter-style with every system, the predefined styles, ::marker and list styles, quotes, the page context's counters and their resets. Proved by the WPT css/css-counter-styles and css/css-lists reftests of the subset; a contract clause fixture whose "7.2.a" numbering extracts exactly; roman page numbers on front matter. Leaves running content.
  10. Running elements and named strings. Delivers running(), element(), string-set, string(), content(), their per-page policies, shared XObjects for running content, pagination artifacts. Proved by CorpusPagedMediaTests.Running_headers_follow_the_section on the paged-media report (below): the section title each page's header shows is the one WeasyPrint's rendering of the same source shows (WeasyPrintRefereeTests, WeasyPrint 70 in a Python container), or the difference is recorded with its reason; pikepdf finding the header as /Artifact /Pagination /Header on every page. Leaves totals.
  11. Forward references. Delivers late-filled counters with the reserved form XObject added to M08's builder, one XObject per distinct forward value, reserved widths and alignment; the pass orchestrator — map passes, speculative writing on a seekable output, map-only passes on a non-seekable one, MaxLayoutPasses, the unsettled report —; counter(pages) in running text. Proved by CorpusPagedMediaTests.Page_totals_are_filled_late_in_one_pass: "page X of Y" correct on every page of documents of 1, 9, 10, 99, 100, 999 and 1,000 pages, extracted page by page, with one layout pass counted; two invoices in one document with their own group totals; a document whose table of contents grows as it is filled converging in its third pass, and a synthetic oscillating one stopping at the bound with its references named; the same document written to a seekable and to a non-seekable stream giving identical bytes. Leaves the budgets.
  12. A page at a time, measured. Delivers HtmlSource.FromChunks with its one-chunk look-ahead and css.streamed-selector-approximated, DOM subtrees released behind the writer on the single-string path, LayoutBenchmarks, the memory checkpoints, the remote row. Proved by CorpusHtmlTests.Generating_a_thousand_pages_holds_memory_flat on both paths; LayoutBenchmarks at 0 B per line once the faces are warm; CorpusHtmlTests.The_us_code_sets_in_flat_memory on a green Remote corpus run; M12.2's acceptance rows. Leaves tables.

M12.3 — Tables​

  1. The table model and fixed layout. Delivers table grid construction with anonymous objects, <col> and <colgroup>, the spans and their clamps, captions, border-spacing, border-collapse and conflict resolution, cell alignment, table-layout: fixed. Proved by the Chromium layout referee on table fixtures (every cell's border box within 0.5 CSS px); the WPT css/css-tables reftests of the subset. Leaves automatic layout.
  2. Automatic layout, streamed. Delivers minimum and maximum content contributions, column distribution with spans and percentages, the two passes over the rows keeping only per-column numbers. Proved by ChromiumLayoutRefereeTests.Tables_lay_out_as_chromium_does on the invoice's two tables and the report's measures table (column widths within 0.5 CSS px); a 1,000-page table whose memory stays within the flat budget; TableBenchmarks. Leaves pages.
  3. Tables across pages. Delivers row breaks, repeated thead and tfoot, rows kept whole, split rows and row spans, borders at fragment edges, direction in tables. Proved by CorpusTableTests.Header_rows_repeat_on_every_page on the report's measures table set in a narrower page so that it spans two, as libreoffice-report-fr.pdf repeats its header row on page 3; the multi-page invoice (below): header and footer on every page, no line item split, a 30-line cell split with its row; pdftotext page by page. Leaves subtotals.
  4. Carried-forward subtotals. Delivers -adc-sum and the -adc-sum() function, decimal parsing and formatting, the footer laid out after the page's rows. Proved by CorpusTableTests.Carried_forward_subtotals_add_up: on every page of the multi-page invoice, the amount carried forward equals the brought-forward amount plus the sum of the amounts PyMuPDF finds in the page's rows, exactly, and the last page's total equals the invoice's; FsCheck — for any list of amounts and any page split, the carried values are exact partial sums. Leaves shrinking.
  5. Shrink-to-fit. Delivers -adc-shrink-to-fit on tables and fixed boxes, the factor reported, overflow reported without it. Proved by a 14-column ERP export fixture fitting its page at the reported factor, its text at the scaled positions PyMuPDF reports; an address window whose text shrinks to fit its 45 × 90 mm box. Leaves the structure hooks.
  6. Right to left, structure hooks and budgets. Delivers mirrored column order under direction: rtl, the structure sink's table calls for M13, TableBenchmarks with MemoryDiagnoser. Proved by a Hebrew and Arabic table fixture whose columns run right to left and whose rows extract in logical order; M12.3's acceptance rows. Leaves flexbox.

M12.4 — Flexbox and grid​

  1. Single-line flexbox. Delivers flex items, the flexible length resolution, justify-content, align-*, auto margins, order, gap, automatic minimum sizes, intrinsic sizes. Proved by ChromiumLayoutRefereeTests.Flex_containers_lay_out_as_chromium_does on the corpus invoice's header and .parties and on fixtures (within 0.5 CSS px); the WPT css/css-flexbox reftests of the subset. Leaves wrapping.
  2. Multi-line and column flexbox. Delivers flex-wrap, align-content, baseline alignment, column direction, nesting, direction reversing main-start. Proved by the same referees on wrapping and column fixtures. Leaves pages.
  3. Flex fragmentation. Delivers breaks between flex lines, parallel fragmentation of a single line, column containers fragmented in order. Proved by a card list of 60 items over four pages — no card split unless taller than a page, order kept —; the text conservation property extended to flex containers. Leaves grid.
  4. Grid tracks and placement by name. Delivers explicit tracks, fr, minmax(), repeat() with auto-fill and auto-fit, fit-content(), named lines, template areas, line-based placement, the track sizing algorithm, alignment, gaps. Proved by the Chromium layout referee on grid fixtures (a two-column invoice header by template areas among them); the WPT css/css-grid reftests of the subset. Leaves auto-placement.
  5. Auto-placement and implicit grids. Delivers sparse and dense placement, implicit tracks, the line clamp, subgrid and masonry reported. Proved by the referee on auto-placement fixtures; a 20,000-line grid clamped and reported. Leaves pages.
  6. Grid fragmentation and budgets. Delivers row breaks, spanning items fragmented, track sizes computed once from kept measurements; FlexGridBenchmarks. Proved by a three-page grid statement fixture: rows whole, spanning items continued, text conserved; memory flat for a 1,000-page grid; M12.4's acceptance rows. Leaves images.

M12.5 — Images, SVG, visual CSS and resources​

  1. The network resolver. Delivers AllowOrigin, the checked connect callback, the address classes, manual redirects, streamed size bounds, time bounds, content sniffing, the limit.resource-* guards, CachingResourceResolver. Proved by SsrfRefereeTests in a Testcontainers network — a request-logging HTTP server on a private address, a DNS server (CoreDNS) mapping names to loopback, private, link-local and metadata addresses, an allowed origin that redirects to them — in which the hostile templates load nothing and the logger records no request; FsCheck — every textual form of an address (decimal, octal, hexadecimal, shortened, IPv4-mapped, 6to4, NAT64) classifies as its canonical form does; CorpusResourceTests for each refusal's rule and location. Leaves images.
  2. Raster images passed through. Delivers <img>, <picture> and srcset, backgrounds and list images as placed images, object-fit, object-position, image-orientation, image-rendering, M07's containers, deduplication, the excessive-resolution report. Proved by CorpusHtmlImageTests.Jpeg_images_pass_through_byte_for_byte: the JPEG streams of the committed corpus that M07 takes out as files, placed with <img>, come back byte for byte from pdfimages -all; M07's orientation set upright as Pillow's transposed decoding (pdftoppm); the invoice's logo on a thousand pages written once (pikepdf counts one image XObject). Leaves Skia's formats.
  3. Images decoded by Skia. Delivers GIF, WebP and BMP through SkiaSharp after our header check and MaxImagePixels, lossless Flate output with /SMask, the refusals. Proved by the samples pdfimages -png returns equal, pixel for pixel, to Pillow's decoding of the same files in a container (VP8 decoding is exact by its specification, so lossy WebP too); header mutation fuzzing that never reaches the native decoder with a size past the guard. Leaves borders.
  4. Borders, backgrounds and shadows. Delivers every border style, border-radius and its clipping, background layers, tiling patterns, outline, box-shadow as shadings, print-color-adjust. Proved by ChromiumRenderingRefereeTests on a fixture page per feature, within the threshold of Chromium's rendering; pikepdf finding shadings rather than images for the shadows. Leaves color.
  5. CSS Color 4 and 5. Delivers every color function, gamut mapping, color-mix(), relative colors, light-dark(), device-cmyk() converted and reported. Proved by conversions agreeing with colorjs.io — the library by the editors of CSS Color 4 — run under Node in a container, within 0.0005 per component, over a table of colors in every space; the Chromium rendering referee on a color chart. Leaves gradients.
  6. Gradients, transparency, transforms and clipping. Delivers the three gradients and their repeating forms, the interpolation stops, transparency groups, alpha, blend modes, isolation, page groups, 2D transforms, clip-path. Proved by the Chromium rendering referee on a fixture per feature; pikepdf finding the shading types, /Group and /BM entries expected; the version raised to 1.4 by transparency and reported. Leaves SVG.
  7. SVG. Delivers the SVG model from both parsers, the renderer, text through the text pipeline, <use> under its guard, the skipped features reported. Proved by the chart set (below): pdftotext finding every axis label and legend, and each chart within the threshold of Chromium's rendering; the WPT svg painting reftests of the subset; a <use> bomb stopped by limit.svg-elements. Leaves barcodes.
  8. Barcodes in HTML. Delivers the barcode: scheme through M10's BarcodeSpecification, intrinsic sizes, alternative text kept. Proved by CorpusHtmlBarcodeTests.The_qr_bill_reads_as_pdfbox_writes_it: the Swiss QR-bill payment part rebuilt in HTML with the SIX specimen data of vendor/swissqrbill/pdfbox-swiss-qr-bill-a4.pdf is read by zxing-cpp, from our page rasterized by pdftoppm and by MuPDF, to the exact payload it reads from PDFBox's page; an EPC QR on the invoice read back exactly. Leaves the budgets.
  9. Budgets. Delivers ImageBenchmarks and PaintBenchmarks with MemoryDiagnoser, the memory of a 1,000-page document sharing one logo and one shadow shading. Proved by the budgets recorded; M12.5's acceptance rows. Leaves navigation.

M12.6 — Navigation, web fonts, metadata, the public API​

  1. Links and named destinations. Delivers link annotations per line fragment with QuadPoints, internal, external and remote targets, refused javascript:, named destinations as a sorted name tree. Proved by PdfJsNavigationRefereeTests (pinned pdfjs-dist under Node): each of the report's eight table-of-contents links resolves to the page and position of its heading, as the eight links of chromium-report-fr.pdf resolve through its /Dests; PyMuPDF's get_links() agreeing; M06's link check finding no dangling destination. Leaves bookmarks.
  2. Bookmarks. Delivers the three bookmark-* properties, the user-agent defaults, forward writing through M07's outline model. Proved by pdf.js's getOutline() on the report equal to its headings in order and depth; M07's outline JSON export equal to a recorded expectation; memory of a 1,000-page outline within the flat budget. Leaves cross-references.
  3. Cross-references and the table of contents. Delivers target-counter(), target-counters(), target-text() and leader() on the orchestrator. Proved by CorpusPagedMediaTests.Table_of_contents_page_numbers_are_true on the paged-media report: every page number in its table of contents is the page on which pdftotext finds the heading; WeasyPrint's rendering of the same source gives the same numbers, or the difference is recorded with its reason; two layout passes counted. Leaves web fonts.
  4. @font-face. Delivers src lists, local() in the registry only, WOFF and WOFF2 through M08, unicode-range, the descriptors, faces shared per template, web fonts through the resolver. Proved by the test fonts of M08 (tests/fonts) served as WOFF2 in two unicode-range halves: pdffonts lists the two subsets and the text extracts exactly; a url() font on an origin not allowed refused, the text drawn by the fallback and both reported. Leaves metadata.
  5. Metadata, viewer settings and page labels. Delivers /Title, /Author, /Subject, /Keywords, /Creator, /Lang, DisplayDocTitle, XMP as ADR 40 says, supplied dates only, page labels from the page counter. Proved by pikepdf reading the report's /Title and /Lang fr as Chromium writes them in chromium-report-fr.pdf, with no /CreationDate unless supplied; pdf.js's getPageLabels() giving roman then decimal labels on a document with front matter. Leaves attachments.
  6. Attachments and PDF pages as images. Delivers rel="attachment" at both levels with AFRelationship, PDF pages as <img>, <object> and @page backgrounds through M06's copier, background artifacts, reported losses. Proved by CorpusNavigationTests.Attachments_declared_in_html_are_byte_identical: the factur-x.xml carried by documents/invoice/qpdf-invoice-with-facturx-xml.pdf, taken out by M06 and attached from the invoice's HTML with AFRelationship Data, comes back byte for byte from qpdf --show-attachment; the letterhead (below) under the invoice — pdftotext finding the letterhead's text and the invoice's, pikepdf one XObject for the letterhead page on every page. Leaves the API.
  7. The public API and ASP.NET Core. Delivers IPdfRenderer, PdfRenderer, PdfRenderOptions, HtmlSource, PdfRenderResult, progress and cancellation, RenderToPart for M06's assembly, and AdCodicem.Pdf.AspNetCore. Proved by the API baseline; a minimal API test through WebApplicationFactory returning the invoice as a PDF that qpdf --check accepts; a Razor component rendered through HtmlRenderer; 32 parallel renders of the three sources giving the bytes of sequential ones; a render canceled at page 50 of a thousand leaving the output stream as the documentation says. Leaves batches.
  8. Batch generation. Delivers PdfRenderTemplate, compiled state, per-record style caching by hash, the two output modes, page groups per record, record boundaries for M29, BatchBenchmarks, the CI budget. Proved by CorpusBatchTests.A_thousand_invoices_meet_the_throughput_budget (below); in the one-document mode, each record's "page 1 sur n" extracted correctly and one bookmark per record in pdf.js; records rendered on every core giving the sequential bytes. Leaves migration.
  9. Puppeteer's options. Delivers PuppeteerPdfOptions, the mapping, header and footer templates in the margin areas, the published table. Proved by PuppeteerRefereeTests: the probe set (below) printed by Chromium through the pinned Puppeteer in a Node container and rendered by us from the same options — our page sizes the exact ISO or ANSI sizes the options name, and Chromium's within the rounding it applies (its A4 is 594.96 × 841.92 pt in the corpus's own output, ours 595.28 × 841.89), the tolerance recorded; header and footer text within 1 pt of Chromium's positions (pdftotext -bbox); the pages pageRanges selects; compat.option-ignored for each option without meaning. Leaves stamps.
  10. Stamps from an HTML fragment. Delivers HtmlStamp into a target document's fonts and resources, the field placeholders for M09's variable part, the XObject reusable as an annotation appearance. Proved by CorpusHtmlStampTests (below): an exhibit stamp in French and Arabic on three corpus documents, their untouched objects byte-identical (M09's check), the stamp's text extracted in logical order, veraPDF still passing the tagged and the PDF/A one. Leaves the tool.
  11. The html2pdf verb, the corpus and the site. Delivers the verb, AOT measured, our renderings of the reference sources committed to the corpus with their manifest entries, the earlier milestones' corpus-wide tests running on them, the documentation pages. build_corpus.py renders them by calling the html2pdf verb of the tool built from the same commit (dotnet run -c Release --project the tool, with the SDK the session hook installs), as it calls Chromium and LibreOffice; the manifest records the engine's version as the producer, and docs/corpus.md gains our engine as a generator. Proved by CorpusToolTests.Html2pdf_matches_the_api (the AOT binary and the dotnet tool giving the API's bytes); CorpusHtmlTests.Generated_documents_join_the_corpus; M12.6's acceptance rows. Leaves columns and notes.

M12.7 — Footnotes and multi-column layout​

  1. Multi-column containers. Delivers column properties, column sets as fragmentainers, balancing, column breaks, rules, nesting. Proved by CorpusColumnTests.The_annex_sets_in_two_columns_as_chromium_does: the report's annex in two columns, column two starting within one line of where it starts in chromium-report-fr.pdf's page 4, extraction by pdftotext and PyMuPDF in source order; the WPT css/css-multicol reftests of the subset. Leaves pages.
  2. Columns across pages and spanners. Delivers column-span: all, column sets continued on following pages, balancing on the last fragment only, floats and tables in columns. Proved by the terms-of-sale source (below): three columns over two pages with spanning headings, text conserved, rules drawn only between columns with content. Leaves notes.
  3. Footnote calls, markers and the area. Delivers float: footnote, the two pseudo-elements, the counter and its page reset, the @footnote area with its separator, footnote-display, links both ways. Proved by CorpusFootnoteTests.Each_note_sits_on_the_page_of_its_call on the legal memorandum (below): each note's text on the page of its call (pdftotext page by page), numbering reset per page, each call and note linked both ways in pdf.js. Leaves pressure.
  4. Footnotes under pressure. Delivers the shrinking page area, footnote-policy, notes split across pages, notes in tables and columns, layout.footnote-deferred, the allocation-free re-fit. Proved by the memorandum's long note continued on the next page; WeasyPrintRefereeTests — the page of every note equal to WeasyPrint's rendering of the same source, or the difference recorded with its reason —; LayoutBenchmarks.Footnotes at 0 B per re-fit. Leaves the close.
  5. The whole engine, accepted. Delivers the visual acceptance of the four reference documents, the M12 memory and throughput rows run together, docs/status.md's measurements, the documentation completed. Proved by HtmlVisualRefereeTests.Reference_documents_match_their_approved_images and every acceptance row of M12 as a whole.

Tests required​

Unit — tests/AdCodicem.Pdf.Tests, in an Html/ folder, or a test project of its own if keeping the core's test project free of native assets is worth one (decided in M12.1 slice 1):

  • CSS: every token type, escapes, bad strings and URLs, numbers at their edges; each recovery path of the parser; each pseudo-class and pseudo-element, the specificity table, forgiving and unforgiving selector lists; origins, importance, layers nested and anonymous with the important reversal, revert-layer; every unit and math function, var() with fallbacks and cycles, @property; every media feature for print; @supports answers from the table; @import cycles and depth; nesting; the property table's generator and the published page agreeing.
  • Text: the Unicode conformance files (below); script runs; the shape cache's keys and eviction; reshaping at unsafe-to-break edges; TJ justification; hyphenation — patterns, exceptions, minima, the trie's encoding, caller-supplied patterns, the soft hyphen's own code —; box-level direction.
  • Layout: the margin-collapsing matrix; vertical-align values; floats and clearance; positioned containing blocks across pages; each fragmentation rule — orphans and widows at their edges, margin truncation after forced and unforced breaks, the order in which rules relax —; page selector specificity; margin-box sizing on Paged Media 3's examples; every counter system, fallback and range; running-element policies; late-filled counters — reserved width, alignment, a value past the reserve —; the orchestrator's convergence, oscillation, seekable and non-seekable outputs, a streamed source enumerated twice; table grids with anonymous objects, clamped spans, collapsed-border conflicts, repeated groups, split rows and row spans; the flexible length resolution's cases; grid track sizing and placement; column balancing; each footnote policy.
  • Paint: shading functions for stops; the conic mesh within its error; the Gaussian edge function; transforms and QuadPoints; the SVG parser refusing DTDs and external entities; color conversions and gamut mapping.
  • Resources: each policy rule; the file: table; address classification over every textual form (FsCheck); redirects, streamed size bounds, sniffing, the cache.
  • API: options immutability; determinism — the same render twice, and with fonts registered in another order that does not change resolution, gives the same bytes —; 32 parallel renders equal to sequential ones; cancellation and progress per M03's convention; batch outputs equal in both modes; the Puppeteer mapping table option by option; HtmlStamp into a target document.
  • Hostile input everywhere: FsCheck over generated HTML and CSS — a PDF with diagnostics, or a typed exception under ThrowOnLimit, never another exception, a hang, or an allocation the input chose —; each guard reached, raised and thrown, as ReaderLimitsTests does for the reader's.
  • Properties: text conservation across fragments; every character drawn by a face that covers it or named in text.code-point-not-covered (M08's property, lifted to documents); rendering deterministic over shuffled unrelated rules.

Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container (ADR 27):

  • MuPDF (mutool draw), with Pillow and NumPy in the same image — the visual referee; poppler — pdftoppm as its second opinion, pdftotext (with -bbox), pdffonts, pdfimages, pdfinfo.
  • PyMuPDF — text with positions, links, the table of contents; the second extractor on a different engine.
  • pdf.js (pinned pdfjs-dist under Node) — outlines, destinations, page labels, annotations, as a viewer sees them.
  • qpdf — --check on every generated document, --show-attachment; pikepdf — page boxes, /Rotate, XObjects, shadings, groups, artifacts, name trees, attachments.
  • veraPDF — M08's font clauses (PDF/A-2u 6.2.11, PDF/UA-1 7.21) on every generated document; the PDF/A and PDF/UA verdicts of stamped documents before and after.
  • Chromium 141 through Playwright in a Python image — computed styles, querySelectorAll, layout geometry under print emulation, and renderings of fixtures; Puppeteer, pinned, in a Node image — the option probes.
  • WeasyPrint 70 from PyPI — the paged-media peer for what Chromium does not implement: running elements, named strings, cross-references, leaders, page groups, footnotes.
  • Pyphen — hyphenation points from the same patterns; colorjs.io under Node — color conversions; zxing-cpp — barcodes read from both rasterizers.
  • CoreDNS and a request-logging HTTP server in a Testcontainers network — the SSRF referee: what the templates try to reach, and proof that nothing reached it.

External suites. Three suites written by others hold the engine to what it claims:

  • web-platform-tests (BSD-3-Clause), at a pinned commit: the reftests and print reftests of css/CSS2, css/css-syntax, css/selectors, css/css-cascade, css/css-values, css/css-variables, css/css-nesting, css/css-conditional, css/mediaqueries, css/css-color, css/css-fonts, css/css-text, css/css-text-decor, css/css-page, css/css-gcpm, css/css-break, css/css-lists, css/css-counter-styles, css/css-tables, css/css-flexbox, css/css-grid, css/css-multicol, css/css-backgrounds, css/css-images, css/css-transforms, css/compositing, css/css-masking and svg/painting — only those whose test and reference use nothing but the declared level and no script, as a selection script decides by parsing them with the engine itself. Our rendering of each test and of its reference are compared by the visual referee. An expectations file lists the known failures; a test that starts passing fails the run until it leaves the list, so a fix is noticed as surely as a regression. A small subset is vendored for the main CI job; the whole selection runs nightly beside the remote corpus. What is vendored — this subset, css-parsing-tests and the framework fixtures — lives in tests/corpus/sources/third-party/<name>/, pinned, beside its license and a SOURCE file, as docs/corpus.md places inputs written by others.
  • The Unicode conformance files of the version CharUnicodeInfo implements: LineBreakTest.txt, GraphemeBreakTest.txt, BidiTest.txt and BidiCharacterTest.txt, passed whole — CSS's line-breaking tailorings listed as expected differences.
  • css-parsing-tests (Simon Sapin), pinned: the tokenizer and parser's expected outputs as JSON.

Acceptance conditions​

"The reference sources" are tests/corpus/sources/invoice-fr.html, report-fr.html and contract-fr.html, rendered with the corpus's Liberation faces registered — the faces Chromium used for the corpus's chromium-* documents. "The multi-page report" is report-fr.html's first three pages; "the two-column report" its annex on page 4, whose .two-columns block Chromium sets in two columns (feature two-column-text). Rows marked not in the corpus name a need listed below; the milestone does not close until the corpus holds it.

M12 as a whole​

DocumentsBehaviorVerified by
The reference sources: the invoice, the multi-page report, the contract, the two-column reportEvery page within the regression threshold of its approved reference imageHtmlVisualRefereeTests.Reference_documents_match_their_approved_images
The reference sources, against documents/invoice/chromium-invoice-fr.pdf (one page), documents/report/chromium-report-fr.pdf (four) and documents/contract/chromium-contract-fr.pdf (two)The page count Chromium gives; every h2 and every manifest textContains string on the page where Chromium puts itCorpusPaginationTests.Reference_documents_paginate_as_chromium_does
Our renderings of the reference sources, committed by build_corpus.py as documents/invoice/adcodicem-invoice-fr.pdf, documents/report/adcodicem-report-fr.pdf and documents/contract/adcodicem-contract-fr.pdf with their manifest entriesByte-identical to what the engine writes today, regenerated in the commit that changes them; and every earlier milestone's corpus-wide acceptance passes on them — opened clean (M01), no finding (M02), the round trip (M03), repair a no-op (M05), merge and split (M06, M07), every font embedded, subset and extracting exactly (M08), stamped (M09), annotated and flattened (M11)CorpusHtmlTests.Committed_renderings_are_what_the_engine_writes_today, CorpusHtmlTests.Generated_documents_join_the_corpus, and the earlier suites unchanged
A long report of 10, 100 and 1,000 pages — not in the corpus —, from a streamed source and from one stringStreamed: retained memory at the three checkpoints within a budget set from the first measurement and recorded in status.md, its growth from 100 to 1,000 pages no more than the counted records — anchors, destinations — account for. One string: the same growth once the parsed DOM is set aside, its size measured, and DOM subtrees released behind the writerCorpusHtmlTests.Generating_a_thousand_pages_holds_memory_flat
One thousand invoices from invoice-fr.html as a template and a seeded record set — not in the corpus — through one compiled templateInvoices per second on one core and on all cores above the floors recorded in status.md (set at half the first measurement on CI's runner class), bytes allocated per invoice below their budget, enforced in CI; parallel output equal to sequentialCorpusBatchTests.A_thousand_invoices_meet_the_throughput_budget, BatchBenchmarks
The unsupported-CSS fixture: invoice-fr.html with a declaration of every property of the W3C's CSS property index outside the declared level, and every unsupported at-rule and selectorThe render completes; exactly one diagnostic per feature, with its location; every page otherwise within the threshold of the invoice's approved imageCorpusCssTests.Unsupported_css_degrades_and_says_so
The hostile templates: the invoice with <img>, background: url(), @import, @font-face, <link rel="attachment">, SVG <image> and <use> pointing at loopback, private, link-local and metadata addresses — literally, by name and through redirects —, at file: paths outside the declared root, and at an allowed origin that redirects out of itNothing is loaded; the request logger records no request; every refusal is a resource.refused naming its rule and location; the render completes with each resource's box left emptyCorpusResourceTests.Templates_cannot_reach_private_addresses_or_files_outside_their_root, SsrfRefereeTests.Hostile_templates_reach_no_address
Remote: remote/govinfo/us-code-2023-title42.pdf (9,302 pages), its text taken out by pdftotext at test time and never committed (ADR 32), set as HTML sections in a streamed sourceMemory at the 1,000- and 9,000-page checkpoints within the flat budgetCorpusHtmlTests.The_us_code_sets_in_flat_memory

M12.1 — CSS engine​

DocumentsBehaviorVerified by
The reference sourcesNo css.* diagnostic: every feature they use is in the declared level. Every element's computed value of every declared property equal to Chromium 141's getComputedStyle under print emulation, normalized to CSS pixelsCorpusCssTests.Reference_sources_cascade_as_chromium_computes, ChromiumComputedStyleRefereeTests
The framework fixtures — Bootstrap 5, normalize.css, a Tailwind CSS v4 build, MIT test fixtures, not corpus documents — applied to the reference sourcesThe same agreement for every declared property; each feature outside the level reported onceChromiumComputedStyleRefereeTests.Framework_stylesheets_cascade_as_chromium_computes
The DOMs of the reference sources, and the selector fixture setEach selector matches exactly the elements querySelectorAll returnsChromiumSelectorRefereeTests.Selectors_match_the_elements_chromium_matches
css-parsing-testsPasses wholeCssSyntaxConformanceTests.Css_parsing_tests_pass_whole
The hostile set of slice 8No untyped exception, hang or allocation an input chose; each guard reported under its own codeHostileCssTests.Hostile_stylesheets_end_in_a_render_or_a_guard

M12.2 — Block and inline layout, text and pagination​

DocumentsBehaviorVerified by
report-fr.html, against chromium-report-fr.pdfSections 1 to 3 begin on page 1, 4 to 6 on page 2, 7 on page 3 (forced), 8 on page 4 (forced); each h2 on the page of its first paragraph; page 2 begins with the paragraph Chromium begins it with ("Les documents produits ne sont pas balisés"), or the difference is recorded with its causeCorpusPaginationTests.Headings_stay_with_their_text_as_in_chromium
contract-fr.html, against chromium-contract-fr.pdfTwo pages, the second beginning at "Article 7 — Confidentialité", its heading kept with its text; the justified paragraphs each set in the number of lines Chromium sets them, or the difference recorded with its causeCorpusPaginationTests.Paragraphs_set_in_the_lines_chromium_sets
The manifest's textContains strings of vendor/us-federal/indesign-irs-pub1-arabic.pdf (Arabic), remote/census/indesign-census-2020-hebrew-guide.pdf and remote/usda/pdfmaker23-usda-title-vi-fact-sheet-hebrew.pdf (Hebrew) — read from the public manifest, no file fetched —, set as a client's name and address on the invoice beside French text, amounts and an IBAN, under dir="rtl" and dir="auto", in OFL Arabic and Hebrew test facespdftotext and PyMuPDF extract each string in logical order and the French, the amounts and the IBAN unbroken; each line within the threshold of Chromium's rendering of the same fixture with the same facesCorpusTextLayoutTests.Right_to_left_text_extracts_in_logical_order, ChromiumRenderingRefereeTests
The manifest strings of vendor/us-federal/indesign-irs-pub1-russian.pdf, vendor/eu-publications/pdflib-oj-exchange-rates-greek.pdf and vendor/us-federal/indesign-irs-pub1-chinese-traditional.pdf, in a document whose primary face covers Latin onlyExtracted exactly; each script drawn from its fallback face and listed by text.fallback-used; the Chinese broken between ideographs under line-break: normal, and not under word-break: keep-allCorpusTextLayoutTests.Every_script_in_the_corpus_extracts_as_written
The Unicode conformance filesPassed wholeLineBreakConformanceTests.Line_break_test_passes_whole, GraphemeBreakConformanceTests.Grapheme_break_test_passes_whole, BidiConformanceTests.Bidi_tests_pass_whole
contract-fr.html with hyphens: auto and lang="fr", set in a column half its widthHyphenation points equal Pyphen's from the same patterns; the extracted text, soft hyphens removed, equals the source's; no hard hyphen addedCorpusHyphenationTests.Hyphenated_text_extracts_whole, PyphenRefereeTests
The paged-media report — not in the corpusIts running header shows on each page the section title WeasyPrint shows; "page X sur Y" on every page; its named landscape annex written with /Rotate 90; a blank page inserted before a break-before: right chapter and matched by :blank; each group's first page styled by :nth(1 of annex)CorpusPagedMediaTests.Running_headers_follow_the_section, CorpusPagedMediaTests.Named_pages_and_groups_are_honored, WeasyPrintRefereeTests
Generated documents of 1, 9, 10, 99, 100, 999 and 1,000 pages with a footer "page X sur Y"Correct on every page, the width reserved, one layout passCorpusPagedMediaTests.Page_totals_are_filled_late_in_one_pass
Documents from the test grammar (FsCheck)Text conserved across fragments: nothing lost or doubled at a breakLayoutPropertyTests.Text_is_conserved_across_fragments

M12.3 — Tables​

DocumentsBehaviorVerified by
report-fr.html's measures table, on a page short enough that it spans two — as the same table does in documents/report/libreoffice-report-fr.pdf, whose page 3 repeats its header rowThe header row repeated at the top of the second page; no row splitCorpusTableTests.Header_rows_repeat_on_every_page
The tables of the reference sources — the invoice's line items and totals, the report's measuresEvery column's width equal to Chromium's within 0.5 CSS pxChromiumLayoutRefereeTests.Tables_lay_out_as_chromium_does
The multi-page invoice — not in the corpusHeader and footer rows on every page; no line item split; the amount carried forward on each page equal to the amount brought forward plus the page's rows, exactly; the last page's total equal to the invoice'sCorpusTableTests.Carried_forward_subtotals_add_up
A 1,000-page table from a streamed sourceRetained memory within the flat budgetCorpusTableTests.A_thousand_page_table_holds_memory_flat

M12.4 — Flexbox and grid​

DocumentsBehaviorVerified by
invoice-fr.html's header and .parties, both flex containersEvery item's border box equal to Chromium's within 0.5 CSS pxChromiumLayoutRefereeTests.Flex_containers_lay_out_as_chromium_does
The WPT flexbox and grid selectionsEvery test not on the expectations list passes; the list only shrinksWptReftestTests.Selected_reftests_pass_but_the_expected_failures
The card-list and grid-statement fixtures over several pagesNo item split unless taller than a page; order kept; text conserved; memory flat at 1,000 pagesCorpusFlexGridTests.Flex_and_grid_items_fragment_whole, CorpusFlexGridTests.A_thousand_page_grid_holds_memory_flat

M12.5 — Images, SVG, visual CSS and resources​

DocumentsBehaviorVerified by
The JPEG streams of the committed corpus that M07 takes out as files (M07's acceptance names them), placed with <img>pdfimages -all returns each file byte for byte, at its laid-out sizeCorpusHtmlImageTests.Jpeg_images_pass_through_byte_for_byte
M07's photographs in all eight EXIF orientations — not in the corpus, M07's needUpright, as Pillow's transposed decoding shows themCorpusHtmlImageTests.Photographs_stand_upright
The payment part of vendor/swissqrbill/pdfbox-swiss-qr-bill-a4.pdf rebuilt in HTML with its SIX specimen datazxing-cpp reads, from our page rasterized by pdftoppm and by MuPDF, exactly the payload it reads from PDFBox's pageCorpusHtmlBarcodeTests.The_qr_bill_reads_as_pdfbox_writes_it, ZxingRefereeTests
The chart set — not in the corpuspdftotext finds every axis label and legend entry; each chart within the threshold of Chromium's renderingCorpusSvgTests.Charts_keep_their_text, ChromiumRenderingRefereeTests
The visual-effects fixture pages — borders, radii, shadows, gradients, opacity, blend modes, transforms, color spacesWithin the threshold of Chromium's renderingChromiumRenderingRefereeTests.Visual_effects_render_as_chromium_does
The invoice set on 1,000 pagesOne image XObject for the logo, one shading per distinct shadowCorpusHtmlImageTests.An_image_used_on_every_page_is_written_once
The hostile templates (above)As in M12 as a wholeSsrfRefereeTests.Hostile_templates_reach_no_address

M12.6 — Navigation, web fonts, metadata, the public API​

DocumentsBehaviorVerified by
report-fr.html, against chromium-report-fr.pdfIts eight table-of-contents links resolve to the page and position of their headings, as Chromium's eight links resolve through its /Dests; its bookmarks are its headings; /Title and /Lang fr as Chromium writes them, DisplayDocTitle set; no /CreationDate unless supplied — where Chromium's carries its clockPdfJsNavigationRefereeTests.Links_land_where_chromium_lands, CorpusNavigationTests.Metadata_comes_from_the_html
The paged-media report — not in the corpusEvery page number in its table of contents is the page on which pdftotext finds the heading; WeasyPrint's rendering gives the same numbers, or the difference is recorded with its reasonCorpusPagedMediaTests.Table_of_contents_page_numbers_are_true, WeasyPrintRefereeTests
The factur-x.xml of documents/invoice/qpdf-invoice-with-facturx-xml.pdf, taken out by M06 and attached from the invoice's HTMLqpdf --show-attachment returns it byte for byte; AFRelationship is Data; its MIME type text/xmlCorpusNavigationTests.Attachments_declared_in_html_are_byte_identical
The letterhead — not in the corpus, M09's need — as the invoice's @page backgroundpdftotext finds the letterhead's text and the invoice's; one XObject for the letterhead page shared by every page, marked as a background artifactCorpusNavigationTests.Letterheads_are_placed_once
An exhibit stamp from an HTML fragment — "Pièce n° 12", a date, the Arabic manifest string as the client's name — on documents/contract/chromium-contract-fr.pdf (untagged), vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf (tagged, PDF/A-2a and a PDF/UA-1 claim) and vendor/eu-publications/pdflib-oj-exchange-rates-greek.pdf (PDF/A-2a)Every untouched object byte-identical (M09's check); the stamp's text extracted in logical order; the stamp an /Artifact /Pagination; veraPDF's PDF/A-2a and PDF/UA-1 verdicts the same before and afterCorpusHtmlStampTests.Html_stamps_keep_what_they_stamp, VeraPdfRefereeTests
The Puppeteer probes — not in the corpus — rendered by us from the same optionsPage sizes as the options say; header and footer text within 1 pt of Chromium's positions; the pages pageRanges selects; every option without meaning reportedPuppeteerRefereeTests.Options_map_as_chromium_prints_them
Every committed rendering, through the toolhtml2pdf gives the API's bytes, from the AOT binary and from the dotnet toolCorpusToolTests.Html2pdf_matches_the_api

M12.7 — Footnotes and multi-column layout​

DocumentsBehaviorVerified by
The two-column report: report-fr.html's annex, against page 4 of chromium-report-fr.pdfTwo columns; column two begins within one line of where Chromium begins it; pdftotext and PyMuPDF read the annex in source orderCorpusColumnTests.The_annex_sets_in_two_columns_as_chromium_does
The terms of sale — not in the corpusThree columns over two pages, spanning headings, balanced last fragment, text conserved, rules only between columns with contentCorpusColumnTests.Columns_continue_across_pages
The legal memorandum — not in the corpusEach note on the page of its call; numbering reset per page; call and note linked both ways in pdf.js; the long note continued on the next page; each note's page equal to WeasyPrint's, or the difference recorded with its reasonCorpusFootnoteTests.Each_note_sits_on_the_page_of_its_call, WeasyPrintRefereeTests

The remote row closes only on a green Remote corpus run, recorded in status.md with its date.

Corpus​

What the corpus holds​

  • The reference sources and their renderings by other engines. tests/corpus/sources/invoice-fr.html (a flex header, a fixed footer, two tables, text-transform on accented capitals, letter-spacing on the brand), report-fr.html (a linked table of contents, a table with display: table-header-group, legacy page-break-*, a column-count: 2 annex) and contract-fr.html (justified serif text, page-break-after: avoid, a flex signature block); their Chromium 141 renderings documents/invoice/chromium-invoice-fr.pdf, documents/report/chromium-report-fr.pdf (features internal-links and two-column-text; eight link annotations, a /Dests dictionary, DisplayDocTitle, /Lang fr, a structure tree) and documents/contract/chromium-contract-fr.pdf; LibreOffice's libreoffice-invoice-fr.pdf, libreoffice-report-fr.pdf (three pages; the measures table runs onto page 3 with its header row repeated) and documents/archival/libreoffice-report-pdfa2b.pdf; Word's word-invoice-fr.pdf (tagged) and its print-driver and PDF24 siblings; reportlab-invoice.pdf.
  • Other HTML-to-PDF engines' output — what our peers write, and their mistakes: WeasyPrint (remote/py-pdf-sample-files/weasyprint-arabic.pdf, remote/mustang/dwc-weasyprint-facturx-extended-pdfa4f.pdf, remote/mustang/dwc-weasyprint-facturx-extended-no-fx-xmp.pdf; feature weasyprint), wkhtmltopdf (remote/py-pdf-sample-files/wkhtmltopdf-qt5-html-page.pdf — one glyph per operator in a y-down matrix —, remote/qpdf-issues/yousign-signserver-qualified-seal-test-page.pdf), typeset.sh (remote/pdfjs/typeset-sh-corrupt-flate-issue11651.pdf — text before any Tf), Chrome 74's Skia (vendor/node-signpdf/skia-chrome74-node-signpdf-reason-contains-trailer.pdf), Firefox through cairo (remote/pdfjs/cairo-firefox-objstm-index-overflow-bug1978317.pdf — 65,542 objects in one object stream), Google Docs' Skia renderer (remote/py-pdf-sample-files/google-docs-document.pdf), a Chromium invoice from Scoro (remote/pdfpig/scoro-chromium-exiftool-invoice-issue1297.pdf); Apache FOP's XSL-FO renditions (vendor/zugferd/fop26-xrechnung-visualization.pdf, vendor/docentric/fop-factur-x-visualization.pdf). Features html-to-pdf, skia, chrome-print, firefox-print, qt-pdf-engine, typeset-sh, apache-fop.
  • Scripts, with their text in the public manifest: Arabic (vendor/us-federal/indesign-irs-pub1-arabic.pdf; remote, WeasyPrint's), Hebrew (remote indesign-census-2020-hebrew-guide.pdf, pdfmaker23-usda-title-vi-fact-sheet-hebrew.pdf), Cyrillic (indesign-irs-pub1-russian.pdf), Greek (pdflib-oj-exchange-rates-greek.pdf), traditional Chinese (indesign-irs-pub1-chinese-traditional.pdf, distiller6-cdc-west-nile-chinese-traditional.pdf); bidi-control-characters and supplementary-plane-characters in remote handwritten-pdf20-utf8-strings.pdf.
  • Navigation as other producers write it: internal-links (2), named-destinations (5), outlines (60), page-labels (53), toc-structure (2: vendor/pdf-association/indesign13-pdfua1-german-book-chapter.pdf, remote abledocs-pdfua1-textbook-chapter.pdf), display-doc-title (3).
  • Tables: repeated-table-headers (LibreOffice's two renderings of the report; Chromium's measures table fits one page, see Traps), wrapped-table-header (remote afp-batch-processor-bank-statement-2b.pdf), vector-table-grid and dashed-line-rules (a four-page JasperReports statement, vendor/jasper-modular/openpdf-jasperreports-financial-statement.pdf), table-thead-tbody-th-td (the tagged Kraxi invoice, vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf, the kind of invoice M13 aims at).
  • Graphics as real producers write them: axial-shading, blend-mode-multiply and transparency-group-form (remote indesign-cs6-pdfua1-brochure.pdf), isolated-transparency-group-form (Kraxi), page-transparency-group (two Factur-X invoices), smask and image-with-smask, and duplicated-image — remote sap-netweaver-invoice-issue1062.pdf, an invoice whose logo is written twice, which deduplication exists to avoid.
  • Images for M07's pass-through: dct-image (32), jpx (7), flate-image (49), ccitt (25).
  • Barcodes: vector-qr-code-rectangles (the Swiss QR-bill specimen).
  • Attachments: documents/invoice/qpdf-invoice-with-facturx-xml.pdf and the af-relationship-* features.
  • Scale: documents/stress/reportlab-journal-1000-pages.pdf; remote us-code-2023-title42.pdf (9,302 pages).
  • Soft hyphens and letter spacing gone wrong elsewhere: soft-hyphen-in-extracted-text (remote InDesign brochure), soft-hyphen-minus (remote Axapta), letter-spaced-text (two remote ZUGFeRD renditions).

What it lacks​

The corpus is a set of PDFs; an HTML engine's inputs are HTML. Every need below is a source in tests/corpus/sources and its rendering by at least one other engine, so that the milestone is accepted on a producer's real output and not on ours alone (invariant 10).

NeedWhyPriorityLikely source
A paged-media edition of the report: a running header with the section title, "page X sur Y", a table of contents with target-counter() and dotted leader()s, a named landscape annex, page groups, a break-before: right chapter, and WeasyPrint's rendering of itChromium implements none of GCPM, so the three reference sources exercise none of M12.2's paged media or M12.6's cross-references; WeasyPrint is the independent paged-media engine a container can run1Generated here: our source, WeasyPrint 70 from PyPI (and Chromium, for what it does implement)
A multi-page invoice: 120 or more lines over three pages or more, repeated header and footer rows, carried-forward subtotals, a fixed footer, "page X sur Y" per invoiceThe long-invoice case — the one ERP users complain about — is absent; the corpus invoice is one page1Generated here: our source with Chromium and LibreOffice renderings (neither carries subtotals forward; the rows are what they are compared on)
A real multi-page invoice or statement that carries subtotals forward (à reporter, Übertrag)Shows the convention as ERP report writers print it, which our vendor extension imitates2A contribution (W04); remote if it cannot be redistributed
A long report source for 10, 100 and 1,000 pages, seeded and repeatable, streamed and as one stringThe memory acceptance needs a document whose length is a parameter1Generated here: a script in tests/corpus/build from report-fr.html's sections
One thousand invoice records, seeded, as JSON LinesThe throughput acceptance needs a batch; varied line counts exercise pagination1Generated here: a seeded generator in tests/corpus/build
A French invoice with Arabic and Hebrew client names and addresses, an IBAN and amounts inside right-to-left paragraphs, dir="auto" cells, in OFL faces, with Chromium's rendering committedThe bidirectional acceptance otherwise rests on strings; W09 still lacks Hebrew and shaped Arabic we may commit, and Chromium's output of our own source answers that1Generated here: our source, Chromium with Noto Sans Arabic and Noto Sans Hebrew (OFL)
The contract justified and hyphenated (hyphens: auto, French and German), as Chromium and WeasyPrint set itHyphenation has no real rendering to be compared with; the referee is Pyphen alone2Generated here: Chromium and WeasyPrint from our sources
Charts as a chart library writes them in SVG — bars, lines, a pie with a legend — and Chromium's renderingM12.5's SVG acceptance needs the SVG real tools produce: nested groups, transforms, clip paths, text1Generated here: matplotlib's SVG backend from PyPI, and a Vega-Lite chart exported by vl-convert
A one-page letterhead PDF: a vector logo, an address block, a colored bandThe @page background and <img src=…pdf> rows need the business case they exist for (also M09's need)1Generated here: LibreOffice and Chromium from our own source, with an embedded OFL face
A legal memorandum with footnotes — per-page numbering, a note long enough to continue on the next page, notes in a table — and WeasyPrint's rendering; and a real French memorandum or conclusions with footnotes from WordM12.7's footnotes have no source; Word's own footnote layout is the one French legal readers expect1 for the source and WeasyPrint's rendering, 2 for Word'sGenerated here: our source and WeasyPrint; a contribution for Word's (W01)
General terms of sale in three columns of small print over two pages, with spanning headings, and Chromium's renderingThe standard back-of-invoice case for M12.7, beyond the report's short annex1Generated here: our source and Chromium
The Puppeteer probes: the invoice and the report printed by Chromium through Puppeteer with header and footer templates and each option combination the mapping namesThe mapping's acceptance compares with what Puppeteer really prints1Generated here: Chromium through pinned Puppeteer under Node (or Playwright from PyPI)
Third-party HTML templates made for paged media, with the PDF their own engine made (the WeasyPrint project's samples, Paged.js examples)Real templates we did not write are the only test of the declared level against what template authors actually use2A public source, each license checked before use as ADR 23 requires
The three reference sources rendered by WeasyPrint and committedA second engine's output of the same content, committed rather than remote (the WeasyPrint files in the corpus are ShareAlike or unlicensed, and remote)2Generated here: WeasyPrint 70 from PyPI
Image files as HTML meets them: WebP (lossy and lossless), animated GIF, BMP, PNG with alpha, 16-bit and interlaced, a CMYK JPEG with an ICC profileM12.5's Skia path and M07's pass-through need files, not streams inside PDFs; orientation is already M07's need2Generated here with Pillow; M07's public source for orientations (recurser/exif-orientation-examples, MIT)
Korean, and a CJK paragraph long enough to exercise line-break: strictW09 still lacks Korean; CJK line-breaking strictness has no real text3A contribution (W09); a public source under an open license
Output of commercial paged-media engines (Prince, PDFreactor, Antenna House) on the paged-media reportA second peer for GCPM semantics where WeasyPrint and the specification leave room: footnotes, page groups3A contribution

Traps​

  • AngleSharp has no streaming DOM. A 1,000-page report given as one string is parsed whole before a line is laid out. Constant memory needs the streamed source, or DOM subtrees released behind the writer; the documentation must say which path holds which promise.
  • position: fixed repeats on every page, including the pages before it. CSS 2.2 §9.6.1 renders it on every page, and the corpus invoice's footer is the last child of <body>. Found only after layout, it would miss every page but the last.
  • Two engines' A4 differ. Chromium's page box is 594.96 × 841.92 pt in the corpus's own output; ISO A4 is 595.28 × 841.89. A comparison with Chromium to a hundredth of a point fails for the right output.
  • chromium-report-fr.pdf repeats no table header. Its measures table fits on page 3; the repetition is in libreoffice-report-fr.pdf, which ignores the forced break and runs the table onto page 3, and the manifest declares it there. The repetition acceptance must make the table span pages.
  • HarfBuzz does not reorder. It shapes one run in one direction (ADR 43); bidirectional analysis, line breaking on logical text and reordering per line are ours, and a break where HarfBuzz marks a glyph unsafe to break needs the line's edges shaped again, or a ligature or an Arabic joining form straddles two lines.
  • Tw justifies nothing in CID fonts. It applies to the single-byte code 32 only; every font M08 embeds is two-byte. Justification goes through TJ, with the space glyph still drawn.
  • A hyphen added at a line break is not text. Written as a hard hyphen it splits the word in every extraction and every search; it must be the (hyphen glyph, U+00AD) pair (ISO 32000-2's hyphenation clause for tagged PDF).
  • Hyphenation patterns carry their licenses. hyph-utf8 mixes MIT, LPPL and GPL files; LPPL restricts how a modified file is named, and compiling a pattern file into a trie may be a modification. Nothing ships before the review says it may.
  • letter-spacing breaks ligatures on purpose. CSS Text 3 says optional ligatures are not formed when letters are spaced — the invoice's .brand is spaced.
  • text-transform changes what extraction returns. "Émetteur" in the invoice is drawn "ÉMETTEUR"; extraction returns the capitals, as Chromium's output does; language-sensitive mappings (Turkish, German ß) follow lang.
  • An invalid selector invalidates its whole list — except inside :is() and :where(). A template that mixes a supported and an unsupported selector in one list loses both, as it does in a browser: reported, not "fixed".
  • Media queries cannot ask @page. @page may sit inside @media; answering width from @page would be circular. The page size comes from the options.
  • The auto table layout algorithm needs every row. Column widths depend on the widest cell anywhere in the table; a streamed first pass keeping only per-column numbers is what keeps a long table out of memory.
  • The footer of a page depends on the page's rows. A carried-forward subtotal is known only when the page's rows are placed, while its height must be reserved before they are: the footer is laid out with its real value and a height change reflows the page once.
  • A late-filled total cannot know its width. One pass reserves the width of ReservedCounterDigits digits; the text around it cannot move afterwards, so a right-aligned footer aligns inside the reserved box, not around the value.
  • A discarded pass must leave nothing behind. Speculative writing on a seekable stream truncates back to the render's start; on a non-seekable stream it is forbidden, and a map-only pass is the price.
  • Skia is native code. A hostile image header handed to a native decoder can crash the process, which no diagnostic reports. Our parser reads and bounds the header first; only then does Skia see the bytes.
  • DNS answers can change between check and connect. An address check before HttpClient connects is bypassed by rebinding; the check belongs in the connect callback, on the socket's address. Addresses also hide in decimal, octal and hexadecimal forms and inside IPv6.
  • Colors in CSS are sRGB, gradients interpolate elsewhere. A PDF shading interpolates in its own color space; an oklab gradient approximated by its two end stops in sRGB goes muddy in the middle.
  • <use> multiplies. A few kilobytes of nested <use> expand to billions of elements.
  • Chromium tags its output and writes named destinations; the corpus's chromium-* documents carry a structure tree and /Dests. A comparison of structure must wait for M13; one of destinations can start here.
  • A template engine repeats its <style> in every record. Without a cache by content, a batch of ten thousand records parses the same stylesheet ten thousand times.
  • page.pdf() is Chromium's behavior, not a specification. Its header templates ignore the page's stylesheets, its defaults are Letter and no backgrounds, its date comes from a clock; the mapping is measured by probes against a pinned version, and the date comes from the caller.

Documentation​

  • docs/website/docs/tutorials/html-to-pdf.md (new): the first render, the one line that declares where resources come from (ADR 38), fonts, the output, diagnostics — the page a new user reads first, a tutorial whose code is a sample under samples/, held to it by a test (ADR 47).
  • docs/website/docs/concepts/html-engine.md (new): the pipeline, what the engine is and is not (business documents, no JavaScript, ADR 37), degrading and reporting, a page at a time, determinism.
  • docs/website/docs/reference/css-support.md (new, generated): the declared CSS level, property by property, with the sub-milestone that brought each.
  • docs/website/docs/reference/paged-media.md (new): @page, margin boxes, page selectors, named pages and groups, running elements, named strings, counters, cross-references, leaders, footnotes, columns, and the -adc- extensions (carried-forward subtotals, shrink-to-fit, relationship of attachments, stamp fields).
  • docs/website/docs/concepts/resource-loading.md (new): the resolver, the default and why it denies, the address classes refused.
  • docs/website/docs/guides/allowing-resources.md (new): allowing a directory, an embedded resource or an origin, and caching resources for batches.
  • docs/website/docs/guides/batch-generation.md (new): compiled templates, one file per record or one document, Razor components, the measured throughput.
  • docs/website/docs/guides/migrating-from-puppeteer.md (new): the option table, header and footer templates, what differs and why.
  • docs/website/docs/guides/aspnetcore.md (new): registration, results, Razor components, the HttpClient.
  • docs/website/docs/guides/html-stamps.md (new, with M09's stamping guide): stamps from an HTML fragment.
  • docs/website/docs/guides/hyphenation.md (new): the satellite, the languages it ships and why, bringing one's own patterns.
  • docs/website/docs/reference/diagnostics.md: the css.*, html.*, resource.*, layout.*, image.*, paint.* and compat.* codes; docs/website/docs/reference/render-limits.md (new): HtmlRenderLimits, each guard and its code.
  • docs/website/docs/reference/tool/index.md: the html2pdf verb; docs/website/docs/introduction.md: HTML to PDF added to what the library does.
  • docs/corpus.md: our engine as a generator of committed renderings (slice 11 of M12.6), the sources and fixtures this milestone adds, and where the third-party ones — framework stylesheets, the web-platform-tests subset, css-parsing-tests, the Unicode conformance files — live, with their licenses.
  • docs/architecture.md: §2's package table — .Html depending on .Barcodes, the hyphenation package named, .AspNetCore's dependencies —; §4 as built — the layers, the fragment tree as the boundary, break tokens, the streamed source, the pass orchestrator; §5 with the memory measurements.
  • ADRs: the hyphenation patterns' licenses and package (M12.2 slice 5); the -adc- prefix for vendor extensions, on which M13 and M17 build (M12.1 slice 7); ADR 38's and ADR 39's status brought to "implemented".
  • NOTICE: AngleSharp, HarfBuzzSharp, SkiaSharp (MIT); the HTML Standard's rendering section (CC BY 4.0); the Unicode data files (Unicode License v3); each shipped hyphenation pattern set; the vendored web-platform-tests subset (BSD-3-Clause) and css-parsing-tests (CC0); the framework fixtures (MIT).
  • docs/features/features.json: html-engine, paged-media, typography, batch, safe-resources brought to their state; docs/status.md: the regression threshold, the budgets and the measurements.

Exit criteria​

M12.1 — CSS engine​

  • M03's API baseline (#35) covers .Html — .AspNetCore joins it in M12.6 — and fails the build on any change not made in the same commit.
  • The tokenizer and parser follow CSS Syntax 3 with its error recovery; css-parsing-tests pass whole.
  • Selectors 4 as listed match as Chromium matches; the cascade orders origins, importance and layers as Cascade 5 says; computed values agree with Chromium on the reference sources and the framework fixtures.
  • The declared level is generated from the property table and published; everything outside it is reported once and never fails a render.
  • IResourceResolver and the deny-by-default policy exist, file: confined; the CSS guards are classified, documented and tested reached, raised and thrown.
  • CssBenchmarks record their budgets; the tokenizer, parser and cascade join the fuzzing campaign.

M12.2 — Block and inline layout, text and pagination​

  • Block, inline, float and positioned layout agree with Chromium's geometry on single-page fixtures.
  • UAX #14, UAX #29 and UAX #9 pass their conformance files; right-to-left text extracts in logical order.
  • OpenType features, synthesis and fallback come from CSS; letter-spacing disables optional ligatures.
  • The hyphenation satellite exists with its ADR; every shipped language is cleared by the review; the inserted hyphen is soft.
  • @page, margin boxes, page selectors, named pages, groups and page-orientation are honored; fixed elements repeat on every page.
  • Fragmentation follows CSS Fragmentation 3; text is conserved across fragments over the test grammar.
  • Late-filled counters take one pass; in-flow references take the orchestrator's passes, bounded and reported.
  • Memory is flat at 10, 100 and 1,000 pages on the streamed path, and its budget is enforced in CI.

M12.3 — Tables​

  • Fixed and automatic layout agree with Chromium; automatic layout of a long table is streamed.
  • Headers and footers repeat, rows stay whole, split rows and row spans continue across pages.
  • Carried-forward subtotals are exact; shrink-to-fit reports its factor.

M12.4 — Flexbox and grid​

  • Flexbox 1 and the defined grid level agree with Chromium on the invoice and the fixtures; subgrid and masonry are reported.
  • Flex lines and grid rows fragment across pages; long grids hold memory flat.

M12.5 — Images, SVG, visual CSS and resources​

  • ADR 38's resolver is complete: the network only on allowed origins, the address check on the connected socket, redirects, sizes, counts and time bounded, each refusal reported; the SSRF referee records no request.
  • Images pass through as M07 passes them, Skia decodes only GIF, WebP and BMP after our header check, and each distinct image is written once.
  • SVG is drawn as vectors with extractable text; borders, shadows, gradients, transparency, transforms, clip-path and CSS Color 4 and 5 render within the threshold of Chromium.
  • Barcodes in HTML read back exactly through zxing-cpp.

M12.6 — Navigation, web fonts, metadata, the public API​

  • Links, destinations, bookmarks, cross-references and the table of contents resolve as pdf.js reads them.
  • @font-face loads WOFF2 with unicode-range through the resolver only.
  • Metadata comes from the HTML, with no date unless supplied; attachments and PDF pages as images work through the resolver and M06's copier.
  • IPdfRenderer, its options, AdCodicem.Pdf.AspNetCore, batch generation, the Puppeteer mapping and HtmlStamp are public, documented and in the API baseline.
  • The throughput budget of a thousand invoices is enforced in CI.
  • html2pdf gives the API's bytes; our renderings are in the corpus and pass every earlier milestone's acceptance.

M12.7 — Footnotes and multi-column layout​

  • Multi-column layout balances, spans and continues across pages; extraction reads columns in source order.
  • Footnotes sit on the page of their call under every footnote-policy, linked both ways.

M12 as a whole​

  • The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote row on a green Remote corpus run recorded in status.md.
  • Every page of the four reference documents is within the regression threshold of its approved image, and the threshold and the approvals are recorded.
  • Unit tests cover every behavior above, its degenerate and its hostile cases; the FsCheck properties hold; the engine is in the fuzzing campaign with no untyped exception, hang or unbounded allocation.
  • Integration tests run MuPDF, poppler, PyMuPDF, pdf.js, qpdf, pikepdf, veraPDF, Chromium, Puppeteer, WeasyPrint, Pyphen, colorjs.io, zxing-cpp and the SSRF network in containers; the web-platform-tests selection runs nightly against its expectations file.
  • CssBenchmarks, ShapingBenchmarks, LayoutBenchmarks, TableBenchmarks, FlexGridBenchmarks, ImageBenchmarks, PaintBenchmarks, HyphenationBenchmarks and BatchBenchmarks run with MemoryDiagnoser; the memory and throughput budgets are enforced in CI and recorded in status.md.
  • The documentation site publishes the pages above; architecture.md, NOTICE, features.json and status.md are brought in line.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).