Skip to main content

M25 — Rasterization

State: to do — Depends on: M15, M22 — Skia placed by ADR 43; a satellite after the foundations by ADR 16; every pixel of a scan decoded by the codecs of ADR 42

Goal​

Render any page the reader opens to pixels — previews, thumbnails, M24's visual comparison and the visual tests of every later milestone — through the interpreter extraction reads with and the decoders M22 wrote, drawing what a viewer shows for a given intent, hidden layers and annotation flags included, with memory that follows the page and the raster rather than the file.

Since M09 every milestone that changed what a page looks like has asked MuPDF or pdf.js, in a container, whether it still looks right, because the library could not look at a page itself. A case file needs more than a verdict in CI: a thumbnail of each piece for the person who checks the bordereau, a preview for a portal upload, the visual diff of a contract returned for signature, and — M04's open case — the one change only a rendering sees, a field appearance laid over signed text. The failures this milestone exists to prevent are the ordinary ones: a JBIG2 scan drawn as a black page because nothing decoded it; a watermark layer that is off by default drawn anyway, or an OCR layer drawn in black over the scan it transcribes; a 35,000-pixel CCITT page that decodes to 153 MB taking five gigabytes once expanded to RGBA; a Type 3 font that reaches itself hanging the thread; an embedded font program handed to a native font parser — the attack surface ADR 42 keeps away from image codecs — or to the system's font manager, so that Windows and Linux render the same page differently; a page rendered without the /Rotate or the /CropBox it inherits.

Scope​

In:

  • the AdCodicem.Pdf.Rendering satellite, depending on the core, AdCodicem.Pdf.Imaging and SkiaSharp — its CPU raster backend only, and on Linux its NoDependencies native assets, since Skia's font manager is never used (below);
  • the device seam made public: M15's IPdfContentDevice, internal until its first outside consumer, becomes public in the core; and the core's page-raster seam, IPdfPageRasterizer and PdfRaster — which M22 put beside it for OCR and M24's visual comparison consumes —, implemented here, so that the satellite depends on neither AdCodicem.Pdf.Compare nor the OCR orchestration, and M22's PdfOcr rasterizes through it the pages that are not one image (below);
  • a Skia device that paints everything the interpreter reports: paths filled and stroked with the whole line state, clipping by paths and by text, the eight text rendering modes, glyphs from outlines the core reads (TrueType, CFF, Type 1 and CID-keyed programs, Type 3 procedures through the interpreter), images and stencil masks decoded by M22 and downsampled while they are decoded, every color space through M22's evaluation, the seven shading types, tiling patterns, transparency groups, soft masks, the sixteen blend modes;
  • intents: View (the default) and Print, through M11's PdfLayerVisibility for optional content and the annotation flags for annotations; annotation and widget appearances placed as M15 places them, the appearance state honored, missing appearances generated in memory through M11's and M16's generators on request, never written;
  • page geometry: the box rendered (CropBox by default, through inherited resolution, clipped to the MediaBox), /Rotate inherited, UserUnit, a resolution in dots per inch or a target size, a paper or transparent background;
  • band rendering for rasters too large to hold, rows streamed to a sink; memory bounded by the band and by the images at device scale, whatever the page;
  • outputs: the raw raster (Gray8, Rgb24, Rgba32), PNG through the core's encoder, JPEG and WebP through Skia's encoders on request; thumbnails, the page's embedded /Thumb first when asked;
  • M24's visual comparison run in-process on these rasters, its visual row closed here on our own rasterizer;
  • the command-line tool's render verb, and compare --visual without a raster supplied by the caller;
  • a report per render: fonts substituted, images decoded and at what scale, colors approximated, features approximated, each under a render.* code.

Out, explicitly:

  • color-managed conversion — ICC transforms for ICCBased spaces beyond their alternate, output intents simulated, overprint simulated, separation previews — M29, whose managed color-management satellite plugs into the converter seam defined here; until then ICCBased, Lab and CMYK content are converted by M22's evaluation and reported (render.color-approximated);
  • transparency flattening and rasterizing a page into the document (a "print as image" redaction, or PDF/A-1 conversion) — not planned (M19 and M20 say so); M25 renders, nothing writes the raster back;
  • vector outputs (SVG, XPS, EMF) and GPU backends — not planned;
  • printing — spooling to a printer — not planned; the Print intent renders what a printer would receive;
  • executing JavaScript before drawing, and rendering dynamic XFA — never (ADR 37): a dynamic XFA form renders its placeholder page, as its PDF says; 3D, screen and rich-media annotations render their appearance, the poster;
  • text shaping of any kind: a page's glyphs are already chosen; generating a missing appearance for Arabic or Hebrew text is M11's and M16's simple path, reported by them (text.shaping-required);
  • the core in browser WebAssembly is M23's; SkiaSharp's WebAssembly assets are not tested here.

Design​

Where it lives​

PartWhereWhy
IPdfContentDevice, the interpreter's state it receives, the glyph outline sinkCore, Content/ and Fonts/, made publicM15 wrote them; M19 extends them; a device outside the core needs them public, and nothing about them needs Skia
IPdfPageRasterizer, PdfRasterCore, Content/, where M22 put themCompare and the OCR orchestration depend on the core alone, and Rendering must depend on neither; all three meet at the core's seam
IPdfColorConverterCore, Graphics/, beside M22's color evaluationM29's color-management satellite implements it without SkiaSharp; declared in .Rendering, it would drag Skia into that satellite
The Skia device, the rasterizer, bands, thumbnails, the encodersAdCodicem.Pdf.RenderingSkiaSharp is allowed here and never in the core (ADR 43)
JBIG2, JPEG 2000 and JPEG decodingAdCodicem.Pdf.Imaging (M22)ADR 42; CCITT stays in the core
Color spaces and functions evaluatedM22's evaluation, where M22 put itOne evaluator for image export, redaction of pixels and rendering
The OFL faces used for substitutionAdCodicem.Pdf.Fonts, where M08 puts the set (decided 2026-09-27), Liberation Sans regular in the coreADR 11: never the system's fonts

The satellite takes SkiaSharp's native library and no other native code. It never asks Skia for a typeface, a font manager or an image codec: glyph outlines come from the core, pixels from M22. Skia draws paths, images and layers into a raster — the part of it that is small, fuzzed by its own authors and deterministic per version.

The rasterizer​

PdfRasterizer thread-safe engine (invariant 8); implements IPdfPageRasterizer
Render(page, PdfRasterOptions, cancellationToken) -> PdfRaster (pooled, disposable)
RenderRows(page, options, IPdfRasterRowSink, progress, token) -> PdfRasterReport, rows streamed in order
RenderThumbnail(page, maxWidth, maxHeight, options, token) -> PdfRaster
PdfRasterOptions immutable: resolution (dpi, or fit into a box), box (Crop by default, Media, Trim,
Bleed, Art), intent (View, Print), layer configuration (M11's, /D by default), annotations
(All, None, a filter; widgets; popups never), missing appearances (Skip, GenerateInMemory),
background (paper white, transparent), pixel format, anti-aliasing (text, paths, images),
image smoothing (honor /Interpolate, always, never), limits (below)
PdfRaster (core) width, height, stride, format, a pooled buffer, the page frame it covers
IPdfRasterRowSink receives rows in order, a band at a time; the PNG writer is one
PdfRasterEncoder Png(raster or rows) through the core's encoder; Jpeg(raster, quality) and
WebP(raster, quality) through Skia's, lossy and on request only
PdfRasterReport per page: the render.* diagnostics, fonts substituted, images decoded and the scale they
were decoded at, colors approximated, features approximated, bands, peak pooled bytes

A render opens nothing: the caller opens the document, as for every operation, with its PdfReaderOptions; the engine holds no per-document state between calls but a bounded glyph cache keyed by the document's identity, which the caller may disable. Two threads may render two documents at once; one document is rendered by one thread at a time, as PdfDocument is not thread-safe. M03's cancellation and progress convention applies, stage Rendering, units the bands.

The device​

The interpreter (M15) runs the page, its forms, patterns, Type 3 glyphs and annotation appearances; the device receives what they paint, already in user space with the CTM, and draws it into an SKCanvas over a raster SKSurface for the current band.

  • Paths: every segment of m l c v y h re; fill with the non-zero or even-odd rule; stroke with width, cap, join, miter limit and dash array and phase, in user space transformed by the CTM, so that a non-uniform CTM gives the elliptical pen the specification's stroking rules ask for; a zero width drawn as the thinnest line the device can render, one pixel, as §8.4.3.2 requires; stroke adjustment (SA) honored by snapping axis-aligned strokes to the pixel grid; a dash array of zeros drawn solid, as viewers do, and reported.
  • Clipping: W and W* intersect the clip at the next painting operator; clip by text (modes 4 to 7) accumulates the glyphs' outlines until ET; the clip is part of the state M15 saves and restores, and the device mirrors it with Save and Restore on the canvas, never by recomputing it.
  • Text: each glyph's outline, transformed by the text rendering matrix, filled, stroked or both by its mode; mode 3 draws nothing; mode 7 only clips. Horizontal scaling, rise, character and word spacing are the interpreter's; the device receives positioned glyphs.
  • Images and stencil masks (below); inline images as images, through M08's reader.
  • Shadings: types 2 and 3 (axial, radial) as Skia gradients when their function sampled into stops equals the function at every stop within half a color step — otherwise, and for type 1 (function-based), evaluated per device pixel in the band; types 4 to 7 (free-form and lattice triangle meshes, Coons and tensor patches) subdivided into triangles no larger than a device pixel's flatness, drawn with Skia's per-vertex colors; sh and shading patterns both; /Extend, /Background (for patterns only, as the specification says) and /BBox.
  • Tiling patterns: colored and uncolored; one cell rendered into an image at device resolution, then tiled by an image shader with the pattern matrix — never cell by cell, so that a cell a point wide over a page costs one cell's rendering; a cell smaller than a device pixel is filled with its average color and reported.
  • Transparency: constant alpha (CA, ca), the sixteen blend modes of §11.3.5 mapped to Skia's SKBlendMode of the same definition — an unknown mode is Normal, as the specification says, and reported — isolated groups as layers, non-isolated groups drawn straight onto the backdrop when their opacity and blend mode make that exact, and otherwise as layers initialized from the backdrop, reported where that departs from the specification's group compositing, soft masks by luminosity (with /BC and /TR) and by alpha, from SMask in the graphics state and from an image's /SMask; the page group's color space for blending when it is gray or RGB, CMYK groups blended in RGB and reported. Knockout groups are drawn as non-knockout and reported (render.knockout-approximated): Skia has no knockout compositing, business documents rarely carry one, and an exact implementation costs a layer per object.
  • Transfer functions and halftones — TR, TR2, HT — are device-dependent: TR is applied as a color filter when it is an identity or a sampled function, halftones are ignored, as screen viewers do, and both are reported once per document.
  • Overprint (OP, op, OPM) is ignored in RGB output, as viewers do by default; its simulation is M29's.

Glyphs are ours​

  • Outlines come from the core. M08 reads glyf and loca and interprets Type 2 charstrings; M15 reads Type 1 programs. This milestone adds, in the core's Fonts/, an outline sink those readers emit into: TrueType quadratic contours and composite glyphs (walked iteratively, as M08 already does, with their component matrices), CFF and CID-keyed CFF charstrings (FDSelect and /FontMatrix per font dictionary), Type 1 charstrings with their seac accents, flex and hint replacement read and hints ignored. M08's guard, MaxCharstringOperations, bounds the charstring work.
  • No embedded font program ever reaches a native parser, and no system font is ever consulted: FreeType, DirectWrite and CoreText are reached through Skia's font manager only, which is never created. Hostile fonts stay in the managed, bounded, fuzzed readers the core already has, and a page renders the same on every system.
  • No hinting. Outlines are drawn unhinted and anti-aliased, as MuPDF and pdf.js draw them. A few TrueType faces — Chinese ones above all — assemble their glyphs with hinting instructions; a font whose glyphs collapse without them is detected by its name and its fpgm size, drawn as it is and reported (render.font-needs-hinting), since interpreting TrueType bytecode is a virtual machine of its own.
  • Non-embedded fonts are substituted from the registry and the OFL set by the descriptor's flags, weight and name — sans, serif, monospace, italic, bold —, each glyph scaled horizontally to the width /Widths gives, as viewers do, and reported once per font (render.font-substituted). A symbolic font that cannot be substituted draws nothing and is reported; .notdef is drawn as the font draws it — the box PDF24 draws for every capital E is a box here too (documents/invoice/pdf24-invoice-fr.pdf): a rasterizer shows the page, it does not repair it.
  • A glyph cache per document and font holds outlines in font units, bounded in bytes (an option) and evicted least recently used; transformed paths are never cached, since the text matrix varies.

Images​

  • Decoding goes through M22: CCITT in the core, JBIG2 (with its globals), JPEG 2000 and JPEG in Imaging; Flate, LZW and the other filters through M01. An image that cannot be decoded is drawn as nothing — never as a black or gray box, which a reader would take for content — and reported (render.image-undecodable) with the object.
  • Decoded at the scale it is drawn at. The image's device footprint — its unit square through the CTM and the device matrix — gives the number of device pixels it covers. When the samples outnumber them by a factor of two or more, rows are box-filtered into the smaller size while they are decoded, and JPEG and JPEG 2000 are asked for the reduced resolution their formats offer (DCT scaling by 1/2 to 1/8, JPEG 2000 resolution levels) when M22's decoder provides it. The 35,000-pixel CCITT square decodes into a few thousand rows of a few thousand gray pixels, never into 1.2 gigapixels.
  • Masks: /ImageMask stencils painted with the fill color (an uncolored pattern included); /Decode honored, for masks and for samples; /Mask as an explicit stencil or as a color-key range; /SMask and /SMaskInData for JPEG 2000; /Matte un-premultiplied as §11.6.5.2 says.
  • Smoothing: /Interpolate true draws with bilinear filtering, false with nearest neighbor when enlarging and the box filter when reducing; an option overrides both.
  • Decoded images are cached per document and object, at the scale decoded, bounded in bytes — so that a scan is decoded once for all the bands of its page, and a logo once for all the pages that draw it.

Color​

Every color space goes through M22's evaluation to 8-bit sRGB: DeviceGray, DeviceRGB and DeviceCMYK by the formulas M22 records, CalGray and CalRGB through their white point and matrix, Lab through its white point to sRGB, ICCBased by its /Alternate or, without one, by the device space of its /N, Indexed through its base, Separation and DeviceN through their tint transforms (All and None as §8.6.6.4 defines them), functions of types 0, 2, 3 and 4 by M22's evaluator, the type 4 calculator under its bounded stack and budget. Everything that is approximated — an ICC profile not applied, a CMYK conversion without a profile — is reported once per document, not per object. The conversion is a seam, IPdfColorConverter, declared in the core (the table above), which M29's color-management satellite replaces; the device calls it per color set and per image row, never per pixel through an interface.

Intents, layers and annotations​

  • Optional content is decided by M11's evaluator for the intent's event (View or Print) and the configuration the options name, /D by default. The interpreter still runs hidden content, since its state operators change what follows (M11's trap), and its paints reach the device flagged hidden: the device drops them.
  • Annotations are drawn after the page content, in /Annots order, each by its normal appearance — the /AS state's in a subdictionary —, placed by §12.5.5's algorithm as M15 places it. The flags decide: Hidden never; NoView not under View; under Print, only those with Print; NoZoom and NoRotate as M11 draws them; an annotation's /OC through the evaluator. Popups are viewer furniture and never drawn; widgets are annotations like the others.
  • Missing appearances: nothing is drawn, and the annotation is listed (render.appearance-missing), unless the options ask GenerateInMemory — then M11's EnsureAppearances and M16's field appearances build one into a scratch change set that is discarded after the page, never written (render.appearance-generated). A form with NeedAppearances true gets its widgets' appearances generated under that option, as viewers regenerate them.
  • Signature appearances are appearances: Acrobat's legacy layered ones (n0 to n4) are nested forms and draw as their forms say.

Geometry and bands​

  • The page frame: the chosen box, through inherited resolution and clipped to the MediaBox; an inverted box normalized; /Rotate inherited, normalized modulo 360 and applied clockwise — a value not a multiple of 90 is read as 0, as pdf.js reads it, and reported (render.rotate-invalid); UserUnit scaling user space. The raster is ceil(width × dpi / 72 × UserUnit) pixels wide, the device matrix mapping the box's lower-left corner to the raster's bottom-left pixel.
  • Bands: when a raster's bytes exceed PdfRasterOptions.BandBytes, the page is rendered in horizontal bands, the interpreter run once per band with the band as the device clip; paths, images and glyph runs whose device bounds miss the band are culled before any drawing; decoded images come from the image cache, so a band costs an interpretation, not a decoding. Rows go to the sink band by band; the raster is never whole in memory. Render on a raster larger than MaxRasterPixels refuses (render.raster-too-large) and names RenderRows.
  • Thumbnails fit the chosen box into a size, at the resolution that gives; PreferEmbeddedThumbnail returns the page's /Thumb decoded (it is an image like any other) when it is present and not smaller than asked.

Determinism​

Invariant 6 holds as far as Skia lets it, and the documentation says exactly how far:

  • the same raster bytes for the same document, options and SkiaSharp version on the same processor family, across runs, threads and bands — rendering in bands gives the rows rendering whole gives;
  • within the threshold across processor families: Skia's raster pipeline chooses SSE, AVX or NEON code paths at run time, and their rounding differs in the last bit of a blended channel. The report names the SkiaSharp version and the path chosen;
  • PNG output through the core's encoder at a fixed compression level is deterministic per .NET version; JPEG and WebP are Skia's, lossy, and asked for explicitly.

Bounds, classified (invariant 12, ADR 34)​

The rasterizer reads documents only through the core, whose guards apply — MaxContentOperations, MaxGraphicsStateDepth, MaxPageGlyphs (M15), MaxCharstringOperations (M08), MaxDecodedStreamLength and M22's decoder bounds. What it adds is bounded by options, since the raster's size is the caller's choice as much as the file's:

BoundKindWhy
Pixels of a raster held wholeOption, PdfRasterOptions.MaxRasterPixels (proposed 2²⁸, 1 GB in Rgba32), render.raster-too-large; RenderRows has no such boundA valid page may be 200 inches square, and UserUnit enlarges it further
Bytes of a bandOption, BandBytes (proposed 64 MB)Memory follows the band, not the page
Bytes of transparency layers alive at onceOption, MaxLayerBytes; past it, groups are composited without isolation and reported (render.layer-budget)Each nested group or soft mask holds a layer of the band's size; nesting is bounded by MaxGraphicsStateDepth, but its product with the band's size is not
Glyph outlines and decoded images cachedOptions, GlyphCacheBytes, ImageCacheBytes; least recently used evictedCaches are a speed-up; eviction costs time, never correctness
Samples decoded per imageM22's decoders under MaxDecodedStreamLength; decode-time reduction to the device footprintThe dimensions come from the file
Mesh subdivisionInternal constant: triangles are subdivided until they are no larger than one device pixel, at most 16 levelsPast a device pixel, subdivision changes no pixel; 2¹⁶ triangles per edge exceeds any raster the options allow
A tiling cell's sizeNone neededOne cell is rendered once and tiled by a shader, whatever the cell count
A form, pattern or Type 3 glyph that reaches itselfM15's cycle cut, reportedInvalid content

Diagnostics​

CodeSeverityMeaning
render.font-substitutedInformationA non-embedded font drawn with a substitute, named, widths forced to /Widths
render.font-needs-hintingWarningA TrueType face whose glyphs are assembled by instructions, drawn unhinted
render.glyph-undrawableWarningA glyph whose outline cannot be read, drawn empty
render.image-undecodableWarningAn image no decoder could decode, drawn as nothing
render.image-reducedInformationAn image decoded at a reduced scale, and the factor
render.color-approximatedInformationICC, Lab or CMYK content converted without color management
render.knockout-approximated, render.transfer-ignored, render.halftone-ignoredInformationA feature drawn approximately or ignored, as screen viewers do
render.blend-mode-unknownWarningAn unknown blend mode, drawn as Normal
render.rotate-invalidWarningA /Rotate not a multiple of 90, read as 0
render.appearance-missing, render.appearance-generatedInformationAn annotation without an appearance, skipped or generated in memory
render.raster-too-large, render.layer-budgetWarningA limit reached; the option that lifts it is named

The command-line tool​

render FILE [--pages 1-3,7] [--dpi 150 | --fit 256x256] [--box crop|media|trim|bleed|art] [--intent view|print] [--annotations all|none] [--generate-appearances] [--format png|jpeg|webp] [--quality 85] [--out DIR], one file per page, named by page index and, where the document has them, page label. compare --visual rasterizes through the satellite when it is present. The AOT binary ships SkiaSharp's native library beside it; its size is measured and recorded.

Slices​

Each slice ends on a green commit, with its codes documented, its benchmark recorded in docs/status.md when it has one, and every referee in a container (ADR 27).

  1. The satellite, the seams and the page frame. Delivers the package with its API baseline and an AOT and trimming check that loads the native library; IPdfContentDevice public, reviewed as API, M15's devices unchanged; PdfRasterizer implementing the core's IPdfPageRasterizer; the page frame (box, /Rotate, UserUnit, background); filled and stroked paths in the device spaces; PNG out; the referee harness — MuPDF (mutool draw) and poppler (pdftoppm) rendering in a pinned image, Pillow and NumPy comparing inside it, the side-by-side report as a CI artifact; the scan threshold fixed and recorded beside M09's and M12's (below). Proved by unit tests (the device matrix for each rotation and box, an inverted box, UserUnit 0.88; FsCheck — any composition of rotations keeps the raster's area, and rendering a page in bands gives the rows rendering it whole gives); integration: for every committed page, our raster's width and height equal MuPDF's at 72 and 150 dpi; the Swiss QR-bill's code, drawn as vector rectangles, decodes with ZXing from our raster to the payload it decodes from MuPDF's. Leaves the full line state.
  2. Paths, strokes and clips in full. Delivers dashes, caps, joins, the miter limit, stroke adjustment, zero-width lines, the non-uniform pen, even-odd and non-zero clips, clip nesting through q/Q. Proved by unit tests on fixture pages built with M08's builder, one per operator and state entry; integration: the fixture pages and the vector-only regions of the JasperReports statement's table grid within the cross-engine threshold of MuPDF. Leaves text.
  3. Glyph outlines and text. Delivers the outline sink for TrueType, CFF, CID-keyed CFF and Type 1, the eight rendering modes, clip by text, substitution for non-embedded fonts, the glyph cache, Type 3 glyphs through the interpreter. Proved by unit tests (a composite glyph with scaled components, seac, flex, a charstring bomb stopped by M08's guard, a Type 3 font that shows itself cut and reported, a font program that lies about its glyph count); integration: every page of the committed invoices, reports and contract within the cross-engine threshold of MuPDF; PDF24's boxes where MuPDF draws them; the OCR layers of the Acrobat, FineReader and Xerox scans absent from the raster — the page renders as it does with its text removed by M19's pipeline. Leaves images.
  4. Images and the decoders. Delivers M22's decoders behind the device, stencil masks, /Decode, explicit and color-key masks, soft masks, /SMaskInData, /Matte, smoothing, decode-time reduction, the image cache. Proved by unit tests (a 1 × 1 image stretched over a page, a mask whose size differs from its image's, an /Indexed lookup out of range, an image whose /Height contradicts its data); integration: each committed scan rendered at its native resolution, so that one sample maps to one pixel, against MuPDF at the same resolution under the scan threshold; the 35,000-pixel CCITT page within its memory budget. Leaves color.
  5. Color spaces, functions and shadings. Delivers the converter seam over M22's evaluation, the seven shading types, tiling patterns colored and uncolored. Proved by unit tests (each shading type on fixtures; a type 4 function at its stack bound; Separation /All and /None; a pattern cell smaller than a pixel); integration: the shading and pattern fixtures (not in the corpus, below) and the Illustrator publication's separation colors within the cross-engine threshold of MuPDF run without color management (mutool draw -N), so that both sides approximate. Leaves transparency.
  6. Transparency. Delivers constant alpha, the blend modes, isolated and non-isolated groups, soft masks by luminosity and alpha with backdrop and transfer, page groups, the knockout approximation, the layer budget. Proved by unit tests (a soft mask inside a soft mask; a group whose /BBox is empty; the sixteen modes over a known backdrop against the formulas of §11.3.5, within one step); integration: the committed documents with groups and masks and the transparency fixtures (below) within the cross-engine threshold. Leaves intents.
  7. Intents, layers and annotations. Delivers View and Print, M11's evaluator in the device, the annotation flags and states, in-memory appearance generation, NeedAppearances. Proved by unit tests (every flag combination under both intents; an annotation in a layer that is off; a widget with two states); integration: M11's layer documents under both intents against pdf.js's display and print intents, run through Node in a container; M11's annotated set and the committed forms against MuPDF; OmniForm's hidden widgets absent. Leaves bands.
  8. Bands, limits and the heavy pages. Delivers band rendering, the row sink, the limits and their codes, RenderBenchmarks with MemoryDiagnoser. Proved by unit tests (a band boundary through a glyph, a stroke, a mesh triangle and an image row; a page at MaxRasterPixels plus one refused by Render and streamed by RenderRows); the 1000-page journal's thumbnails with flat memory; the hostile documents within their time and allocation budgets; the rendering fuzz target joins M23's nightly campaign. Leaves outputs.
  9. Outputs, thumbnails, concurrency and determinism. Delivers the encoders, RenderThumbnail, embedded /Thumb, the report. Proved by two renders byte-identical; eight documents rendered on eight threads byte-identical to the same renders one after another; PNGs that Pillow decodes to our raster exactly; the PDFWriter 4 standard's embedded thumbnails decoded as MuPDF decodes them. Leaves references.
  10. Approved references and the visual comparison. Delivers the approved images of the reference documents and their regression rule; PdfRasterizer behind M24's comparison and M22's PdfOcr; compare --visual. Proved by the reference rows and M24's visual row, now on our rasters, below, and PdfOcr sending a vector page to an IOcrEngine substituted with NSubstitute as a raster at the resolution it asked for. Leaves the tool.
  11. The tool, the documentation and the remote corpus. Delivers render, the AOT binary with its native library, CorpusToolTests, the documentation. Proved by the tool row and a green Remote corpus run recorded in docs/status.md with its date.

The thresholds​

Two are used as they stand, already recorded in docs/status.md; slice 1 fixes the third on the corpus:

  • Cross-engine — ours against MuPDF, poppler or pdf.js: M09's, unchanged — at 150 dpi, a pixel differs when a channel differs by more than 24 of 255 and no pixel within one pixel of it in the other raster matches it; two renderings agree when at most 1 % of the pixels differ and no 8-connected region of differing pixels exceeds 144 pixels, a 2 mm square.
  • Regression — ours against our own approved image: M12's, unchanged — 0.2 % and the same region clause, with no neighborhood allowance: the same engine should draw the same edges.
  • Scans at native resolution, new here — at most 0.1 % of pixels differ, no region over 16 pixels: with one sample per pixel, two correct decoders and placers agree almost exactly.

Approval follows M12's workflow: references in tests/visual/rendering/<document>/page-NNN.png, each approved by a reviewed commit after a person compared ours, MuPDF's and poppler's side by side, the reason for any visible difference written in tests/visual/rendering/APPROVALS.md; regeneration explicit, never automatic.

Tests required​

Unit — under Rendering/, in tests/AdCodicem.Pdf.Tests as M10 placed its satellite's, or in the test project M12.1's first slice gives the satellites that carry native assets, if it gives them one:

  • Geometry: every box, every /Rotate and inherited /Rotate, UserUnit, an inverted and a degenerate box, thumbnail fitting; bands versus whole, as an FsCheck property over generated pages.
  • Paths and clips: each operator and line-state entry; a dash array of zeros; a clip that empties the page; a path of a million segments within its time budget.
  • Glyphs: each font kind, composite and accented glyphs, a hinting-dependent face detected, substitution by flags, .notdef, Type 3 recursion, a charstring that loops until M08's guard.
  • Images: every mask kind, /Decode, smoothing modes, decode-time reduction by each factor, the cache's eviction.
  • Color: each space and function type; a type 4 function that overflows its stack; Indexed out of range.
  • Transparency: each blend mode's formula; groups nested to MaxGraphicsStateDepth; the layer budget reached.
  • Intents: every annotation flag combination under both intents; layers on, off, and in nested memberships.
  • Hostile: an image claiming 2³¹ × 2³¹ samples; a mesh shading with a million patches; a tiling pattern whose step is 10⁻⁶; a Type 3 glyph that draws a form that draws the glyph; a page whose content is one BI without EI — each ends in a raster and a report, or a typed exception, within time and allocation budgets.
  • Determinism: two renders identical; parallel equals sequential; cancellation between bands leaves no pooled buffer rented.

Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container:

  • MuPDF — mutool draw to raw PAM at the tests' resolutions, with and without color management (-N); the reference renderer of the cross-engine and scan thresholds.
  • poppler — pdftoppm as a second opinion wherever MuPDF and we disagree, and pdftocairo for the print path.
  • pdf.js — through Node, display and print intents, for optional content and annotation flags, as M11 uses it.
  • Pillow and NumPy — every image comparison, inside the referee container, so that the test process decodes no image; Pillow also decodes every PNG we write.
  • ZXing — decoding the codes on the Swiss QR-bill and on M10's barcode fixtures from our rasters.

Acceptance conditions​

"Every committed document" means every document under documents/ and vendor/ the reader opens, encrypted ones through their recorded password once M16 opens them. Thresholds are those of the section above.

DocumentsBehaviorVerified by
Every committed document, every pageRendered at 72 and 150 dpi without exception, each anomaly a render.* or reader diagnostic; width and height equal MuPDF'sCorpusRenderingTests.Every_page_renders_at_the_size_mupdf_renders_it (new)
The reference documents: M12's four reference sources rendered by our engine (invoice, multi-page report, contract, two-column report), M14's PDF/A-3 Factur-X invoices, M20's PDF/A-2 renderings, and the committed documents/invoice/chromium-invoice-fr.pdf, libreoffice-invoice-fr.pdf, word-invoice-fr.pdf, word-print-driver-invoice-fr.pdf, reportlab-invoice.pdf, documents/report/chromium-report-fr.pdf, libreoffice-report-fr.pdf, documents/contract/chromium-contract-fr.pdf, documents/archival/libreoffice-report-pdfa2b.pdfEvery page within the regression threshold of its approved reference image, and each approved image within the cross-engine threshold of MuPDF's renderingRenderingVisualTests.Reference_documents_match_their_approved_images (new), MuPdfRenderRefereeTests.Approved_images_agree_with_mupdf (new)
documents/invoice/pdf24-invoice-fr.pdfA box for every capital E of the regular face, where MuPDF draws one; the rest of the page within the cross-engine thresholdCorpusRenderingTests.A_missing_glyph_renders_as_the_font_draws_it (new)
JBIG2: vendor/us-federal/xerox-workcentre-treasury-imf-report-scan.pdf (global segments), xerox-workcentre-5335-ocr-hud-fonsi-linearized.pdf (no globals, a bitonal full page), xerox-workcentre-5755-ocr-hud-fonsi-mrc.pdf (JBIG2 masks with globals over a JPEG background, a deskewed image matrix, a negative MediaBox origin), docusign-pdfkit-gsa-sf30-contract-modification.pdf (/Rotate 270); remote, remote/pdf-association/abledocs-pdfua1-tagged-textbook-scan.pdf (82 pages), remote/opf-format-corpus/jhove-hul-136-acrobat7-paper-capture-scanned-report.pdfRendered at each image's native resolution, within the scan threshold of MuPDF at the same resolutionCorpusRenderingTests.Jbig2_scans_render_as_mupdf_renders_them (new)
CCITT: vendor/us-federal/acrobat3-import-irs-1040-1988-scan.pdf (G4), vendor/pikepdf/scanner-ccitt-endofline.pdf (G3 with EndOfLine), vendor/us-federal/finereader8-frb-sr0115-examiner-guidance.pdf, vendor/uk-ogl/indesign-acrobat-hmcts-n208-form.pdf; remote, remote/pdfjs/itext5-ccitt-g4-mask-issue4379.pdf (a G4 stencil as an explicit /Mask), remote/ocrmypdf/epson-scan-indirect-rotate.pdf (an indirect /Rotate, /Interpolate), remote/ocrmypdf/kodak-capture-scan-broken-outline.pdf, remote/ecan/konica-bizhub-c554e-letter-scan.pdf (CCITT masks), remote/pdfjs/ricoh-3heights-scan-issue5747.pdfWithin the scan threshold of MuPDF, at native resolutionCorpusRenderingTests.Ccitt_scans_render_as_mupdf_renders_them (new)
JPEG 2000: vendor/opf-format-corpus/imagemagick-false-pdfa1b-jpx.pdf; remote, remote/pdfjs/acrobat8-jpx-precincts-issue5475.pdf (several precincts, no color space), remote/pdf-association/abledocs-pdfua1-textbook-chapter.pdf (JPEG 2000 soft masks, CMYK JPEG), remote/pdf-association/indesign-cs6-pdfua1-brochure.pdf (a JP2 with several colr boxes), remote/usgs/omnipage-usgs-professional-paper-1-1902.pdf (125 pages up to 29 megapixels)Within the scan threshold of MuPDF at native resolution; the Professional Paper within the memory budget recorded in status.md, every pageCorpusRenderingTests.Jpeg2000_pages_render_as_mupdf_renders_them (new)
remote/ocrmypdf/tiff2pdf-35000px-ccitt-image.pdf (one 35,000-pixel square G4 image, 153 MB decoded, on an 8,400-point page)Rendered at 150 dpi through RenderRows, the image reduced while decoded; peak memory within the budget recorded in status.md, far below the decoded size; the thumbnail within the scan threshold of MuPDF's at the same sizeCorpusRenderingTests.A_huge_scan_renders_in_bounded_memory (new)
The OCR layers: vendor/us-federal/hp-mfp-acrobat-ocr-nih-report.pdf, finereader8-frb-sr0115-examiner-guidance.pdf (text under the image), xerox-workcentre-5335-ocr-hud-fonsi-linearized.pdf (render mode 3 at up to 2000 % horizontal scaling); remote, abledocs-pdfua1-tagged-textbook-scan.pdf (OCR text painted under the page image)No pixel from the text layer: each page equals, within the regression threshold, the same page with its text removed through M19's pipelineCorpusRenderingTests.Invisible_text_draws_nothing (new)
Layers: vendor/uk-ogl/pdfmaker21-ozev-sample-invoice.pdf (Acrobat's Watermark with /AS for three events), indesign-acrobat-hmcts-n208-form.pdf, vendor/pdf-association/handwritten-utf16le-strings.pdf (groups off by default, XObjects with /OC), vendor/opf-format-corpus/pdfmaker707-word-va-esig-developer-guide.pdf, pdfmaker7-powerpoint-va-cancer-database-course.pdf; remote, remote/pdf20examples/handwritten-pdf20-utf8-strings.pdf, remote/usgs/us-topo-washington-west-2023.pdf (31 layers, a 109.5-megapixel image, opened under its readerLimits), remote/us-states/powerbi-pdfium-acrobat-covid-dashboard-nine-updates.pdfUnder View and under Print, within the cross-engine threshold of pdf.js's display and print intents; where the document's usage and /AS disagree, the specification's reading, recorded as M11 records itCorpusRenderingTests.Layers_render_as_viewers_show_and_print_them (new), PdfJsRenderRefereeTests (new)
Annotations: vendor/opf-format-corpus/reader10-openoffice32-annotated-object-streams.pdf (a note, a highlight, their popups), pdfmaker10-word-file-attachment-annotation.pdf, pdfmaker9-word-distiller-embedded-quicktime.pdf (a screen annotation's poster), M11's annotated set on the pages its acceptance names, with every flag combinationEach drawn, or not, as MuPDF draws it under View and pdf.js under print; popups never drawnCorpusRenderingTests.Annotations_render_by_their_flags_and_appearances (new)
Forms and signatures: documents/form/reportlab-subscription-form.pdf filled by M16; vendor/pdf-association/indesign15-pdfua1-form.pdf (widgets without appearances); vendor/us-federal/omniform-usda-rd1924-5-hidden-widgets.pdf; vendor/fr-licence-ouverte/fop-dictao-dila-signed-joafe-notice.pdf (a layered n0/n2 signature appearance); remote, remote/qpdf-issues/yousign-signserver-qualified-seal-test-page.pdf, remote/pdfium-tests/foxit-phantompdf-certification-signature-visible.pdfFilled values and signature appearances within the cross-engine threshold of MuPDF; widgets without appearance drawn only under GenerateInMemory, then within the threshold of pdf.js's own drawing; OmniForm's hidden widgets nowhereCorpusRenderingTests.Forms_and_signatures_render_as_viewers_draw_them (new)
Transparency: vendor/pdf-association/pdflib-pps-kraxi-pdfa2a-pdfua1-invoice.pdf (an isolated group form), vendor/zugferd/itext-pdfbox-weclapp-facturx-en16931-invoice.pdf and vendor/docentric/aspose-d365-facturx-extended-invoice.pdf (page groups), vendor/jasper-modular/openpdf-jasperreports-financial-statement.pdf, vendor/uk-ogl/pdfmaker10-word-dh-care-bill-factsheet.pdf (an image with a soft mask), vendor/fr-licence-ouverte/word365-dila-text-drawn-as-images.pdf (text as soft-masked images); remote, indesign-cs6-pdfua1-brochure.pdf (multiply, overprint, an axial shading), remote/opf-format-corpus/acrobat9-portfolio-signed-3d.pdf (DeviceN, a type 4 function, a soft mask); the transparency and shading fixtures — not in the corpus (below)Within the cross-engine threshold of MuPDF; knockout groups reported approximated, and where MuPDF's knockout differs from our approximation, the region recordedCorpusRenderingTests.Transparency_and_shadings_render_as_mupdf_renders_them (new)
Color: vendor/us-federal/illustrator-irs-pub1-english.pdf (separation colors, overprint), vendor/opf-format-corpus/pdfmaker5-distiller5-va-select-agents.pdf (CalRGB), vendor/eu-publications/distiller10-eu-consolidated-regulation-2015.pdf (a CMYK output intent); remote, remote/ocrmypdf/photoshop-cc2015-pdfx3-cmyk.pdf (CMYK JPEG with ColorTransform, a text clip), remote/py-pdf-sample-files/wkhtmltopdf-qt5-html-page.pdf (a pattern color space)Within the cross-engine threshold of MuPDF without color management (-N); each approximation reported onceCorpusRenderingTests.Color_spaces_render_as_an_unmanaged_renderer_renders_them (new)
Type 3 and fonts: vendor/opf-format-corpus/distiller3-dea-cfr-damaged.pdf (Type 3), the Type 1 and Type 1C documents vendor/opf-format-corpus/distiller4-congress-hr1904-enrolled-bill.pdf, distiller952-pscript5-kb-pdf-risk-inventory.pdf, indesign-cs-puppet-guild-event-flyer.pdf (CID-keyed CFF), vendor/pdf-association/indesign13-pdfua1-german-book-chapter.pdf; vendor/jp-nta/indesign-distiller18-nta-gift-tax-vertical.pdf (vertical CID fonts) once M16 opens it; remote, remote/pdfjs/pdflib-zos-card-statement-issue6605.pdf and remote/pdfminer/afp-batch-processor-bank-statement-2b.pdf (bitmap Type 3 fonts from inline images)Within the cross-engine threshold of MuPDF; no font program handed to a native parser — the test runs with Skia's font manager stubbed to failCorpusRenderingTests.Fonts_render_from_our_own_outlines (new)
Geometry: vendor/pdf-association/handwritten-compacted-syntax.pdf (UserUnit 0.88), pdfmaker7-powerpoint-va-cancer-database-course.pdf (/Rotate 90), docusign-pdfkit-gsa-sf30-contract-modification.pdf (/Rotate 270), fop-dictao-dila-signed-joafe-notice.pdf (a bleed and trim box), vendor/swissqrbill/pdfbox-swiss-qr-bill-a4.pdf; remote, remote/pdfplumber/pypdf2-inverted-mediabox-issue932.pdf (an inverted MediaBox, a CropBox that differs), remote/ocrmypdf/pdfpen-imprints-split-content.pdf (a MediaBox overridden below a pages node)Each box and rotation rendered as MuPDF renders it, with --box for the trim and bleed boxes; the QR-bill's code decodes with ZXing from our raster at 150 and 300 dpiCorpusRenderingTests.Page_geometry_renders_as_mupdf_renders_it (new), ZxingRefereeTests.Rendered_codes_decode (new)
Hostile: vendor/pdf-association/handwritten-type3-recursion.pdf, handwritten-indexed-color-out-of-range.pdf, handwritten-content-stream-indirect-refs.pdf, handwritten-inline-image-abbreviations.pdf, vendor/opf-format-corpus/acrobat11-image-conversion-image-height-altered.pdf, every damaged/* document; remote, remote/pdfjs/canon-scan-junk-after-eof-scan-bad.pdf (a corrupt JPEG under Flate), remote/pdfjs/aspose-pdf-java-decodeparms-null-issue7200.pdf, remote/eu-dss/dss-signed-widget-self-parent-startxref-past-eof.pdfNo untyped exception, no hang: each within its time and allocation budget, each anomaly reported; what the damaged invoices still hold renders within the cross-engine threshold of the undamaged original where the reader recovers it wholeCorpusRenderingTests.Hostile_documents_never_break_the_rasterizer (new)
M24's edited pairs and damaged copies, and M04's shadow replace fixture whose field appearance lies over signed contentM24's visual rows, now on our rasters: regions equal NumPy's on the same rasters, every text change inside a region, none for the damaged copies the reader recovers whole; the field over signed content a visual-only regionCorpusVisualComparisonTests.Changed_regions_agree_with_an_independent_pixel_diff (new), CorpusVisualComparisonTests.A_field_laid_over_signed_content_is_seen (new)
documents/stress/reportlab-journal-1000-pages.pdf; remote, remote/govinfo/us-code-2023-title42.pdf (9,302 pages)Thumbnails of every page with peak memory flat across 10, 100 and 1,000 pages, within the budget recorded in status.md; throughput recordedCorpusRenderingTests.Thumbnails_of_a_long_document_hold_their_budget (new), RenderBenchmarks (new)
Any committed documentTwo renders byte-identical; eight concurrent renders byte-identical to sequential ones; bands equal wholeCorpusRenderingTests.Rendering_is_deterministic (new)
vendor/us-federal/pdfwriter4-usda-dry-whey-standard-2000.pdf (embedded thumbnails sharing an indexed palette)PreferEmbeddedThumbnail returns each page's /Thumb as MuPDF decodes the imageCorpusRenderingTests.Embedded_thumbnails_are_used_when_asked (new)
The same operations through the toolThe AOT binary writes the PNGs the API writes, byte for byteCorpusToolTests.Render_matches_the_api (new)

The remote rows close only on a green Remote corpus run, recorded in docs/status.md with its date.

Corpus​

What the corpus holds​

  • Scans in every codec: JBIG2 with and without globals, as masks over a JPEG background (the Xerox MRC file), under a rotation (DocuSign); CCITT G4, G3 with EndOfLine, as stencils and masks, with an indirect /Rotate, and one image of 35,000 pixels square (remote); JPEG 2000 with precincts, soft masks, several colr boxes and 125 heavy pages (remote); DCT from scanners and CMYK DCT with ColorTransform (remote). Features jbig2*, ccitt*, jpx*, jp2*, dct-*, huge-decoded-image, heavy-scan, image-only.
  • Invisible text: OCR layers from Acrobat, FineReader 8 (text under the image), two copiers and OmniPage (ocr-layer, text-under-image, ocr-text-painted-under-page-image).
  • Layers: sixteen documents with optional-content, groups off by default, /AS, a GIS map with 31 layers (remote), the layer rows of M11.
  • Annotations and appearances: notes, highlights, popups, a file attachment, a screen annotation, widgets with and without appearances, hidden widgets, layered signature appearances (layered-signature-appearance*).
  • Transparency and color: isolated and page groups, soft masks, text drawn as soft-masked images, separation colors and overprint, CalRGB, CMYK output intents, DeviceN with a type 4 function and a pattern color space (remote), one axial shading and one multiply blend (remote).
  • Fonts: Type 1, Type 1C, CID-keyed CFF, TrueType subsets, Type 3 (bitmap ones remote), vertical CID fonts (encrypted until M16), and a symbolic TrueType subset that draws a box (renders-wrong).
  • Geometry: UserUnit, /Rotate 90 and 270, an indirect /Rotate, an inverted MediaBox, a negative origin, bleed and trim boxes.
  • Hostile content: Type 3 recursion, an indexed color out of range, inline images whose keys contradict each other, an image whose height lies, a corrupt JPEG, every damaged invoice.
  • Scale: the 1000-page journal; the 9,302-page US Code and the 109.5-megapixel map (remote).

What it lacks​

NeedWhyPriorityLikely source
Shading fixtures: function-based (type 1), axial and radial with and without /Extend, free-form and lattice meshes (types 4 and 5), Coons and tensor patches (6 and 7), tiling patterns colored and uncolored, a shading pattern with /BackgroundThe device implements every type; the corpus holds one axial shading, remote, and no mesh at all — invariant 10 wants each proven on a file another implementation wrote1Generated here: pycairo (a pypi producer whose mesh patterns become patch shadings), ReportLab's axial and radial shadings, and for types 1, 4 and 5 hand-written fixtures through pikepdf, recorded in build_corpus.py
Transparency fixtures: the sixteen blend modes over a known backdrop, luminosity soft masks with /BC and /TR, nested groups, a knockout group, CSS opacity and mix-blend-mode from a browserOnly multiply is in the corpus, remote; knockout, luminosity masks with a backdrop and the non-separable modes are nowhere1Generated here: Chromium from an HTML fixture (its Skia backend writes blend modes and soft masks), pycairo's operators, and a hand-written knockout group through pikepdf
A committed page with a CMYK or ICC-based photograph and an output intent, in PDF/A or PDF/XThe color row rests on remote files; the committed ones carry CMYK only as an output intent2Generated here: ReportLab with a CMYK JPEG and LibreOffice's PDF/A export; a contribution of print-shop output (W21 in docs/corpus-contributions.md)
A color-key image mask (/Mask [ranges])The mask kind no corpus document carries2Generated here: ReportLab's drawImage(mask=…) writes one
A TrueType face whose glyphs need their hinting instructions (a Chinese Ming face)render.font-needs-hinting is designed but met in no file3A public source among pdf.js's or PDFium's test files, remote if their terms require; a contribution
Our own approved reference images for the reference documentsThe regression rule needs them; they are test data under tests/visual/rendering/, not corpus documents1Generated here, by this milestone, approved by review
A page larger than MaxRasterPixels at 150 dpi, committedThe band path is proven only on the remote 8,400-point page2Generated here: ReportLab, a 200-inch page with vector content and UserUnit

Traps​

  • Hidden content still changes the state. The interpreter runs it; the device drops its paints. Skipping a hidden section wholesale breaks every page that sets a color or a font inside one.
  • An OCR layer is invisible by mode 3, or by being painted under the image. Both must stay invisible, and the second only works if images are drawn in content order over it.
  • The CropBox and /Rotate are inherited, like Resources and MediaBox: read them through inherited resolution, never from the page dictionary.
  • A zero-width line is not invisible: it is the thinnest line the device draws.
  • Knockout, overprint, halftones and transfer functions are device concepts; screen viewers ignore most of them, and so do we, saying so. Faking overprint in RGB makes more pages wrong than right.
  • Decoding a scan whole to draw it small is how a thumbnail takes gigabytes. Reduce while decoding.
  • Native font parsers are an attack surface, and system font managers make rendering machine-dependent. Outlines are ours; Skia's font manager is never created.
  • Anti-aliasing seams: adjacent vector rectangles (a QR code, a table grid) show hairline gaps when each is anti-aliased on its own. Stroke adjustment and pixel snapping of axis-aligned fills are what keep a QR code decodable from a raster.
  • Skia's SIMD paths differ across processors. Byte identity holds per processor family, not across them; the documentation must not promise more.
  • CMYK without a profile is an approximation, and MuPDF with color management on draws CMYK differently from any formula: compare with -N, and leave color fidelity to M29.
  • /Interpolate is a hint. Viewers differ on it; compare scans at native resolution, where it changes nothing.
  • pdf.js applies usage directly where the specification applies it only through /AS (M11's trap): where they disagree, follow the specification and record the difference.
  • A render is not a repair. PDF24's box, a missing font's substitute, an image that cannot be decoded — draw what the file says, report what could not be drawn, invent nothing.

Documentation​

  • docs/website/docs/guides/rendering.md (new): rendering a page, resolution and boxes, intents and layers, annotations and missing appearances, formats, thumbnails, rendering in bands.
  • docs/website/docs/concepts/rendering.md (new): what is drawn and what is approximated, fonts from our own outlines and substitution, color without management until M29, determinism per processor family, the limits.
  • docs/website/docs/guides/visual-comparison.md: M24's page, now with the in-process rasterizer.
  • docs/website/docs/reference/tool/: render; compare --visual.
  • docs/website/docs/reference/diagnostics.md: the render.* codes.
  • docs/website/docs/introduction.md and docs/features/features.json: the rasterization entry brought to its state.
  • docs/architecture.md: the Rendering satellite's content, the device seam, IPdfPageRasterizer and IPdfColorConverter in the core, the dependency on AdCodicem.Pdf.Fonts, where M08 placed the OFL set.
  • docs/corpus.md and docs/corpus-contributions.md: the shading and transparency fixtures, and the new wants.
  • docs/status.md: the scan threshold, the budgets, the AOT binary's size, the measurements.

Exit criteria​

  • The satellite renders every page of every committed document, with the device seam public, and implements the core's IPdfPageRasterizer for M22's OCR and M24's comparison.
  • Glyphs come from the core's outlines, and no font program reaches a native parser.
  • Images decode through M22, reduced while decoded; color through M22's evaluation behind the converter seam.
  • Shadings, patterns, transparency, intents, layers and annotations render as the acceptance conditions say.
  • Band rendering holds memory to the band; the limits are options with their codes.
  • The scan threshold is fixed and recorded beside M09's and M12's; the reference images approved and recorded in APPROVALS.md.
  • M24's visual rows pass on our rasters; compare --visual works without a caller's rasterizer.
  • The priority-1 gaps above are filled; each remaining gap is recorded in docs/corpus-contributions.md.
  • The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on a green Remote corpus run recorded in docs/status.md.
  • Unit tests cover each behavior, its degenerate cases and its hostile ones; the FsCheck properties hold.
  • Integration tests run MuPDF, poppler, pdf.js, Pillow, NumPy and ZXing, each in a container.
  • RenderBenchmarks runs with MemoryDiagnoser; docs/status.md records the budgets and the throughput.
  • The render verb ships in the dotnet tool and the AOT binary, documented.
  • The documentation site publishes the pages listed above.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).