PDF 檔案附件儲存在文件的內嵌檔案樹中,大多數檢視器會將此結構呈現為迴紋針面板或附件側邊列;在 Delphi 程式碼中,PDFium 元件透過 TPdf 上的一組索引屬性來公開該樹狀結構:您可以使用整數索引進行反覆檢視、讀取名稱和位元組資料、建立新位置,以及刪除現有位置;該 API 介面非常窄,在圍繞它編寫生產環境程式碼之前,只需了解幾個順序限制和一個淨化規則即可
從開啟的文件中讀取附件
AttachmentCount 提供文件宣告的內嵌檔案數量;它直接從 PDFium 的底層呼叫中讀取,因此僅反映 PDF 實際包含的內容;從該處開始,AttachmentName[Index] 會將顯示名稱作為 WString 傳回,而 Attachment[Index] 則以 TBytes 陣列形式傳遞原始位元組;兩者皆以 0 為基準;在查詢這兩個屬性之前,文件必須處於開啟狀態(Pdf.Active = True),在關閉的文件上呼叫它們會傳回零或空的結果,且不會引發異常
需要記住的一點是:每次讀取 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;
將附件擷取至磁碟
沒有 SaveAttachment 輔助程式;您讀取位元組並將其寫入到您需要的任何地方,這使路徑建置和淨化完全由您的程式碼負責;當附件名稱來自不受信任的文件時,這點非常重要;PDF 附件名稱是儲存在檔案內部的字串,它們可能包含路徑分隔符號、Unicode 相似字元以及其他字元,如果您直接將它們傳遞給 TFileStream.Create,將會產生非預期的結果;在建置逆向輸出路徑之前,務必先透過 ExtractFileName 處理名稱,並考慮拒絕以點開頭或包含您系統預期之外之字元的名稱
由 Attachment[Index] 傳回的位元組陣列是由呼叫者擁有的;使用一般的 TFileStream 將其寫出,您就可以隨心所欲地處理它,包含檢查前幾個位元組以驗證實際的檔案格式,而不是盲目信任宣告的名稱
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;
新增附件與兩步驟寫入
建立附件需要兩個呼叫,而不是一個;CreateAttachment(Name) 在內嵌檔案樹中註冊一個新位置,並在成功時傳回 True;該位置一開始是空的;然後您透過寫入 Attachment[AttachmentCount - 1] 來指派資料,針對最近建立的項目;如果 CreateAttachment 傳回 False,則該位置未建立,指派將會損壞剛好是最後一個索引處的附件
修改附件清單後,變更僅存在於記憶體中;呼叫 SaveAs 以寫入帶有更新後內嵌檔案樹的新檔案;PDFium 元件目前不支援儲存回當前開啟的同一個檔案中,因為引擎持有對該來源檔案的讀取控制代碼;就地更新的標準模式是儲存到暫存路徑、關閉文件、刪除或重新命名原始檔案,然後將暫存檔案重新命名到該位置並重新開啟
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;
附件類型資訊
除了名稱和位元組資料之外,如果在檔案最初附加時有記錄 MIME 類型字串,AttachmentType[Index] 會將其傳回;許多產生器會將此欄位留空,或將其設定為通用值(例如 application/octet-stream),因此在生產流程中,您無法依賴它進行格式偵測;為了可靠的識別,請讀取資料的前幾個位元組並檢查已知的檔案簽章:%PDF 用於巢狀 PDF,ZIP 本地檔案標頭 PK\x03\x04 用於 Office Open XML 文件,\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 作為文件點收的一環,是非常值得做的,這有兩個獨立的原因;第一,內嵌檔案可能包含您想要擷取和處理的資料,例如發票 PDF 內部的 XML;第二,內嵌檔案可能包含任意的可執行內容,因此即使您從不打算擷取它,了解其中存在什麼也很重要;這兩個原因都不需要您執行任何複雜的操作:讀取計數、檢查名稱,然後決定如何處理這些位元組
此處顯示的附件屬性是適用於 Delphi 和 C++Builder 的 PDFium 元件 的一部分