技術文章

用 PDFlibPas 在 Delphi 中產出 PDF/E-1 工程文件

PDF/E-1 是給工程文件用的封存設定檔,PDFlibPas 把它實作成一個用 SetPDFEMode 打開的作者模式,加上一段有邊界限制、逐運算子讀過內容串流的 preflight。這個設定檔不是換張標籤的 PDF/A:它有自己的識別命名空間、自己的生命週期詮釋資料要求,還有一條讓內容驗證比您遇過任何封存設定檔都嚴的規則

工程交付物是這個設定檔存在的理由。一套圖集要能在二十年後依然可讀、而且可證明未被更動,修訂歷程要留得住,色彩到了另一棟建築的繪圖機上還得是同一個意思。這些需求造就了一份規格,其要求大半落在頁面內容之外,落在詮釋資料與色彩管理裡,而這恰恰是一般 PDF 產生器做錯的地方

自己的識別,不是 PDF/A 的變形

第一件要做對的事:PDF/E-1 的識別沒辦法靠改一改 PDF/A 或 PDF/X 的套路得出來。它用一個不同的 XMP 命名空間 http://www.aim.org/pdfe/ns/id/,而且版本值必須出現在兩個地方:文件資訊項目裡一處,帶命名空間限定的 XMP 屬性裡一處。只寫 XMP 屬性、或只寫資訊項目,做出來的是一個心有餘而驗證不過的檔案

輸出意圖的形狀同樣講究。PDF/E-1 要求一個帶子類型識別碼 ISO_PDFE1 的內嵌 ICC 設定檔,而且設定檔的分量數必須與文件實際使用的裝置色彩家族相符。最後這一條正是實作悄悄出錯的地方,因為它代表輸出意圖不能事先選好就丟著不管

為什麼裝置色彩需要整份文件的大掃描?

因為色彩空間會躲在頁面層掃描永遠走不到的資源字典裡。PDF/E-1 把 DeviceRGB 與 DeviceCMYK 當成一份文件裡互斥的兩個家族,所以驗證設定檔意味著知道檔案裡任何東西用到的每一個裝置色彩空間。form XObject 有自己的資源,pattern 有,image 也有。頁面裡的 form XObject 裡再嵌一個平鋪圖樣,已經深達三層,只檢查頂層頁面資源的驗證器,會放行一份兩個家族都用了的文件

所以這場掃描把頁面、form、image 與 pattern 當成單一次走訪,邊走邊登記色彩空間,然後才判斷文件是否一致、輸出意圖是否相符。同樣的道理也撐起整個 preflight 架構:走訪不完整就會產生假通過,而符合性檢查上的假通過比不檢查更糟,因為它會被記成一項證據

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('assembly-drawings.pdf');

    if Lib.SetPDFEMode(1) = 0 then
      raise Exception.Create('PDF/E author mode was refused');

    // Author 模式在每次存檔時都讓生命週期詮釋資料保持同步。
    // 存檔前先問一聲:這份文件過得了自己的關卡嗎
    if not Lib.PDFEReadyForSave then
    begin
      Diag := Lib.GetPDFEDiagnostics;
      Writeln('PDF/E blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('assembly-drawings-pdfe.pdf');
  finally
    Lib.Free;
  end;
end;

生命週期詮釋資料是每次存檔的義務

PDF/E-1 要的不只是一個文件識別碼。最低需求集合包含媒體管理文件識別碼、版本識別碼、rendition class、建立時間、修改時間、詮釋資料時間與標題。那是一套追蹤修訂的詞彙,它存在是因為工程交付物被期待會改版重發,而不是寫一次就算

對實作來說,後果是這些欄位不能在建立文件時設定一次就了事。如果修改時間在您打開模式的那一刻寫入,之後文件又繼續編輯,XMP 快照與文件實際狀態就漂移了,比對兩者的驗證器會回報一個沒任何人想要的矛盾。因此作者模式在每次存檔前一刻同步這些欄位,讓詮釋資料描述的是即將寫出的那些位元組,而不是模式打開當下存在的那批

這是符合性詮釋資料的通則,值得把它從 PDF/E 獨立出來講:衍生出來的詮釋資料屬於存檔路徑,不屬於編輯路徑。任何從文件狀態計算出來的欄位,都必須在狀態凍結的那一刻重算,否則它就是一個沒有失效機制的快取

PDFlibPas PDF/E-1 圖解:整份文件的裝置色彩掃描走過頁面、form XObject、平鋪圖樣與 image 的資源字典,收集 DeviceRGB 與 DeviceCMYK 家族後才判定一致性;旁邊是作者模式在每次存檔前一刻重新同步的生命週期詮釋資料欄位,讓 XMP 快照與即將寫出的位元組相符
色彩一致性只有在一次走訪觸及每個資源字典之後才能判定,衍生的生命週期詮釋資料則在文件狀態凍結那一刻重算,不是模式打開的時候

讓內容驗證變嚴的那條規則

PDF/E-1 不允許相容段落的運算子把不認得的內容吞掉。在普通 PDF 裡,BXEX 夾出一段區域,消費端在其中必須忽略自己不認得的運算子,這是讓產生端放心輸出較新建構、又不弄壞舊閱讀器的逃生口。在 PDF/E-1 底下這條逃生口被焊死了,所以 preflight 不認得的任何運算子都無條件回報,管它在不在相容段落裡

這對驗證器的影響很實在。它不能跳過自己看不懂的區域,代表運算元解析器必須真的把每個內容串流裡的每個運算子都解析一遍。邊界限制就是在這裡登場。走訪上限是 128 層巢狀、一百萬個物件與 64 MiB 內容,這些限制不是效能調校。一個懷有惡意、或單純壞掉的檔案,可以端出一張帶環的物件圖、或深到把遞迴驗證器變成堆疊溢位的巢狀,這些限制就是讓一次驗證不至於變成阻斷服務(DoS)管道的那道閘。同樣的防禦姿態記錄在安全解析不可信任的 PDF

// 對一個不是您產出的檔案做獨立驗證,
// 不必把它載入任何文件實例
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('incoming.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFE(Stream, '', 0, Issues) = 0 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/E: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

存檔關卡修什麼、拒什麼

關卡把工作拆成兩個階段,而這個拆法本身就是一個拿去就能用的設計點子。第一階段先正規化那些可以安全修復的東西:註釋的列印旗標、文字註釋上的禁止縮放與禁止旋轉旗標,以及表單字典上的外觀產生旗標。這些都是在設定檔底下只有一個正確值、不帶任何資訊量的設定,所以默默修掉是對的,為這個拒絕反而是刁難

第二階段檢查那些不動到文件語意就修不了的約束:版本、識別、加密、輸出意圖、裝置色彩一致性,以及動態表單內容的存在。任一項不過的文件就拒收,因為替作者憑空發明一個輸出意圖、或替他挑一個色彩家族,做出來的是一份通過驗證卻對內容言不由衷的檔案

PDFlibPas 在 Delphi 的 PDF/E-1 存檔關卡圖解:有界 preflight 在 128 層巢狀、一百萬物件與 64 MiB 上限內掃過每個內容串流運算子,默默修復註釋的列印、縮放與旋轉旗標,拒絕錯誤的版本、識別、加密、輸出意圖、裝置色彩或動態表單內容,並透過 GetPDFEDiagnostics 回報 blockers
關卡只默默修復不帶資訊量的東西,拒絕每一個修復會扭曲的約束,並在任何位元組抵達磁碟之前,透過 GetPDFEDiagnostics 把拒絕化成一張 blocker 清單

存檔前先用 GetPDFEDiagnostics 把診斷讀回來,那次拒絕就從一個失敗的操作變成一張可以動手的清單。批次管線裡,對每份文件都呼叫它,逐檔記下 blockers,把失敗的導進一個有人看的佇列。這比一個直接丟例外的存檔有用得多,因為 blockers 通常會扎堆:四十份文件因為同一個缺漏的輸出意圖不過,那是一次修復,不是四十次

在封存設定檔之間做選擇

當交付物是帶修訂生命週期的工程文件,特別是輸出要進繪圖機與大圖輸出設備、裝置色彩一致性因此要緊的時候,PDF/E-1 是對的目標。當目標是一般文件的長期可讀性,PDF/A 是對的目標,而且它是驗證器支援最廣的設定檔。兩者不可互換,一份文件可以滿足其中一個卻過不了另一個

PDFlibPas 給 Delphi 的 PDF/E-1 與 PDF/A 封存設定檔比較決策圖:PDF/E-1 面向帶修訂生命週期、繪圖機色彩與合約式驗證的工程交付物,用自己的 XMP 命名空間與 ISO_PDFE1 輸出意圖;PDF/A 面向一般長期可讀性,驗證器支援最廣
從檔案終點由誰驗證想起:兩個設定檔要求的識別、詮釋資料與色彩保證不同,一份文件可以滿足一個而過不了另一個

如果您正在選,從檔案終點由誰驗證想起。PDF/A 驗證工具到處都是,PDFlibPas 對應的 preflight 記錄在PDF/A 與 PDF/UA preflight。PDF/E 驗證更專門,通常是合約要求而非預設。當既有的封存必須被拉拔到一個它當初寫作時沒有的設定檔,轉換 PDF/A 並修復詮釋資料的詮釋資料修復路徑就是該照抄的範式,這裡也是同一個形狀:識別,修安全的,其餘連同一張清單一起拒絕

作者模式、有界內容 preflight 與獨立的符合性檢查都隨 PDFlibPas Delphi PDF library 出貨,一份文件可以在設定檔底下產出,之後再透過另一條程式路徑獨立驗證——符合性宣稱值得信賴的安排,只有這一種