PDFのフォント欠落
PDFに日本語を描くとき、使うフォントに字形がない文字を渡すと、エラーも警告も出ずに「☒」になる。 型検査も、単体テストも、lintも通る。PDFを開いて初めて気づく。
ここには、実際にこれが起きたときの再現と、置き換えの表、ソースの文字列をフォントと照合して再発を止めるテストの作り方を置く。
何が起きたか
PDFを作るコード(pdf-lib)で、埋め込んでいるフォントは @openfonts/noto-sans-jp_japanese の日本語サブセットだった。このフォントには、次の文字の字形がない。
| 文字 | 例 | 置き換え |
|---|---|---|
| 丸数字 | ① ② ③ ④ ⑤ ⑥ | 数字(1、2、3) |
| 三角 | △ ▲ | 文字ではなく、三角形の図形で描く |
| 米印 | ※ | 「注意:」または「*」 |
| 四角 | □ | 「[ ]」 |
| 全角チルダ | ~(U+FF5E) | 波ダッシュ 〜(U+301C)。こちらはフォントにある |
ASCIIの記号と、全角の数字・かな・漢字は、フォントにある。
見つかった場所
別表十四(決算書類のひとつ)の値出力PDFを実際に出力して開き、欄の番号が「☒」になっているのを見つけた。そこで、PDFを作るソース33ファイルの文字列を、フォントの字形と照合した。
- 別表十四のPDF: 欄の番号(①〜⑥)。この出力で見つかった
- 請求書PDF: 注記の印と凡例の「※」
- 貸借対照表PDF: 警告の頭の「※」
- 手入力確認PDF 2本: チェックリストの「□」
最初の1つ以外は、前からあったコードだった。新しく書いたコードの検査では、古いコードの同じ穴は見つからない。 出力を開いたことと、全ファイルを照合したことで、同じ原因をまとめて拾えた。
再発を止めるテスト
ソースの文字列を、実際に埋め込むフォントの字形表と照合して、1つでも欠けていればテストを落とす。
字形の有無は、@pdf-lib/fontkit の hasGlyphForCodePoint で引ける。フォントの読み込みは次の形だ。
import fontkit from "@pdf-lib/fontkit";
// woff を ttf へ変換してから渡している
const font = fontkit.create(woffToTtf(readFileSync(FONT_PATH)));
font.hasGlyphForCodePoint("①".codePointAt(0)!); // false
font.hasGlyphForCodePoint("法".codePointAt(0)!); // true
テストは3つに分けた。
- 対象のファイルが見つかる(検査が空振りしていない)
- フォントに丸数字・△・※・□がないことを前提にしている(フォントを替えたら、この検査を見直す)
- 文字列に、フォントにない文字が入っていない
修正前のコードで、3つ目が失敗することを確かめてから、置き換えた。
注意
- 置き換えで出力が変わる。 請求書の「※」を「*」に変えたように、利用者に見える印が変わる場合は、変えてよいかを先に確認する
- コメントは描かないので、テストの対象から除いている。文字列の中に書いた文字だけが対象になる
- 公式様式の背景画像に値を重ねる方式では、様式にもとから印刷された文字は画像なので影響を受けない。影響するのは、こちらが描く文字だけ
- フォントを替えたら、「前提」のテストが落ちる。落ちたときは、置き換え表と例外の登録を見直す
この知見を得た日記
任せた実装が、型検査もテストも通っていたのに、PDFを開いたら欄の番号が「☒」だった。そこに至る経緯はminiに実装を任せた結果に書いた。同じ型の「通ったが、中身を見ていなかった」は、ガードが素通りする書き方にもある。この記事の対象になったPDF出力は、ちょうぼっちの機能のひとつだ。