技術記事

DelphiでPDFに日本語と中国語の縦書きテキストを配置する

PDFlibPasは日本語と中国語のテキストをページの下方向に描画する。SetVerticalWritingModeで縦書きをオンにすると、以降の通常のDrawText呼び出しは下向きに走り、GetVerticalWritingModeが現在の状態を返す。これが存在する前は、縦書きにするにはすべての文字を手作業で配置し、間隔がまともに見えることを祈るしかなかった

縦書きは水平テキストを90度回転させたものではない。文字は正立したままで、送りは横ではなく下へ走る。そして多くの文字が形を完全に変える。この最後の点こそが、自然に読めるドキュメントと、日本語の読者が一目で機械生成と分かるドキュメントを分ける

PDFの中で何が変わるのか

この方法で描かれたテキストは、フォント自身の縦書きメトリクスを伴うType0フォントの縦書きモードを経由する。これは2つの方向で効いてくる。第1に、リーダーは各文字を一律のステップではなくデザイナーが意図した距離で送るので、列はその書体のために描かれたリズムを持つ。第2に、テキストをコピーして取り出すと元の文字が戻る。縦書きのランは位置決めされたグリフの並びではなく、適切なマッピングを持つ本物のテキストだからだ

縦書きメトリクスを自身で持たない書体は、1文字につき1emずつ送る。これはリーダーがデフォルトで行うことと同じだ。このフォールバックを知っておく価値がある。正しいCJK書体で正しく描画されるドキュメントと、たまたまいくつかの仮名を含むラテン書体で機械的にスペーシングされたドキュメントの違いは、ここから来るからだ

var
  Lib: TPDFlib;
  H: Double;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('MS Mincho', 1);      // 1 = embed the face
    Lib.SetTextSize(12);
    Lib.SetVerticalWritingMode(1);            // ordinary DrawText now runs down
    H := Lib.GetVerticalTextHeight('MS Mincho', 12, '第三章 保守点検');
    Lib.DrawText(480, 72, '第三章 保守点検');
    Lib.SetVerticalWritingMode(0);            // back to horizontal
    Lib.DrawText(72, 72 + H, 'Chapter 3');
    Lib.SaveToFile('manual-ja.pdf');
  finally
    Lib.Free;
  end;
end;

なぜ縦書きでカッコがおかしく見えるのか

カッコには2つの形式があり、列に属するのはそのうち1つだけだからだ。カッコ、長音記号、小さい仮名は、テキストがページを下に走るとき異なる形で描かれる。括弧は列に沿うように回転し、長音記号は縦のストロークになる。水平の形式を縦の列に描けば、すべてが横倒しになる

PDFlibPasは書体自身の縦書きフィーチャーから形式を取得する。だから各書体は、文字から推測された置換ではなく、デザイナーが描いたものを供給する。この区別は正確さに関わる。推測による置換テーブルは一般的なケースでは正しいが、ある文字を異なる扱いにする書体では間違う。そして縦書き形式を名指さない書体は、テーブルを強制されるのではなく、以前とまったく同じように描かれる

GetVerticalTextHeightは実際に描かれる形式を測るので、文字が形を変える列でも正しく測られる。水平の形式を測って縦の形式を描くと、数文字分ボックスからはみ出す列の典型的な原因になる

モードを変えずに1ランだけ描く

DrawVerticalTextは位置、フォント名、サイズ、テキストを受け取り、書写モードをいじらずに1ランだけ縦書きで描く。水平ドキュメントの中の縦書きの例外、つまり背表紙のラベル、スタンプ、名前の単一列に使う。こうした場面で毎回グローバルモードをオン・オフするのは、タスクが求める以上の状態管理だ

1つの書体の水平形式と縦書き形式は内部的に別々に保たれるので、1つのページが両方を持ち、どちらかがもう一方を乱すことはない。これこそが混在ページを現実的なものにする。縦書きの本文と水平のヘッダを持つ日本の書籍ページや、水平の詳細行の上に縦書きのタイトルを持つ中国語の証明書などだ

// One vertical run inside an otherwise horizontal page
Lib.DrawVerticalText(520, 96, 'MS Mincho', 14, '保守点検記録');
// The horizontal text around it is unaffected
Lib.DrawText(72, 96, 'Maintenance inspection record');

何より先にフォントを正しくする

縦書きは完全に書体に依存する。適切な縦書きメトリクスと縦書きフィーチャーを持つCJKフォントは、追加作業なしで正しい出力を生む。それらを持たないフォントは、1emずつ送る正立の文字を生み、形の変化をまったく起こさない。縦書きテキストが微妙に間違って見えるなら、コードを見る前にフォントを調べよ

埋め込みは通常のルールと通常のコストに従う。完全なCJK書体は大きいので、どこかに出荷するドキュメントではサブセット化は選択肢ではなく必須だ。PDFファイルサイズ最適化とフォントサブセット化のノートが何を期待すべきかを扱い、既存PDFへの欠落フォント埋め込みの解説は、縦書きドキュメントがフォントなしで届いた修復ケースを扱う

縦書きがそれでもあなたの判断を必要とする場所

列の順序だ。日本語の縦書きテキストは列単位で右から左へ走るので、2段組のページは右端から始まる。そして書写モードの設定はテキストからそれを推論できない。全体として右から左に読むドキュメントのページ順序や、ふりがな、脚注、図のキャプションがどこに置かれるかについても同じだ

ライブラリが保証するのは、各ランが正しく組まれるということだ。正しい形式、正しい送り、抽出可能なテキスト。ランがページ上のどこに行くかはレイアウトの問題であり、データから組み立てられるドキュメントでは、テキスト検索とページ要素の列挙の解説が、ページに着地したものが意図したものかどうかを事後検証する上で有用だ

PDFlibPasはDelphi、C++Builder、Lazarus向けのネイティブPascal PDFライブラリであり、縦書きCJKはアドオンではなく描画APIの一部である。テキストとフォントの機能一覧はPDFlibPas製品ページを参照のこと