24 KiB
PDF font broker 実装計画
調査日: 2026-09-19。対象: PDFium 155.0.8057.0、固定 commit
a5a7089234f121990b336b3841008009dca143bf、Qt 6.11.2。
この文書は実装前の調査・計画を保存したものである。現在の実装契約はfont-broker-contract.md、実行結果はVALIDATION.mdを参照。以下の「現行」「未実装」は調査時点を指し、Windowsの実行確認を示すものではない。
結論
最小の推奨構成は、Main 側の FontBroker が OS font 索引と選択を管理し、
PDFWorker の公開 FPDF_SYSFONTINFO version 2 callback から、既存 IPC を使う
限定的な逆方向 RPC で選択済み font のバイト列を取得する方式である。
font の表・glyph の解析は PDFWorker 内に残す。Main に PDFium をリンクしない。
新しい socket、継承 handle、任意パス指定、font ディレクトリーの Worker 読取り権限は追加しない。
バイト列方式なら Linux の SCM_RIGHTS と Windows の起動後 handle 複製を別実装にせず、 現在の socket/匿名 pipe で共通化できる。全 font の先行転送ではなく、要求された font のみを 64 KiB ごとの明示的な read 要求で取得する。巨大な TTC の一括 IPC frame も避けられる。
現行 src/pdf/main.cpp:16 は /usr/share/fonts、/usr/local/share/fonts、
/etc/fonts、/var/cache/fontconfig を Worker に許可している。これは削除対象である。
固定版 Linux PDFium の既定実装は fontconfig ではなく、フォルダー列挙と独自の
CJK 代替候補を使う。したがって Broker を fontconfig に替えるだけで、同一 Linux 上でも
未埋込み font の選択が変化し得る。
固定版 Linux 実装
PDFium 公開 API の適用
ローカル .deps/pdfium/include/fpdf_sysfontinfo.h の 49–52 行に version 2 の
per-request matching があり、86–87 行では EnumFonts を呼ばないことが明記されている。
version 2 は experimental のため、この固定版への依存と更新時の回帰試験を明記する。
private header の include や private symbol の利用は不要である。
| Callback/公開関数 | 推奨実装 |
|---|---|
FPDF_SetSystemFontInfo |
FPDF_InitLibraryWithConfig 後、文書読込み前に Worker 専用 adapter を登録 |
MapFont |
weight/italic/charset/pitch-family/face を Broker へ送り、選択済みバイト列を得て Worker 内 FontBlob を返す |
GetFont |
version 2 では主経路でないが、同じ選択処理への限定 wrapper として用意 |
GetFontData |
Worker の immutable blob から全体または選択 face の table を返す。パスを開かない |
GetFaceName/GetFontCharset |
Broker の選択結果を返す。要求名を実際の選択名として偽装しない |
DeleteFont |
Worker の callback handle を破棄。PDFium が保持するコピーとは寿命を分離 |
Release |
FPDF_DestroyLibrary まで adapter 自体を生存させる。二重解放しない |
MapFont の face は公開 header 上「system local encoding」であり、無条件に UTF-8 と
解釈しない。callback 側で最大 256 bytes まで NUL を探し、raw bytes を RPC に載せる。
Broker が当該 OS の同一規約で解釈し、別名表に照合する。埋込み font の処理は既存 PDFium
のまま維持する。custom adapter が欠けた状態で既定 filesystem font mapper に戻す経路は設けない。
公開 API と固定版 wrapper
想定する内部 API は次の程度に限定する。名前は実装時に確定する。
struct FontRequest {
QByteArray faceLocal; // <= 256 bytes, no embedded NUL
int weight; // 0..1000
bool italic;
int charset; // supported FXFONT_* values
int pitchFamily; // accepted pitch/family bits only
};
struct SelectedFont {
QByteArray fontId; // opaque, scoped to this worker session/generation
QByteArray actualFaceLocal;
int charset;
quint64 size;
QByteArray sha256;
int faceIndex; // Linux FC_INDEX for a supported static face
quint64 faceOffset; // Windows GDI-derived TTC face offset; otherwise unset
bool substituted;
};
// Broker-owned asynchronous service; no PDFium dependency.
// select(request), read(fontId, offset, length), release(fontId), revoke(session)
// Worker-owned synchronous callback adapter over the routed async transport.
// fetchSelected(request) -> immutable FontBlob
RPC 契約
新 operation は font.map、font.read、font.close の三つとする。
既存 envelope の protocolVersion/sessionId/generation/requestId/operation と
payload XOR result/error を維持する。requestId は現在実行中の親 PDF 要求の ID、
各 payload と result/error の fontRequestId は Worker が単調増加させる副要求 ID とする。
同時に保留する副要求は一つだけである。
| Operation | Worker → Broker payload | Broker → Worker result | 主な照合 |
|---|---|---|---|
font.map |
fontRequestId, faceLocal, weight, italic, charset, pitchFamily |
fontRequestId, found。found=true のとき fontId, actualFaceLocal, charset, size, sha256, faceIndex/faceOffset, substituted |
PDFWorker にだけ有効。現在の親 requestId/session/generation 一致。文字列をパスや fontconfig pattern 構文として解釈しない |
font.read |
fontRequestId, fontId, offset, length |
fontRequestId, fontId, offset, data |
ID は同セッションに発行済み、1 <= length <= 65536、加算 overflow と offset+length <= size を確認。返却サイズは要求どおり |
font.close |
fontRequestId, fontId |
fontRequestId, released:true |
発行済み ID を一度だけ解放。revoke 後の ID は無効 |
全応答は control frame のまま 1 MiB 以下とし、64 KiB を超える data は拒否する。
more は使わず、一回の read に一回の応答を返す。最終 size と SHA-256 が一致してから
blob を PDFium に公開する。CRC 等の font 内容検証を Main で行う意味ではなく、転送を
途中で使わないための完整性確認である。
Broker の受信側は通常 PDF 応答を照合する前に、payload を持つ三つの font operation を
専用分岐へ送る。通常の active_ を完了させず、キューを進めず、親 deadline を延長しない。
不正な role/ID/offset/連続要求は既存の protocol failure として Worker を停止する。
ArchiveWorker と試験用一般 worker には、このサービスを明示的に登録しない。
ファイル名や fontconfig の FC_FILE は応答に含めない。
font が存在しない場合は found:false として PDFium 内部代替と UI の代替警告を許す。
通信破損、資源上限超過、timeout は font 不在と区別し、当該 PDF 操作を失敗させる。
新規 error code を増やす場合は data contract に登録し、未定義の code を先行実装しない。
同期 callback、deadlock、取消
現在の src/pdf/main.cpp は IpcChannel::messageReceived の direct slot 内で PDFium
を呼ぶ。そのまま callback 内で QEventLoop を回して font を待ってはいけない。
Qt の readyRead は再帰的に再送されず、Windows の現 WindowsPipeDevice::completeRead
も emit readyRead() の後に次の beginRead() を置いている。いずれも受信処理の
stack を抜ける前に次の受信を待つ構成が停止の原因になる。
QIODevice の通知規約
- Worker の受信 slot は envelope と状態を検証する router にする。通常の PDF 要求は
一件だけ保存し、
Qt::QueuedConnectionまたは queued invocation で後から実行する。 queued 実行が始まる前から reserved 状態とし、二件目の通常要求を通さない。 - PDFium 処理は受信通知の stack を抜けた後、引き続き同じ Worker thread で実行する。 PDFium の並列呼出しや新 Worker thread は導入しない。
- callback の同期待ちは、該当 font 応答・transport failure・副要求 deadline だけで解決する。 router は font 応答を通常要求の handling guard より先に処理する。待機中に他の PDF 操作を実行しない。callback から C++ exception を C ABI 越しに送出しない。
- Main の選択/ファイル読取りは専用 Broker thread に非同期で渡す。HDC と fontconfig config はその thread が所有する。Main UI thread で同期待ちをせず、font 選択処理から Worker の応答を待たない。送信時に Worker の生存・session・generation を再確認する。
- Worker 停止/新 generation/staging 取消で Broker の ID と待機処理を revoke する。 進行中 OS API の終了を GUI が待つ必要はない。遅れて来た結果は捨てる。既存の process kill と親 deadline が最終的な中断手段になる。単なる検索キュー破棄では、現在の render に必要な font を revoke しない。
QueuedConnection は受信側 event loop へ制御が戻ってから slot を実行する。
同じ thread で BlockingQueuedConnection を使う方式は採らない。
Qt connection type
OS ごとの Broker 選択
Linux
Broker が FcInitLoadConfigAndFonts/FcFontList または FcConfigGetFonts で許可索引を作り、
font のファミリー・style・charset coverage・FC_FILE・FC_INDEX を内部に保存する。
Worker の値は FcPatternAddString 等の typed API の値として渡し、FcNameParse に流さない。
固定 alias と優先順位を先に適用し、FcConfigSubstitute、FcDefaultSubstitute、
FcFontMatch/必要なら限定した FcFontSort の順で選ぶ。OpenType weight は
FcWeightFromOpenType で変換し、固定幅/serif/italic と charset に対応する言語を保持する。
FcFontMatch は事前 substitution が必要であり、FC_INDEX はファイル内の face index である。
Fontconfig 一次リファレンス
結果は Broker が構築した索引内の候補に再照合する。Worker からファイル、ディレクトリー、
fontconfig config、FC_FT_FACE、任意 pattern オブジェクトを受け取らない。選ばれた索引の
ファイルを Broker が read-only で開き、通常ファイルとサイズを確認して bounded snapshot
を作る。索引作成時と file identity が変わっていれば、その候補を再索引または拒否する。
font 更新との競合でバイト列が壊れても、font 内容を解析するのは隔離 Worker である。
索引の入力は OS/アプリの font 設定だけであり、PDF 内の添付 font を fontconfig に登録しない。
単に sans:lang=ja に委ねることは固定順序の代わりにならない。今回の環境では
fc-match 'sans:lang=ja' と serif:lang=ja が両方 FORM UDPGothic を返した。
これは環境の観測であり一般的な fontconfig の保証ではない。
Windows
Broker が EnumFontFamiliesExW で名前・style・charset の索引を作る。候補を
LOGFONTW と CreateFontIndirectW で作成し、Broker 専有 HDC へ SelectObject して
GetTextFaceW/GetTextMetricsW で実際の選択を検証する。要求名がそのまま選ばれる保証は
ないので、許可索引と alias 規則に照合する。HDC と HFONT は Broker thread だけで使い、
元の選択 object を戻してから DeleteObject/DeleteDC する。
列挙、
論理 font の選択、
LOGFONTW
GetFontData で選択済み font をバイト列にする。collection は GDI の 'ttcf'
0x66637474 で全体を取得し、table 0 のサイズとの差から選択 face の offset を求める。
通常の TTF/OTF は table 0。GDI_ERROR、0、上限超過を失敗として扱う。Worker に HFONT
や HDC は渡さない。font ファイルパスの探索も Worker へ移さない。必要追加 library は
Broker target の gdi32 だけである。
GetFontData の全体・collection・サイズ取得
OS font のバイト列はプロセス内の表示用途とし、文書への再埋込みや配布用ファイル生成を この機能に含めない。同梱 font は別途、配布許諾・LICENSE・hash を固定する。
TTC、table、short buffer の具体的な注意
公開 API に faceIndex を直接 PDFium へ渡す欄はない。固定版 GetCachedTTCFace は
ttc_size - data_size を face offset とし、それを collection 内の face index に戻している。
そのため Linux の FC_INDEX も Windows の選択済み HFONT も、下表の callback の振舞いへ
変換しなければならない。全ファイルだけを table 0 として返す実装では collection の
選択 face が失われる。
固定版 mapper の TTC 経路
| 入力/状況 | Worker adapter の契約 |
|---|---|
| 単独 TTF/OTF、table 0 | ファイル全体の size/bytes |
単独 TTF/OTF、0x74746366 (ttcf) |
0、collection ではない |
TTC、ttcf |
collection 全体の size/bytes |
| TTC、table 0 | collectionSize - selectedFaceOffset。必要な bytes は selectedFaceOffset から末尾まで |
| 通常の table tag | 選択 face の directory を参照。tag は PDFium 側の big-endian 数値、offset/length は checked arithmetic で blob 内に制限 |
buffer == nullptr または size 0 |
必要サイズだけ返す |
| buffer が足りる | 全対象 bytes をコピーし、コピーしたサイズを返す |
| 小さい非 NULL buffer | 最大 buf_size だけ prefix を埋め、公開 header に従い必要サイズを返す。後述の固定版 1024-byte 読取りを試験する |
TTC header の offsets と各 SFNT table directory を Worker の bounded reader で解析する。 offset は collection の先頭を基準とする。face count、table count、offset + length、重複 tag、 選択 index を検証し、Main にこの font parser を置かない。 OpenType collection と table directory
固定版 GetChecksumFromTT は 1024 bytes の buffer を渡し、返却サイズを使わず、その
buffer から checksum を計算する。したがって「短い buffer では何も書かず必要サイズだけ
返す」という解釈では未初期化 bytes が残る。adapter は buffer 範囲を初期化し、存在する
prefix を必ず埋める。短い collection の場合の余りはゼロにする。この挙動は公開 API
記述だけから推定せず、固定版実装を対象とする回帰試験にする。
固定版 checksum 呼出し
Windows GDI は tag の整数 byte order が異なる。PDFium の既定 Windows adapter も
FromBE32(table) で変換してから GDI に渡している。Worker の SFNT reader と GDI の
GetFontData を同じ tag 値で無条件に呼ばない。
固定版 Windows adapter
初期対応は static TTF/OpenType/TTC/OTC とする案を推奨する。variable font の named instance、色付き font、Type1 の扱いを実装せずに対応済みとはしない。索引にはあっても 未対応なら別の静的候補を選び、代替を記録する。variable instance を黙って face 0 として 渡す方式は採らない。これらは既存 mapper に対する潜在的な互換性縮小である。
上限と lifetime の初期案
以下は設計書に既存の値ではなく、font broker 向けの提案値である。CJK collection の実測と 性能試験で妥当性を確認してから contract に固定する。
| 対象 | 初期上限案/扱い |
|---|---|
| face 名 | raw bytes 256、embedded NUL 拒否 |
| 1 font/collection snapshot | 64 MiB。超過時は別候補、なければ明示的制限 |
| 1 read | 64 KiB、同時副要求 1、write queue に全体を先積みしない |
| font 選択 handle | session 内 16、通常は map→read 全体→close で短く保持 |
| Broker snapshot cache | 全体 256 MiB、参照中を避ける LRU。複数 session 合計で計測 |
| Worker font blob cache | 128 MiB。callback handle が参照中の blob を捨てない |
| session の異なる選択数/転送累計 | 256/512 MiB。repeated malicious names による無制限列挙を防ぐ |
| RPC 待機 | 1応答 5秒、1 font 取得全体 15秒。成功 chunk ごとに全体期限を延長しない |
| 全体の処理 | 既存親要求期限と 1.5 GiB Worker 制限を維持。font 待機もその中に含める |
64 MiB は未検証の大きな CJK collection を切り捨て得るため、上限到達を「font が無い」と 黙って扱わない。PDFium 自身の font cache も bytes を複製するので、adapter cache のみを 数えて 1.5 GiB を守れたと判断しない。Broker の snapshot と OS index は Worker から 呼び出せる任意ファイル読み取りサービスに拡張しない。
固定 alias と回帰 corpus
fontconfig の全ホスト設定を置き換えるのではなく、アプリが管理する候補順を先に試す。 例として Base14 の Courier/Helvetica/Times と各 bold/italic を個別の entry にし、 承認した同梱書体、既知の metrically compatible OS 書体、最後に charset/family に沿う OS 選択という順を固定する。Symbol/ZapfDingbats は generic sans に置換せず、既存の PDFium 内蔵経路または専用の承認候補を維持する。
日本語は Gothic/Mincho、等幅/比例、weight/italic、英語名/ローカル名を分ける。
@ で始まる縦書き face、Shift-JIS に由来する face 名、CJK の charset を別ケースとして
試験する。alias の具体的な font を決める前に、対象 OS に存在することと配布条件を確認する。
試験用 BIZ UDPGothic の埋込み許諾が確認済みでも、それだけで製品の全 fallback 資産が揃った
ことにはならない。
| Corpus/試験 | 固定する観測と合格条件 |
|---|---|
| 既存埋込み日本語/画像/透明合成/CropBox PDF | font broker を入れる前後で選択・glyph・配置が変わらない。埋込み font を system font に置換しない |
既存 unembedded-font.pdf |
現在は Standard-14 の限定 corpus。選択 family/file hash/style を記録し、Poppler 対照と文字位置・幅を比較 |
| 新規未埋込み日本語 | 正しい CID encoding を持つ horizontal/vertical の原本を生成。単に subset font stream を削除する方式は glyph ID が変わるため用いない |
| alias/weight/italic/symbol | exact installed name、Base14 alias、不在名、日本語ローカル名、記号をそれぞれ検証。Broker が選んだ実名と hash も期待値に含める |
| TTC の複数 face | 同一 collection の異なる faceIndex を描画し、face 0 固定にならないこと。table offset/collection read/1024-byte prefix を単体試験 |
| malformed font bytes | truncated directory、過大 table count、範囲外 offset、加算 overflow、重複 tag、短い buffer を Worker adapter 単体で拒否 |
| sandbox 攻撃 | font broker 正常動作中でも Worker から font directory/設定/無関係ファイルを open できない。path 風 face 値を使っても任意パスを読まない |
| RPC/取消 | 偽 fontId、別 generation、重複応答、途中切断、読取り範囲超過、response timeout、font 取得中の close/kill、遅い応答を投入。UI が待ち合わせで停止しない |
| lifecycle/メモリー | 一覧にない要求の連打、font 名の大量変化、TTC 再利用、長時間読書を測定。Broker と Worker 両方のメモリー、送信 queue、UI 応答を記録 |
| 両 OS | alias policy、選択結果、PDFium adapter が同じでも OS font の bytes が違う点を記録。Windows は native 環境で実施するまで未検証 |
この検証は固定 corpus に対する互換性範囲を増やすもので、欠けた元 font の完全再現や 任意 PDF の一致を保証しない。既存 Linux/Poppler 比較は embedded 日本語と Standard-14 を 中心にしており、未埋込み日本語・TTC・variable instance の新経路の合格証拠にはならない。
必要変更箇所と実装順
| 場所 | 変更 |
|---|---|
新 src/common/font_contract.* |
font RPC の型・値・サイズ・role・ID 検証。QtCore のみ |
新 src/broker/font_broker.* |
OS index、alias、選択、snapshot と quota。PDFium 非依存。OS backend は別ファイルに分割 |
新 src/pdf/system_font_adapter.* |
公開 SYSFONTINFO、immutable blob、checked SFNT/TTC reader。PDFWorker/adapter test にだけリンク |
src/common/ipc.* |
三 operation の許可と envelope の限定拡張。control/data 上限は増やさない |
src/common/worker_process.* |
font request の role 分岐、親 active 保持、応答送信、deadline、stop 時 revoke |
src/pdf/main.cpp |
router と queued PDF 実行、font response 待機、font directory 権限の全削除 |
src/pdf/pdf_document.* |
adapter を init 時登録し destroy まで保持。テストは explicit in-memory font provider を注入可能にする |
src/app/controller.* |
PDF session だけに Broker を登録。代替・制限警告と寿命管理 |
CMakeLists.txt |
Broker Linux backend に fontconfig、Windows backend に gdi32。Main に docview_pdf/PDFium を追加しない |
| tests/fixtures/results | 契約、callback、実 IPC、sandbox denial、両 OS 描画比較を追加 |
実装順は、(1) memory-only callback と TTC 単体試験、(2) bounded font 契約+偽 provider で 同一 transport の応答/取消/deadlock 試験、(3) Linux index と alias、(4) font directory 権限を外した PDFWorker で回帰、(5) Windows GDI backend と native gate とする。 最初の callback 実装と IPC 試験は独立して進められるが、権限削除だけを先行して 既定 font mapper の失敗を内部 fallback で隠さない。
調査の検証範囲
公開 local header、固定 commit の PDFium 公式ソース、Fontconfig・Qt・Microsoft の一次資料を
読んで確認した。Linux の fontconfig 2.18.3 と現在の fc-match 選択を読み取りで確認した。
この調査では production code を変更せず、callback prototype の実行、逆方向 RPC、GDI の
native 実行は行っていない。上限値・alias の最終集合・描画閾値は、実装と corpus 計測で
確定する必要がある。