This document captures the design principles, package contracts, and ground rules for the Folio PDF library. Use it to evaluate whether a proposed change (issue, PR, feature request) fits the project's direction or would introduce a deviation that makes the library harder to maintain.
-
Spec-first, not viewer-first. Implement what ISO 32000 says, not what a particular viewer happens to accept. When the spec is ambiguous, match the behavior of the major viewers (Adobe Acrobat, Chrome's PDFium, macOS Preview) and document the choice.
-
Standard library cryptography only. All encryption (AES, RC4) and signing (CMS/PKCS#7, RSA, ECDSA) use
crypto/*andencoding/asn1from the Go standard library. No external cryptography dependencies — ever. -
Minimal external dependencies. The only direct dependencies are
golang.org/x/image(TIFF decoding; sfnt is now test-only since #260),golang.org/x/net(HTML parsing), andgolang.org/x/text(Unicode bidi algorithm for RTL text). A new external dependency requires a compelling justification and must come from thegolang.org/xecosystem or be similarly vetted. Convenience is not a compelling justification. -
Zero-allocation PDF output path. The writer (
coretypes →io.Writer) should not allocate beyond what the data requires. No intermediate[]bytebuffers for the full document. Objects stream directly. -
Deterministic output. Given the same inputs, produce byte-identical PDFs. This means ordered dictionaries (insertion order, not map iteration order), stable subset tags, and no timestamps unless the caller opts in. This enables testing by byte comparison.
-
Immutable layout results. The layout phase produces a
LayoutPlanthat is pure data — no closures that capture mutable state, no side effects. This enables caching, concurrent layout, and straightforward testing. -
Errors, not panics. Public API functions return errors. Panics are reserved for true programming errors (nil where non-nil is required, violated internal invariants). Never panic on malformed PDF input.
Each package owns one concern. If a change touches multiple packages, check that it respects these boundaries.
| Package | Owns | Does NOT own |
|---|---|---|
core |
PDF object types (§7.3), serialization, encryption (§7.6) | Parsing, document structure, page layout |
content |
Content stream builder (operators → bytes) | Interpreting content streams, text extraction |
font |
Font loading, parsing, metrics, subsetting, PDF embedding | Text layout, line breaking, glyph rendering |
image |
Image decoding (JPEG/PNG/TIFF) and PDF XObject construction | Image manipulation, resizing, format conversion |
barcode |
Barcode generation (QR, Code 128, EAN-13) as module grids | Barcode scanning/reading |
layout |
Element model, box layout, pagination, rendering to content streams | HTML parsing, CSS parsing, document assembly |
forms |
AcroForm field creation and form filling | Field validation logic, JavaScript actions |
sign |
PAdES digital signatures (B-B through B-LTA), CMS, TSA, OCSP, DSS | Certificate management, key storage |
reader |
PDF parsing, object resolution, text extraction, page merging, content transforms (redaction, form flattening) | PDF modification in place (use incremental writes) |
html |
HTML+CSS → layout.Element conversion |
Direct PDF generation (that's document's job) |
svg |
SVG parsing and rendering to content stream operators | SVG creation/generation, raster export |
document |
Top-level document assembly: pages, fonts, images, outlines, metadata, PDF/A, tagged PDF | Low-level PDF object wiring (that's core) |
export |
C ABI for FFI consumers | Business logic — it delegates everything to other packages |
cmd/folio |
CLI tool for merge, info, text extraction, signing | Library API (it's a thin consumer) |
cmd/wasm |
WebAssembly entry point — exposes folioRender for browser use |
Library API (thin consumer) |
cmd/gen-metrics |
Code generator — parses AFM files, emits Go width/kern tables | Runtime logic (build-time only tool) |
integration |
Cross-package integration tests (header/footer, import, running headers) | Production code — test-only package |
Imports flow downward. A package must never import a package above it or at the same level (no cycles).
document
/ | \
html layout sign
/ | | \ \
svg font image barcode
\ \ | / \
content forms reader
\ / /
core
Exceptions:
documentimportshtmlto offer HTML-to-PDF convenience methods (AddHTML) that delegate to the converter.layoutimportsimagefor image element rendering and background image support in divs.readerimportsdocumentfor struct tree types andfontfor standard font metrics (used in text extraction width calculation).formsimportsreaderfor form filling on existing PDFs.
These are intentional — they exist because the functionality requires
access to types defined in another layer. If these cross-layer imports
grow, consider extracting shared types into core or a new model
package.
These are deferred refactors that would improve the architecture but
have high churn cost relative to immediate benefit. Tackle them when a
natural trigger arises (e.g., a new package needs Color).
-
TODO(A4): Unify
layout.Colorandsvg.Color. Two separate Color types serve the same purpose. Extract a shared color type intocoreor a newstylepackage to eliminate conversion friction. Trigger: when a third package (e.g.,markdown) needs a Color type. -
TODO(A5): Reduce
layoutpackage surface area. At ~75 exported types,layoutis the largest package. Primitives likeColor,Margins,Padding,UnitValuecould move to a shared package. Trigger: whenlayoutgrows further or when multiple packages need these primitives independently. -
TODO(A6): Split
readerinto focused packages. Thereaderpackage handles parsing, text extraction, merging, and redaction — four distinct concerns. Splitting intoreader,merge, andredactwould improve cohesion. Trigger: when any of these areas grows significantly or gains its own public API surface.
Constructor names follow a consistent pattern across packages:
| Pattern | Meaning | Example |
|---|---|---|
New* |
Construct from Go types or config | NewDocument(), NewDiv(), NewEmbeddedFont(face) |
Parse* |
Construct from raw bytes ([]byte) |
ParseTTF(data), Parse(pdfBytes) |
Load* |
Construct from a file path (string) |
LoadTTF(path), LoadJPEG(path) |
When a package offers both file and bytes constructors, prefer Load*
for paths and Parse* / New* for bytes. Value-type constructors
(borders, colors) may use descriptive names without a prefix
(e.g., SolidBorder, DashedBorder).
These are things the library intentionally does not do. PRs that introduce these should be redirected or declined.
- PDF rendering / rasterization. Folio creates and reads PDFs; it
does not render them to pixels. Use a viewer or a tool like
mupdf/popplerfor that. - JavaScript / actions. PDF supports embedded JavaScript. We don't and won't — it's a security surface with minimal value.
- Multimedia annotations. Sound, video, 3D, and rich media annotations are out of scope.
- XFA forms. XFA is deprecated in PDF 2.0. We support AcroForms only.
- Full CSS compliance. The HTML converter supports a practical subset of CSS for document generation. It is not a browser engine and does not aim to be one.
- Image manipulation. No resizing, cropping, color space conversion, or format transcoding. Accept what the caller provides.
- Certificate / key management. The signing package accepts keys and certificates; it does not generate, store, or manage them.
| Dependency | Justification | Replacement plan |
|---|---|---|
golang.org/x/image/font/sfnt |
Retained only for tests (cross-checking parse output). Production metric reads now go through Folio's in-tree parsers (font/{head,hhea,maxp,hmtx,os2,name,cmap}.go); see #260 for the migration. |
None planned; remove from go.mod once tests stop importing it. |
golang.org/x/image/tiff |
TIFF decoding. Rarely used but required for completeness. | Could drop if TIFF support is removed. |
golang.org/x/net/html |
HTML tokenization and tree construction. Correct HTML parsing is hard and well-solved. | No replacement planned. |
golang.org/x/text/unicode/bidi |
Unicode Bidirectional Algorithm (UAX #9) for RTL text support. The bidi algorithm is complex (100+ pages of spec) and the Go team's implementation is well-tested against Unicode conformance vectors. | No replacement planned. layout.resolveLineBidi wraps the library; if we ever need a custom implementation, the wrapper isolates calling code. |
All other functionality (PDF parsing, encryption, signing, content streams, subsetting, barcode generation, SVG rendering) is implemented from scratch using only the Go standard library.
Every local or remote asset reference in the html package — <img src>, inline SVG, <link rel="stylesheet">, @font-face url(),
background-image: url(), and Options.FallbackFontPath — routes
through one contract: resolveLocalAsset in html/converter.go. The
function's doc comment is the canonical spec for the rules (URL
fetch, BaseFS-relative read, absolute-path-when-BaseFS-nil, and the
FallbackFontPath programmatic carve-out).
When evaluating a proposal that adds a new resolver — a new HTML
attribute, CSS property, or Options field that consumes a path or
URL — check that it routes through resolveLocalAsset rather than
calling os.ReadFile / http.Get directly. No future resolver should
grow its own rules.
Public API functions return error. Errors are wrapped with
fmt.Errorf("package: context: %w", err) so callers can use
errors.Is and errors.Unwrap to inspect causes. Each package
prefixes its errors with the package name (e.g., "reader: ...",
"sign: ...").
Sentinel errors are used sparingly — only when callers need to branch
on the error kind (e.g., reader.ErrMemoryLimitExceeded). Most
functions return wrapped standard errors.
Panics are used only for violated preconditions in constructors (nil font, negative size, invalid enum) where the caller has a programming error. They are never used for recoverable conditions or malformed input.
Individual Folio objects (documents, readers, layout elements) are not safe for concurrent use from multiple goroutines. This matches the standard Go convention: callers synchronize access if needed.
The layout phase is designed for concurrent-friendly use: PlanLayout
produces immutable LayoutPlan values with no shared mutable state,
so multiple elements can be laid out concurrently if the caller
manages goroutines.
The reader package is safe to use from multiple goroutines after
parsing completes (Open/Parse return), since the parsed state is
read-only. Concurrent calls to ExtractText on different pages are
safe.
The library follows semantic versioning. While
on v0.x, breaking API changes may occur in minor releases. Each
release includes a changelog documenting additions, changes, and
breaking changes.
The goal is to reach v1.0 with a stable public API. Until then,
breaking changes are batched into releases rather than sprinkled
across patches, and deprecated symbols are kept for at least one
minor release when feasible.
When reviewing an issue or PR, ask:
- Does it respect package boundaries? If it adds image resizing
to
imageor CSS animation tohtml, it's out of scope. - Does it add a dependency? If yes, can the same thing be done with the standard library in reasonable effort?
- Does it produce deterministic output? If the change introduces maps, random values, or timestamps without opt-in, it breaks reproducibility.
- Does it follow the layering rules? Check that no new import cycles are introduced and that dependencies flow downward.
- Is it spec-grounded? New PDF features should reference the relevant ISO 32000 section. "Adobe Reader does X" is not sufficient justification without a spec reference.
- Does it fit the non-goals? If the feature is explicitly listed as a non-goal, the bar for inclusion is very high — it requires a change to this document first.