Skip to main content

Architecture decisions

One file per decision, in the format Michael Nygard proposed, recorded so that the reasoning behind the current shape of the project stays discoverable instead of living in closed pull request threads.

A settled decision is not reopened without new evidence — that is what writing them down is for.

#DecisionWas
1Record architecture decisions—
2Fully managed renderingD01
3Full scope: generation and manipulationD02
4Business documents with a modern CSS subsetD03
5SkiaSharp and HarfBuzzSharp allowed, in .Html onlyD04
6Our own PDF writer rather than Skia's PDF backendD05
7Conformance designed in from the startD06
8Facade, immutable options, dependency injectionD07
9A dependency-free core plus satellitesD08
10net10.0 only, C# 14D09
11Fonts: an explicit registry, an embedded OFL set, optional web fontsD10
12GitHub Actions, published to nuget.orgD11
13Lazy reading; output as a full rewrite or an incremental updateD12
14A tolerant reader with a diagnostic reportD13
15Full text extraction, tagged structure preferredD14
16Rasterization as a satellite, after the foundationsD15
17Conformance actively preserved, plus a built-in validatorD16
18Signing: space reservedD17
19Milestones are accepted on real documentsD18
20Everything is written in EnglishD19
21Validation is a rule engine, and conformance is a profile of itD20
22Repair is driven by findings, and conservative by defaultD21
23Third-party corpus documents are vendored only under attribution-only licensesD22
24Package identifiers, and a reserved prefixD23
25Trusted publishing rather than an API keyD24
26Test stack: xUnit v3, AwesomeAssertions, NSubstituteD25
27Integration tests run the referees in containersD26
28A documentation site, published from the repositoryD27
29Warnings are errors, and suppressions are local and justifiedD28
30Previews on every merge, stable releases on demand—
31Versioned documentation: stable lines, and the preview beside them—
32Documents that cannot be redistributed are fetched on demand, never committed—
33A remote document may be a member of a pinned archive—
34Every valid PDF is readable, and the reader's guards are options—
35Unsafe code, where a measurement asks for it—
36Validation lives in the core, and the conformance profiles in a satellite—
37Out of scope: active content, and PDF to Office—
38The HTML engine loads resources deny-by-default—
39Forward references: late-filled XObjects, and two bounded passes—
40The caller chooses PDF 1.7 or PDF 2.0 output—
41Cryptography lives in satellites—
42Image codecs and scans—
43Skia in .Html and .Rendering, and text analysis is ours—
44Object-shape rules generated from the Arlington model—
45A finding's severity says whether the file reads as written—
46Every milestone ends with an adversarial review by a fresh session—
47The user documentation follows Diátaxis—
48One version for every package, independent of .NET—
49Previews weekly, when a package input changed—

Decisions too small for a record of their own​

  • Milestones are numbered in the order they are worked (since 2026-09-26): a milestone inserted later renumbers those after it, everywhere in the repository, and docs/roadmap.md keeps the mapping from the numbers older commits use.
  • The layout engine works in CSS pixels; conversion to points (× 0.75) happens only when painting.
  • The PDF coordinate system starts bottom-left and layout works top-left: the conversion lives in exactly one place, in painting.
  • PDF names are interned; common integers are cached.
  • The command-line tool, AdCodicem.Pdf.Tool (M06), installs the command adpdf (since 2026-09-27).
  • A stream copied between documents travels encoded, with no decompress/recompress cycle.
  • A dictionary entry whose value is null is dropped on parse: the specification says it is equivalent to an absent entry, and every later stage is spared a null it would have to ignore.
  • A key a dictionary gives more than once keeps the last value given, and a null given last removes it, as an absent entry (since 2026-10-03, #172): ISO 32000-1 (7.3.7) forbids the repeat and says nothing of which value counts. qpdf, pdf.js, PDFBox, MuPDF, pdfium, poppler and veraPDF's parser read it so, pypdf and Ghostscript keep the first. Before then, << /B 3 /B null >> read as << /B 3 >>: so do pypdf and Ghostscript, keeping the first value, while every reader that keeps the last reads B absent; and /B 5 0 R, object 5 null, never read as 3. Keys compare as they read, so /F#69lter repeats /Filter. A key given with no value, before >> or endobj, is one given null; a value a guard's cut leaves short removes nothing. Each repeat is reported, syntax.key-repeated, a null on either side included.
  • CodeQL runs as GitHub's default setup, on every pull request and every push to main, with the query suite chosen in the repository's settings; the repository keeps no CodeQL workflow or configuration of its own (since 2026-09-26, T35). Until then a workflow ran the security and quality suites with four queries excluded as wrong for this codebase — cs/path-combine, cs/catch-of-all-exceptions, cs/linq/missed-where, cs/complex-block. If the default setup raises one of them, the alert is dismissed in the Security tab with that reason (git history keeps the configuration that gave each one), rather than configured away again.
  • The document /ID is derived from content, or supplied by the caller, never random — determinism comes first, and a random identifier would make fingerprint tests impossible.