PDFで「𠮷」が豆腐になる理由TCPDF とサロゲートペア
純PHPのPDFライブラリTCPDFがBMP外の漢字を出力できず「豆腐」になる。フォント差し替えでは直らない理由を、符号がGID 0へ落ちるエンコード経路から追う。
現象:フォントを替えても消えない豆腐
行政の帳票をPDFで生成している。純PHPのPDFライブラリTCPDFに、日本語フォントを埋め込んで出力する構成だ。あるとき、氏名に含まれる「𠮷」(つちよし、U+20BB7)が □□ の二つ並びで出た。いわゆる豆腐、フォントに該当グリフが無いときの .notdef である。
素直に考えれば「このフォントに字が無い」だ。だが「𠮷」のグリフを持つフォントに差し替えても直らなかった。しかも豆腐が一文字ではなく二つ出る。同じ外字でも、はしごだかの「髙」(U+9AD9)や「葛」(U+845B)は IPAmj明朝のような適切な日本語フォントを使えば正しく出る。何が違うのか。境界は「フォントにグリフがあるか」ではなく、符号位置がBMPの内か外か、にあった。
なぜ落ちるか:UTF-16BE とサロゲートペア
TCPDFは描画時、本文テキストを内部でUTF-16BEに変換する。Unicodeの基本多言語面(BMP、U+0000〜U+FFFF)に収まる文字は16bitひとつで表せる。だが面2以降(SMP、U+10000以降。CJK統合漢字拡張Bもここ)は16bitに入りきらず、サロゲートペア=二つの16bitコード単位で表現される。「𠮷」は D842 DFB7 の二単位になる。
問題は埋め込みフォント側の対応表だ。TCPDFが生成するCIDToGIDMapはBMPを前提とした65,536字固定サイズで、フォント生成時に符号位置が0xFFFF以下のグリフしか登録しない。結果、サロゲート領域のコード単位(0xD800〜0xDFFF)は対応先を持たず、すべてGID 0=.notdefに落ちる。
"𠮷" (U+20BB7)
→ UTF-16BE: D842 DFB7 // 2つの16bitコード単位
→ CIDToGIDMap[D842] = 0 // BMP外は未登録 → .notdef
→ CIDToGIDMap[DFB7] = 0 // 同上
→ 豆腐が2つ
豆腐が二つ出たのは、二つのコード単位が独立にGID 0へ落ちたからだ。フォントを替えても効かないのは当然で、そもそもマップにグリフを登録する経路に乗っていない。字が無いのではなく、字にたどり着く前に符号が失われている。
なぜ直せないか:Identity-H という前提
回避策として本体に手を入れることも考えたが、ここにアーキ上の壁がある。TCPDFはテキストのエンコーディングにIdentity-Hを使う。これは「1文字=2バイトCID」を前提とした写像で、CID空間そのものが16bit幅に固定されている。サロゲートペアのように1文字を可変長で扱う設計にはなっていない。CIDToGIDMapを広げようにも、その手前のCID表現が2バイト固定なので、面2以降をひとつのグリフとして通す口が無い。TCPDF本体を改造しての回避も、ほぼ不可能だと結論した。
つまりこれはフォントの問題でもバグでもなく、「BMP内だけを扱う」という前提の上に組まれたライブラリの構造的な上限である。
切り分けと現実解
ここで大事なのは影響範囲を正しく引くことだ。「外字が全部ダメ」ではない。BMP内の外字(髙・葛など)は IPAmj明朝で出る。出せないのはBMP外=拡張B以降だけ。日本語の実務では、この「面2以降」に落ちる字は少数だが確実に存在するので、境界を知っていること自体が設計判断になる。
座標を保ったままSMPを扱えるPDFエンジンは、調べた範囲では実質商用しかない。そこで現実解として立てているのが、BMP外の符号だけを検出し、その一文字のグリフをベクター画像としてPDFに差し込むフォールバックだ。テキスト層からは外れるが、少なくとも豆腐は消える。実装の詳細は稿を改める。
余談をひとつ。この「TCPDFでは無理」という結論には、私自身も過去に何度もたどり着いていた。その都度 web を調べ、同じ壁の前で引き返していた。だがあるとき、AIなら別の角度から解けるかもしれないと、コーヒーブレイクのついでに軽く相談してみた。するとあっという間に、いま挙げたような解決の筋を探し当ててくれた。何年も「無理」で止まっていた問題が、雑談まじりの数分で動き出す。諦めていた壁ほど、道具を替えてもう一度叩いてみる価値がある。
教訓の核はエンコード経路の見方にある。文字化けを見たとき、多くの人はまず「フォントが無い」を疑う。だが実際には、符号がどこかの層でGID 0に落ちているケースが少なくない。豆腐が出たら、フォントを差し替える前に、テキストがどの表現に変換され、どのマップを引き、どこでグリフを失うかを一段ずつ追うべきだ。BMPの内と外という一本の線が、日本語PDFの実務では効いてくる。