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
.Htmlonly 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 andvar(),@property; CSS Nesting 1;@media(Media Queries 4,print),@supports,@importwith 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 fordata:, declared directories and embedded resources, since@importand<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;@pagewith page-margin boxes, page selectors, named pages,page-orientationand 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-pathbasic 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 andleader();@font-facewith WOFF2 andunicode-range; metadata from<title>,<meta>andlang; attachments declared in HTML; PDF pages as images and@pageletterheads; 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'spage.pdf()mapped for those who migrate; the command-line tool'shtml2pdfverb. - M12.7 — Footnotes and multi-column layout. GCPM footnotes —
float: footnote,::footnote-call,::footnote-marker, the@footnotearea,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: fixedrepeats as/Artifact /Paginationand leaders as/Artifact /Layoutfrom 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;marksandbleedare 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, andjavascript: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 andbackdrop-filter,maskandmask-imagein 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.
| Sub | Content | Inside M12 | Outside M12 |
|---|---|---|---|
| M12.1 | CSS engine, the resolver seam | — | M08 (the first page of text) |
| M12.2 | Block and inline layout, text, pagination | M12.1 | M08 (registry, glyph runs, content builder), M06 (page labels, used from M12.6) |
| M12.3 | Tables | M12.2 | — |
| M12.4 | Flexbox and grid | M12.2 | — |
| M12.5 | Images, SVG, visual CSS, the network resolver | M12.2 | M07 (image container parsers, EXIF orientation), M10 (barcodes) |
| M12.6 | Navigation, web fonts, metadata, API, batch, Puppeteer options, HTML stamps, html2pdf | M12.2 to M12.5 | M06 (copier, attachments, page labels), M07 (outline model, open parameters), M09 (stamp pipeline), M11 (link annotations) |
| M12.7 | Footnotes and multi-column layout | M12.2, M12.3 | — |
Design
Where it lives
AdCodicem.Pdf.Htmldepends 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 onAdCodicem.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,IResultand 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.Htmlfor theIHyphenatorseam 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 beforeFinish— 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/andPaint/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:
| State | Held | Released |
|---|---|---|
| DOM | the 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 fragments | the page being laid out, and boxes built up to the next break opportunity | when the page is written |
| Break token | one position per open formatting context | replaced page by page |
| Counters, named strings, running elements | the values in scope; one laid-out running element per name and policy | as 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() names | at the end |
| Late-filled form XObjects | one per distinct forward value — the document's page total, a page group's | when the value is known: a group's at the group's end |
| Outline | the open path of headings, at most one per bookmark level | each item written as soon as its next sibling or its parent's end is known |
| Named destinations | one entry per destination written | the name tree, sorted, at the end |
| Fonts, images | M08's (glyph, text) maps per face; one reference per distinct image | at Finish |
| Automatic table layout | per-column minimum and maximum widths | at the table's end |
Threads, determinism and cancellation
PdfRendererandPdfRenderTemplateare 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:
CancellationTokenlast,IProgress<PdfProgress>before it, stageLayingOut(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.
| Bound | Kind | Why |
|---|---|---|
| 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 — 3 | Guards, MaxResourceCount, MaxResourceLength, MaxTotalResourceLength, MaxLoadingTime, RequestTimeout, MaxRedirects; limit.resource-count, limit.resource-length, limit.resource-total-length, limit.resource-time, limit.redirects | ADR 38. A catalog with 400 product photographs is valid |
| Decoded pixels of one raster image — 100 megapixels | Guard, MaxImagePixels, limit.image-pixels | Checked 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 — 512 | Guard, MaxBoxDepth, limit.box-depth | Layout 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 cut | Guard, MaxImportDepth, limit.import-depth | A valid chain can be longer; a cycle is cut whatever the depth, and reported |
| Tokens one custom property may expand to — 65,536 | Guard, MaxCustomPropertyExpansion, limit.custom-property-expansion | Nested 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 million | Guard, MaxSelectorMatchingSteps, limit.selector-cost | :has() and nested :is() make matching superlinear. Past the guard, the remaining :has() evaluations are false |
| Pages per document — 100,000 | Guard, MaxPages, limit.pages | A valid document can be longer; a monolithic box that never fits must not paginate forever |
SVG elements after <use> expansion — 100,000 | Guard, MaxSvgElements, limit.svg-elements | Nested <use> of groups of <use> is SVG's billion laughs |
| Layout passes — 4 | Option of ADR 39, MaxLayoutPasses; reaching it is layout.forward-reference-unsettled | A 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 — 4 | Option, ReservedCounterDigits; layout.reserved-width-exceeded | ADR 39's price for one pass; a value past it is drawn condensed into the reserved box |
colspan 1,000, rowspan 65,534 | Internal constants | The HTML Standard's own clamps |
| Grid lines per axis — 10,000 | Internal constant | CSS Grid 1 lets a user agent clamp the implicit grid; items past it are placed on the last line, reported |
| Counter values — 32-bit integers | Internal constant | CSS Lists 3 lets a user agent clamp them |
| Column balancing — 32 iterations | Internal constant | A 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 page | Internal constant, by design | CSS 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.
| Code | Severity | Meaning |
|---|---|---|
css.syntax-error | Information | A declaration or rule dropped by CSS error recovery — ordinary in real stylesheets |
css.property-unsupported, css.value-unsupported, css.at-rule-unsupported, css.selector-unsupported | Warning | Outside the declared level: the declaration, rule or selector has no effect |
css.invalid-at-computed-value-time | Information | A var() substitution produced an invalid value; the property took its inherited or initial value |
css.custom-property-cycle | Warning | Custom properties referring to each other; all of them are guaranteed-invalid |
css.streamed-selector-approximated | Warning | A 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-ignored | Information | <script>, event handler attributes or a javascript: URL — never executed (ADR 37) |
html.element-unsupported | Warning | <iframe>, <audio>, <math>, <ruby> annotations, vertical writing: what was printed instead, if anything |
resource.refused | Warning | A 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.undecodable | Warning | Absent; a transport or HTTP failure; bytes that are not the kind expected; bytes that do not decode |
limit.* | Warning | The guards above; the message names the HtmlRenderLimits property that lifts it |
layout.overflow | Information | Content wider than its containing block and not clipped |
layout.monolithic-overflow | Warning | A monolithic box taller than a page, sliced across pages |
layout.break-rule-relaxed | Information | An avoid, orphans or widows rule could not be honored |
layout.shrink-applied | Information | A box scaled to fit (M12.3), with its factor |
layout.fixed-element-late | Warning | On a streamed source, a position: fixed element found after pages were written appears from its own page onward |
layout.reserved-width-exceeded | Warning | A late-filled counter's value is wider than the width reserved for it |
layout.forward-reference-unsettled | Warning | ADR 39's passes ran out: the references named here may be wrong by a page |
layout.source-changed | Warning | A streamed source yielded different chunks on its second enumeration |
layout.footnote-deferred | Information | A footnote, or its call's line, moved to the next page (M12.7) |
text.hyphenation-unavailable | Information | hyphens: auto for a language with no patterns; manual hyphens only |
image.format-unsupported, image.animation-dropped, image.resolution-excessive | Warning, Information, Information | AVIF, 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-converted | Warning, Warning, Information | A filter, backdrop-filter, blurred text shadow; an SVG feature skipped; device-cmyk() or an out-of-gamut color mapped |
compat.option-ignored | Warning | A 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'spdftoppmas 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'schromium-*documents), LibreOffice's where one exists, and the difference image — as a CI artifact; the reviewer records intests/visual/APPROVALS.mdwhat 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 acss.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 —h1toh6carrybookmark-level1 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 itsmedia;styleattributes.
Selectors.
- Supported: type, universal, class, id, attribute selectors with every operator and the
iandsflags; 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-typefamily;:lang()with wildcard ranges,:dir();:link,:any-link;:checked,:disabled,:enabled,:required,:optional,:read-only,:read-write,:placeholder-shown,:default,:indeterminate,:definedfrom 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-calland::footnote-markerparsed here and matched in M12.7. - Never matching in print, silently, since that is their correct meaning:
:hover,:focusand 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 againstMaxSelectorMatchingSteps.
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;
styleattributes 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. ComputedStyleis 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 (
pxis 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 incontent; typedattr()(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;@propertyregistrations 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 bydirection, inhorizontal-tbonly (vertical writing is M30's). - Legacy aliases:
page-break-before,-afterand-insidemap tobreak-*(alwaystopage),word-wraptooverflow-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:
printandallmatch,screendoes not;width,height,aspect-ratioandorientationare answered against the page size the options declare (A4 by default) — never against@pagerules, which may themselves sit inside@media;resolutionanswers the options' value (300 dpi by default);prefers-color-scheme: light,scripting: none,hover: none,pointer: none,update: none,colortrue; range syntax supported. @supportsanswers 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, andfont-format()andfont-tech()from M08's decoders.@importwithlayer(),supports()and a media list, resolved throughIResourceResolver, 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-valuesand@font-palette-valuesare 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,::afterand::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 unlessopen;<dialog>is hidden unlessopen. - 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-familylists resolve through M08's registry by the CSS Fonts 4 matching M08 implements; generic families map to the registry's generics (system-uiandui-*tosans-serif);@font-facefaces (M12.6) are layered over the caller's registry for one document; per-cluster fallback is M08's;font-synthesisdecides whether M08 may synthesize bold, oblique and small capitals;line-height: normaluses 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_bufferreused per render context; features fromfont-kerning,font-variant-ligatures,-caps(realsmcpandc2sc, synthesized when the face lacks them, reported),-numeric(lnum,onum,pnum,tnum,frac,ordn,zero),-position(supsandsubswhen the face covers the run, synthesized otherwise, as CSS Fonts 4 says),-east-asian,font-feature-settings; the element'slangas HarfBuzz's language, soloclapplies. A non-zeroletter-spacingdisables 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
ActualTextwhere a cluster's glyphs cannot map one to one, and rounds widths as/Wwill 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
CharUnicodeInfoimplements (M08's rule, with the same version test), by M08's generator, the only one (ADR 43): the tables.Htmlalone reads — line break, grapheme cluster break, bidirectional brackets and mirroring — compiled into.Html,ScriptandBidi_Classheld 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(anywherecounting in min-content,break-wordnot),white-spaceandtext-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: balanceby a bounded binary search on the width for blocks of at most ten lines;prettytreated aswrap(it is a hint);text-indentwithhangingandeach-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-heightand the strut; inline boxes' padding and borders across lines underbox-decoration-break; atomic inlines;text-overflow: ellipsis;line-clampand-webkit-line-clamp; text decoration lines withtext-decoration-thickness,text-underline-offsetand-position;text-transformwith 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
TJadjustments, notTw, 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: autoasks theIHyphenatorregistered 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(automeaning 5, 2 and 2),hyphenate-limit-lines; soft hyphens (U+00AD) always count undermanualandauto.- 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
directionordir="auto"; explicit embeddings, overrides and isolates fromunicode-bidi(normal,embed,isolate,bidi-override,isolate-override,plaintext),<bdi>,<bdo>anddir; 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'srtlm). directionalso 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 —;
ActualTextis written only where M08's rule asks for it. M13's structure tree will carry the logical order explicitly.
Pages.
@pagerules:size(lengths,A5,A4,A3,B5,B4,JIS-B5,JIS-B4,letter,legal,ledger,portrait,landscape,auto), margins, the page'sbackgroundpainted over the whole page box,page-orientation(rotate-leftandrotate-rightwritten as/Rotate 270and90, the content laid out in the unrotated box),marksandbleedparsed and reported (M29). A4 is 210 × 297 mm exactly.- Page selectors:
:first,:leftand:rightby page progression,:blank, named pages (thepageproperty; 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
contentis notnone, sized by Paged Media 3's resolution of the variable dimension between a center box and its neighbors, with their default alignments. position: fixedrenders 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 setposition: 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,-afterand-inside(auto,avoid,avoid-page,page,left,right,recto,verso, andcolumnfrom M12.7); forced breaks, with a blank page inserted forleftandrightand matched by:blank;orphansandwidows. - 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 | clonefor 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 ofReservedCounterDigitsof the counter style's widest digit (digit advances, exact withtabular-nums); the value is aligned inside the reserved box by the margin box'stext-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 atarget-text()names, and their text when met after the reference (a backwardtarget-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 toMaxLayoutPasses, 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;colspanandrowspanclamped as the HTML Standard clamps them;caption-side;border-spacing;border-collapsewith the conflict resolution of CSS 2.2 §17.6.2 (wider wins, then style, then color by origin);empty-cells;vertical-alignin cells;directionreversing 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 andtfoot(table-footer-group) at the bottom, their height reserved first; the caption stays with the first fragment;break-inside: avoidon 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,directionreversing 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-columnsand-rowswith lengths, percentages,fr,auto,min-content,max-content,minmax(),fit-content(),repeat()withauto-fillandauto-fit; named lines;grid-template-areas;grid-area,grid-row,grid-columnand their longhands; auto-placement,sparseanddense;grid-auto-rows,-columnsand-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 anHttpClientthe caller supplies or the policy builds. The address check runs in aSocketsHttpHandler.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 theContent-Typesays. - 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>withsrcset(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-imageapplying the EXIF orientation through the placement matrix (M07's rule: four of the eight orientations are mirror images);image-renderingparsed, and/Interpolatenever 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 (FlateDecodewith 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
.Htmlonly: GIF (first frame; an animated one reported), WebP (lossy and lossless), BMP. Our own parser reads the header first and checks the dimensions againstMaxImagePixels; the pixels are written losslessly asFlateDecode, alpha as an/SMask. AVIF, HEIC and JPEG XL are refused withimage.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,dottedwith round dots,double,groove,ridge,inset,outset),border-radiuswith elliptical corners clipping backgrounds and, underoverflow, content; background layers with-size,-position,-repeat(as tiling patterns),-clip,-originand-attachment;outline;print-color-adjust.border-imageis reported. box-shadowon 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; atext-shadowwithout blur drawn as offset text; a blurred one drawn unblurred and reported.opacityas a transparency group (a form XObject with/Group /S /Transparency); alpha colors as/caand/CA;mix-blend-mode,background-blend-modeandisolationas/BMand 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).transformandtransform-originin 2D ascm, affecting painting and overflow, never layout; link areas transformed alike (M12.6).clip-pathwithinset(),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 withXmlReader, DTD processing prohibited, into the same model. Both are drawn by our own renderer onto M08's content builder: shapes and paths, transforms,viewBoxandpreserveAspectRatio, 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 underMaxSvgElements, markers,<image>through the resolver,<a>as links (M12.6). - Text is text.
<text>and<tspan>, with theirx,y,dx,dyandrotatepositioning, 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#idbecomes aGoToto a named destination;http,https,mailtoandtelbecomeURIactions; a relative URL is resolved against the document's base;other.pdf#page=3and#nameddest=becomeGoToRthrough M07's open parameters; ajavascript:URL becomes nothing, reported (ADR 37). One link annotation per line fragment, withQuadPointsand 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 anAFRelationshipfromdata-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/Artifactof 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()registeringIPdfRendereras a singleton with its options;Results.Extensions.Pdf(…)for minimal APIs and aPdfResultfor 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'sHtmlRenderer, which is how a .NET caller compiles a template; the resolver'sHttpClientfromIHttpClientFactory, with the checked connect callback as its primary handler.
Batch generation.
PdfRenderTemplateis compiled once: the user-agent and template stylesheets parsed and indexed, counter styles and@pagerules resolved,@font-facefaces 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
pageandpagescounters, 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
@pagerule that the document's own@pageoverrides only underpreferCSSPageSize;printBackground: falseomits backgrounds, as Chromium does;scalelays out at the page area's width divided by the scale and paints scaled;pageRangeslays out every page and writes the selected ones; headerTemplateandfooterTemplaterendered 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 classespageNumberandtotalPagesbecomecounter(page)and a late-filled total,titlethe document's title,urlits base URL, anddatea value the caller supplies, empty and reported otherwise (invariant 6);outlineasks for bookmarks from headings;taggedis accepted and takes effect with M13;timeoutbecomes a cancellation deadline the caller's token observes;waitForFontsandpathhave 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,-afterand-inside: columnandavoid-column; multicol containers nested, withinMaxBoxDepth.- 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: footnotemoves an element to the page's footnote area;::footnote-callis generated where it was,::footnote-markerbefore its body; thefootnotecounter increments per note and is reset per page when the@pagerule sayscounter-reset: footnote; the@footnotearea inside@pagetakesmax-height, a border (the separator) and padding;footnote-display: block | inline(compactreported).- 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-policydecides —autosplits the note across pages, continuing it at the top of the next page's area;linemoves the call's line to the next page;blockmoves 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
GoTolinks; M13 tags the noteNote, with its/ID, and the callReference, in 1.7 and 2.0 output alike —FENoteand the/Refentry 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
--baseor--allow-origindoes not allow;--jsonprints 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),
html2pdfexists 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
- A walking skeleton, the API baseline and the visual harness. Delivers the
AdCodicem.Pdf.Htmlproject; AngleSharp parsing with scripting disabled and source positions kept; a stub cascade (the user-agent stylesheet'sdisplayandfont-*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.Htmland 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 instatus.md. Proved bypdftotextand PyMuPDF extracting a French paragraph oftests/corpus/sources/contract-fr.htmlexactly;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. - 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.Tokenizeat 0 B allocated per token; the tokenizer and parser in the fuzzing campaign. Leaves meaning. - Selectors 4. Delivers selector parsing, the rule index, the ancestor Bloom filter, right-to-left matching,
specificity,
:is(),:where(),:not(),:has()with memoization andMaxSelectorMatchingSteps, the linguistic and form-state pseudo-classes, the pseudo-elements, page selectors parsed. Proved byChromiumSelectorRefereeTests: over the DOMs of the three corpus sources and a selector fixture set, each selector matches exactly the elements Chromium 141'squerySelectorAllreturns (Playwright, in a container); a:has()bomb stopping at its guard withlimit.selector-cost;CssBenchmarks.Matchat 0 B per element. Leaves the cascade. - Cascade 5, the user-agent stylesheet and presentational hints. Delivers origins, importance, layers,
order,
styleattributes,revertandrevert-layer, inheritance, the HTML Standard's rendering section adapted to print with itsNOTICE, the presentational hints, the legacy aliases. Proved byChromiumComputedStyleRefereeTestson the three corpus sources for the properties typed so far (display, box model, fonts, color): every element's computed values equalgetComputedStyle's under print emulation, normalized to CSS pixels; a layered fixture whose!importantreversal matches Chromium's. Leaves values. - 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 withvar(), 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 oncalc()— simplification never changes the value, over random expressions —; an exponentialvar()chain stopped by its guard. Leaves conditions. - Conditional rules, imports, nesting and the resolver seam. Delivers Media Queries 4 for print,
@supports,@importwith its conditions, cycles and depth guard, CSS Nesting 1,IResourceResolver,ResourcePolicywithdata:,AllowDirectoryandAllowEmbedded, andfile:confinement complete. Proved by the computed-style referee on nested and conditional fixtures; a table of hostilefile:URLs (encoded.., a link out of the root, UNC and device paths, alternate data streams) each refused with the rule it broke; an@importcycle cut and reported. Leaves the declared level. - 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, thecss.*-unsupportedfamily, once per feature and location, and the result'sUnsupportedcounts. Proved byCorpusCssTests.Unsupported_css_degrades_and_says_soon the unsupported-CSS fixture (below); a test that fails when the published page and the table part. Leaves the budgets. - Budgets and the hostile set. Delivers
CssBenchmarkswithMemoryDiagnoser(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, exponentialvar(),:has()chains,@importcycles. 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
- The box tree and block layout. Delivers box generation, anonymous boxes, white space processing,
::beforeand::afterwith strings,attr()and quotes, block formatting contexts, margin collapsing, sizing,overflowclipping, solid borders and background colors,MaxBoxDepth. Proved byChromiumLayoutRefereeTests: the border boxes of block fixtures equalgetBoundingClientRectunder print emulation at the page area's width, within 0.5 CSS px; the WPTcss/CSS2block and margin-collapsing reftests of the pinned subset. Leaves text. - 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-spacingand ligatures,font-synthesis, glyph runs to M08. Proved by a fixture of ligatures, kerning,tabular-nums, small capitals andsupsextracting exactly throughpdftotextand PyMuPDF;pdffontsshowing every face embedded and subset;ShapingBenchmarksat 0 B per cache hit; the corpus invoice's.brand, whoseletter-spacingmust leave "AD CODICEM" without ligatures. Leaves lines. - 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 byLineBreakConformanceTestspassing the whole ofLineBreakTest.txtfor the pinned Unicode version, with CSS's tailorings listed as expected differences; the invoice footer's IBAN in a 40 mm cell breaking underoverflow-wrap: anywhereand never inside a grapheme;CorpusPaginationTestsat 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. - Bidirectional text. Delivers UAX #9 whole,
direction,unicode-bidi,<bdi>,<bdo>,dir="auto", mirroring, box-level direction for blocks and markers. Proved byBidiConformanceTestspassing the whole ofBidiTest.txtandBidiCharacterTest.txt;CorpusTextLayoutTests.Right_to_left_text_extracts_in_logical_orderon 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. - Justification and hyphenation. Delivers
text-align: justifythroughTJ,text-justify,text-align-last, theIHyphenatorseam, the soft inserted hyphen; theAdCodicem.Pdf.Hyphenationsatellite with Liang's algorithm, the trie, lazy loading, caller-supplied patterns; the license ADR and the first languages it clears. Proved byPyphenRefereeTests: 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; aHyphenationBenchmarkswalk at 0 B per word. Leaves pages. - Pages and margin boxes. Delivers
@pagerules, 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_doesfor 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. - 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 WPTcss/css-breakprint reftests of the pinned subset; the report source paginated as Chromium paginateschromium-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, eachh2kept 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. - 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 WPTcss/CSS2float 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. - Counters, counter styles and generated content. Delivers counters,
counters(),@counter-stylewith every system, the predefined styles,::markerand list styles,quotes, the page context's counters and their resets. Proved by the WPTcss/css-counter-stylesandcss/css-listsreftests of the subset; a contract clause fixture whose "7.2.a" numbering extracts exactly; roman page numbers on front matter. Leaves running content. - Running elements and named strings. Delivers
running(),element(),string-set,string(),content(), their per-page policies, shared XObjects for running content, pagination artifacts. Proved byCorpusPagedMediaTests.Running_headers_follow_the_sectionon 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 /Headeron every page. Leaves totals. - 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 byCorpusPagedMediaTests.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. - A page at a time, measured. Delivers
HtmlSource.FromChunkswith its one-chunk look-ahead andcss.streamed-selector-approximated, DOM subtrees released behind the writer on the single-string path,LayoutBenchmarks, the memory checkpoints, the remote row. Proved byCorpusHtmlTests.Generating_a_thousand_pages_holds_memory_flaton both paths;LayoutBenchmarksat 0 B per line once the faces are warm;CorpusHtmlTests.The_us_code_sets_in_flat_memoryon a greenRemote corpusrun; M12.2's acceptance rows. Leaves tables.
M12.3 — Tables
- 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-collapseand 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 WPTcss/css-tablesreftests of the subset. Leaves automatic layout. - 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_doeson 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. - Tables across pages. Delivers row breaks, repeated
theadandtfoot, rows kept whole, split rows and row spans, borders at fragment edges,directionin tables. Proved byCorpusTableTests.Header_rows_repeat_on_every_pageon the report's measures table set in a narrower page so that it spans two, aslibreoffice-report-fr.pdfrepeats 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;pdftotextpage by page. Leaves subtotals. - Carried-forward subtotals. Delivers
-adc-sumand the-adc-sum()function, decimal parsing and formatting, the footer laid out after the page's rows. Proved byCorpusTableTests.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. - Shrink-to-fit. Delivers
-adc-shrink-to-fiton 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. - Right to left, structure hooks and budgets. Delivers mirrored column order under
direction: rtl, the structure sink's table calls for M13,TableBenchmarkswithMemoryDiagnoser. 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
- Single-line flexbox. Delivers flex items, the flexible length resolution,
justify-content,align-*, auto margins,order,gap, automatic minimum sizes, intrinsic sizes. Proved byChromiumLayoutRefereeTests.Flex_containers_lay_out_as_chromium_doeson the corpus invoice'sheaderand.partiesand on fixtures (within 0.5 CSS px); the WPTcss/css-flexboxreftests of the subset. Leaves wrapping. - Multi-line and column flexbox. Delivers
flex-wrap,align-content, baseline alignment, column direction, nesting,directionreversing main-start. Proved by the same referees on wrapping and column fixtures. Leaves pages. - 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.
- Grid tracks and placement by name. Delivers explicit tracks,
fr,minmax(),repeat()withauto-fillandauto-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 WPTcss/css-gridreftests of the subset. Leaves auto-placement. - Auto-placement and implicit grids. Delivers
sparseanddenseplacement, 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. - 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
- The network resolver. Delivers
AllowOrigin, the checked connect callback, the address classes, manual redirects, streamed size bounds, time bounds, content sniffing, thelimit.resource-*guards,CachingResourceResolver. Proved bySsrfRefereeTestsin 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;CorpusResourceTestsfor each refusal's rule and location. Leaves images. - Raster images passed through. Delivers
<img>,<picture>andsrcset, backgrounds and list images as placed images,object-fit,object-position,image-orientation,image-rendering, M07's containers, deduplication, the excessive-resolution report. Proved byCorpusHtmlImageTests.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 frompdfimages -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. - 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 samplespdfimages -pngreturns 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. - Borders, backgrounds and shadows. Delivers every border style,
border-radiusand its clipping, background layers, tiling patterns,outline,box-shadowas shadings,print-color-adjust. Proved byChromiumRenderingRefereeTestson a fixture page per feature, within the threshold of Chromium's rendering; pikepdf finding shadings rather than images for the shadows. Leaves color. - 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. - 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,/Groupand/BMentries expected; the version raised to 1.4 by transparency and reported. Leaves SVG. - 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):pdftotextfinding every axis label and legend, and each chart within the threshold of Chromium's rendering; the WPTsvgpainting reftests of the subset; a<use>bomb stopped bylimit.svg-elements. Leaves barcodes. - Barcodes in HTML. Delivers the
barcode:scheme through M10'sBarcodeSpecification, intrinsic sizes, alternative text kept. Proved byCorpusHtmlBarcodeTests.The_qr_bill_reads_as_pdfbox_writes_it: the Swiss QR-bill payment part rebuilt in HTML with the SIX specimen data ofvendor/swissqrbill/pdfbox-swiss-qr-bill-a4.pdfis read by zxing-cpp, from our page rasterized bypdftoppmand by MuPDF, to the exact payload it reads from PDFBox's page; an EPC QR on the invoice read back exactly. Leaves the budgets. - Budgets. Delivers
ImageBenchmarksandPaintBenchmarkswithMemoryDiagnoser, 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
- Links and named destinations. Delivers link annotations per line fragment with
QuadPoints, internal, external and remote targets, refusedjavascript:, named destinations as a sorted name tree. Proved byPdfJsNavigationRefereeTests(pinnedpdfjs-distunder Node): each of the report's eight table-of-contents links resolves to the page and position of its heading, as the eight links ofchromium-report-fr.pdfresolve through its/Dests; PyMuPDF'sget_links()agreeing; M06's link check finding no dangling destination. Leaves bookmarks. - Bookmarks. Delivers the three
bookmark-*properties, the user-agent defaults, forward writing through M07's outline model. Proved by pdf.js'sgetOutline()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. - Cross-references and the table of contents. Delivers
target-counter(),target-counters(),target-text()andleader()on the orchestrator. Proved byCorpusPagedMediaTests.Table_of_contents_page_numbers_are_trueon the paged-media report: every page number in its table of contents is the page on whichpdftotextfinds 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. @font-face. Deliverssrclists,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 twounicode-rangehalves:pdffontslists the two subsets and the text extracts exactly; aurl()font on an origin not allowed refused, the text drawn by the fallback and both reported. Leaves metadata.- 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 thepagecounter. Proved by pikepdf reading the report's/Titleand/Lang fras Chromium writes them inchromium-report-fr.pdf, with no/CreationDateunless supplied; pdf.js'sgetPageLabels()giving roman then decimal labels on a document with front matter. Leaves attachments. - Attachments and PDF pages as images. Delivers
rel="attachment"at both levels withAFRelationship, PDF pages as<img>,<object>and@pagebackgrounds through M06's copier, background artifacts, reported losses. Proved byCorpusNavigationTests.Attachments_declared_in_html_are_byte_identical: thefactur-x.xmlcarried bydocuments/invoice/qpdf-invoice-with-facturx-xml.pdf, taken out by M06 and attached from the invoice's HTML withAFRelationship Data, comes back byte for byte fromqpdf --show-attachment; the letterhead (below) under the invoice —pdftotextfinding the letterhead's text and the invoice's, pikepdf one XObject for the letterhead page on every page. Leaves the API. - The public API and ASP.NET Core. Delivers
IPdfRenderer,PdfRenderer,PdfRenderOptions,HtmlSource,PdfRenderResult, progress and cancellation,RenderToPartfor M06's assembly, andAdCodicem.Pdf.AspNetCore. Proved by the API baseline; a minimal API test throughWebApplicationFactoryreturning the invoice as a PDF thatqpdf --checkaccepts; a Razor component rendered throughHtmlRenderer; 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. - 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 byCorpusBatchTests.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. - Puppeteer's options. Delivers
PuppeteerPdfOptions, the mapping, header and footer templates in the margin areas, the published table. Proved byPuppeteerRefereeTests: 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 pagespageRangesselects;compat.option-ignoredfor each option without meaning. Leaves stamps. - Stamps from an HTML fragment. Delivers
HtmlStampinto a target document's fonts and resources, the field placeholders for M09's variable part, the XObject reusable as an annotation appearance. Proved byCorpusHtmlStampTests(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. - The
html2pdfverb, 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.pyrenders them by calling thehtml2pdfverb of the tool built from the same commit (dotnet run -c Release --projectthe tool, with the SDK the session hook installs), as it calls Chromium and LibreOffice; the manifest records the engine's version as the producer, anddocs/corpus.mdgains our engine as a generator. Proved byCorpusToolTests.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
- 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 inchromium-report-fr.pdf's page 4, extraction bypdftotextand PyMuPDF in source order; the WPTcss/css-multicolreftests of the subset. Leaves pages. - 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. - Footnote calls, markers and the area. Delivers
float: footnote, the two pseudo-elements, the counter and its page reset, the@footnotearea with its separator,footnote-display, links both ways. Proved byCorpusFootnoteTests.Each_note_sits_on_the_page_of_its_callon the legal memorandum (below): each note's text on the page of its call (pdftotextpage by page), numbering reset per page, each call and note linked both ways in pdf.js. Leaves pressure. - 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.Footnotesat 0 B per re-fit. Leaves the close. - 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 byHtmlVisualRefereeTests.Reference_documents_match_their_approved_imagesand 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;@supportsanswers from the table;@importcycles 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;
TJjustification; 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-alignvalues; 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,fallbackandrange; 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;
HtmlStampinto 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, asReaderLimitsTestsdoes 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 —pdftoppmas 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-distunder Node) — outlines, destinations, page labels, annotations, as a viewer sees them. - qpdf —
--checkon 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-maskingandsvg/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 intests/corpus/sources/third-party/<name>/, pinned, beside its license and aSOURCEfile, asdocs/corpus.mdplaces inputs written by others. - The Unicode conformance files of the version
CharUnicodeInfoimplements:LineBreakTest.txt,GraphemeBreakTest.txt,BidiTest.txtandBidiCharacterTest.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
| Documents | Behavior | Verified by |
|---|---|---|
| The reference sources: the invoice, the multi-page report, the contract, the two-column report | Every page within the regression threshold of its approved reference image | HtmlVisualRefereeTests.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 it | CorpusPaginationTests.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 entries | Byte-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 string | Streamed: 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 writer | CorpusHtmlTests.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 template | Invoices 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 sequential | CorpusBatchTests.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 selector | The render completes; exactly one diagnostic per feature, with its location; every page otherwise within the threshold of the invoice's approved image | CorpusCssTests.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 it | Nothing 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 empty | CorpusResourceTests.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 source | Memory at the 1,000- and 9,000-page checkpoints within the flat budget | CorpusHtmlTests.The_us_code_sets_in_flat_memory |
M12.1 — CSS engine
| Documents | Behavior | Verified by |
|---|---|---|
| The reference sources | No 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 pixels | CorpusCssTests.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 sources | The same agreement for every declared property; each feature outside the level reported once | ChromiumComputedStyleRefereeTests.Framework_stylesheets_cascade_as_chromium_computes |
| The DOMs of the reference sources, and the selector fixture set | Each selector matches exactly the elements querySelectorAll returns | ChromiumSelectorRefereeTests.Selectors_match_the_elements_chromium_matches |
| css-parsing-tests | Passes whole | CssSyntaxConformanceTests.Css_parsing_tests_pass_whole |
| The hostile set of slice 8 | No untyped exception, hang or allocation an input chose; each guard reported under its own code | HostileCssTests.Hostile_stylesheets_end_in_a_render_or_a_guard |
M12.2 — Block and inline layout, text and pagination
| Documents | Behavior | Verified by |
|---|---|---|
report-fr.html, against chromium-report-fr.pdf | Sections 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 cause | CorpusPaginationTests.Headings_stay_with_their_text_as_in_chromium |
contract-fr.html, against chromium-contract-fr.pdf | Two 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 cause | CorpusPaginationTests.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 faces | pdftotext 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 faces | CorpusTextLayoutTests.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 only | Extracted 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-all | CorpusTextLayoutTests.Every_script_in_the_corpus_extracts_as_written |
| The Unicode conformance files | Passed whole | LineBreakConformanceTests.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 width | Hyphenation points equal Pyphen's from the same patterns; the extracted text, soft hyphens removed, equals the source's; no hard hyphen added | CorpusHyphenationTests.Hyphenated_text_extracts_whole, PyphenRefereeTests |
| The paged-media report — not in the corpus | Its 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 pass | CorpusPagedMediaTests.Page_totals_are_filled_late_in_one_pass |
| Documents from the test grammar (FsCheck) | Text conserved across fragments: nothing lost or doubled at a break | LayoutPropertyTests.Text_is_conserved_across_fragments |
M12.3 — Tables
| Documents | Behavior | Verified 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 row | The header row repeated at the top of the second page; no row split | CorpusTableTests.Header_rows_repeat_on_every_page |
| The tables of the reference sources — the invoice's line items and totals, the report's measures | Every column's width equal to Chromium's within 0.5 CSS px | ChromiumLayoutRefereeTests.Tables_lay_out_as_chromium_does |
| The multi-page invoice — not in the corpus | Header 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's | CorpusTableTests.Carried_forward_subtotals_add_up |
| A 1,000-page table from a streamed source | Retained memory within the flat budget | CorpusTableTests.A_thousand_page_table_holds_memory_flat |
M12.4 — Flexbox and grid
| Documents | Behavior | Verified by |
|---|---|---|
invoice-fr.html's header and .parties, both flex containers | Every item's border box equal to Chromium's within 0.5 CSS px | ChromiumLayoutRefereeTests.Flex_containers_lay_out_as_chromium_does |
| The WPT flexbox and grid selections | Every test not on the expectations list passes; the list only shrinks | WptReftestTests.Selected_reftests_pass_but_the_expected_failures |
| The card-list and grid-statement fixtures over several pages | No item split unless taller than a page; order kept; text conserved; memory flat at 1,000 pages | CorpusFlexGridTests.Flex_and_grid_items_fragment_whole, CorpusFlexGridTests.A_thousand_page_grid_holds_memory_flat |
M12.5 — Images, SVG, visual CSS and resources
| Documents | Behavior | Verified 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 size | CorpusHtmlImageTests.Jpeg_images_pass_through_byte_for_byte |
| M07's photographs in all eight EXIF orientations — not in the corpus, M07's need | Upright, as Pillow's transposed decoding shows them | CorpusHtmlImageTests.Photographs_stand_upright |
The payment part of vendor/swissqrbill/pdfbox-swiss-qr-bill-a4.pdf rebuilt in HTML with its SIX specimen data | zxing-cpp reads, from our page rasterized by pdftoppm and by MuPDF, exactly the payload it reads from PDFBox's page | CorpusHtmlBarcodeTests.The_qr_bill_reads_as_pdfbox_writes_it, ZxingRefereeTests |
| The chart set — not in the corpus | pdftotext finds every axis label and legend entry; each chart within the threshold of Chromium's rendering | CorpusSvgTests.Charts_keep_their_text, ChromiumRenderingRefereeTests |
| The visual-effects fixture pages — borders, radii, shadows, gradients, opacity, blend modes, transforms, color spaces | Within the threshold of Chromium's rendering | ChromiumRenderingRefereeTests.Visual_effects_render_as_chromium_does |
| The invoice set on 1,000 pages | One image XObject for the logo, one shading per distinct shadow | CorpusHtmlImageTests.An_image_used_on_every_page_is_written_once |
| The hostile templates (above) | As in M12 as a whole | SsrfRefereeTests.Hostile_templates_reach_no_address |
M12.6 — Navigation, web fonts, metadata, the public API
| Documents | Behavior | Verified by |
|---|---|---|
report-fr.html, against chromium-report-fr.pdf | Its 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 clock | PdfJsNavigationRefereeTests.Links_land_where_chromium_lands, CorpusNavigationTests.Metadata_comes_from_the_html |
| The paged-media report — not in the corpus | Every 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 reason | CorpusPagedMediaTests.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 HTML | qpdf --show-attachment returns it byte for byte; AFRelationship is Data; its MIME type text/xml | CorpusNavigationTests.Attachments_declared_in_html_are_byte_identical |
The letterhead — not in the corpus, M09's need — as the invoice's @page background | pdftotext finds the letterhead's text and the invoice's; one XObject for the letterhead page shared by every page, marked as a background artifact | CorpusNavigationTests.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 after | CorpusHtmlStampTests.Html_stamps_keep_what_they_stamp, VeraPdfRefereeTests |
| The Puppeteer probes — not in the corpus — rendered by us from the same options | Page sizes as the options say; header and footer text within 1 pt of Chromium's positions; the pages pageRanges selects; every option without meaning reported | PuppeteerRefereeTests.Options_map_as_chromium_prints_them |
| Every committed rendering, through the tool | html2pdf gives the API's bytes, from the AOT binary and from the dotnet tool | CorpusToolTests.Html2pdf_matches_the_api |
M12.7 — Footnotes and multi-column layout
| Documents | Behavior | Verified by |
|---|---|---|
The two-column report: report-fr.html's annex, against page 4 of chromium-report-fr.pdf | Two columns; column two begins within one line of where Chromium begins it; pdftotext and PyMuPDF read the annex in source order | CorpusColumnTests.The_annex_sets_in_two_columns_as_chromium_does |
| The terms of sale — not in the corpus | Three columns over two pages, spanning headings, balanced last fragment, text conserved, rules only between columns with content | CorpusColumnTests.Columns_continue_across_pages |
| The legal memorandum — not in the corpus | Each 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 reason | CorpusFootnoteTests.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-transformon accented capitals,letter-spacingon the brand),report-fr.html(a linked table of contents, a table withdisplay: table-header-group, legacypage-break-*, acolumn-count: 2annex) andcontract-fr.html(justified serif text,page-break-after: avoid, a flex signature block); their Chromium 141 renderingsdocuments/invoice/chromium-invoice-fr.pdf,documents/report/chromium-report-fr.pdf(featuresinternal-linksandtwo-column-text; eight link annotations, a/Destsdictionary,DisplayDocTitle,/Lang fr, a structure tree) anddocuments/contract/chromium-contract-fr.pdf; LibreOffice'slibreoffice-invoice-fr.pdf,libreoffice-report-fr.pdf(three pages; the measures table runs onto page 3 with its header row repeated) anddocuments/archival/libreoffice-report-pdfa2b.pdf; Word'sword-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; featureweasyprint), 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 anyTf), 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). Featureshtml-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 (remoteindesign-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-charactersandsupplementary-plane-charactersin remotehandwritten-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, remoteabledocs-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(remoteafp-batch-processor-bank-statement-2b.pdf),vector-table-gridanddashed-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-multiplyandtransparency-group-form(remoteindesign-cs6-pdfua1-brochure.pdf),isolated-transparency-group-form(Kraxi),page-transparency-group(two Factur-X invoices),smaskandimage-with-smask, andduplicated-image— remotesap-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.pdfand theaf-relationship-*features. - Scale:
documents/stress/reportlab-journal-1000-pages.pdf; remoteus-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).
| Need | Why | Priority | Likely 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 it | Chromium 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 run | 1 | Generated 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 invoice | The long-invoice case — the one ERP users complain about — is absent; the corpus invoice is one page | 1 | Generated 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 imitates | 2 | A 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 string | The memory acceptance needs a document whose length is a parameter | 1 | Generated here: a script in tests/corpus/build from report-fr.html's sections |
| One thousand invoice records, seeded, as JSON Lines | The throughput acceptance needs a batch; varied line counts exercise pagination | 1 | Generated 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 committed | The 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 that | 1 | Generated 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 it | Hyphenation has no real rendering to be compared with; the referee is Pyphen alone | 2 | Generated 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 rendering | M12.5's SVG acceptance needs the SVG real tools produce: nested groups, transforms, clip paths, text | 1 | Generated 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 band | The @page background and <img src=…pdf> rows need the business case they exist for (also M09's need) | 1 | Generated 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 Word | M12.7's footnotes have no source; Word's own footnote layout is the one French legal readers expect | 1 for the source and WeasyPrint's rendering, 2 for Word's | Generated 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 rendering | The standard back-of-invoice case for M12.7, beyond the report's short annex | 1 | Generated 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 names | The mapping's acceptance compares with what Puppeteer really prints | 1 | Generated 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 use | 2 | A public source, each license checked before use as ADR 23 requires |
| The three reference sources rendered by WeasyPrint and committed | A second engine's output of the same content, committed rather than remote (the WeasyPrint files in the corpus are ShareAlike or unlicensed, and remote) | 2 | Generated 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 profile | M12.5's Skia path and M07's pass-through need files, not streams inside PDFs; orientation is already M07's need | 2 | Generated 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: strict | W09 still lacks Korean; CJK line-breaking strictness has no real text | 3 | A contribution (W09); a public source under an open license |
| Output of commercial paged-media engines (Prince, PDFreactor, Antenna House) on the paged-media report | A second peer for GCPM semantics where WeasyPrint and the specification leave room: footnotes, page groups | 3 | A 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: fixedrepeats 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.pdfrepeats no table header. Its measures table fits on page 3; the repetition is inlibreoffice-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.
Twjustifies nothing in CID fonts. It applies to the single-byte code 32 only; every font M08 embeds is two-byte. Justification goes throughTJ, 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-spacingbreaks ligatures on purpose. CSS Text 3 says optional ligatures are not formed when letters are spaced — the invoice's.brandis spaced.text-transformchanges what extraction returns. "Émetteur" in the invoice is drawn "ÉMETTEUR"; extraction returns the capitals, as Chromium's output does; language-sensitive mappings (Turkish, German ß) followlang.- 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.@pagemay sit inside@media; answeringwidthfrom@pagewould 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
ReservedCounterDigitsdigits; 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
HttpClientconnects 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
oklabgradient 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 undersamples/, 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, theHttpClient.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: thecss.*,html.*,resource.*,layout.*,image.*,paint.*andcompat.*codes;docs/website/docs/reference/render-limits.md(new):HtmlRenderLimits, each guard and its code.docs/website/docs/reference/tool/index.md: thehtml2pdfverb;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 —.Htmldepending 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-resourcesbrought 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—.AspNetCorejoins 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.
-
IResourceResolverand the deny-by-default policy exist,file:confined; the CSS guards are classified, documented and tested reached, raised and thrown. -
CssBenchmarksrecord 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-spacingdisables 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 andpage-orientationare 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-pathand 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-faceloads WOFF2 withunicode-rangethrough 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 andHtmlStampare public, documented and in the API baseline. - The throughput budget of a thousand invoices is enforced in CI.
-
html2pdfgives 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 corpusrun recorded instatus.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,HyphenationBenchmarksandBatchBenchmarksrun withMemoryDiagnoser; the memory and throughput budgets are enforced in CI and recorded instatus.md. - The documentation site publishes the pages above;
architecture.md,NOTICE,features.jsonandstatus.mdare brought in line. - Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).