技術記事

/AP のない PDF フォームフィールドを Linux で描画する

フィールド値はあるのに外観ストリームを持たない PDF フォームは、Linux のレンダージョブから空白の箱や、間違ったフォントや揃えで出てくることがあります。あるいは「Widget field requires a name field type」という EHPDFHeadlessRenderError です。HotPDF のヘッドレスレンダラーはテキストとボタン、チョイスのウィジェットに一時的な外観を描き、/FT と /V、/DA、/Q をフィールドツリーを通して継承します

ウィジェットはフィールド設定をどこに保持するのか

問題の大半は、フィールドとそのウィジェットの分離から始まります。フィールド辞書はフォームコントロールの論理部分を保持します。型と値、フラグ、デフォルト外観です。ウィジェット注釈が保持するのはページが要るものです。/Rect と枠線、ページへのポインタ。ウィジェット 1 個のフィールドはたいてい 1 つの辞書に統合されています。同じ値を複数のページに出す必要が生じると、ウィジェットはフィールドの子になり、幾何と /Parent 参照しか運ばないことが少なくありません。ISO 32000-1 §12.7.3.1 は /FT と /Ff、/V、/DV をそのチェーンに沿って継承可能にし(Table 220)、§12.7.3.3 は Table 222 の継承集合に可変テキスト属性を 2 つだけ加えます。必須ながら祖先から来てもよい /DA と、/Q です。対話フォーム辞書(Table 218)は /DA と /Q の文書全体のデフォルトに加え、/DR、つまり /DA のフォント名を引きに行く先のデフォルトリソース辞書を供給します。/DR は Table 222 にはありません。フォーム全体のものだからです。ウィジェット辞書だけを読むレンダラーは、下のファイルに型も値も /DA も見つけられず、自分のデフォルトフォントへ後退します

1 0 obj  % カタログ
<< /Type /Catalog /Pages 2 0 R
   /AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
                /DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj  % 端末フィールド:型と値、外観のデフォルトはここに置かれる
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
   /DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
   /Kids [11 0 R 12 0 R] >>
endobj
11 0 obj  % ページ 1 のウィジェット:幾何のみ、角は右上を先に指定
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj  % ページ 2 のウィジェット:幾何のみ
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj

HotPDF は各フィールドエントリをウィジェット上で探し、なければ /Parent を 1 辞書ずつ登ります。最も近い値が勝ち、チェーンを使い果たしたら /DA と /Q だけが AcroForm 辞書へ後退します。上のファイルの期待値はこうです。2 つのウィジェットとも INV-2026-0417 を 10 ポイントの Courier で右寄せ、ダークブルーで示し、文書全体の /Helv 0 Tf は登場しません。オブジェクト 10 が自分の /DA を持つからです。フィールドが 12 ポイントの黒い Helvetica で出てきたら、第一の容疑者は Linux ボックスに Courier フォントがないことです。サイズが物語っています。ウィジェットにもどの祖先にも AcroForm 辞書にも /DA が現れないとき、HotPDF が描くのはこれなのです。だからウィジェットをダンプし、/Parent を手でたどり、プロデューサーが /DA をどこに置いたか見つけてください。たとえばウィジェットが決して逆参照しないフィールド辞書の上に

Delphi の HotPDF ヘッドレス描画における AcroForm オブジェクトグラフ。幾何のみのウィジェットが /FT と /V、/DA、/Q を保持するフィールドへ /Parent を登る
/Parent チェーンに沿う最も近い値が勝ち、チェーンを使い果たしたら /DA と /Q だけが AcroForm 辞書へ後退します

/DA のフォント名は、ウィジェットか親フィールドの /DR、AcroForm の /DR、ページのフォントリソースの順で解決されます。フィールドレベルの /DR は Table 222 の外ですが、存在すれば曖昧さがないので HotPDF は受け入れます。登りには上限があります。ツリーのループは「Widget field inheritance contains a cycle」で止まり、辞書でない /Parent は「Widget Parent requires a dictionary」で、描画オプションの MaxDepth(デフォルト 32)を超えるチェーンは「Widget field inheritance depth budget exceeded」で止まります。VCL コンポーネントは継承された AcroForm 値を読んでフィールドをリセットするとき、同じ継承を適用します

ヘッドレスレンダラーは何を描くかをどう決めるのか

HPDFRenderHeadlessPagePNG とそのファイル版がフォールバックを走らせるのは、/AP を欠くか null である可視ウィジェットだけです。ISO 32000-1 §7.3.7 は、null の辞書値は欠けたエントリと同じだと言い、レンダラーは /AP null をそのように扱います。存在する通常外観は常に勝ちます。/NeedAppearances が何と言ってもです。以前の記入で古くなった外観は、何かが作り直すまで表示され続けます。ロードされた文書は決して変更されません。何が描かれるかは、解決されたフィールド型とフラグ次第です

フィールド合成される外観
1 行テキスト、/FT /Tx文字列 /V を、解決された /DA と /Q で /Rect の内側に組んだもの
チェックボックス、/FT /Btn/Border の角丸付き矩形枠、/MK /BG の塗り、オンならストロークのベクターチェック
ラジオボタン、ビット 16 の Btnオンなら塗りつぶしドット付きの円形枠
プッシュボタン、ビット 17 の Btn/MK /CA のキャプションを中央に置いた矩形枠
コンボボックス、ビット 18 付きの /FT /Ch選択された /Opt エントリの表示テクストを中央のキャプションとして
リストボックス、ビット 18 を下ろした Ch/TI から始まるオプションごとの 1 行、選択行の後ろにハイライト帯

チェックマークとラジオドットはベクターパスであって ZapfDingbats グリフではありません。多くの生成器がそう描くのですが、小さめの Linux コンテナには ZapfDingbats が入っていないことがあるからです。プッシュボタンとコンボボックスの面は、HotPDF の VCL 外観生成器が同じフィールドについて描くものと一致します。どちら側が画像を作ってもフォームは同じように見えます。キャプションはフォントとサイズ、色を /DA から取ります。キャプションのレイアウトは自動合わせ込みをしないので、/DA のサイズ 0 はボックス高の 70 パーセントになり、12 ポイントで頭打ちです。チェックボックスとラジオの状態はウィジェット自身の /AS から来て、継承された /V へ後退し、Off 以外の状態はマークを描きます。ラジオの子にはそれぞれ固有の /AS を与えてください。親の /V はグループ全体の選択状態を名指すので、子を区別できません

Delphi 向け HotPDF ヘッドレスレンダラーの判断フロー。存在する /AP は常に勝ち、なければ解決されたフィールド型が合成面を選ぶ
一時的な外観が描かれるのは /AP を欠くか null のときだけで、ロードされた文書自体は決して変更されません
uses
  SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;

function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
  out Diagnostic: string): Boolean;
var
  Doc: THPDFHeadlessDocument;
  Options: THPDFHeadlessRenderOptions;
  Info: THPDFHeadlessRenderResult;
begin
  Result := False;
  Diagnostic := '';
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    Options := THPDFHeadlessRenderOptions.Default;  // RenderAnnotations = True, MaxDepth = 32
    Options.Usage := hruPrint;  // ビューアの印刷相当:Print フラグのないウィジェットはスキップされる
    try
      // PageIndex は 0 起点
      Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
        PngName, Options);
      Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
      Result := True;
    except
      on E: EHPDFHeadlessRenderError do
        // 例:'Widget field requires a name field type'。記入済みフォームでは
        // RenderAnnotations := False での再試行をしない。全ウィジェットと値が落ちる
        Diagnostic := E.Message;
    end;
  finally
    Doc.Free;
  end;
end;

テキストフィールドのフォールバックが描くのは、文字列値を保持する 1 行フィールドです。それ以上のテキストレイアウトが必要なら、記入の時点で Windows 側に外観を生成させてください。そこでは EnsureLoadedFieldAppearanceStream がファイルのフラット化や出荷の前に欠けた /AP ストリームを作るので、Linux のジョブは本物の外観ストリームを受け取り、ヘッドレスレンダラーはまだそれを要するフィールドに出会えば名指しします。ただし Linux 出力は Linux レンダラーで裁いてください。RenderLoadedPageToBitmap はWindows GDI バックエンドを通して描くのであり、このフォールバックは決して走りません。そこで正しく見えるページは、Cairo デバイスが同じファイルから何を作るかについて何も教えません

フィールドフラグのビットは 1 から数える

ISO 32000-1 §12.7.3.1 は、/Ff ワードのビット位置が下位端の 1 から 32 まで走ると述べ、Table 226 と 228、230 はみなこの番号付けを使います。Delphi ではしたがってビット N は 1 shl (N - 1) です。ビット番号そのものでシフトすると、すべてのテストが 1 つ上の位置へずれます。「radio、ビット 16」用に書いた 1 shl 16 は実はプッシュボタンフラグを試し、「combo、ビット 18」用に書いた 1 shl 18 は Edit を試します。間違ったマスクのどちらも本物のフラグです。コードはコンパイルされ、走り、フィールドを間違った種類に振り分けます

Delphi における PDF フィールドフラグのビット番号付け。ものさしが示すのは、ラジオが 1 shl 15 であり 1 shl 16 はプッシュボタンに届く理由
/Ff ワードのビット N は Delphi マスク 1 shl N 引く 1 を取り、1 ずれたマスクはどれも隣のビットの本物のフラグです
フラグ表ビットDelphi マスク
Multiline228(テキスト)131 shl 12 = 4096
Password228(テキスト)141 shl 13 = 8192
Comb228(テキスト)251 shl 24 = 16777216
RichText228(テキスト)261 shl 25 = 33554432
Radio226(ボタン)161 shl 15 = 32768
Pushbutton226(ボタン)171 shl 16 = 65536
Combo230(チョイス)181 shl 17 = 131072
uses
  Classes, SysUtils, HPDFHeadlessDocument;

const
  // ISO 32000-1 は Ff のビットを 1(下位)から数える。ゆえにビット N は 1 shl (N - 1)
  TxMultiline   = 1 shl 12;  // ビット 13
  TxPassword    = 1 shl 13;  // ビット 14
  TxFileSelect  = 1 shl 20;  // ビット 21
  TxDoNotScroll = 1 shl 23;  // ビット 24
  TxComb        = 1 shl 24;  // ビット 25
  TxRichText    = 1 shl 25;  // ビット 26
  TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
    TxDoNotScroll or TxComb or TxRichText;

procedure ListTextFieldsForProducerAppearance(const FileName: string;
  Report: TStrings);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  I: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    for I := 0 to Doc.FormFieldCount - 1 do
    begin
      // FieldType と Flags、MaxLength は祖先からの継承済みで届く
      Field := Doc.GetFormField(I);
      if Field.FieldType <> 'Tx' then
        Continue;
      if (Field.Flags and TxProducerLayout) <> 0 then
        Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
          [Field.Name, Field.Flags]))
      else if Field.MaxLength > 0 then
        Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
          [Field.Name, Field.MaxLength]))
      else if Field.Value = '' then
        Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
    end;
  finally
    Doc.Free;
  end;
end;

スキャンが読むのはフィールドレベルのフラグで、プロデューサーが置くのもそこです。レンダラーは分離ウィジェットの /Ff も尊重するので、リストは計画の補助と考え、描画の診断を最終の言葉としてください。空白チェックが大事なのは、/V も /AP も持たないテキストフィールドには合成すべきものが何もないからです。描画は「A text-field widget without a value has no appearance to synthesize」で止まり、治療はファイルがレンダラーに届く前の値か外観です

「Degenerate appearance geometry」は何を意味するのか

意味するのは、矩形か外観ボックスが面積を持たないことであり、逆向きの /Rect は原因ではありません。ISO 32000-1 §7.9.5 は矩形に、対角で向かい合うどの 2 隅も名指させてもよく、その注は処理ソフトに正規化の備えを求めます。幅を x2 - x1 で計算する素朴なレンダラーは [300 720 160 700] から負の数を得て、注釈を拒否するか外観を鏡映します。HotPDF は座標をどんなサイズチェックの前にも左下と右上の順へ入れ替えます。合成面にも既存の /AP ストリームにも同じです。だから上のウィジェット 11 は、隅が伝統的な順だったかのように描画されます。エラーが上がるのは、外観ストリームを持つ注釈についてだけです。正規化の後に /Rect の幅か高さが 0 であるとき、あるいはストリームの /BBox がその /Matrix の下で線か点に潰れるときです。現れたら、ウィジェットの /Rect をストリームの /BBox と /Matrix と一緒にダンプしてください。3 つのうちどれかがゼロの寸法を持っています

/AP を持たず面積ゼロの /Rect を持つウィジェットは別の道を行きます。フォールバックはそれを飛ばしてページを続けます。ISO 32000-2 §12.5.2 が外観要求から免除するのは、Popup と Projection、Link 注釈、そして 1 点に潰れる矩形だけです。ただの線である矩形は PDF 2.0 の下で今も外観を負っています。レンダラーはそれも飛ばします。塗る面積が単にないのです

フィールド型が見つからないとき

「Widget field requires a name field type」は、レンダラーがウィジェットから /Parent リンクの続く限り登ったのに /FT を見つけられなかったことを意味します。レンダラーはページの /Annots 配列から始めるので、上向きの /Parent リンクだけがフィールドへの道です。ロードは逆方向に、AcroForm の /Fields 配列から /Kids を下って歩きます。LoadFromFile が、後でページの描画に失敗するファイルの上で成功しかねないのはこのためです。典型的な原因は、フィールドの /Kids に載っているのに逆指ししないウィジェットです。ISO 32000-1 Table 188 はそこにいくらかの言い訳を与えます。ウィジェットに /Parent を要求するのは、ウィジェットが複数の子のうちの 1 つであるときだけで、「absent otherwise」と言うのです。これを文字どおり読んだライターは、分離ウィジェット 1 個のフィールドからリンクを落とします。ISO 32000-2 は同じエントリを「optional otherwise」に緩めました。うちの方針は、フィールドと統合されないすべてのウィジェットに /Parent を書くことです。ISO 32000-2 がそれを許し、ページ駆動のリーダーが、うちの製であれ誰の製であれ、フィールドへ届く唯一の道だからです。プロデューサーを直せないなら、孤立ウィジェットをフィールド辞書へ統合するのが同じ結果を与えます

親へ逆指しして、上り途上のどこかで /DA を見つけられるウィジェットは、HotPDF Delphi component のヘッドレスレンダラーで /AP なしに正しく描画されます。Linux のフォームパイプラインで最初に正しくすべきは、外観ストリームよりもフィールドツリーだということです