PDFの添付ファイルは、ドキュメントの組み込みファイルツリーに格納されます。この構造は、ほとんどのビューアでクリップのマークが付いたパネルや添付ファイルサイドバーとして表示されます。Delphiのコードから見ると、PDFium ComponentはTPdfのインデックス付きプロパティの小さなセットを介してそのツリーを公開します。これにより、整数のインデックスで反復処理し、ファイル名や生のバイトデータを読み取り、新しいスロットを作成し、既存のものを削除することができます。API自体は小規模であり、本番環境のコードを作成する前に知っておくべきいくつかの順序の制約と1つのサニタイズルールがあるだけです
開いているドキュメントからの添付ファイルの読み取り
AttachmentCountは、ドキュメントが宣言している組み込みファイルの数を返します。PDFiumの内部の呼び出しから直接読み取るため、PDFが実際に含んでいるデータのみを反映します。そこから、AttachmentName[Index]は表示名をWStringとして返し、Attachment[Index]は生のバイトデータをTBytes配列として提供します。どちらも0から始まるインデックスです。いずれのプロパティをクエリする前にも、ドキュメントが開いている(Pdf.Active = True)必要があります。閉じたドキュメントに対して呼び出した場合、例外は発生せず、単に0または空の結果が返されます
心に留めておきたい点が1つあります。Attachment[Index]は、読み取りのたびにファイル全体のペイロードを確保して返します。大きな埋め込みアセットを持つドキュメントで表示リストを作るためにすべての添付ファイルを反復処理すると、呼び出しごとにその確保コストを払うことになります。表示目的で名前だけが必要なら、まずAttachmentNameを読み、バイトの取得はユーザーが実際にファイルを要求するときまで遅らせてください
procedure ListAttachments(Pdf: TPdf);
var
I: Integer;
Data: TBytes;
begin
if not Pdf.Active then
Exit;
for I := 0 to Pdf.AttachmentCount - 1 do
begin
Data := Pdf.Attachment[I];
Writeln(Format('%d: %s (%d bytes)',
[I, Pdf.AttachmentName[I], Length(Data)]));
end;
end;
添付ファイルのディスクへの抽出
抽出は生のバイトに対して行われます。読み取り、そして必要な場所への書き込みはすべて自前のコードが担うため、パスの構成とサニタイズも完全にコード側の責任です。添付ファイル名が信頼できないドキュメントから来るとき、これが問題になります。PDFの添付ファイル名はファイル内部に格納された文字列であり、パス区切り文字やUnicodeの類似文字など、TFileStream.Createへ直接渡すと予期しない結果を生む文字を含むことがあります。出力パスを組み立てる前に、名前を必ずExtractFileNameに通してください。さらに、ドットで始まる名前や、システムが想定する範囲外の文字を含む名前は拒否することも検討してください
Attachment[Index]が返すバイト配列の所有者は呼び出し側です。通常のTFileStreamで書き出せば、あとは自由に扱えます。宣言された名前を信用するのではなく、先頭の数バイトを検査して実際のファイル形式を確認することもその1つです
procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
SafeName: string;
OutPath: string;
Data: TBytes;
FS: TFileStream;
begin
SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
if SafeName = '' then
SafeName := Format('attachment_%d', [Index]);
OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
Data := Pdf.Attachment[Index];
FS := TFileStream.Create(OutPath, fmCreate);
try
if Length(Data) > 0 then
FS.WriteBuffer(Data[0], Length(Data));
finally
FS.Free;
end;
end;
添付ファイルの追加と2ステップの書き込み
添付ファイルの作成には、1回ではなく2回の呼び出しが必要です。CreateAttachment(Name)は組み込みファイルツリーに新しいスロットを登録し、成功するとTrueを返します。このスロットは最初は空です。次に、直前に作成されたエントリを対象として、Attachment[AttachmentCount - 1]に書き込むことでペイロードを割り当てます。CreateAttachmentがFalseを返した場合、スロットは作成されておらず、この代入を行うと、たまたま最後にあるインデックスの添付ファイルを破損させてしまいます
添付ファイルのリストを変更した後も、変更はメモリ上にのみ存在します。更新された組み込みファイルツリーを持つ新しいファイルを書き出すにはSaveAsを呼びます。SaveAsは、現在開いているファイルとは別のパスをターゲットにしてください。エンジンがソースに読み取りハンドルを保持しているためです。インプレース更新の定番パターンは、一時パスへ保存し、ドキュメントを閉じ、オリジナルを削除またはリネームし、それから一時ファイルを所定の位置へリネームして開き直す、というものです
procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
FS: TFileStream;
Data: TBytes;
AttachName: string;
begin
if not Pdf.Active then
Exit;
FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
try
SetLength(Data, FS.Size);
if FS.Size > 0 then
FS.ReadBuffer(Data[0], FS.Size);
finally
FS.Free;
end;
AttachName := ExtractFileName(FilePath);
if Pdf.CreateAttachment(AttachName) then
Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;
添付ファイルのタイプ情報
ファイル名とバイトのペイロードに加えて、AttachmentType[Index]は、ファイルが最初添付された際にPDFの組み込みファイル辞書に格納されたMIMEタイプ文字列があればそれを返します。多くのジェネレーターは、このフィールドを空のままにするか、application/octet-streamのような汎用的な値に設定するため、本番のデータ処理パイプラインにおける形式検出にこれを信頼することはできません。信頼性の高い識別を行うには、ペイロードの最初の数バイトを読み取り、既知のファイル署名を確認してください。ネストされたPDFの場合は%PDF、Office Open XMLドキュメントの場合はZIPローカルファイルヘッダーPK\x03\x04、レガシーの複合ファイルバイナリの場合は\xD0\xCF\x11\xE0です。辞書から取得したタイプ情報は、UIラベルに表示するには問題ありませんが、実際のバイトデータが利用可能である場合には、それを処理の決定基準にすべきではありません
添付ファイルの削除
DeleteAttachment(Index)はその位置のエントリを削除し、成功するとTrueを返します。削除後、残りのエントリは下にシフトされるため、ループで複数の添付ファイルを削除する場合は、各シフト後のエントリのスキップを避けるために、最初のインデックスからではなく最後のインデックスから逆方向に反復処理する必要があります。この変更は、SaveAsを呼び出すまでメモリ上にのみ留まります
ドキュメント処理パイプラインにおける一般的なシナリオは、セキュリティやサイズ制限の観点から、受信したPDFを下流の処理に渡す前にすべての添付ファイルを取り除くことです。ループの前に一度数をカウントし、逆方向に処理を行います:
procedure StripAllAttachments(Pdf: TPdf);
var
I: Integer;
begin
for I := Pdf.AttachmentCount - 1 downto 0 do
Pdf.DeleteAttachment(I);
end;
実務におけるPDF添付ファイルの出現ケース
添付ファイル用APIはPDFiumが開くことのできる任意のPDFで動作しますが、実際に組み込みファイルに遭遇するドキュメントは、いくつかの特定のユースケースに集中しています。PDF/A-3(ISO 19005-3)は、アーカイブ表現の隣にソースデータをバンドルするための仕組みとして、準拠した組み込みファイルを明示的に許可しています。ZUGFeRDおよびFactur-Xの電子請求書は、まさにこの仕組みを利用して、人間が読めるPDFレイアウトの内部に構造化されたXMLペイロードを埋め込んでいます。電子メールに由来するPDFは、元のメッセージの添付ファイルを組み込みファイルツリーに転送して保持していることがあります。構造化された文書作成システムから生成された技術ドキュメントも、同様の方法でサポートアセットをバンドルしている場合があります
組織外から受信したPDFをアプリケーションで処理する場合、ドキュメントの受け入れ処理の一環としてAttachmentCountを確認する価値があります。これには2つの独立した理由があります。第一に、組み込みファイルには、請求書PDF内のXMLのように、抽出して処理したいデータが含まれている可能性があるためです。第二に、組み込みファイルには任意の実行可能なコンテンツが含まれている可能性があるため、抽出するつもりがなくても、何が存在するかを知ることは重要です。どちらの理由においても、複雑な処理を行う必要はありません。数を確認し、名前をチェックし、バイトデータをどのように処理するかを決定するだけです
ここに記載されている添付ファイル用のプロパティは、DelphiおよびC++Builder用のPDFium Componentに含まれています