Architecture overview
Orifold follows a unidirectional flow: SwiftUI views send intents to one observable view model, local PDF engines behind protocol seams do the work, and a staged export pipeline writes validated artifacts. Nothing crosses a network.
Layers
Section titled “Layers”| Layer | Responsibility |
|---|---|
| SwiftUI app | Document window, sidebar, canvas, annotation toolbar, search, inspector, password prompts, export controls, empty-state Recently Viewed shelf, language switcher |
| Workspace state | Imported documents, page order, text/object edit operations, canonical replay bases, undo snapshots, comments, tags, signatures, form summaries, decorations, recent-file history |
| PDF services | PDFKit display/composition, versioned shared PDFium page inspection, deterministic text+object replay, PDFium structural object editing/image compression/validation/glyph geometry, qpdf repair/AES-256/sanitize/structural validation, Vision OCR, form flattening, decoration baking |
| Signing | CMS/PAdES signature construction, PKCS#12 · Keychain · self-signed identities, certificate trust evaluation, RFC-3161 timestamping |
| Local storage | Saved PDF data, workspace metadata, source payloads, comments, signatures, page edit state |
| Release tooling | One-line installer, package builder, Desktop update launcher, uninstaller, GitHub Actions release asset, validation tests |
Project layout
Section titled “Project layout”Orifold/ App/ App entry point, command wiring, keyboard-shortcut registry DesignSystem/ Shared visual tokens and styling Document/ macOS document package read/write support Engine/ PDF loading, repair, conversion, OCR, compression, encryption, sanitize, forms, export, Recently Viewed store Models/ Workspace, page, annotation, comment, export, decoration, comfort, and recent-file models Pet/ Gami & Ori, the in-app companions, plus hint bubble/placement logic Resources/ App metadata, entitlements, assets, Localizable.xcstrings (6 languages) Signing/ Digital-ID providers, CMS construction, timestamping, trust evaluation, appearance ViewModels/ Workspace state, document operations, search, export, undo Views/ SwiftUI interface componentsPackages/ Vendored binary engines — PDFiumBinary, QPDFBinary (universal static libs)Tests/ Test suites run in the release gateThe staged export pipeline runs in a fixed order, and validation gates the write
Export is a sequence, not a single call: flatten annotations and form answers → bake decorations (stamps, watermarks, Bates labels, page numbers) → compress (optional image downsampling + lossless qpdf repack) → sanitize (optional strip of auto-run actions, JavaScript, embedded files, metadata) → encrypt (optional AES-256) → structural validation. The final qpdf structural check gates the write: if the artifact isn’t sound, it never reaches disk. This ordering is deliberate — flattening before decoration burn-in means nothing can be silently edited after the fact.
Committed text and object edits replay through one member-atomic path
WorkspaceEditReplayEngine takes canonical member bytes plus the committed text and object operations. It applies structural object changes first, imports transparent text-edit overlays as Form XObjects, grafts live annotations/form state with qpdf, and finishes rotations plus combined bake stamps through PDFium SaveAsCopy. PDFKit may serialize the live view as an annotation/form source, but those bytes are never used as the replay destination’s page-content base. Load/export reconciliation uses the same path, so stale bytes self-heal without one editing lane reverting the other.
Page-object consumers share one versioned PDFium inspection snapshot
PDFPageObjectInspection walks an admitted page’s PDFium objects once and projects three views: text render-mode regions, PageGraphicsIndex rule lines, and the editable PageObjectMap. The workspace assigns canonical member bytes a revision token and retains each admitted PageRef for that revision, so text and object consumers reuse one snapshot even after visiting many pages. Per-revision and process-wide page/projection-cost ceilings fail closed rather than evicting and rescanning accepted snapshots; replacing canonical bytes invalidates the token. Permission and live page rotation are projected without rescanning.
Canvas event ordering is a state-machine contract
CanvasInteractionSession turns tool changes, Delete/Escape, geometry changes, and object mutations into ordered actions. The AppKit coordinator interprets those actions, keeping undo-manager alignment, document swaps, selection cleanup, and overlay refresh order in one testable place.
Import goes through a text-layer-preserving normalizer, not a plain re-serialize
PDFKit’s dataRepresentation re-serialization drops certain text layers (Type3 / Skia-drawn glyphs), which historically caused edits to “land on top” of invisible original text. Import instead runs through a qpdf-preserving normalizer so the real text layer survives into the workspace, and text-edit geometry is measured from PDFium’s actual glyph transforms rather than re-derived. See the Developer FAQ for the full story.
Engines sit behind protocol seams, so they're swappable and testable
Each engine is reached through a protocol (PDFProcessingEngine, signing contracts, and so on) rather than called directly. That seam is why the compression backend can be swapped, why signing can be driven by a test harness with a self-signed identity, and why the suite can exercise the pipeline without the real binaries in every path.
By the numbers
Section titled “By the numbers”Swift 5.9+, 100% SwiftUI. 186 source files, roughly ~68,000 lines, 752 tests gating every release, 6 languages, 4 sandbox entitlements.