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.Renderingsatellite, depending on the core,AdCodicem.Pdf.Imagingand SkiaSharp — its CPU raster backend only, and on Linux itsNoDependenciesnative 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,IPdfPageRasterizerandPdfRaster— which M22 put beside it for OCR and M24's visual comparison consumes —, implemented here, so that the satellite depends on neitherAdCodicem.Pdf.Comparenor the OCR orchestration, and M22'sPdfOcrrasterizes 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) andPrint, through M11'sPdfLayerVisibilityfor 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 (
CropBoxby default, through inherited resolution, clipped to theMediaBox),/Rotateinherited,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/Thumbfirst 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
renderverb, andcompare --visualwithout 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
ICCBasedspaces beyond their alternate, output intents simulated, overprint simulated, separation previews — M29, whose managed color-management satellite plugs into the converter seam defined here; until thenICCBased,Laband 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
Printintent 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
| Part | Where | Why |
|---|---|---|
IPdfContentDevice, the interpreter's state it receives, the glyph outline sink | Core, Content/ and Fonts/, made public | M15 wrote them; M19 extends them; a device outside the core needs them public, and nothing about them needs Skia |
IPdfPageRasterizer, PdfRaster | Core, Content/, where M22 put them | Compare and the OCR orchestration depend on the core alone, and Rendering must depend on neither; all three meet at the core's seam |
IPdfColorConverter | Core, Graphics/, beside M22's color evaluation | M29'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 encoders | AdCodicem.Pdf.Rendering | SkiaSharp is allowed here and never in the core (ADR 43) |
| JBIG2, JPEG 2000 and JPEG decoding | AdCodicem.Pdf.Imaging (M22) | ADR 42; CCITT stays in the core |
| Color spaces and functions evaluated | M22's evaluation, where M22 put it | One evaluator for image export, redaction of pixels and rendering |
| The OFL faces used for substitution | AdCodicem.Pdf.Fonts, where M08 puts the set (decided 2026-09-27), Liberation Sans regular in the core | ADR 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:
WandW*intersect the clip at the next painting operator; clip by text (modes 4 to 7) accumulates the glyphs' outlines untilET; the clip is part of the state M15 saves and restores, and the device mirrors it withSaveandRestoreon 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;
shand 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'sSKBlendModeof the same definition — an unknown mode isNormal, 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/BCand/TR) and by alpha, fromSMaskin 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:TRis 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
glyfandlocaand interprets Type 2 charstrings; M15 reads Type 1 programs. This milestone adds, in the core'sFonts/, 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 (FDSelectand/FontMatrixper font dictionary), Type 1 charstrings with theirseacaccents, 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
fpgmsize, 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
/Widthsgives, as viewers do, and reported once per font (render.font-substituted). A symbolic font that cannot be substituted draws nothing and is reported;.notdefis 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:
/ImageMaskstencils painted with the fill color (an uncolored pattern included);/Decodehonored, for masks and for samples;/Maskas an explicit stencil or as a color-key range;/SMaskand/SMaskInDatafor JPEG 2000;/Matteun-premultiplied as §11.6.5.2 says. - Smoothing:
/Interpolatetrue 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 (
VieworPrint) and the configuration the options name,/Dby 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
/Annotsorder, each by its normal appearance — the/ASstate's in a subdictionary —, placed by §12.5.5's algorithm as M15 places it. The flags decide:Hiddennever;NoViewnot underView; underPrint, only those withPrint;NoZoomandNoRotateas M11 draws them; an annotation's/OCthrough 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 askGenerateInMemory— then M11'sEnsureAppearancesand M16's field appearances build one into a scratch change set that is discarded after the page, never written (render.appearance-generated). A form withNeedAppearancestrue gets its widgets' appearances generated under that option, as viewers regenerate them. - Signature appearances are appearances: Acrobat's legacy layered ones (
n0ton4) 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;/Rotateinherited, 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);UserUnitscaling user space. The raster isceil(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.Renderon a raster larger thanMaxRasterPixelsrefuses (render.raster-too-large) and namesRenderRows. - Thumbnails fit the chosen box into a size, at the resolution that gives;
PreferEmbeddedThumbnailreturns the page's/Thumbdecoded (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:
| Bound | Kind | Why |
|---|---|---|
| Pixels of a raster held whole | Option, PdfRasterOptions.MaxRasterPixels (proposed 2²⁸, 1 GB in Rgba32), render.raster-too-large; RenderRows has no such bound | A valid page may be 200 inches square, and UserUnit enlarges it further |
| Bytes of a band | Option, BandBytes (proposed 64 MB) | Memory follows the band, not the page |
| Bytes of transparency layers alive at once | Option, 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 cached | Options, GlyphCacheBytes, ImageCacheBytes; least recently used evicted | Caches are a speed-up; eviction costs time, never correctness |
| Samples decoded per image | M22's decoders under MaxDecodedStreamLength; decode-time reduction to the device footprint | The dimensions come from the file |
| Mesh subdivision | Internal constant: triangles are subdivided until they are no larger than one device pixel, at most 16 levels | Past a device pixel, subdivision changes no pixel; 2¹⁶ triangles per edge exceeds any raster the options allow |
| A tiling cell's size | None needed | One cell is rendered once and tiled by a shader, whatever the cell count |
| A form, pattern or Type 3 glyph that reaches itself | M15's cycle cut, reported | Invalid content |
Diagnostics
| Code | Severity | Meaning |
|---|---|---|
render.font-substituted | Information | A non-embedded font drawn with a substitute, named, widths forced to /Widths |
render.font-needs-hinting | Warning | A TrueType face whose glyphs are assembled by instructions, drawn unhinted |
render.glyph-undrawable | Warning | A glyph whose outline cannot be read, drawn empty |
render.image-undecodable | Warning | An image no decoder could decode, drawn as nothing |
render.image-reduced | Information | An image decoded at a reduced scale, and the factor |
render.color-approximated | Information | ICC, Lab or CMYK content converted without color management |
render.knockout-approximated, render.transfer-ignored, render.halftone-ignored | Information | A feature drawn approximately or ignored, as screen viewers do |
render.blend-mode-unknown | Warning | An unknown blend mode, drawn as Normal |
render.rotate-invalid | Warning | A /Rotate not a multiple of 90, read as 0 |
render.appearance-missing, render.appearance-generated | Information | An annotation without an appearance, skipped or generated in memory |
render.raster-too-large, render.layer-budget | Warning | A 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).
- 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;
IPdfContentDevicepublic, reviewed as API, M15's devices unchanged;PdfRasterizerimplementing the core'sIPdfPageRasterizer; 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,UserUnit0.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. - 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. - 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. - 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/Indexedlookup out of range, an image whose/Heightcontradicts 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. - 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 /Alland/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. - 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
/BBoxis 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. - Intents, layers and annotations. Delivers
ViewandPrint, 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'sdisplayandprintintents, run through Node in a container; M11's annotated set and the committed forms against MuPDF; OmniForm's hidden widgets absent. Leaves bands. - Bands, limits and the heavy pages. Delivers band rendering, the row sink, the limits and their codes,
RenderBenchmarkswithMemoryDiagnoser. Proved by unit tests (a band boundary through a glyph, a stroke, a mesh triangle and an image row; a page atMaxRasterPixelsplus one refused byRenderand streamed byRenderRows); 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. - 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. - Approved references and the visual comparison. Delivers the approved images of the reference documents and
their regression rule;
PdfRasterizerbehind M24's comparison and M22'sPdfOcr;compare --visual. Proved by the reference rows and M24's visual row, now on our rasters, below, andPdfOcrsending a vector page to anIOcrEnginesubstituted with NSubstitute as a raster at the resolution it asked for. Leaves the tool. - 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 greenRemote corpusrun recorded indocs/status.mdwith 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
/Rotateand 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;
Indexedout 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
BIwithoutEI— 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 drawto raw PAM at the tests' resolutions, with and without color management (-N); the reference renderer of the cross-engine and scan thresholds. - poppler —
pdftoppmas a second opinion wherever MuPDF and we disagree, andpdftocairofor the print path. - pdf.js — through Node,
displayandprintintents, 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.
| Documents | Behavior | Verified by |
|---|---|---|
| Every committed document, every page | Rendered at 72 and 150 dpi without exception, each anomaly a render.* or reader diagnostic; width and height equal MuPDF's | CorpusRenderingTests.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.pdf | Every page within the regression threshold of its approved reference image, and each approved image within the cross-engine threshold of MuPDF's rendering | RenderingVisualTests.Reference_documents_match_their_approved_images (new), MuPdfRenderRefereeTests.Approved_images_agree_with_mupdf (new) |
documents/invoice/pdf24-invoice-fr.pdf | A box for every capital E of the regular face, where MuPDF draws one; the rest of the page within the cross-engine threshold | CorpusRenderingTests.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.pdf | Rendered at each image's native resolution, within the scan threshold of MuPDF at the same resolution | CorpusRenderingTests.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.pdf | Within the scan threshold of MuPDF, at native resolution | CorpusRenderingTests.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 page | CorpusRenderingTests.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 size | CorpusRenderingTests.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 pipeline | CorpusRenderingTests.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.pdf | Under 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 it | CorpusRenderingTests.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 combination | Each drawn, or not, as MuPDF draws it under View and pdf.js under print; popups never drawn | CorpusRenderingTests.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.pdf | Filled 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 nowhere | CorpusRenderingTests.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 recorded | CorpusRenderingTests.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 once | CorpusRenderingTests.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 fail | CorpusRenderingTests.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 dpi | CorpusRenderingTests.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.pdf | No 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 whole | CorpusRenderingTests.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 content | M24'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 region | CorpusVisualComparisonTests.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 recorded | CorpusRenderingTests.Thumbnails_of_a_long_document_hold_their_budget (new), RenderBenchmarks (new) |
| Any committed document | Two renders byte-identical; eight concurrent renders byte-identical to sequential ones; bands equal whole | CorpusRenderingTests.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 image | CorpusRenderingTests.Embedded_thumbnails_are_used_when_asked (new) |
| The same operations through the tool | The AOT binary writes the PNGs the API writes, byte for byte | CorpusToolTests.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, severalcolrboxes and 125 heavy pages (remote); DCT from scanners and CMYK DCT withColorTransform(remote). Featuresjbig2*,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,/Rotate90 and 270, an indirect/Rotate, an invertedMediaBox, 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
| Need | Why | Priority | Likely 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 /Background | The 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 wrote | 1 | Generated 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 browser | Only multiply is in the corpus, remote; knockout, luminosity masks with a backdrop and the non-separable modes are nowhere | 1 | Generated 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/X | The color row rests on remote files; the committed ones carry CMYK only as an output intent | 2 | Generated 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 carries | 2 | Generated 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 file | 3 | A 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 documents | The regression rule needs them; they are test data under tests/visual/rendering/, not corpus documents | 1 | Generated here, by this milestone, approved by review |
A page larger than MaxRasterPixels at 150 dpi, committed | The band path is proven only on the remote 8,400-point page | 2 | Generated 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
CropBoxand/Rotateare inherited, likeResourcesandMediaBox: 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. /Interpolateis 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: therender.*codes.docs/website/docs/introduction.mdanddocs/features/features.json: therasterizationentry brought to its state.docs/architecture.md: the Rendering satellite's content, the device seam,IPdfPageRasterizerandIPdfColorConverterin the core, the dependency onAdCodicem.Pdf.Fonts, where M08 placed the OFL set.docs/corpus.mdanddocs/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
IPdfPageRasterizerfor 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 --visualworks 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 corpusrun recorded indocs/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.
-
RenderBenchmarksruns withMemoryDiagnoser;docs/status.mdrecords the budgets and the throughput. - The
renderverb 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).