# Font broker contract This implementation supplements design 03 §3.2 and 05. The selected font is transferred as an immutable, bounded byte snapshot through the existing IPC connection. No filesystem path or OS font handle crosses the boundary. The main process selects fonts through Fontconfig on Linux and GDI on Windows; PDFium and SFNT/TTC table parsing remain inside the sandboxed PDF worker. See [the API investigation](font-broker-plan.md) for pinned upstream evidence. Windows implementation and native Windows execution are separate claims; consult [the validation record](VALIDATION.md) for actual execution coverage. ## Request envelope and identity Only a `WorkerProcess` with an explicitly registered font handler accepts `font.map`, `font.read`, or `font.close` requests. Production registers the handler only for PDF workers. Requests use the current parent operation's `sessionId`, `generation`, and `requestId`. The payload contains a positive, monotonically increasing `fontRequestId`; the result or error repeats it. Only one font request may be outstanding. These are control frames, with no streaming `more` field and the existing 1 MiB encoded control-frame limit. Unknown fields, wrong types, stale identities and unissued handles are rejected. The main process routes font requests before ordinary PDF responses. It never finishes or replaces the active PDF operation or extends its deadline as a side effect. The callback API removes `fontRequestId` from request/result maps; the transport layer owns that field. Broker completion returns either the inner result map or `{error: {code, message}}`. Cancellation discards pending completion and invalidates issued handles. A parent timeout error can end a pending font request without interpreting it as successful font acquisition. | Operation | Additional payload fields | Additional result fields | | --- | --- | --- | | `font.map` | `faceLocal` bytes, `weight` integer, `italic` boolean, `charset` integer, `pitchFamily` integer | `found` boolean. If true: `fontId`, `actualFaceLocal`, `charset`, `size`, `sha256`, `substituted`, and exactly one of `faceIndex` / `faceOffset` | | `font.read` | `fontId`, `offset`, `length` | Same `fontId` / `offset`, and exactly `length` bytes in `data` | | `font.close` | `fontId` | `released: true` | Names use the system local encoding as required by the pinned PDFium public API, contain no NUL, and are at most 256 bytes. Requested names may be empty; selected names must not be empty. Weight is 0–1000. Charset is one of 0, 1, 2, 128, 129, 134, 136, 161, 163, 177, 178, 204, 222, 238. Pitch/family accepts family values 0x00–0x50 in steps of 0x10 with pitch 0–2. Names are typed family values, never fontconfig pattern expressions or paths. The font ID is 32 lowercase hexadecimal characters, scoped to one service. SHA-256 is 32 raw bytes. Face index is 0–65535; face offset is inside the snapshot. ## Limits and errors | Resource | Bound | | --- | --- | | Snapshot | 64 MiB per font/collection | | Read | 1–65536 bytes; checked against the issued snapshot's actual size | | Open font handles | 16 per session | | Different selection requests | 256 per session, including misses | | Total transferred font bytes | 512 MiB per session | | Worker snapshot cache | 128 MiB; live callback references cannot be evicted | | Broker snapshots | 256 MiB across active, staging and revoked services; global LRU of unreferenced snapshots | | OS font threads | Two per application process; excess services fail explicitly | | Broker task queue | Eight entries per service (production wire permits only one pending request) | | One reply / complete font fetch | 5 seconds / 15 seconds, within the unchanged parent deadline | `E_FONT_LIMIT` is a new registered error for these resource bounds. It is not reported as a missing font. `E_FONT_FAILED` is a new registered error for OS font acquisition, unsupported/invalid font data. A snapshot hash or wire mismatch uses the protocol-failure path. Existing `E_WORKER_TIMEOUT` and `E_WORKER_CRASH` represent transport/deadline failure. All four font error maps contain `fontRequestId`, `code`, and a nonempty message of at most 4096 characters. Malformed reverse RPC uses the existing worker protocol-failure path. A genuine absent match is the success result `found: false`; PDFium may use its built-in fallback and the adapter records a warning. A communication/resource error must fail the PDF operation. Fatal font errors remain sticky for the adapter lifetime, because PDFium may have cached an internal substitute during the failed call. Retry requires a new worker/document; clearing only the error would hide that failed selection. ## Execution and font data Normal PDF operations run from a queued invocation after the transport's receive stack unwinds. Font responses are routed during the bounded nested wait without reentering PDFium or starting another PDF operation. Main-thread GUI processing never waits for Fontconfig/GDI. Revocation prevents new work and drops late callbacks, while an already running OS call retains its bounded thread and snapshot charges until cleanup. Snapshot eviction considers unused entries across services, so an old document cannot monopolize reclaimable cache space while a new document opens. In-process test/low-memory constructor arguments may lower the cache bound; they cannot raise the production cap. The worker always installs `FPDF_SYSFONTINFO` version 2. A missing provider does not reactivate PDFium's default filesystem font mapper. The production Linux worker receives no font-directory grants. Downloaded chunks must match declared size and SHA-256 before the immutable snapshot reaches PDFium. For a TTC, `ttcf` returns the complete collection and table zero returns the bytes from the selected face offset to the end. A normal table tag resolves through that face's checked directory. A short non-null caller buffer is zero-initialized and filled with the available prefix; the required size is returned. This also supports the pinned PDFium mapper's 1024-byte checksum read. Font parsing rejects out-of-range tables, invalid collection indices and duplicate table tags. Static TTF/OpenType/collections are the initial scope; named variable instances are not silently mapped to face zero. A collection may contain at most 256 faces and each face at most 4096 table entries. These parser bounds are separate from the wire field range for an OS face index. Font substitution depends on installed candidates. Fixed aliases and family priorities are maintained in `src/broker/font_backend.cpp`; selected family, charset, face metadata and substitution status describe the actual result. The synthetic unembedded Japanese H/V corpus exercises native CID writing modes. Its independent-renderer comparison is additional evidence, not a human-approved golden image or a claim about every PDF CMap.