技術文章

在 Delphi 中驗證 PDF/raster 掃描文件

PDF/R,標準化為 ISO 23504-1,是針對掃描文件的 PDF 設定檔:每一頁恰好帶著一張條帶影像,其他什麼也沒有。PDFium Component 透過 ValidatePdfRCompliance 從 Delphi、Lazarus 與 C++Builder 對它進行驗證,該函式讀取一個串流,並回傳一致性等級加上一組具體問題

這個設定檔存在的原因,是掃描器與文件擷取系統需要一個比 PDF/A 更窄的目標。一份封存 PDF 可以包含該部分允許的任何東西;一份光柵 PDF 則刻意貧乏,於是任何符合規範的閱讀器都能以完全相同的方式顯示它,而任何符合規範的寫入器都能從一份掃描產出它,而不需要撰寫引擎

PDFium Component for Delphi 的 PDF/R 驗證地圖:ValidatePdfRCompliance 把串流讀進 TPdfRValidationResult,其議題集合涵蓋 pvriFontForbidden 與 pvriBadMediaBox 等頁面規則、ISO 23504-1 影像過濾器白名單,以及 pvriObjStmPresent 等容器規則
一次呼叫回傳一致性等級加一組具體問題;檢查分成頁面規則、四種篩選器的影像白名單,以及在讀取任何頁面之前就觸發的容器規則

PDF/R 禁止了哪些 PDF/A 允許的東西?

實務上,是文字。一個光柵頁面承載掃描影像,其他什麼也沒有,所以頁面上的一個字型資源就是違規——在 ISO 23504-1 §6.5.2 之下被回報為 pvriFontForbidden。這會讓那些為了可搜尋性而加入不可見 OCR 文字層的人感到意外,這在 PDF/A 工作流程中是正常且有用的事,但單純不是 PDF/R

頁面與影像的關係同樣嚴格。§6.5.1 規定每一頁恰好是一張條帶影像,所以當影像數與頁數不符時,pvriPageImageMismatch 會觸發——沒有影像的頁面和有兩張影像的頁面兩者都不符合規範。而 pvriBadMediaBox 則回報 MediaBox 不具有 [0 0 w h] 形式的頁面(§6.5.3),因為一份掃描沒有理由坐落在偏移的原點上

uses FPdfPdfr;

var
  Src: TFileStream;
  Res: TPdfRValidationResult;
begin
  Src := TFileStream.Create('scan-batch-0142.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfRCompliance(Src);
    if Res.IsCompliant then
      Memo1.Lines.Add('PDF/R-1 conformant')
    else
    begin
      if pvriFontForbidden in Res.Issues then
        Memo1.Lines.Add('A page names a font resource; a raster page carries no text');
      if pvriPageImageMismatch in Res.Issues then
        Memo1.Lines.Add('Image count does not match page count');
      if pvriForbiddenImageFilter in Res.Issues then
        Memo1.Lines.Add('A strip image uses an encoding outside the white list');
    end;
  finally
    Src.Free;
  end;
end;

哪些影像編碼被允許

四種,而這份白名單之所以短,是有理由的。§6.6 承認 /CCITTFaxDecode、/DCTDecode、/JPXDecode 與 /FlateDecode——二值傳真、JPEG、JPEG 2000 與無失真 deflate,這四者合起來涵蓋了所有重要的掃描器輸出。其他一切都被回報為 pvriForbiddenImageFilter,包含 /LZWDecode、/RunLengthDecode、/ASCII85Decode、/ASCIIHexDecode、/JBIG2Decode 與 /Crypt

這些拒絕之中,有兩個值得去理解而不是死記。/JBIG2Decode 對二值掃描的壓縮極為有效,而且在 PDF/A 中完全合法,但它的符號字典重建會替換成視覺上相似的字形——這是掃描數字的一個有紀錄的失敗模式——而一個整個目的就是忠實光柵重現的設定檔,不能容許這種風險。ASCII 篩選器被排除則是相反的原因:它們讓檔案膨脹,卻沒有增添任何光柵設定檔所需要的東西

Delphi 驗證的 PDF/R 影像過濾器白名單:CCITTFaxDecode、DCTDecode、JPXDecode 與 FlateDecode 可用於 strip 影像;LZW、RunLength、ASCII85、ASCIIHex、JBIG2 與 Crypt 過濾器則觸發 pvriForbiddenImageFilter
ISO 23504-1 只認四種影像編碼;兩種值得玩味的拒收各有理由:JBIG2 會在掃描數字上替換出相似字形,ASCII 篩選器則只會把檔案越弄越大

在讀取任何頁面之前就觸發的結構規則

PDF/R 也約束容器。pvriObjStmPresent 會回報一個 /Type /ObjStm 串流,設定檔完全禁止它——物件串流會使一個光柵閱讀器本應能執行的簡單循序剖析變得複雜。pvriBadHeader 回報 %PDF-1.4 到 1.7 與 %PDF-2.0 之外的標頭,而 pvriEncryptVersionMismatch 則依 §6.2.3,回報一份標頭不是 %PDF-2.0 的加密檔案

Catalog 與 Info 字典是白名單制,而不僅僅是被檢查。pvriProhibitedCatalogEntry 與 pvriProhibitedInfoEntry 會在項目落在允許集合之外時觸發,而 pvriInfoXmpMismatch 會在 Info 項目與其 XMP 對應項不一致時觸發。一份缺少 Catalog /Metadata 串流、缺少 trailer /ID,以及缺少 %PDF-raster-1.0 頁尾標記,也各有自己的問題項目

為什麼儲存選項記錄省略了 Title 與 Author

TPdfRSaveOptions 帶有 Creator、Producer、CreationDate、ModDate、DocumentId 與 InstanceId,而且刻意沒有 Title、Author、Subject 或 Keywords 的欄位。這四個是 §6.4.3 所禁止的項目,所以一份公開了它們的記錄,等於是在邀請呼叫端透過一個符合規範的 API 寫出不符合規範的檔案

兩個布林選項控制轉換既有 PDF 時的清理動作。StripInfoOptionalEntries 預設為 True,會從來源 Info 字典移除 Title、Author、Subject、Keywords 與 Trapped。StripCatalogOptionalEntries 同樣預設為 True,會移除 Names、Outlines、StructTreeRoot、OutputIntents、Lang 等項目,只留下 §6.3 的白名單。把任一項設為 False,你會保留這些項目——並失去一致性,而這偶爾正是呼叫端對內部檔案真正想要的

var
  Opts: TPdfRSaveOptions;
  Src, Dest: TFileStream;
begin
  Opts := TPdfRSaveOptions.Default;
  Opts.Creator := 'Capture Station 4';
  Opts.Producer := 'PDFium Component';
  Src := TFileStream.Create('scan-in.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Dest := TFileStream.Create('scan-pdfr.pdf', fmCreate);
    try
      InjectPdfRMarkers(Src, Dest, Opts);   // 標記 + 中繼資料,不處理頁面內容
    finally
      Dest.Free;
    end;
  finally
    Src.Free;
  end;
end;

請注意標記注入不做的事:它增添中繼資料與識別,而無法供應頁面內容。一份帶有未承載條帶影像的來源頁面,在注入後仍然會在 pvriPageImageMismatch 上失敗,因為缺少的影像從來就不是中繼資料問題

PDF/R 在擷取管線中的定位

在交付物就是掃描本身、而保真度就是整份合約的情況下使用它——證據影像、支票與匯款擷取、來自大尺寸掃描器的工程圖面封存。一旦文件需要可搜尋的文字、標記、嵌入附件或任何其他光柵設定檔剝除的東西,就改用 PDF/A

一種常見且可行的安排是兩者都產出:一份永不改變的 PDF/R 原件,以及一份帶有 OCR 層以供檢索的 PDF/A 衍生檔。兩個驗證器各自獨立,所以同一項批次工作可以根據每份產物實際宣稱的設定檔來檢查它。關於這一對裡的封存側,請見 PDF/A 封存合規與 PDF/A 預檢驗證的筆記;而對於面向列印的輸出,請見 驗證可列印 PDF/X 文件的逐步解說

PDFium Component 將 PDFium 引擎帶進 Delphi、C++Builder 與 Lazarus,提供一套 VCL API,以及針對 PDF/A、PDF/X、PDF/E、PDF/UA 與 PDF/R 的一致性驗證器——支援的標準與 IDE 版本請見 PDFium Component 產品頁