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.
| # | Decision | Was |
|---|---|---|
| 1 | Record architecture decisions | — |
| 2 | Fully managed rendering | D01 |
| 3 | Full scope: generation and manipulation | D02 |
| 4 | Business documents with a modern CSS subset | D03 |
| 5 | SkiaSharp and HarfBuzzSharp allowed, in .Html only | D04 |
| 6 | Our own PDF writer rather than Skia's PDF backend | D05 |
| 7 | Conformance designed in from the start | D06 |
| 8 | Facade, immutable options, dependency injection | D07 |
| 9 | A dependency-free core plus satellites | D08 |
| 10 | net10.0 only, C# 14 | D09 |
| 11 | Fonts: an explicit registry, an embedded OFL set, optional web fonts | D10 |
| 12 | GitHub Actions, published to nuget.org | D11 |
| 13 | Lazy reading; output as a full rewrite or an incremental update | D12 |
| 14 | A tolerant reader with a diagnostic report | D13 |
| 15 | Full text extraction, tagged structure preferred | D14 |
| 16 | Rasterization as a satellite, after the foundations | D15 |
| 17 | Conformance actively preserved, plus a built-in validator | D16 |
| 18 | Signing: space reserved | D17 |
| 19 | Milestones are accepted on real documents | D18 |
| 20 | Everything is written in English | D19 |
| 21 | Validation is a rule engine, and conformance is a profile of it | D20 |
| 22 | Repair is driven by findings, and conservative by default | D21 |
| 23 | Third-party corpus documents are vendored only under attribution-only licenses | D22 |
| 24 | Package identifiers, and a reserved prefix | D23 |
| 25 | Trusted publishing rather than an API key | D24 |
| 26 | Test stack: xUnit v3, AwesomeAssertions, NSubstitute | D25 |
| 27 | Integration tests run the referees in containers | D26 |
| 28 | A documentation site, published from the repository | D27 |
| 29 | Warnings are errors, and suppressions are local and justified | D28 |
| 30 | Previews on every merge, stable releases on demand | — |
| 31 | Versioned documentation: stable lines, and the preview beside them | — |
| 32 | Documents that cannot be redistributed are fetched on demand, never committed | — |
| 33 | A remote document may be a member of a pinned archive | — |
| 34 | Every valid PDF is readable, and the reader's guards are options | — |
| 35 | Unsafe code, where a measurement asks for it | — |
| 36 | Validation lives in the core, and the conformance profiles in a satellite | — |
| 37 | Out of scope: active content, and PDF to Office | — |
| 38 | The HTML engine loads resources deny-by-default | — |
| 39 | Forward references: late-filled XObjects, and two bounded passes | — |
| 40 | The caller chooses PDF 1.7 or PDF 2.0 output | — |
| 41 | Cryptography lives in satellites | — |
| 42 | Image codecs and scans | — |
| 43 | Skia in .Html and .Rendering, and text analysis is ours | — |
| 44 | Object-shape rules generated from the Arlington model | — |
| 45 | A finding's severity says whether the file reads as written | — |
| 46 | Every milestone ends with an adversarial review by a fresh session | — |
| 47 | The user documentation follows Diátaxis | — |
| 48 | One version for every package, independent of .NET | — |
| 49 | Previews 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.mdkeeps 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 commandadpdf(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#69lterrepeats/Filter. A key given with no value, before>>orendobj, 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
/IDis derived from content, or supplied by the caller, never random — determinism comes first, and a random identifier would make fingerprint tests impossible.