技術記事

PDFlibPasのハイフネーションと段組みバランス調整

PDFlibPasは、保持されたテキストパッセージをDrawTextFlowColumnsによって1列から64列までの等幅段組みに流し込み、SetTextFlowLanguageSetTextFlowHyphenationを呼び出すことで、有界な言語対応ハイフネーションによって改行します。9言語に対応しており、言語はフローごとに設定する代わりにドキュメントCatalogの/Lang値から継承することもできます

両機能は同じ理由で存在しています。狭い段組みこそ、素朴な改行処理が組版らしさを失い、バグ報告のように見え始める場所だからです

両端揃えのテキストが狭い段組みで崩れるのはなぜか?

なぜなら、両端揃えは余った空間を行内の単語間隔に分配する仕組みであり、余る量は何が収まるかによって決まるからです。幅が広い段組みでは余りはわずかで、目にはほとんど留まりません。幅を半分にすると、収まりきらない1つの長い単語が次の行に押し出され、その前にある単語たちがその空間をすべて吸収することになります。このような行が3行続くと、タイポグラファーが「リバー」と呼ぶ縦の白い筋が発生し、読者は理由もわからないまま読みにくいテキストとして体感します

ハイフネーションは、単語の内部での改行を許すことで、症状ではなく原因を修正します。ドイツ語やオランダ語の複合語では、これは譲れない要件になります。60ミリメートルの段組みに24文字の名詞が入るとき、改行ポイントなしでうまく収まる方法はありません。英語はこの欠如への耐性が比較的高いため、英語を前提に作られた製品ほど、ドイツ語の顧客が初めて使った瞬間に破綻するレイアウトコードを出荷しがちです

対応言語と、言語情報はどこから得られるのか

ハイフネーションは英語、ドイツ語、オランダ語、フランス語、スペイン語、イタリア語、ポルトガル語、ロシア語、トルコ語をカバーします。SetTextFlowLanguageでフローごとに明示的に設定するか、タグ付けされたアクセシブルなドキュメントがすでに保持している値である、ドキュメントCatalogの/Langエントリから継承させることもできます

この継承は、上書きするよりも活用する価値があります。Catalogで言語を宣言しているドキュメントは、スクリーンリーダー、検索インデクサー、そしてハイフネーションに対して、同じ事実をたった1か所から伝えていることになり、事実というものは本来そのように1か所に存在すべきものです。すでにアクセシブルなPDFの自動タグ付けで説明されているタグ付き出力を生成しているなら、言語エントリはすでに設定済みであり、フローはそれにただ従うだけで済みます

var
  Lib: TPDFlib;
  Flow, Drawn: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('Georgia', 1);
    Lib.SetTextSize(10.5);

    Flow := Lib.NewTextFlow(ArticleBody);
    try
      Lib.SetTextFlowLanguage(Flow, 'de');
      // 有効化、改行前に最低3文字、改行後に3文字
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // 1行だけが孤立しないようにする

      repeat
        // 480ptの領域を3段組み、ガター18pt、バランス調整あり
        Drawn := Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
        if (Drawn = 0) or (Lib.TextFlowFinished(Flow) = 1) then
          Break;
        Lib.NewPage;
      until False;
    finally
      Lib.ReleaseTextFlow(Flow);
    end;

    Lib.SaveToFile('newsletter.pdf');
  finally
    Lib.Free;
  end;
end;

MinPrefixとMinSuffixは検証ではなく組版上の判断

有効化フラグの後に続く2つの整数は、改行の前後に残すべき最小文字数を指定します。3と3は、ほとんどのハウススタイルで受け入れられる控えめなデフォルト値です。2と2にすると改行の選択肢は増えますが、行末に2文字の断片がぶら下がるように残るため見た目は明らかに悪化し、誤字のように読めてしまいます

文字サイズが大きく、各断片が視覚的に目立つ場合は最小値を引き上げ、段組みが本当に狭く、詰まった行幅のほうがきれいな行幅より重要だと判断した場合にのみ引き下げてください。これは技術的な判断ではなくハウススタイルの判断であり、だからこそ定数ではなくパラメータになっています

ここでの「バランス調整」とは実際には何を意味するのか

Balanceパラメータが挙動を変えるのは、パッセージの末尾でだけです。バランス調整を有効にすると、残りすべてが領域内に収まる場合には各段が正確に等しい行数まで短縮され、これによって最終ページで2段が満杯なのに3段目だけに1行ぽつんと残るという事態を防げます。パッセージが収まりきらない場合は、各段はそのまま最大の高さを保ち、ページはできる限り多くのテキストを収容して、残りは次のページに続きます

この非対称性こそが、連続的なドキュメントにとって正しいデフォルト動作です。流し込み記事の途中でバランス調整を行うと、どの段も元々満杯なので誰の目にも入らない装飾効果のために、すべてのページで縦方向の空間を無駄にしてしまいます。実際に目に留まるのは末尾でのバランス調整であり、まさにそこにだけ適用される仕組みになっています

改行処理は単語全体を単位に計測する

改行アルゴリズムは、文字幅を積み上げていくのではなく、単語全体を単位に計測します。そして、URLや管理番号のように1行にまったく収まらない過大なトークンについては、探索範囲を有界に限定して処理します。これにより、一般的なケースは高速に保たれ、病的なケースも範囲が限定される、という望ましい順序が実現します

任意のソフトハイフンや自動ハイフンは、それがマークしている改行位置が実際に採用された場合にのみ描画されます。当たり前のように聞こえるかもしれませんが、これは典型的な不具合の温床です。素朴な実装では計測中にハイフン文字を書き込んでしまい、改行位置が移動すると、そのハイフンだけが行の途中に取り残されます。単語の途中に取り残されたハイフンほど、テキストエンジンが壊れていることを物語るものはありません

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // 何かを描画する前にレイアウトを決定する
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // 1段の幅で残りのパッセージに必要な行数
    Needed := Lib.MeasureTextFlow(Flow, 148);
    if Needed > 3 * LinesPerColumn then
      UseTwoPageSpread
    else
      UseSinglePage;

    Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);

    if Lib.TextFlowFinished(Flow) <> 1 then
      CarryOver(Lib.GetTextFlowRemaining(Flow));
  finally
    Lib.ReleaseTextFlow(Flow);
  end;
end;

フォント設定はボックス間で必ず同一にする

フロー方式のレイアウトすべてを支配する1つのルールがあり、これははっきり言っておく価値があります。DrawTextFlowDrawTextFlowColumnsMeasureTextFlowはいずれも、呼び出された時点で選択されているフォントを使って改行します。同じフローの2つのボックスの間でフォントやサイズを変更したり、フォントを選び直さずに新しいページを開始したりすると、2つ目のボックスは1つ目が計測した結果とは異なる位置で改行されてしまいます

この症状は、まるで断続的に発生しているように見えるせいで厄介です。1ページ目では収まっていたテキストが2ページ目では溢れたり、計測した行数と実際に描画された行数が食い違ったりします。ループの前に一度フォントを選択し、NewPageのたびに選び直せば、フローは期待どおりに動作します。1つのパッセージ内に複数の文字体系が混在する場合は、CJKと絵文字向けの自動フォントフォールバックで説明されている解決処理が計測と描画の両方に適用されるため、フォールバックランをまたいでも幅の一貫性が保たれます

フローがヘッダー、フッター、データ駆動ブロックと並ぶ1要素にすぎないレポートレイアウトでは、データセットレポートエンジンにある構成パターンが段組みフローときれいに組み合わさります。先に計測し、固定要素を配置してから、残った領域をフローに渡すという流れです

PDFlibPasはDelphi、C++Builder、LazarusのPDFライブラリであり、作成、描画、計測、検査、巻き戻し、解放というTextFlowの完全なライフサイクルは、DLLおよびActiveXインターフェースからも同様に利用できます。完全なドキュメントはPDFlibPas Delphi PDFライブラリページにあります