技術文章

PDFium 元件在 Delphi 中的 PDF/X 驗證

Delphi 的 PDFium 元件透過 TPdf.ValidatePdfX 驗證可供列印的 PDF/X 文件。這實作了兩層的 ISO 15930 檢查:八個位元組層級的內容檢查(禁止使用的 LZW 壓縮、JavaScript、表單欄位、OPI 參照、遺漏的 TrimBox、未設定的 Trapped 鍵值等等),加上一個 PDFium 物件模型掃描,它使用 FPDFFont_GetIsEmbedded 來驗證每個頁面上每個文字物件的字型內嵌。結果會是一個 TPdfXValidationResult 記錄,指出偵測到的遵循層級,並以型別化的列舉列出每個違規事項,如此一來,您的 Delphi 應用程式就可以在製版之前,準確地告訴客戶為什麼檔案會被印刷廠退件

如果您曾將專案寄給商業印刷廠,卻收到只有一行字的退件回覆——「沒有 TrimBox」、「字型未內嵌」、「未設定 Trapped」——您就會知道太晚發現問題的代價。PDF/X 是 PDF/A 在印前作業中的對等標準:具備封存性質的 PDF/A 保證文件在幾十年後也能呈現出完全相同的畫面,而 PDF/X 則保證文件能在明天早上別人機台的 RIP(光柵影像處理器)上,以相同的方式進行分色、顯像與裁切。這兩個標準共用了一些機制(XMP 識別、OutputIntents、內嵌的 ICC 描述檔),但解決的是不同的問題,這也是為什麼該元件會為兩者提供獨立驗證器的原因——PDF/A 的部分已在使用 PDFium 元件進行 PDF/A 印前預檢驗證中討論

ISO 15930 對於可供列印的 PDF 實際上有哪些要求?

ISO 15930 的存在是為了讓盲交換(blind exchange)成為可能:設計師將檔案交給從未交談過的印刷廠,印刷廠就能產出正確的輸出,而不需要打電話、不用發送遺失字型的電子郵件,也不會有被留在設計師筆記型電腦裡的連結圖片。標準中的每一條規則都是為了這個目標服務。字型必須內嵌,因為不能假設接收端的 RIP 擁有這些字型。禁止外部參照,因為檔案本身必須是完整的。禁止互動功能,因為墨水沒有 onclick 處理常式

PDFium 元件可識別三個遵循系列,並在驗證結果中透過 TPdfXConformance 列舉回報它們:pxc1a 代表 PDF/X-1a:2001(ISO 15930-1,以 PDF 1.3/1.4 為基礎,嚴格限制僅能使用 CMYK 加上特別色的基準),pxc3 代表 PDF/X-3:2002(ISO 15930-3,允許 RGB、Lab 與受 ICC 管理的色彩),而 pxc4 代表 PDF/X-4:2010(ISO 15930-7,終於允許在 PDF 1.6 的基礎上使用即時透明度與圖層)。完全不帶有任何 PDF/X 識別的檔案會回傳為 pxcNone,這本身也是一個有用的答案:該文件從未聲明可供列印,而驗證器回報的其他所有資訊都解釋了要達到這個目標需要做些什麼

只要用 RIP 供應商的角度思考,這些禁止事項就合情合理了。在每一種 PDF/X 變體中都禁止使用 /LZWDecode,如此一來符合規範的使用端就永遠不需要依賴一個有著相容性與授權包袱的過濾器;Flate 能在沒有這些包袱的情況下完成同樣的工作。禁止 JavaScript、AcroForm 欄位以及 /AA 額外動作字典,因為列印檔案必須是對紙張標記的固定描述——任何能在開啟時改變外觀的東西,都會打破「打樣所見即為列印所得」的保證。禁止 OPI(開放式印前介面)佔位符號,因為它們在設計上就是對儲存在其他地方的高解析度圖片的參照,而「其他地方」正是盲交換所禁止的

為什麼印刷廠會拒絕沒有 TrimBox 的 PDF?

TrimBox(裁切框)就是完成的頁面——在裁刀切下後保留下來的矩形。每個 PDF 頁面都有的 MediaBox,指的僅僅是紙張:它包含了出血、裁切標記、對位標記和導表。落版軟體是透過 TrimBox 將頁面定位在印張上;如果沒有它,操作人員就必須猜測您的名片實際邊界在哪裡,而猜錯可能會裁掉您的出血,或是讓某個邊緣留下一條白邊。這就是為什麼 ISO 15930 要求每個頁面都要有 TrimBox(或 ArtBox),以及為什麼當文件的任何頁面上找不到 /TrimBox 鍵值時,ValidatePdfX 會引發 pvxiMissingTrimBox 的原因

/Trapped 鍵值回答了另一個生產問題。補漏白(Trapping)是一種稍微重疊相鄰色彩的印前技術,目的是確保微小的印刷對位誤差不會在它們之間產生白色縫隙。印刷廠需要知道這項工作是否已經完成:對已經補漏白的檔案進行補漏白會使重疊部分加倍,而跳過對未補漏白檔案的處理則有出現明顯縫隙的風險。因此,PDF/X 要求資訊(Info)字典明確聲明 /Trapped /True/Trapped /False——遺漏該鍵值或為 /Unknown 會迫使人員檢查檔案,這正是盲交換意圖消除的對話。元件會將此標記為 pvxiTrappedNotSet

使用 TPdf.ValidatePdfX 執行兩層驗證

TPdf.ValidatePdfX 不接受任何引數,並回傳一個 TPdfXValidationResult 記錄,其中包含三個成員:Conformance(偵測到的 PDF/X 風格)、Issues(一個包含 TPdfXValidationIssue 值的 Pascal 集合),以及一個 IsCompliant 輔助屬性。在內部,它會將載入的文件序列化到一個記憶體串流,在其上執行位元組層級的檢查器,然後走訪 PDFium 物件模型來進行每一字型的內嵌檢查。一個最基礎的預檢閘門如下所示:

uses PDFium, FPdfPdfx;

procedure CheckPrintReadiness(const FileName: string);
var
  Pdf: TPdf;
  Res: TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;

    Res := Pdf.ValidatePdfX;
    Writeln('Detected conformance: ',
      Ord(Res.Conformance)); // pxc1a, pxc3, pxc4, pxcNone...

    if Res.IsCompliant then
      Writeln('PDF/X checks passed')
    else
    begin
      if pvxiMissingTrimBox in Res.Issues then
        Writeln('REJECT: no /TrimBox on the pages');
      if pvxiTrappedNotSet in Res.Issues then
        Writeln('REJECT: /Trapped missing or /Unknown');
      if pvxiPdfiumFontNotEmbedded in Res.Issues then
        Writeln('REJECT: a page uses a non-embedded font');
      if pvxiLzwForbidden in Res.Issues then
        Writeln('REJECT: LZWDecode filter present');
    end;
  finally
    Pdf.Free;
  end;
end;

因為 Issues 是一個普通的 Pascal 集合,您可以根據工作流程的需要任意分割它——將結構性問題視為硬性退件,將 pvxiMissingTitle(在標準中是 SHOULD 而非 MUST)視為警告,並將其餘事項記錄下來。這個相同的記錄型別也提供了元件的報告產生器所需的資料,所以如果您偏好輸出人類可讀的文件而不是針對列舉進行分支,那麼在使用 PDFium 元件建置批次預檢報告 CLI 中的模式,也能直接套用到 PDF/X 上

位元組層級抓出了什麼——又漏掉了什麼

位元組層級是指對文件的結構位元組進行的代碼(token)掃描,其中串流主體會被清空,因此碰巧包含 /JavaScript 位元組模式的 JPEG 圖片就不會觸發誤判。除了標記檢查(XMP pdfxid:GTS_PDFXVersion、帶有內嵌 ICC 描述檔的 OutputIntent、檔案結尾 /ID、加密禁令)之外,內容掃描還增加了八項檢查,每項檢查都有自己的列舉值:

  • pvxiLzwForbidden — 檔案中任何地方出現了 /LZWDecode 過濾器(禁止在所有 PDF/X 變體中使用)
  • pvxiJavaScriptForbidden — 存在 /JavaScript 動作或名稱樹
  • pvxiFormFieldsForbidden — 存在 /AcroForm 字典或 /XFA 項目
  • pvxiAdditionalActions — 存在 /AA 額外動作字典
  • pvxiEmbeddedFilesForbidden — 存在 /EmbeddedFiles/FileAttachment 註解
  • pvxiOpiForbidden — 有一個 /OPI/Alternates 項目參照了可替換的圖片內容
  • pvxiMissingTrimBox — 在任何頁面上都找不到 /TrimBox
  • pvxiTrappedNotSet/Trapped 遺失或被設定為 /Unknown

位元組掃描速度快且不需要繪圖引擎,但它在字型方面有一個固有的盲點:在那個層級,檢查器只能套用一種粗略的啟發式方法——當它完全找不到內嵌的字型程式碼時,就會標記該文件。一個內嵌了九種字型卻混入一種系統字型的檔案,在位元組掃描看來並無異常。這個漏洞就是第二層存在的理由

透過 PDFium 物件模型進行每一字型的內嵌檢查

PDFium 元件的物件模型層精準地回答了字型的問題。在位元組層級的掃描之後,TPdf.ValidatePdfX 會疊代每一頁,向 FPDFPage_CountObjects 查詢物件清單,並針對每個文字物件透過 FPDFTextObj_GetFont 解析字型控制代碼,然後查詢 FPDFFont_GetIsEmbedded。文件中任何地方只要有一個未內嵌的字型,就會將 pvxiPdfiumFontNotEmbedded 加入到問題集合中。這種走訪會在兩個層級上短路(short-circuit)——在確認問題的當下,它會停止掃描頁面上的物件,並停止載入更多頁面——因此在一個違規的 300 頁型錄上,判決往往在第一頁之後就出爐了

有兩個值得注意的邊界事項。首先,這一層需要載入 PDFium 函式庫,並需要能匯出 FPDFFont_GetIsEmbedded 的建置版本;當缺少這個匯出函式時,這項檢查會被跳過而不是判定失敗,因此較舊的 DLL 永遠不會產生虛假的退件。其次,這項檢查只回答「是否已內嵌」而已——它並不區分完全內嵌還是子集化,也不會檢查字符(glyph)涵蓋範圍。當檔案驗證失敗而您需要知道是哪一頁上的哪種字型時,在在 Delphi 中使用 PDFium 分析 PDF 字型屬性中的列舉技巧,能完美接續驗證器布林值所留下的工作

無需載入文件或 DLL 即可驗證串流

位元組層級檢查器也以獨立函式的形式公開在 FPdfPdfx 單元中,也就是 ValidatePdfXCompliance(Source: TStream)。這是純 Object Pascal 實作,不依賴於 PDFium DLL。這讓它能夠部署在不歡迎繪圖引擎的地方:網頁伺服器上的輕量級上傳閘門、審查生成圖稿的 CI 作業,或是位於您寧可不附帶原生二進位檔的平台上的 Lazarus 服務。將任何可定址(seekable)的串流餵給它即可:

uses Classes, FPdfPdfx;

function QuickPdfXGate(const FileName: string): Boolean;
var
  Fs: TFileStream;
  Res: TPdfXValidationResult;
begin
  Fs := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfXCompliance(Fs);
    Result := Res.IsCompliant and (Res.Conformance <> pxcNone);
  finally
    Fs.Free;
  end;
end;

這當中的取捨非常明確:這個獨立的途徑會執行標記檢查以及全部八項內容檢查,但不會執行每一字型的 PDFium 檢查層,因此其字型判決會退回到粗略的啟發式方法。一個明智的架構會將 ValidatePdfXCompliance 視為成本低廉的第一道閘門,並為那些通過了這道閘門的檔案保留完整的 TPdf.ValidatePdfX 檢查

這個驗證器的極限,以及完整預檢的起點

誠實對於預檢工具非常重要,所以這裡必須說明其界線。ValidatePdfX 會驗證識別標記、結構禁令、頁面幾何鍵值、Trapped 宣告,以及深入到個別文字物件的字型內嵌。它不會測量總油墨覆蓋率、驗證宣告變體中每個色彩空間是否合法(例如 X-1a 僅限 CMYK 的規則)、檢查圖片解析度與線數的對應關係,也不會評估疊印和透明度平面化行為——那些需要具備色彩管理功能的預檢引擎,而單元本身的文件也說明應將其與預檢引擎搭配使用,以進行最終認證。這項兩層檢查能帶給您的是,在毫秒之間於您自己的 Delphi 程式碼中,及早攔截到佔退件原因 80% 且能被提早偵測的結構性問題,而不是在明天從印刷廠收到的電子郵件裡才發現

包含兩個驗證層、用於產出合規輸出的 PDF/X 標記注入 API,以及共用相同架構的 PDF/A、PDF/UA、PDF/E 與 PDF/VT 驗證器,全都包含在適用於 Delphi 與 C++Builder 的 PDFium 元件中——只需單一元件,從繪圖渲染到印前守門一網打盡