Files
docview/docs/font-broker-contract.md
2026-09-21 13:41:40 +09:00

108 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.