技術文章

HotPDF PDF/X-1a 色彩檢查:揪出 RGB 內容

把 PDFXCompliance 設成 'X-1a' 之後,HotPDF 在整個工作的最末端驗證內容色彩:EndDoc 會呼叫 THotPDF.EnsurePDFX1aContentColorSpaces,拒絕任何頁面上出現的 DeviceRGB、CalRGB 或 ICCBased RGB 內容,並擲出指名問題資源的例外;檔案絕不會以一份會被印刷廠退件的 PDF/X-1a 文件形式被寫出

這最後半句才是重點。帶著一個走丟的 RGB logo 的 PDF/X-1a 檔,在螢幕上看起來毫無異狀,草草審查也會過關,然後在開印前兩天卡在印刷廠的預檢;在 Delphi 建置階段就攔下它,而且訊息告訴你哪一頁哪張影像,比從不爽的印前操作員口中得知便宜太多了

PDF/X-1a 為什麼拒絕 RGB 內容?

PDF/X-1a 拒絕 RGB,因為這個格式承諾每個顏色都已經為某個特定 CMYK 列印條件定案。ISO 15930-1:2001 §6.2.3 把內容色彩限於 DeviceCMYK、DeviceGray 與特別色——即 alternate 為 CMYK 或 Gray 的 Separation 與 DeviceN 空間。內嵌在檔案裡的 GTS_PDFX OutputIntent 指名那個列印條件(例如 FOGRA39 coated offset),收件工作流程有權把數值直接送上版,不做任何色彩轉換

RGB 破壞了這份合約。RGB 值描述的是相對於某個顯示器或工作空間的顏色,不是一張油墨配方,下游總得有人決定怎麼分色,而 X-1a 的存在恰恰就是要把這個決定權從收件端拿走。ISO 32000-1 §8.6 把 PDF 色彩空間分成 device、CIE-based 與 special 三族,RGB 成員出現在不止一族:DeviceRGB 是 device 空間,CalRGB 與三分量 ICCBased 是 CIE-based,Indexed 或 Separation 空間還能在底下藏 RGB base 或 alternate;只找 rg 運算子的檢查器會漏掉大多數。較晚的 PDF/X-1a:2003 版(ISO 15930-4)保留同一條色彩規則;注意 HotPDF 的 'X-1a' 值在 XMP packet 裡寫的是 PDF/X-1a:2001 識別

如果你的內容真的是 RGB 而且應該保持 RGB,X-1a 就是錯的目標。HotPDF 把這道檢查限定在 'X-1a',所以容許 ICC-based 與校準色彩的 'X-3' 和 'X-4' 不受影響

EnsurePDFX1aContentColorSpaces 實際檢查什麼

EnsurePDFX1aContentColorSpaces 把繪製期旗標與對每頁資源的遞迴巡檢結合起來,在 PDF/X 關卡中殿後執行。EndDoc 先強制一般 PDF/X 要求:Title 必須設定、Trapped 必須是 'True'、'False' 或 'Unknown'、必須存在 GTS_PDFX OutputIntent(EnsurePDFXOutputIntent)、每頁需要 TrimBox 或 ArtBox(EnsurePDFXPageBoxes);這些全過了,色彩檢查才登場

Delphi HotPDF EndDoc 內 PDF/X 關卡的執行順序:先查 metadata、OutputIntent 與頁面框,再做 RGB 內容色彩檢查
色彩檢查在 X-1a 關卡中最後執行,繪製期旗標代表合規指派之前畫的 RGB 照樣被抓到

第一條路徑是全文件旗標。HotPDF 的 RGB 顏色 setter、點陣影像內嵌與附註顏色條目在你繪製時記錄 DeviceRGB 用量,涵蓋從不出現在資源字典裡的行內 rg 與 RG 運算子。第二條路徑巡覽每頁的 /Resources 並檢查:

  • /ColorSpace 裡的具名色彩空間,包括行內影像縮寫 RGB(DeviceRGB 的同義詞)
  • 每個 Image XObject 的 /ColorSpace
  • Form XObject 與 tiling Pattern,遞迴進它們自己的巢狀 /Resources
  • Shading Pattern 與頁面 /Shading 字典裡的條目,經由 shading 的 /ColorSpace
  • Indexed base 空間、Separation 與 DeviceN 的 alternate、Pattern base 空間,一路追到巢狀深度 8

被拒絕的有三樣:DeviceRGB、CalRGB,以及 profile 串流宣告 N = 3 的 ICCBased 空間。Form XObject 與 pattern 共用的資源字典只拜訪一次,互相引用的一組 form 不會把巡檢帶進死迴圈。這個方法在 THotPDF 上是公開的,但正常使用中你永遠不必自己呼叫,因為 EndDoc 會代勞

RGB 在 PDF 頁面資源裡的藏身處:巢狀 form、pattern、shading 與 alternate 會把 Delphi HotPDF 的巡檢一路帶到 DeviceRGB、CalRGB 或 ICCBased RGB
巡檢沿巢狀資源追到深度 8,DeviceRGB、CalRGB 與 N = 3 的 ICCBased 空間無論在哪裡冒頭都被拒絕

在 Delphi 畫出能通過檢查的 X-1a 頁面

合規的 HotPDF X-1a 文件需要 CMYK OutputIntent、每頁 TrimBox,以及只透過 CMYK、Gray 與特別色 API 設定的顏色。下面的骨架用 FOGRA39 描述檔;AddPDFXOutputIntent 收輸出條件識別碼、資訊字串、ICC profile 串流、分量數與 alternate 空間

var
  Pdf: THotPDF;
  Profile: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure-x1a.pdf';
    Pdf.Title := 'Spring brochure, press edition';
    Pdf.Trapped := 'False';
    Pdf.PDFXCompliance := 'X-1a';
    Pdf.BeginDoc;

    Profile := TFileStream.Create('CoatedFOGRA39.icc', fmOpenRead or fmShareDenyWrite);
    try
      Pdf.AddPDFXOutputIntent('FOGRA39 (ISO 12647-2:2004)',
        'Coated FOGRA39 offset', Profile, 4, 'DeviceCMYK');
    finally
      Profile.Free;
    end;
    Pdf.CurrentPage.SetTrimBox(0, 0, 612, 792);   // 每一頁都必須設定

    Pdf.CurrentPage.SetCMYKFillColor(0, 0.5, 0.8, 0.1);
    Pdf.CurrentPage.Rectangle(72, 600, 468, 120);
    Pdf.CurrentPage.Fill;

    Pdf.CurrentPage.SetGrayFillColor(0.2);         // DeviceGray 文字
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    Pdf.CurrentPage.TextOut(72, 560, 0, 'Spring catalog, press edition');
    Pdf.EndDoc;                                    // 色彩檢查在這裡執行
  finally
    Pdf.Free;
  end;
end;

在這個語境下,每個以 TColor 為基礎的呼叫都是陷阱。SetRGBFillColor、SetRGBStrokeColor 與 SetRGBColor 都收 GDI 顏色並輸出 DeviceRGB,所以多年前為螢幕導向報表寫的幫手常式,一旦被重用就會污染印刷工作。請用分量在 0 到 1 之間的 SetCMYKFillColor、SetCMYKStrokeColor 或 SetCMYKColor,或 Gray 版本

特別色與 CMYK 漸層

X-1a 容許特別色,只要它的 alternate 是 CMYK 或 Gray。RegisterSeparation 建立 Separation 空間並回傳其資源名稱;RegisterAxialGradient 按你傳入的分量數挑 shading 色彩空間,四分量代表 DeviceCMYK

var
  SpotCS, Ramp: AnsiString;
begin
  // 特別色油墨,以近似 CMYK 組合作為 alternate
  SpotCS := Pdf.RegisterSeparation('Corporate Red', 'DeviceCMYK',
    [0, 1, 0.8, 0.05]);
  Pdf.CurrentPage.SetFillColorSpace(SpotCS);
  Pdf.CurrentPage.SetFillColor([1.0]);           // 100% 淡色
  Pdf.CurrentPage.Rectangle(72, 420, 200, 80);
  Pdf.CurrentPage.Fill;

  // 每個停靠點 4 個分量:shading 是 DeviceCMYK,不是 DeviceRGB
  Ramp := Pdf.RegisterAxialGradient(72, 300, 540, 300,
    [0, 0, 0, 0], [1, 0.6, 0, 0.2]);
  Pdf.CurrentPage.SetFillPattern(Ramp);
  Pdf.CurrentPage.Rectangle(72, 260, 468, 80);
  Pdf.CurrentPage.Fill;
end;

同一個漸層呼叫只傳三分量,得到的就是 DeviceRGB shading,資源巡檢會以 shading pattern 的名義拒絕它。以 'DeviceRGB' 為 alternate 註冊的 Separation 同理:特別色名稱看起來無害,但巡檢跟著 alternate 下去就找到 RGB。檢視器或 RIP 退回 alternate 時這類特別色油墨如何合成,收錄在在 Delphi 渲染 Separation 與 DeviceN 特別色一文

點陣影像需要同樣的紀律。HotPDF 把 Flate 壓縮點陣圖釘在 DeviceRGB,而以 icJpeg 內嵌的 JPEG 保留自己的分量數,所以四分量 CMYK JPEG 或灰階 JPEG 沒問題,普通 RGB JPEG 不行;在影像進到 PDF 之前,先在你的影像管線裡把稿圖轉到印刷描述檔

/DefaultRGB 為什麼救不了 PDF/X-1a 檔案?

/DefaultRGB 幫不上 X-1a,因為規則管的是印刷機收到什麼,不管 RGB 有沒有被描述檔刻畫。做 PDF/A 時,HotPDF 讓 RegisterDefaultColorSpace('DefaultRGB', ...) 吸收 DeviceRGB 用量:一旦 Default 色彩空間把 DeviceRGB 映射到 ICC profile,內容就是裝置無關的,PDF/A 一致性檢查會接受;走過那套流程的開發者往往在這裡如法炮製

EnsurePDFX1aContentColorSpaces 刻意無視那個映射。被描述檔刻畫過的 RGB 仍然是 RGB,而 X-1a OutputIntent 是 CMYK 列印條件,總還是得有人轉換它。兩種典藏與印刷 profile 之間的差異如果只能記一條,記這條:PDF/A 問的是顏色是否無歧義,PDF/X-1a 問的是它是否已經是油墨

繪製順序,以及不算數的頁面設定

檢查在 EndDoc 對已寫好的內容執行,所以你按什麼順序做設定無關緊要;在指派 PDFXCompliance 之前、或註冊 OutputIntent 之前畫的 RGB,照樣被抓到。這對把頁面建置放進共用常式、輸出 profile 很晚才決定——有時來自文件做到一半才選的使用者設定——的程式碼很重要

Pdf.BeginDoc;
// ... OutputIntent 與 TrimBox 同前 ...
Pdf.CurrentPage.SetRGBFillColor(clRed);   // 遺留幫手,輸出 DeviceRGB
Pdf.CurrentPage.Rectangle(72, 72, 200, 100);
Pdf.CurrentPage.Fill;

Pdf.PDFXCompliance := 'X-1a';             // 繪製之後才決定
Pdf.EndDoc;                               // 照樣擲出:已記錄 DeviceRGB

如果你要自己寫類似檢查,繪製期旗標有一個值得知道的陷阱。每個新頁面都透過 SetRGBColor(0) 以黑色圖形狀態初始化,發出一對 0 0 0 rg 與 0 0 0 RG;若放任這段頁面設定樣板把 DeviceRGB 旗標立起來,每份文件都會看起來被 RGB 污染,本檢查與 PDF/A 色彩一致性檢查都一樣。HotPDF 在那段初始化前後保存並還原旗標,所以只有你真正畫的顏色才計入

EndDoc 擲出例外時,怎麼找到 RGB 內容?

讀訊息開頭的位置,它會告訴你是兩條路徑中的哪一條抓到問題。訊息形如 PDFXCompliance 'X-1a': <where> uses <what>,後面跟著 ISO 15930-1 出處,以及轉成 CMYK 或 Gray、或改用 X-3 / X-4 的建議。驗證器在第一個違規處擲出普通的 Exception,所以請比對文字,不要比對例外類別

Delphi HotPDF EndDoc RGB 例外的解剖:繪製期旗標回報全文件用量,資源巡檢則指名確切頁面與鍵
讀訊息開頭的位置——全文件回報要你去看繪製程式碼,頁面與資源名則指向某一個鍵
  • document content uses DeviceRGB (rg/RG operators, RGB images or annotation colours recorded during drawing) 代表繪製期旗標觸發;沒有頁碼,因為旗標是全文件性質且先於巡檢被檢查。請在繪製程式碼裡搜尋 SetRGB* 呼叫、Flate 壓縮點陣圖、RGB JPEG 與 TColor 附註顏色
  • page 3 image XObject /Im2 uses DeviceRGB content 或 page 1 shading Pattern /P1 uses DeviceRGB content 來自資源巡檢,指向確切的資源鍵
  • uses an ICCBased RGB colour space (N=3) 出現在 ColorSpace /... 位置之後,是用 RegisterICCProfile 註冊 sRGB 描述檔再透過 SetFillColorSpace 選中的典型結果——繪製期旗標看不到這條路徑
  • 巢狀位置會串接,例如 page 2 Form XObject /Fm1 ColorSpace /CS1,讓你能從頁面一路追到帶 RGB 空間的那個資源
try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    if Pos('X-1a', E.Message) > 0 then
      LogPrepressFailure(Pdf.FileName, E.Message);  // 你自己的 logger
    raise;
  end;
end;

由於只回報第一個違規,修一份帶多個 RGB 來源的工作是迭代式的:修、重建、讀下一條訊息。批次工作值得把每個檔案的完整訊息記下來,並維護一份已知的慣犯清單,例如某個 logo 點陣圖,或仍在吃 TColor 的圖表幫手

RGB 關卡在你預檢流程中的位置

EnsurePDFX1aContentColorSpaces 是跑在你建置過程裡的針對性 RGB 關卡,它與完整 PDF/X-1a 預檢搭配,而不是取代後者。它涵蓋 RGB 各族——經由從每頁 /Resources 出發的資源巡檢,以及 HotPDF 顏色 setter 為行內 rg、RG 運算子維護的繪製期旗標;巢狀色彩空間追到深度 8 以防畸形循環,每個 EndDoc 回報第一個違規。Lab 或 CalGray 等其他色彩族是完整預檢的事,所以請把成品檔送進外部驗證器,作為建置流程的一部分拿到完整合規結論。合規輸出的整體圖像見以 HotPDF 產出 PDF/A、PDF/X 與 PDF/UA 指南,把驗證器接進無人值守管線則收錄在在 Delphi 自動化 PDF 預檢

HotPDF 是給 Delphi 與 C++Builder 的原生 VCL PDF 元件,支援 PDF/X、PDF/A 與 PDF/UA 輸出、CMYK 與特別色 API,以及 ICC OutputIntent;細節、授權與試用下載見HotPDF Delphi PDF component 產品頁