技術記事

HotPDF でアラビア語・ヘブライ語のコムフィールドを扱う(Delphi)

HotPDF は AcroForm のコムフィールドに、アラビア語、ヘブライ語、結合文字、混在方向のテキストを THPDFHeadlessDocument.SetTextFieldValueWithFamilies で埋められます。コムグリッドは Unicode スカラーで数え、リガチャや結合マーク付きの基底文字は、そのスカラーが占める k 個のセルにわたって 1 つのクラスタとして中央揃えされ、行全体はセルが割り当てられる前に Unicode 双方向アルゴリズムを通ります

この 3 つの規則はそれぞれ、ラテン文字のデモでは問題なく見えて、最初の実フォームで崩れる近道を置き換えるものです。アラビア語の氏名のための 10 マスのコムフィールドを持つ役所の申請書を思い浮かべてください。1 文字ずつのロジックで埋めると、分離形の文字が左から右へ行進し、ラム・アリフのリガチャが 1 マスに押し込まれて隣が空席のままになり、ファトハが間違ったセルの上に浮きます。どの欠陥も、数える単位を間違えたか、並べ替える瞬間を間違えたことに由来します

Delphi で HotPDF を使いアラビア語のコムフィールドを埋める 2 つのやり方。1 文字ずつの近道はラム・アリフのリガチャを潰し、シェーピング埋めは各クラスタを中央揃えする
Unicode スカラーを数え、セル割り当ての前に行を並べ替えれば、リガチャは 2 マスにわたって中央に座り続け、空セルも生まれません

Comb フラグは PDF ライタに実際に何を求めているのか

Comb フラグは、フィールドをちょうど MaxLen 個の等間隔の位置に分割し、1 文字ずつ置くようライタに求めます。ISO 32000-1 §12.7.4.3 は Comb を表 228 のビット位置 25、テキストフィールド固有のフィールドフラグとして定義し、表 229 の MaxLen エントリが存在し、Multiline、Password、FileSelect の各フラグが立っていないときにのみ意味を持たせています。ビット自体は 32 ビットの /Ff 値における $1000000 です。浮動小数点変数を経由させたあとコムフィールドが壊れた経験があるなら、32 ビット AcroForm フィールドフラグの記事が、最初に消えるのがこのビットである理由を説明します

仕様は「文字」と言って黙ります。等幅のラテン文字マスを想定して書かれており、アラビア語の 2 文字組が 1 グリフになるとき、母音マークが幅を持たないとき、文字とは何かを語りません。HotPDF は単位を選ばなければならず、その際に構造規則は厳密に強制します。multiline を兼ねるコムフィールドや、正の MaxLen を持たないコムフィールドは診断付きで拒否され、password フィールドはそもそもコムフィールドになれません

なぜ UTF-16 単位やグリフ、書記素ではなく Unicode スカラーを数えるのか

HotPDF がコムセルを Unicode スカラーで数えるのは、MaxLen の強制に使う単位と同じだからです。グリッドと長さ検査は一致しなければならず、一致しなければ、検証を通った値がマスに収まりません。魅力的に見える 3 つの代替は、それぞれ別の形でこの一致を壊します

  • UTF-16 コードユニットは Delphi の Length が返すものです。U+20BB7 のような補助文字は 2 つのマスと、フィールド予算の 2 スロットを食みます
  • グリフ数はフォントに依存します。大半のアラビア語フォントはラムとアリフを 1 グリフにするため、書体を替えた瞬間にセル数が変わります
  • 書記素数は基底文字とマークを併合します。ユーザーが打った全スカラーを数える MaxLen 検査と、グリッドが食い違うことになります

THPDFHeadlessDocument の MaxLen 検証は、文字列の UTF-16 長が上限を超えるときだけ文字列をたどり、そのとき有効なサロゲートペアを 1 つと数え、対にならないサロゲートは丸ごと拒否します。UI が例外を捕まえる代わりに呼び出しの前に警告したいなら、同じ数え方をしてください

Delphi の HotPDF コムセルについて UTF-16 単位、グリフ、書記素を Unicode スカラーと比較するマトリクス。ラム・アリフ、ベー+ファトハ、U+20BB7、アクセント付き e を例に
MaxLen 検査と一致するのは Unicode スカラーの列だけです。サロゲートペアは 1 と数え、セル数はフォントに決して依存しません
// コムグリッドと同じ方法で Unicode スカラーを数える:
// 有効なサロゲートペアは 1 スカラー
function CountScalars(const S: UnicodeString): Integer;
var
  I: Integer;
begin
  Result := 0;
  I := 1;
  while I <= Length(S) do
  begin
    if (Ord(S[I]) >= $D800) and (Ord(S[I]) <= $DBFF) and
       (I < Length(S)) and
       (Ord(S[I + 1]) >= $DC00) and (Ord(S[I + 1]) <= $DFFF) then
      Inc(I, 2)
    else
      Inc(I);
    Inc(Result);
  end;
end;

いくつかの値が、単位の間のずれがどれほど広がるか、そしてスカラーの列だけがコムフィールドの予算と一致する理由を示します

値UTF-16 単位スカラー(セル)典型的なグリフ
ラム+アリフ(لا)221 リガチャ
ベー+ファトハ(بَ)222、マークは基底文字に付く
U+20BB7(𠮷)211
分解済み e + U+0301221 か 2、フォント次第

リガチャと結合マークはどうセルを分け合うのか

k 個のスカラーをカバーするシェーピング済みクラスタは、k 個の連続したセルを占有し、それらにわたって 1 つの単位として中央揃えされます。シェーピング後、すべてのクラスタは出所のテキストの正確なスパンへ逆写像されるため、HotPDF はどのスカラー、したがってどのセルを自分が所有するのかを知っています。前述のラム・アリフのリガチャは 2 マスを受け取り、両方にわたって中央に座ります。シャッダとファトハを載せたベーは 3 スカラーなので 3 マスを受け取り、マークは文字に付いたままです

中央揃えは、素直な実装が崩れる場所です。各グリフをそれぞれのマスで中央揃えすると、マークは基底から引き剥がされます。マークの位置は、装飾対象の文字との相対でのみ意味を持つからです。HotPDF はクラスタ内のすべてのグリフを、シェーピングされた送り幅と X・Y オフセットのまま保ち、自然な送り幅、配置済みグリフ原点、実際のグリフインクの合成水平範囲でクラスタごと中央揃えします。クラスタは塊として動き、内部は再配置されません

見落としやすい帰結があります。U+200F RIGHT-TO-LEFT MARK、U+061C ARABIC LETTER MARK、埋め込み・分離の各制御文字のような方向制御は、グリフを持たない Unicode スカラーです。MaxLen の計上対象となり、空白のままのセルを保持します。名前データにこれらのマークを撒き込むシステムから来るなら、埋める前に剥ぎ取ってください。さもないとフィールドに隙間が現れ、ユーザーの期待より 1 文字早く値を拒否します

なぜ双方向の並べ替えはセル割り当ての前に起きなければならないのか

HotPDF は行全体をまず Unicode 双方向アルゴリズム(UAX #9)に通し、その後に各スカラーへ視覚位置からセルを与えます。論理順でセルを割り当ててから結果を反転させるやり方は、純アラビア語の文字列では通用しますが、行に数字やラテンのトークンが混ざった途端に破綻します。右から左の行の中で、これらの実行は左から右に読み続けなければならないからです。アラビア語の接頭辞と数字を持つ参照コードのような値は、並べ替えが行全体を一度に見るときにだけ、数字を正しい順に保てます。複雑スクリプトのテキストシェーピング概観が、論理順と視覚順の問題をより深く扱います

Delphi の HotPDF における数字入りアラビア語値のコムセル割り当て。論理順は数字を反転し、UAX 9 を先に通すと数字の並びが保たれる
行全体にまず双方向アルゴリズムを通し、その後に各スカラーへ視覚位置のセルを与えれば、埋め込まれた数字は並びを保ちます

揃えはそのうえで、使用済みセルのグリッド全体をフィールド内へ配置します。フィールドの継承した /Q エントリがあればそれが勝ち、Options.Alignment はフォームに何もないときにのみ効きます。右から左のフォームではここが効いてきます。左揃えのコムフィールドに入る短いアラビア語の値は左端のマスから埋まります。仕様どおりであり、フォーム設計者の意図とはほぼ永遠に乖離します。フォームの /Q を直すか、フィールドが独自の quadding を持たないなら htaRight を設定します。ウィジェットの回転は /MK の /R エントリから来て、90 度と 270 度では論理テキストビューポートが幅と高さを入れ替えますが、セルのロジックは変わりません

Delphi から混在フォントのコムフィールドを埋める

混在フォントのコムテキストは、供給されたすべてのフォントに対して 1 つのセルグリッド、1 つのフォントサイズ、1 つのベースラインを使います。FallbackFontFamilies を True にすると、HotPDF は完全な書記素ごとに、設定順に従って供給ファミリーからフォントを選びます。アラビア語書体が文字を担い、ラテン書体が埋め込み ASCII を担う、という組み合わせが可能です。各フォントスパンは行全体を文脈としてシェーピングされ、マークがフォントをまたいで基底から引き剥がされることはなく、フォント境界を越えたリガチャは作られません。スパンごとに局所グリッドでレイアウトしたり、固有のバウンディングボックスへスケールしたりするほうが単純でしょうが、フォールバックが行の途中でフォントを替えた途端、セル位置も回転フィールドも壊れます

uses
  System.SysUtils, System.IOUtils,
  HPDFHeadlessDocument, HPDFHeadlessAppearance;

procedure FillArabicComb(const SourceFile, TargetFile, Value: string);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  Families: THPDFHeadlessRichTextFontFamilies;
  Options: THPDFHeadlessTextAppearanceOptions;
  FieldIndex: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(SourceFile);
    FieldIndex := Doc.FindFormField('applicant.name_ar');
    if FieldIndex < 0 then
      raise Exception.Create('Field not found');
    Field := Doc.GetFormField(FieldIndex);
    if (Field.Flags and $1000000) = 0 then   // ビット 25:Comb
      raise Exception.Create('Not a comb field');

    SetLength(Families, 2);                  // 1 から 16 までのファミリー
    Families[0].FamilyName := 'Noto Naskh Arabic';
    Families[0].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoNaskhArabic-Regular.ttf');
    Families[1].FamilyName := 'Noto Sans';
    Families[1].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoSans-Regular.ttf');

    Options := THPDFHeadlessTextAppearanceOptions.Default;
    Options.ShapeComplexText := True;        // すでに既定
    Options.FallbackFontFamilies := True;    // 既定は False
    Options.FontSize := 0;                   // 自動フィット、上限 12
    Options.Alignment := htaRight;           // /Q がないときにのみ使われる

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // SourceFile と異なる必要がある
  finally
    Doc.Free;
  end;
end;

例が設定していないものに注目してください。文書フィリングの間、Width、Height、Rotation、CombCells は各ウィジェット矩形とその /MK エントリ、フィールドの MaxLen から導出されるため、これらのメンバーへ書いた値は上書きされます。ゼロの FontSize は自動サイズを要求します。すべてのクラスタの範囲を、k 個のセルの幅と、参加フォント全部の共有メトリクス・インク高さと突き合わせて検査します。固定サイズは単純にウィジェットでクリップされます。CharacterSpacing と WordSpacing はコム位置を動かしません。クラスタの着地点を決めるのはグリッド単独だからです。HotPDF は HarfBuzz と FriBidi のランタイムを、値が実際にシェーピングを要求するときだけロードし、シェーピング不要の値は既存の直接コムレイアウトをそのまま使います。権限や保存を含むロード済み PDF への値設定の一般ワークフローは、ロード済み PDF でのフォームフィールド値の設定を参照してください

シェーピング済みコムの見た目が保証しないもの

シェーピング済みコムの見た目が保証するのは、正しいセル割り当て、無傷のクラスタ、保存された論理値です。保証しないのは、すべてのツールでのシームレスな筆記体接続と完璧なテキスト抽出です。どちらの限界もコムフィールドというものの性質から来るため、顧客に何かを約束する前に知っておくべきです

1 つ目はセルをまたぐ筆記体の接続です。アラビア文字は行全体の文脈でシェーピングされるため、各文字は正しい語頭形・語中形・語尾形を得ます。しかし固定のセルピッチは別々のクラスタの間に空白を置き、接続の線が連続テキストのように出会うことはありません。接続文字をマスに描く以上、本質的なものであり、どれだけ中央揃えしても隠せません

2 つ目はテキスト抽出です。フィールドの /V は素の論理値を保持し、見た目の親 Form XObject は完全な入力を持つ 1 つの /ActualText スパン(ISO 32000-1 §14.9.4)を運びます。子の Form は何も運びません。ActualText を尊重する抽出器は元の文字列を復元します。無視する抽出器はグリフ位置からテキストを再構築し、別の順序を返したりクラスタテキストを重複させたりします。複数グリフのクラスタには、グリフごとの明白な単一 Unicode 値が存在しないからです。下流システムが値を要するなら、ページテキストではなくフィールドを読んでください

残りの境界は機械的なものです。コムテキストはタブなしの 1 行でなければならず、セル数より多いスカラーを持つ値は切り詰めではなく失敗します。失敗はトランザクショナルです。欠落グリフ、無効なフォント、予算超過、キャンセルは以前の値と見た目に触れないため、拒否されたフィリングが半分描かれたフィールドを置き去りにすることはありません

try
  Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
  on E: EHPDFHeadlessDocumentError do
    if E.Code = hdLimitExceeded then
      ShowFieldError('The value has more characters than the field allows')
    else
      raise;
  on E: EHPDFHeadlessAppearanceError do
    if E.Failure = haMissingGlyph then
      ShowFieldError('None of the supplied fonts covers this text')
    else
      raise;
end;
// どちらのエラーでも、フィールドは以前の値と見た目を保つ

フォームパイプラインの中でシェーピング済みコムが座る場所

シェーピング済みコム対応が最も効くのは、すでに存在するフォームです。マス付きフィールドで設計され、今やアラビア語、ヘブライ語、ペルシア語、ウルドゥー語で埋められる税務申告、ビザ申請、銀行の口座開設パック、身分登録です。スカラーで数え、元の値を /V に保持し、全フォントに 1 つのグリッドを治めさせ、右から左の文字、数字、結合マークを混ぜた値でテストしてください。その 1 文字列が、この記事の全規則を一度に運動させます。ここで説明したヘッドレス文書 API、コムシェーピング、フォントフォールバックは、HotPDF Delphi PDF component の一部です