Skip to content

Architecture

npond edited this page Aug 24, 2026 · 1 revision

Architecture

One shipping library, one test project, .NET 10. Data flows one direction:

Packaging → Ooxml → Styling → Layout → Pdf
                 (Fonts serves Layout and Pdf)

Word's page origin is top-left and PDF's is bottom-left; the flip happens once, in PdfRenderer, and nowhere else.

Directory map

src/n8PDF/
  Converter.cs      the public API; everything else is internal
  Packaging/        OPC container: zip, content types, relationships, PackageLimits
  Ooxml/            WordprocessingML model and parsers, plus Units (twips/EMU/points)
  Styling/          the formatting cascade, producing Resolved*Format
  Fonts/            SFNT parsing, metrics, resolution, shaping (+ OpenType/, Aat/), subsetting
  Text/             the bidirectional algorithm and its generated Unicode tables
  Images/           PNG, GIF, BMP, TIFF, EMF and JPEG decoding
  Layout/           measurement, line breaking, page composition, list counters
  Pdf/              object model, writer, content streams, Type0 embedding
  Diagnostics/      LayoutTrace — the testing spine
tests/n8PDF.Tests/
  Fixtures/Minimal/     hand-authored .docx, one feature each (generated, committed)
  Fixtures/Real/        documents Word itself wrote
  Fixtures/Reference/   Word-exported reference PDFs, named after the fixtures
  Golden/               committed layout traces
tools/                  build-input generators only: Unicode/hyphenation tables, reference PDFs

The pipeline, stage by stage

  1. Packaging opens the ZIP as an OPC package: content types, relationships, part resolution. This is also where the untrusted-input bounds live (PackageLimits, the DTD-prohibiting XML reader) — see Security.
  2. Ooxml parses WordprocessingML into a document model: body blocks, runs, tables, sections, notes, charts, diagrams, equations. Converter.LayoutDocument orchestrates loading the related parts — styles, numbering, theme, headers/footers, footnotes/endnotes, images, hyperlinks, diagrams, charts, embedded fonts — each part owning its own relationship scope (a header's rId1 and the body's are different pictures).
  3. Styling resolves the cascade — document defaults, Word's built-in style fallbacks (below docDefaults, optional), table styles with their thirteen conditional formats, paragraph and character styles, toggle-property cancellation, direct formatting — producing Resolved*Format objects that layout consumes.
  4. Fonts resolves families (theme-aware, embedded-first), reads real metrics, shapes text (OpenType GSUB/GPOS, Apple AAT, the Indic engines, the Universal Shaping Engine), and later subsets faces for embedding.
  5. Layout measures text against the real font files, breaks lines (Unicode line breaking, hyphenation, bidi reordering), composes pages (floats, columns, notes, tables breaking across pages, widow control), and places every run at exact coordinates — with baselines snapped to Word's 1/300-inch grid. A document holding page-dependent fields is laid out twice: the first pass records where everything fell, the second uses it (converging, as Word converges).
  6. Pdf serialises the laid-out pages: content streams, the object graph and cross-reference table (hand-rolled, qpdf-validated), subset font embedding with ToUnicode maps, outline, structure tree, and optionally PDF/A-2b metadata. The single top-left→bottom-left coordinate flip happens here.

Load-bearing design decisions

  • Layout is the product; the PDF is its serialisation. The fidelity suite asserts against the layout trace (Diagnostics/LayoutTrace), not against rendered pixels, which is what makes failures diagnostic: a golden failure names the run that moved and by how much.
  • Deterministic by construction. No wall-clock, no randomness in the output path: the same document converts to identical bytes (given a fixed CreationDate), which is what makes golden comparison possible at all.
  • Errors cost their placement, not the conversion. A missing or unreadable image, chart, or embedded font is skipped where it stands; the document loses the piece rather than the page. The bounds and the conversion-level exception net behind this are described in Security.
  • Generated tables are generated. The Unicode bidi/shaping tables under Text/ and Fonts/ and the hyphenation patterns are output of tools/make-*-tables.py. The library has no dependencies and no run-time data files, so the generators' output is committed as source — and never hand-edited.
  • Per-part relationship scoping. Relationship ids are scoped to the part that declares them; ids are rewritten to include the part name as they are collected, so the whole document shares one address/image table without two parts' ids colliding.
  • Font discovery is paid once. The platform scan builds a 1.6MB index (name, style, file) shared for the process; a face reads its own file only when a document asks for it. First conversion ~600ms for the scan; after that, a page of text converts in about 1.4ms.

Where a change belongs

The one-way flow is the review heuristic: a fix that makes a later stage reach back into an earlier one, or that makes Pdf aware of Word semantics, or a second place that flips coordinates, is in the wrong place. Fonts is the deliberate exception — it serves both Layout (measurement, shaping) and Pdf (subsetting, embedding).

Clone this wiki locally