檔案在您的機器上乾淨地開啟了。Acrobat 顯示了它,列印預覽看起來正確,每一頁都在那裡。然後它被送到印刷局,或者進入攝取您每月批次的封存系統中,接著它被退回拒絕了:CMYK 作業中出現 RGB 圖片、沒有 /Trapped 鍵、與印刷機不符的輸出意圖。任何人都能看到的檔案沒有任何問題。它錯在與設定檔(profile)不符,而設定檔在您不在的地方被檢查了。預檢(Preflight)是印前作業對該檢查的稱呼,而真正的問題是,當 PDF 是從您自己的 Delphi 程式碼產出,而不是設計師的桌面產出時,它應該放在哪裡
HotPDF 沒有為您提供可呼叫的預檢函式。該元件在其 GUI 展示中帶有一個預檢報告視窗,但其背後沒有服務或建置腳本可以呼叫的 API,假裝它有只會讓您去尋找一個不存在的方法。這聽起來像是一個漏洞,直到您注意到,對於您自己產生的檔案,對自己的輸出呼叫驗證器無論如何都是錯誤的形式。您已經控制了驗證器會檢查的每個屬性。有用的分工是讓產生器無法發出不良檔案,然後用一個您沒有編寫的工具來證明它
為什麼您用不同的方式檢查自己的輸出
傳統的預檢假設是一個陌生人的檔案。某個設計師、某個其他應用程式、某個未知的編輯鏈產生了它,而您檢查它因為您不知道裡面有什麼。您的程式碼產生的檔案不是陌生人。字型嵌入、色彩空間、輸出意圖、中繼資料區塊:您的程式在檔案寫入磁碟的前幾毫秒就決定了這一切。事後檢查它以發現您剛做出的選擇是無用功。更廉價的舉動是限制這些選擇,這樣不合規的檔案就永遠不會存在而被抓到
保持驗證外部化還有一個信譽上的原因。一個保佑自己輸出的函式庫是在批改自己的考卷。當客戶的封存系統或印刷廠的 RIP 拒絕您的檔案時,「我們的元件說沒問題」毫無份量。來自 veraPDF 或 Acrobat 的裁決則有,因為對方也執行相同的工具
讓合規性成為一個設定,而不是一個檢查清單
預防層只是配置。在 BeginDoc 之前設定 PDFACompliance 或 PDFXCompliance,HotPDF 就會在整個產生過程中保持相應的規則:它會嵌入字型、根據您宣告的輸出意圖監視 DeviceRGB 和 DeviceCMYK 的使用情況,並拒絕設定檔禁止的功能。矛盾會在 EndDoc 浮出水面,合規性閘門會在那裡升起,而不是悄悄地發布某些會在下游失敗的東西。檔案儲存後,相同的屬性會讀回實際強制執行的內容,這是您的管道日誌最需要的一個事實:
// After EndDoc: record the enforced profiles with the run metadata
if Pdf.PDFACompliance <> '' then
Log('Generated as PDF/A level ' + Pdf.PDFACompliance);
if Pdf.PDFXCompliance <> '' then
Log('Generated as PDF/X profile ' + Pdf.PDFXCompliance);
將這些旗標與輸入資料雜湊(hash)和 HotPDF 版本放在同一日誌行。當驗證器和您的產生器對某個檔案有分歧的那一天,這一行會告訴您是哪個範本產生了它,以及載入了哪個版本的函式庫,否則會吃掉一個下午的爭論就變成了一個 grep 指令。這些旗標背後的輸出意圖、ICC 設定檔和標記在 使用 HotPDF 進行 PDF/A、PDF/X 和 PDF/UA 輸出指南 中有詳細說明
對於非您產生的檔案一個廉價的第一道閘門
並非每個管道都是純粹生成的。客戶上傳 PDF,掃描器將它們放入資料夾中,合作夥伴將它們附加在電子郵件中。將每一個都推過完整的結構驗證器會浪費佇列時間在甚至無法開啟的檔案上。HotPDF 的直接檔案 API(Direct File API)讀取了足夠的檔案結構來回答「這究竟是不是一個可用的 PDF」,而不需要載入整個物件樹,這使它成為一個快速失敗的好地方:
function TriagePdf(Pdf: THotPDF; const FileName: string): Boolean;
var
Handle, Pages: Integer;
begin
Result := False;
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit; // structurally unreadable: quarantine, do not validate
try
Pages := Pdf.DAGetPageCount(Handle);
Result := Pages > 0;
finally
Pdf.DACloseFile(Handle);
end;
end;
關於這個 API 的兩個事實決定了您如何包裝它。扁平記憶體(flat-memory)捷徑僅適用於未加密的輸入;交給 DAOpenFileReadOnly 一個密碼,它會悄悄退回到完整解析,因此您知道已加密的檔案應該在分類(triage)之前透過 DecryptFile 進入純文字工作副本。而且 DAGetPageCount 對於未乾淨開啟的控制代碼(handle)沒有任何意義,因此控制代碼檢查保持嚴格,非正結果即為拒絕,而非重試。更多這樣的模式存在於 大型 PDF 工作流程的直接檔案 API 文章 中
veraPDF,作為建置的一部分執行
對於任何您聲稱為 PDF/A 或 PDF/UA 的東西,veraPDF 是要接入的驗證器。它無頭(headless)執行,接受一個批次,發出 XML 或 JSON,並以其 ISO 條款命名每個失敗,因此針對 ISO 19005-1 第 6.2.2 條的規則失敗會直接指向產生器設定,而不是讓您去猜測。從 Delphi 驅動它就是純粹的程序控制:
function RunVeraPdf(const PdfFile, ReportFile: string): Cardinal;
var
Cmd: string;
SI: TStartupInfo;
PI: TProcessInformation;
begin
Cmd := Format('cmd /c verapdf.bat --format xml "%s" > "%s"',
[PdfFile, ReportFile]);
FillChar(SI, SizeOf(SI), 0);
SI.cb := SizeOf(SI);
if not CreateProcess(nil, PChar(Cmd), nil, nil, False,
CREATE_NO_WINDOW, nil, nil, SI, PI) then
RaiseLastOSError;
try
WaitForSingleObject(PI.hProcess, 120000); // bound the wait per file
GetExitCodeProcess(PI.hProcess, Result);
finally
CloseHandle(PI.hThread);
CloseHandle(PI.hProcess);
end;
end;
那個逾時(timeout)贏得了它的價值。格式錯誤的檔案可以把任何解析器逼到永遠出不來的角落,而佇列工作程序(queue worker)內無限制的等待會把佇列的其餘部分拖垮。限制等待時間,給予逾時自己的失敗代碼,並將檔案放在一旁交由人工處理。當您讀取結果時,請解析 XML 中的規則識別碼,而不是人類可讀的文字。規則 ID 能在驗證器升級中存活下來;訊息的措辭則不能,而穩定的代碼是支援工程師可以在舊工單(ticket)中搜尋的東西
您如何執行批次與每個檔案是否通過一樣重要。每個檔案一個處理程序,而不是每個批次一個,這樣有毒的輸入只會花費您該檔案的逾時時間,沒有別的。將驗證器處理程序的數量限制在核心數量(core count),因為建置 XML 報告受限於 CPU(CPU-bound),而超額訂閱(oversubscription)只會造成系統顛簸(thrashing)。並在攝取時設定一個大小上限,因為一本 2 GB 的掃描書會佔據整個佇列,無論解析器多麼有耐心。嚴格來說,這些都不是預檢。這是一個能熬過月底大量資料的閘門,與一個在凌晨兩點癱瘓管道後第一晚就被關閉的閘門之間的區別
這就是 PDF/X 不足的地方。veraPDF 不驗證它,所以實際有效的檢查仍然是 Acrobat 的 Preflight 加上您印表機命名的 ISO 15930 設定檔。Acrobat 需要人類操作,這意味著抽樣而不是全面涵蓋:來自新範本的第一個檔案,加上從每個批次中隨機抽取的一小部分,而自動化閘門處理不需要人類即可處理的所有事情。一個實際執行的抽樣檢查,勝過一個永遠處於半完成狀態的完全自動化
一份您在一年後仍然會想要的報告
預檢閘門會獲得兩次報償。一次是在門口攔截不良檔案時,另一次是更久之後,當有人問為什麼特定檔案被放行時。第二個時刻應該決定格式,因為那是一個內容單薄的報告會讓您陷入困境的時刻。對於檢查的每個檔案,保留輸入雜湊、上面日誌行中的產生器合規性旗標和函式庫版本、驗證器名稱和版本、檢查所對照的設定檔、通過或失敗,以及無論驗證器在哪裡給出的帶有頁碼的失敗規則 ID。將該報告儲存在它所描述的檔案旁邊。將它放入一個獨立的系統中,該系統會在它所記錄的封存檔之前被退役
例外情況也需要寫下來。當客戶堅持要發布一個閘門不喜歡的檔案時,答案不是為所有人放寬規則。記錄是誰批准了這個檔案、基於什麼理由、直到什麼日期,然後將該豁免(waiver)附加到其報告中。帶有名字和有效期限的豁免是某人擁有的決定。被「暫時」註解掉的檢查則是一個等待其日期到來的事件
還有一個習慣能帶來回報:當檔案失敗時,在任何人觸碰它之前,將其複製到名為迴歸(regression)的資料夾中。幾乎每一個值得除錯的預檢問題都能追溯到一個特定的輸入,而保留這些輸入的團隊會在一小時內修復重現的問題,而不是等待它在生產環境中再次浮出水面。這裡顯示的合規屬性和直接檔案 API 是 Delphi 和 C++Builder 適用的 HotPDF Component 的一部分,其檔案完整涵蓋了每個呼叫