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

24 KiB
Raw Permalink Blame History

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 の通知規約

  1. Worker の受信 slot は envelope と状態を検証する router にする。通常の PDF 要求は 一件だけ保存し、Qt::QueuedConnection または queued invocation で後から実行する。 queued 実行が始まる前から reserved 状態とし、二件目の通常要求を通さない。
  2. PDFium 処理は受信通知の stack を抜けた後、引き続き同じ Worker thread で実行する。 PDFium の並列呼出しや新 Worker thread は導入しない。
  3. callback の同期待ちは、該当 font 応答・transport failure・副要求 deadline だけで解決する。 router は font 応答を通常要求の handling guard より先に処理する。待機中に他の PDF 操作を実行しない。callback から C++ exception を C ABI 越しに送出しない。
  4. Main の選択/ファイル読取りは専用 Broker thread に非同期で渡す。HDC と fontconfig config はその thread が所有する。Main UI thread で同期待ちをせず、font 選択処理から Worker の応答を待たない。送信時に Worker の生存・session・generation を再確認する。
  5. 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 計測で 確定する必要がある。