HotPDFは、フィールド値を端から端まで配列のまま保つことで、マルチセレクトリストボックスの値をFDFとXFDFで往復させます。バージョン2.755.0以降、ExportLoadedFormToFDF、ExportLoadedInterchangeToFDF、ExportLoadedFormToXFDFは、選択された各オプションを、それぞれ独自のFDF文字列かXFDFの<value>要素として書き、対応するインポートメソッドは、何かを変える前に、すべての値をフィールドのオプションと照合し、/Iの選択インデックスを組み直します。途中で1つの文字列に張り合わされるものはありません
これで直る失敗は、簡単に再現できます。製品オプションのマルチセレクトリストボックスを持つ発注書を用意し、ユーザーに2つ選ばせ、バックオフィスシステム向けにフォームデータをエクスポートし、編集済みのファイルをPDFへインポートし直す。この変更の前は、リストボックスは空か、間違った状態で戻ってきました。理由は、エクスポート値の1つが改行を含んでおり、旧経路が選択を単一の行区切り文字列へ平坦化していたからです。あの文字列から複数選択を取り戻すのは最初から信頼できず、改行そのものを含むエクスポート値では、まったく動きようがありません
マルチセレクト値を改行で結合すると往復が壊れるのはなぜか
選択を1つの文字列へ結合すると、値の間の境界が捨てられます。そして値は区切り文字を含めます。だからどのインポーターも、文字列を正しく分割し直せません。ISO 32000-1 §12.7.4.4は、choiceフィールドの/Vエントリーを、単一のテキスト文字列かテキスト文字列の配列のどちらにも許しており、MultiSelectフラグ(/Ffのビット22)を持つリストボックスは、複数のオプションが選ばれたら配列形式を使います。同じセクションは/Iを、昇順の0ベースオプションインデックスの配列と定義しています。ビューアーはこれを使って、たまたまエクスポート値を共有する2つのオプションを区別します。HotPDFでは、スカラーgetterのGetFormFieldValueは文字列形式しか読まないので、配列をこれに通すとエクスポートが空文字列に劣化しました。そして旧XFDFインポートは、繰り返される<value>要素をLFで結合していました。エクスポートされたオプションが、Deep、改行、Blueだと想像してください。結合後は、Deep\nBlue\nRedが2つの選択かもしれないし3つかもしれず、ファイルにはどちらかを見分ける手がかりがありません。修正は、往復の真ん中でスカラーを使うのを完全にやめることでした
エクスポートされたFDFとXFDFのファイルは何を含むか
HotPDFはマルチセレクト値を、FDFでは型付き配列として、XFDFでは選択ごとの1つの<value>要素として書くので、境界はディスク上で見えたままです。FDFでは各項目は、ソースPDFでの綴りを保ちます。16進文字列はhexとして出て行き、リテラル文字列は、CRとLFを\rと\nへ変える1つのヘルパーでエスケープされます。XFDFでは、ルートはISO 19444-1が要求するとおりxml:space="preserve"を運びます。これは、テキスト要素の中の空白はすべてデータとして数える、という意味です。だからHotPDFは、各<value>の開始タグ、エスケープされたテキスト、終了タグをひとかたまりで書き、インデントは要素の外に保ち、CR、LF、TABを文字参照としてエンコードします。改行の正規化を適用するXMLパーサーが、元のバイト列を変えられないようにするためです
<!-- FDF:フィールドごとに1つの型付き配列 -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF:選択ごとに1つの<value> -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
呼び出しコードを書く前に知っておく価値のある、エクスポートのエッジケースが2つあります。1つ目、ExportLoadedFormToFDFは、ターゲットファイルを作る前に、完全なFDFボディをメモリーで組み立てます(2.755.1で修正)。だから文字列以外を含む配列のような、エクスポートできない値は、既存のファイルを切り詰めることなく例外を上げます。2つ目、空文字列のエクスポート値も提供するリストボックスでの空の選択は、XFDFでは曖昧です。<value/>は、何も選ばれていない意味にも、空のオプションが選ばれた意味にもなり得るからです。ExportLoadedFormToXFDFはこの場合、当て推量の代わりに例外を上げます。そして例外は、ターゲットファイルが開かれる前に上がります。FDFにはこの曖昧さはありません。/V []と/V [()]は別物だからです。両方のFDFエクスポーターはさらに、/T名を持たないウィジェット専用の末端を飛ばします。XFDFエクスポーターと同じく、です。どのインポーターも、これらのエントリーをフィールドへ対応させ直せないからです
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// マルチセレクトリストボックスは/V [(...) (...)]として書かれる
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// 空の選択+空のエクスポートオプション:XFDFには見分けがつかず、
// 既存の.xfdfファイルは無変更のまま残される
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
HotPDFはインポート時にマルチセレクト値をどう検証するか
HotPDFがインポートされた配列を受け入れるのは、ターゲットがMultiSelectフラグの立ったchoiceフィールドで、配列のすべての値が、フィールドの/Opt配列内のエクスポート値と一致するときだけです。各オプションスロットは1回しか使えません。だからエクスポート値bを共有する2つのオプションを持つリストは、[<62> <62>]を2つの別個の選択として受け入れ、3つ目のbは拒否します。組み直された/Iは、入ってくる値の順序ではなく/Optの順序に従います。§12.7.4.4が昇順のインデックスを要求するからです。HotPDFは新しい/Vと/Iを独立したオブジェクトとして組み立て、すべての値が検証を通った後でのみ代入します。だから拒否された値が、半分の配列や古いインデックスを残すことはありません。コピーは、共有された祖先の配列ではなく、インポートされるフィールドへ書かれ、FDFから届いたhexの綴りは保存までhexのまま保たれ、リストボックスに計算が依存するフィールドは再計算の対象として印を付けられます。単一の値を設定するだけなら、読み込み済みPDFで1つのフォームフィールド値を設定するがスカラー経路を通ります。こちらは設計上、複数選択を扱いません
他のツールの中には、バイト順マークなしでプレーンASCIIのエクスポート値をhex文字列として書くものがあります。たとえば<416272>。そしてそれらのhex桁をテキストとして書き出すことでXFDFをエクスポートします。帰り道の厳格なリテラル比較は失敗し、インポートは中断します。v2.755.1は1回のリトライを加えました。値がどのオプションにも一致しないとき、HPDFHexSpellingTextがテキストをhexペイロードとしてデコードし、結果をもう一度比較します。リトライは、そうでなければraiseしていた入力にしか適用されないので、すでに一致した値を変えることは決してありません。同じリリースはさらに、スカラー経路と配列経路に同じUnicodeデコーダーを使わせました。PDFDocEncoding、どちらのバイト順マークのUTF-16、そしてUTF-8を理解するデコーダーです。それ以前は、エンコードの混ざった文書で、1つの論理的な値が片方の経路では一致し、もう片方では失敗し得ました
有効なFDFファイルが解析中にフィールドを失い得るのはなぜか
16進文字列を追跡しないFDFスキャナーは、hex値が辞書の終端子の真横で終わるとき、フィールド辞書を真っ二つに切れます。<< /T (region) /V <416273>>>では、最初の>がhex文字列を閉じますが、素朴なスキャナーはそれを次の>と合わせて辞書の終わりと読み、フィールドを黙って落とします。ファイルレベルのFDFインポーターは、hex文字列の内側にいるかどうかをすでに追跡しており、2.755.1では、ImportLoadedInterchangeFromFDFの背後にある配列と辞書のスキャナーが同じことをします。2つ目の問題は間接参照です。FDFファイルは、自分のオブジェクト番号付けを持つ、小さなPDF構文の文書です(ISO 32000-1 §12.7.7)。だから/V [11 0 R]のような値は、あなたが記入しているPDFのオブジェクト11ではなく、FDFファイルのオブジェクト11を指します。HotPDFの簡略化されたFDFパーサーは、ファイル内の参照を解決しないので、そのような配列は、ターゲット文書でたまたまオブジェクト11であるものを読む代わりに、拒否します
ファイル、ストリーム、XFDFのインポートはエラーの報告が違う
3つのインポート経路は同じように検証しますが、失敗の報告は違います。意図的に1つを選ぶ価値があります。ImportLoadedFormFromFDFは、検証に落ちたフィールドを飛ばし、実際に適用したフィールド数を返します。期待より低いカウントが、問題の唯一の兆候です。ImportLoadedInterchangeFromFDFとImportLoadedFormFromXFDFは、最初に拒否されたフィールドで例外を上げます。各フィールドは自分専用にコミットされるので、例外の前に処理されたフィールドは新しい値を保ちます。これらのどれも、交換ファイル全体のトランザクションとして扱わないでください。オールオアナッシングの振る舞いが必要なら、例外が起きたときに、保存する代わりに読み込み済みの文書を捨ててください
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// フィールドのみ。/Optの外の値や非マルチセレクトのターゲットはraiseする
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
既存の呼び出し側を壊さずにXFDFコールバックを拡張する
下位レベルのXFDFユニットでの配列対応は、別のレコードTHPDFXFDFArrayAccessと、HPDFXFDFExportFieldsとHPDFXFDFImportFieldsの新しいオーバーロードに住んでいます。既存のTHPDFXFDFAccessレコードの末尾へ足された追加フィールドには住んでいません。理由はバイナリ互換です。ローカル変数としてTHPDFXFDFAccessを満たすコードは、知っているスロットだけを設定して残りをクリアしないことがよくあります。だからこのレコードに足された新しい関数ポインターはスタックのゴミを含み、ライブラリーはそれを本物のコールバックと見なしてしまいます。別のレコードなら、旧呼び出し側は旧レイアウトと旧オーバーロードを保ち、それらのオーバーロードは内部で全nilの配列レコードを渡します。元のスカラーインポートオーバーロードは、互換のために繰り返される値をLFで結合し続け、配列対応のオーバーロードだけがそれらを別々に保ちます。自分のデータストアを束縛するときは、Default(THPDFXFDFArrayAccess)から始めてください。GetFormFieldValueArrayは、何も選ばれていないものを含め、リスト値のフィールドならどれでもTrueを返し、スカラーコールバックへフォールバックするときはFalseを返します
uses HPDFXFDF;
// プレーンな関数ポインター、"of object"ではない。Contextがあなたのストアを運ぶ
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // あなたの既存のスカラー束縛
ArrayAccess := Default(THPDFXFDFArrayAccess); // 未使用のスロットはすべてnil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
マルチセレクトの交換は、すでに存在し、/FfにMultiSelectビットの立ったリストボックスで動きます。choiceフィールドとそのフラグビットがどう作られるかについては、読み込み済みPDFにListBoxと他のAcroFormフィールドを追加するを参照してください。XFDFの<annots>ツリーを通る注釈マークアップについては、HotPDFでのXFDF注釈のインポートとエクスポートを参照してください。完全なAPIリファレンスとトライアルのダウンロードは、HotPDF Delphi PDF componentページにあります