技術記事

HotPDFのRtLTextOut: Delphiでの右から左へのPDFテキスト

アラビア語の文章 يوضح ملف PDF هذا をプレーンな TextOut に送信すると、返ってくるページは一度に2つの点で間違っています。単語が右から左ではなく左から右に走り、文字がつながった単語にならずに孤立した形で離れて配置されます。何もエラーになりません。Delphiはコンパイルされ、ファイルは開き、アラビア語を読むレビュアーは出力が使用できないと伝えます。修正はライブラリの交換ではなく、1つの呼び出しです。HotPDFは、プレーンな TextOut では実行されない並べ替えを処理する別のメソッド、 RtLTextOut を通じて、右から左へのテキストをルーティングします。このページは、そのメソッドの実用的なリファレンスです。シグネチャとそのパラメータ、スクリプトを選択するcharset引数、ドキュメントレベルの副作用、最初に必要となるフォント設定、そして実際にサポートに届く失敗のそれぞれの修正方法について説明します

シグネチャとパラメータ

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

XY は、ページの独自の座標系で実行をアンカーします。下から上に向かってYが増加する左下隅から測定され、すべての TextOut 呼び出しが使用するのと同じ原点です。 RtLTextOut はグリフの順序を変更しますが、ページの測定基準は変更しません。 angle は、 TextOut とまったく同じようにベースラインを回転させるため、0は水平線を描画します。 Text は論理的な順序、つまり入力する順序の文字列であり、2番目のオーバーロードは、明示的なコード単位のカウントを持つ生の PWORD バッファとして同じUTF-16データを受け取ります。これは、テキストがDelphiの文字列ではなくAPIから到着する場合に使用する形式です。これらのタイプのオーバーロード解決より前の古いDelphiバージョンでは、文字列形式は同じパラメータリストを持つ RtLTextOutStr という名前で公開されています

2つの出力呼び出し間の分業は厳密です。 TextOut は渡された順序でコードポイントを描画します。これはラテン語、キリル語、CJKには正しく、アラビア語とヘブライ語には間違っています。 RtLTextOut は、最初に各行を視覚的な右から左への順序に並べ替えてから描画し、行内に埋め込まれたラテン語の単語と数字は左から右へ読むように維持します。HotPDFは、文字から方向を推測するのではなく、意図的に2つのメソッドを分離しているため、どちらを呼び出すかの選択は、どのスクリプトの動作を取得するかの選択になります。右から左への実行には RtLTextOut を使用し、それ以外すべてには TextOut を使用し、一方を他方を通してルーティングしないでください。並べ替えがそもそも存在する理由、Unicode双方向アルゴリズムとアラビア語のコンテキスト結合が実際に何を行うか、そしてHotPDFのシェーピングがどこで止まるかについては、 HotPDFを使用したアラビア語とRTLテキストのシェーピング に関する関連記事の主題です。以下のすべては実践的なセットアップです

RtLTextOutがPDFに描画する前に、アラビア語とラテン語が混在する行を視覚的な右から左への順序に並べ替える方法の図
RtLTextOutは、各行を描画する前に視覚的な順序に並べ替えます。右から左への実行はその順序を維持しますが、行内に埋め込まれたラテン語の単語と数字は左から右へ読みます。

charset引数がスクリプトを決定します

アラビア語をレイアウトしているのかヘブライ語をレイアウトしているのかを RtLTextOut に伝えるのはメソッドではなく、フォントです。 SetFont は4番目の引数としてWindowsのcharset(文字セット)を受け取り、その値はスクリプトのルールを右から左への呼び出しに運びます。178はアラビア語を選択し、177はヘブライ語を選択します。文字セットを設定してから描画すると、以下の2行はそれ以上の構成なしに正しい読み取り順序で出力されます

// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

見落としがちな順序の細部が1つあります。現在のフォントは、文字セットを含め、改ページ後も持続しないため、 SetFont を最初に実行し、 AddPage の後に毎回繰り返す必要があります。繰り返しを忘れると、2ページ目はアクティブなフォントにフォールバックし、アラビア語の場合は通常、空のボックスになります

すでに逆にしたテキストは逆にしません

ここで最もデバッグ時間を浪費する単一の間違いは、手動ですでに反転させた文字列を RtLTextOut に供給することです。人々は、プレーンな TextOut による最初の試みが逆に出てきた後、このメソッドにたどり着きます。そして、一般的な一時しのぎは、描画する前にコード内の文字を反転させることです。 RtLTextOut はそれ自体で内部的に反転するため、事前に反転された文字列は2回反転され、開始した場所の右に戻ります。入力して声に出して読む順序である論理的な順序でテキストを渡し、呼び出しに並べ替えを任せます

すべてアラビア語の1つのテストフレーズでは二重反転された文字列が正しく見える可能性があり、ラテン語の単語や数字が含まれる行では瞬間に壊れるため、この罠は単純な反転よりも厄介です。右から左への行内では、それらの埋め込まれた実行は左から右に読むことになっており、手動反転はそのネストを破壊しますが、純粋なアラビア語のケースはたまたま生き残ります。そのため、バグは最初のスモークテストを通過し、後で口座番号を含む実際の請求書に現れます。 RtLTextOut に切り替えた瞬間、すべての手動反転を取り除いてください

知っておくべきDirectionの副作用

RtLTextOut を呼び出すと、描画している行以外にも変更が加えられます。また、ドキュメントの読み取り方向の設定も右から左に反転します。これは、 Direction プロパティを通じて手動で設定する場合と同じです。このセッターは vpDirection をドキュメントの ViewerPreferences に追加します。これは、見開きページの配置方法と、見開きページのレイアウトがどちら側から始まるかをビューアに伝えます。ドキュメント全体がアラビア語またはヘブライ語の場合、これはまさに必要なことであり、無料で手に入ります

単一のページでは目に見えないため、これについて知っておく価値があります。ドキュメントのほとんどが左から右であり、右から左のブロックが1つだけの場合、最初の RtLTextOut 呼び出しは依然としてファイル全体の設定を傾けます。1ページのプルーフでは何も表示されません。その症状は数週間後、誰かが両面印刷の小冊子を印刷し、見開きが鏡文字で出てきたときに現れます。それが望ましくない場合は、右から左への実行の後に明示的に Direction を元に戻してください

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

純粋に右から左へ読むドキュメントの場合は、そのままにしておきます。重要なのは、小冊子のサプライズが起こらないように、呼び出しがドキュメント全体に影響を与えることを知っておくことです

インストールされていると期待するフォントではなく、出荷するフォントを登録する

フォントに描画するグリフがない場合、並べ替えは意味を持ちません。典型的な失敗は、 Arial Unicode MS がたまたま存在する開発者のマシンで完璧にレンダリングされるレポートが、Windowsがアラビア語をまったくカバーしていないフォントに静かに置き換えた顧客のサーバー上で空のボックスの行として出てくることです。解決策は、インストールされているシステムフォントを信用するのをやめ、アプリケーションとともに出荷するフォントを登録することです

// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

登録には2つの境界が伴います。 RegisterUnicodeTTF を通じて持ち込まれたフォントは埋め込まれ、HotPDFの埋め込みUnicode処理はPDF 1.5以降のドキュメントを必要とします。これは、ダウンストリームの何かがPDF 1.4を要求する場合にのみ噛み付きますが、その場合、障害は静かです。もう1つは技術的なものではなく法的なものです。TrueTypeファイルには埋め込み許可のビットが含まれており、画面上では見栄えの良い書体でも、顧客のドキュメント内に同梱して出荷することを禁止するライセンス条件である場合があります。クレームの後ではなく、埋め込む前にライセンスを確認してください

完全なコンソールの例

各部分をまとめると、これはアラビア語の行、ヘブライ語の行、およびラテン語の製品名を持つ混在行の1ページを書き込む自己完結型のプログラムです。各ブロックは文字セットを設定し、論理的な順序で描画します

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // HotPDF main unit

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // A Latin heading goes through the ordinary TextOut path
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabic: charset 178, logical order, RtLTextOut does the reordering
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Hebrew: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Mixed line: the embedded Latin word still reads left to right
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

実行して結果を開いてください。アラビア語とヘブライ語の行は右から左に読み、スクリプトが文字を結合する場所で文字が結合し、最後の行では HotPDF トークンがアラビア語の実行の内側で左から右に配置されています。初めてのレビュアーは日常的にそれをバグとして報告しますが、そのネストはバグではなく、正しい双方向の結果です。上でリンクされているシェーピングの記事では、Unicodeルールがそれを要求する理由と、レポートが提出されないように受け入れ基準を記述する方法について説明しています

一般的なエラーとその修正

以下のすべての障害は実際のサポートスレッドに現れたものであり、それぞれ上記のセクションのいずれかに遡ることができます

  • 出力が逆方向に読まれるか、混在する行でスクランブルされる — 通常、 TextOut 試行からの回避策の残骸である、呼び出しの前に文字列が手動で反転されていました。すべての手動反転を削除し、論理順序を渡します。 RtLTextOut は内部的に反転します
  • 文字が孤立した形で切断されて印刷される — テキストがプレーンな TextOut を通過したか、 SetFont が右から左への文字セットなしで呼び出されました。 RtLTextOut で描画し、4番目の SetFont 引数としてアラビア語には178、ヘブライ語には177を渡します
  • 顧客のマシン上の空のボックス — Windowsがアラビア語またはヘブライ語をカバーしていないフォントを置き換えました。インストールされているフォントを指定するのをやめます。 RegisterUnicodeTTF を通じて出荷する書体を登録し、その名前で SetFont します
  • 2ページ目が間違ったフォントでレンダリングされる — 現在のフォントは AddPage を乗り切りません。すべての改ページの後に、文字セットを含めて SetFont の呼び出しを繰り返します
  • 主にLTR(左から右)のドキュメントで両面見開きが鏡像で印刷される — 最初の RtLTextOut 呼び出しにより、副作用としてドキュメントの Direction が反転しました。右から左への実行の後に Pdf.Direction := LeftToRight を設定します
  • 埋め込まれたUnicodeテキストがダウンストリームで静かに劣化する — パイプライン内の何かがPDF 1.4を強制しており、HotPDFの埋め込みUnicode処理は1.5以降を必要とします。ドキュメントのバージョンを上げるか、ダウンストリームの制約を削除してください

形式を出荷する前に、目視を超えて確認してください。ビューアからテキストをコピーして戻し、ドキュメント内検索を実行し、開発フォントのないマシンでファイルを開き、ネイティブの読者の前に1つの本物のドキュメントを置きます。完全な検証チェックリスト、スクリプトごとのカバレッジマップ、および構築する価値のあるテスト文字列コーパスはすべて、 HotPDFを使用したアラビア語とRTLテキストのシェーピング に関する関連記事にあります

ここに示されている RtLTextOutSetFont 、および RegisterUnicodeTTF の呼び出しは、DelphiおよびC++Builder用のHotPDFコンポーネントの一部です