技術記事

Delphi と HotPDF を使用した PDF での CJK 縦書きテキスト

日本の小説をページにレイアウトすると、最初に気づくのは、テキストが横ではなく列に沿って下に向かって流れること、そして列が用紙の右端から左に向かって進むことです。それで育った読者は、横書きのテキストを少し冷淡(clinical)に感じます。エンジニアリング上の問題は、PDF がほぼすべてのデジタルテキストシステムと同様に、左から右へと伸びる水平のベースラインを中心に構築されていることであり、コンテンツストリームには「代わりにこの段落を下に向かって書く」という概念がないことです。したがって、Delphi アプリケーションが台湾、日本、または韓国の読者向けに証明書、詩、看板、または伝統的な形式の法的文書を作成する必要がある場合、レイアウトは手作業で組み立てる必要があります:ある文字を次の文字の下に配置し、ある列を前の列の左に配置します

HotPDF は、文字ごとの帳簿付け(bookkeeping)を代行するスイッチを提供します。設定するフォントには IsVertical フラグが備わっており、それがオンになると、単一の TextOut 呼び出しは、ベースラインに沿って実行するのではなく、文字列全体を垂直の列に積み重ねます。このページの残りの部分で取り組むのは、列の配置、右から左への順序、そして目立たないが重要な 1 つのグリフ置換についてです

A4 PDF page produced by HotPDF showing Chinese, Japanese, and Korean text stacked into vertical columns that read from right to left
1 つの Delphi プロシージャから生成された、縦書きの列に中国語、日本語、韓国語のテキストが配置された単一の A4 ページ。

スイッチは SetFont にあります

縦書きのレイアウトは、ページやドキュメントのプロパティではありません。それは描画に使用するフォントのプロパティであり、SetFont の 5 番目の引数でオンにします:

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// 5 番目の引数は、現在のフォントを縦書きモードに切り替えます。
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // 横書き
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // 縦書き

フラグはフォントに乗るため、最後の引数を変更して SetFont を再度呼び出すだけで、横書きと縦書きを切り替えることができます。ページの上部に横書きのタイトルを配置し、その下に縦書きの本文を配置することができ、HotPDF は 2 つのモードをページごとではなくフォントオブジェクトごとに区別して保持します。これにより、あなた側で特別なモード処理を行わなくても、混在するレイアウトが可能になります:縦書きの SetFont 後のすべての TextOut は積み重ねられ、横書きの後のすべての TextOut はベースラインに沿って実行され、最後に実行された SetFont が優先されます

4 番目の引数は Windows キャラクターセットであり、横書きの呼び出しで受け取るものと同じです。DEFAULT_CHARSET を渡すと、システムは文字列ごとにグリフを解決できるようになります。これは、縦書きのドキュメントで文字体系(scripts)が混在することが多いため、ここでは重要です。フォントに関する他のすべての事項は引き続き適用されます:フォントはビルドマシンにインストールされている必要があり、Arial Unicode MS を持っていない読者でも同じ CJK グリフがレンダリングされるように、ほとんどの場合 FontEmbedding := True を設定する必要があります

1 つの TextOut 呼び出しは 1 つの列です

縦書きフォントがアクティブな場合、TextOut 呼び出しは、指定された点から文字列を横に広げることはありません。最初の文字を上部に配置し、残りの文字をまっすぐ下に歩かせ、各グリフのフォントの行の高さ分だけ進みます。渡した X は列を固定し、渡した Y は列の上部が始まる場所を固定します。したがって、実際の文章をレイアウトするには、列ごとに 1 回の TextOut を発行し、呼び出しの間に X を左にずらします。これは、CJK の列が右から左に読まれるためです

var
  Pdf: THotPDF;
const
  ColTop = 760;   // すべての列の最初のグリフの y(ポイント)
  ColGap = 28;    // 列間の水平距離
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // 移植可能なレンダリングのために CJK 書体を埋め込む
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // 最初に通常の書き込みモードで横書きの見出し。
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 800, 0, 'Tang poem, vertical layout');

    // フォントを縦書きモードに切り替えます。これより下のすべての TextOut は積み重ねられます。
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // 列は右から左に進むため、呼び出しごとに X が減少します。
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

詩は本来あるべきように読まれます:一番右の列を最初に上から下へ読み、次に視線は左の次の列へジャンプします。文字列自体にその順序をエンコードするものはありません。X 座標でそれをエンコードし、呼び出しごとに減少させます。方向を逆にしてしまうと、詩句が逆になって出力されます。これは、横書きのレイアウトを縦書きに移植する際によくある最も一般的な間違いです

座標:ページの下へ、右から左へ

2 つの軸が関係しており、それらは反対方向に引っ張るため、正確に理解する価値があります。HotPDF はページの左下隅から測定し、Y は上に向かってポイント単位で増加します。したがって、縦書きの列は高い Y(用紙の上部付近)から始まり、HotPDF が各文字に対して行の高さを差し引くにつれて文字はそこから下降します。列ごとにその開始 Y を 1 回設定すると、コンポーネントが下降を処理します

水平軸はあなたが手動で制御するものです。各列は独自の X にあり、読み取り順序が右から左であるため、連続する列はより小さな X 値に移動します。理にかなったリズムは、最も右の列の X を選び、その後の各呼び出しで固定の列ギャップを差し引くことです。例では ColGap を使用してそうしています。ギャップはあなたが自由に選ぶことができます。きつすぎると隣接する列が接触し、緩すぎるとブロックがまばらに見えます。12 から 18 ポイントの本文テキストの場合、フォントサイズより少し大きいギャップが快適に読めます

形を変えなければならないグリフ

いくつかの文字は、単に横書きの形を回転させたコピーではありません。テキストが縦向きに変わるときに、それらを置き換える必要があります。HotPDF があなたに代わって処理してくれるのは、日本語の長音記号 (U+30FC)です。これは「コーヒー」のようなカタカナの単語に現れる長い母音のバーです。横書きにすると、ベースライン上にある短いダッシュになります。その同じグリフを列に積み重ねると、列を横切って平らに置かれることになり、これは間違っています:縦書きの日本語では、記号はそれが位置する 2 つの文字を結ぶ縦線になります。HotPDF は縦書きのパスで U+30FC を検出し、それを縦線(U+007C)としてレンダリングするため、あなたがいかなる作業も行うことなく、長音記号は正しい方向を向きます

その 1 つの置換で、素朴な実装のほとんどを壊すケースをカバーできますが、一般的な問題がどこでより深くなるかを知っておく価値はあります。完全な縦書きのタイポグラフィでは、ラテン文字と西洋の句読点も 90 度回転させ、小さな仮名(small kana)を移動させ、括弧とカンマを縦書きの形に再配置します。その完全な実装は、固定されたルールではなく、フォントの OpenType 縦書き機能(OpenType vertical features)の中に存在します。HotPDF は、フォントにそれらが備わっている場合にその機能を活用できます:縦書きの代替文字(vert および vrt2 GSUB 機能)と縦書きのカーニング(vkrn および vpal GPOS ルックアップ)はオプトインであり、アクティブなフォントがそれらを実際に定義している場合にのみ、縦書きのパスに沿って適用されます。Arial Unicode MS のような単一の広範なカバレッジを持つ書体での CJK 混在テキストの場合、組み込みの U+30FC 処理と均等な行の高さのステップで、正確で読みやすい列を生成するのに十分です。精細な縦組み用に設計されたフォントに移行し、そのネイティブな仮名の配置とグリフ間のスペースが必要な場合、OpenType 機能が重要になります

1 つのページ上で文字体系と向きを混在させる

実際のドキュメントが純粋なことはめったにありません。縦書きの日本語のページに横書きの英語のキャプション、下部に沿ったページ番号、または日本語の横に縦書きされた韓国語のブロックが含まれる場合があります。縦書きフラグはフォントレベルのスイッチであるため、ページ全体のステートを管理するのではなく、SetFont の呼び出しを交互に行うことでこれらを構成します。横書きフォントを設定し、ランニングヘッダーとフォリオ(ページ番号)を書き、縦書きフォントを設定して列を配置し、フッター用に再び横書きフォントを設定します。各領域は最も最近の SetFont のモードを取得するため、必要な規律は、方向を変えるたびにそれを呼び出すことだけです

文字体系が混在する場合に計画しておくべき詳細が 1 つあります:中国語、日本語、韓国語の表意文字(ideographs)は正方形に近く、均等なステップで積み重なりますが、縦書きの列に埋め込まれたラテン文字の連続にはその均等な前進がありません。縦書きのテキストの中にいくつかのラテン文字の単語が必要な場合は、列を下に向かって流れるようにそれらを回転させるか、短い横書きの挿入として直立して配置するかを意図的に決定し、表意文字用の垂直ステップに任せるのではなく、独自の TextOut でその断片を配置します。混在したランを独自の配置問題として扱うことで、列のリズムが損なわれないように保たれます

サンプルからプロダクションへ

ピースは小さく、予想通りに構成されます。SetFont で縦書きフラグをオンにし、固定された上部 Y から列ごとに 1 回の TextOut を発行し、呼び出しごとに X を減らして列が右から左へ読まれるようにします。フォントを埋め込んで CJK グリフが読者のマシンに伝わるようにし、コンポーネントが U+30FC の長音記号と、フォントがサポートしている場合は OpenType 縦書き機能を処理するようにします。そこから、レイアウトのプロダクション化(本番対応)は主に算術演算になります:測定されたブロック幅から列の X位置を導出する、長い文章をページの高さに収まる列に分割する、そして横書きのヘッダーとフォリオのためのスペースを確保するなどです

これが基盤としている、横書きの TextOut 規則や Unicode フォントの登録など、より広範なテキストとフォントの構成面については、多言語 Hello World の例TextOut サンプルを参照してください。ここに示されている SetFont 縦書きスイッチと TextOut 呼び出しは、Delphi および C++Builder 向けの HotPDF コンポーネント の一部です