技術文章

HotPDF 的 CMYK Overprint 打樣與算繪裝置

一個青色方塊疊在黃色背景上,畫它的圖形狀態帶著 /op true 與 /OPM 1。螢幕上重疊處是一片純青色。印刷紙上同樣的重疊變成綠色,因為 RIP 沒動黃版、把青色疊在它上面。兩種結果都嚴格遵循檔案;螢幕轉譯只是從沒問過成像裝置會怎麼處理那個旗標,所以螢幕上做的核可,蓋住的是一個永遠到不了紙上的顏色

為什麼頁面印出來跟預覽不一樣?

疊印是給輸出裝置的指示,講的是色版的事,而 RGB 螢幕沒有色版。ISO 32000-1 §8.6.7 定義了 ExtGState 的 OP(描邊)或 op(填色)項為 true 時會發生什麼:目前繪料沒點名的色料,保留其色版上既有的內容,而不是被擦成零。這樣畫出的青色物件不從黃版上拿走任何東西。以 RGB 合成的檢視器沒地方放黃色,於是把青色畫在上層、然後繼續

兩種輸出都沒有壞掉。分歧內建在兩種成像模型裡,直到有人把印刷打樣拿到螢幕旁對照,它一直隱形。對那個不小心把黑字設成挖空、或把白 logo 設成疊印還期待它顯示的設計師來說,發現的時機都太晚

HotPDF 中的 CMYK 疊印比較:一般檢視器把青色底下的黃色挖空,疊印預覽可校對兩種印刷色墨,並暴露不小心疊印的白色文字
普通檢視器只畫上層油墨,疊印打樣則把兩種油墨疊加,意外疊印的白字這類陷阱因此現形

用 RenderOverprintPreview 開啟打樣

HotPDF 把打樣暴露成文件上的一個布林值 THotPDF.RenderOverprintPreview,預設 False。設了之後,點陣圖轉譯會追蹤每個疊印繪料底下的製程色版,並在寫像素之前套用 §8.6.7 規則,所以重疊處顯示的是印刷機會產生的油墨組合。一般檢視保持預設關閉是對的,辦公室 PDF 的讀者期待它長得跟平常的檢視器一樣。在輸出要交給印前操作員的那個步驟打開它

var
  Pdf: THotPDF;
  Device: THPDFBitmapRenderDevice;
  Proof: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('cover-cmyk.pdf');
    Pdf.RenderOverprintPreview := True;    // 打樣色版,預設為 False
    Device := THPDFBitmapRenderDevice.Create;
    try
      if Pdf.RenderLoadedPageToDevice(0, 150, Device) then
      begin
        Proof := Device.TakeBitmap;        // 從這裡起 Proof 歸呼叫端所有
        try
          Image1.Picture.Assign(Proof);
        finally
          Proof.Free;
        end;
      end;
    finally
      Device.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

色版規則取決於疊印繪料的色彩空間,值得了解,因為每一條各自解釋打樣上一種不同的意外:

  • /OPM 1 下的 DeviceCMYK:分量為零的成分不動其色版,非零分量取而代之。黃底上青 100/黃 0 保留黃色、變成綠色
  • DeviceGray:繪料只點名黑版,底下的青、洋紅與黃因此存活。設成疊印的白色 DeviceGray logo 疊在 CMY 底上於是消失
  • Separation 與 DeviceN:名為 Cyan、Magenta、Yellow 或 Black 的色料直接取代那個製程色版,/None 什麼都不畫,/All 寫進每一個製程色版
  • 特別色色料:其他任何名稱都當特別色印墨處理;其色調轉換給出一個替代色,與底下製程色版相乘,所以特別色讓它覆蓋之處變暗,而不是取代它們

打樣適用於填色與描邊路徑,也適用於文字,包括描邊渲染模式的文字——那裡由描邊旗標 OP 決定而非 op。影像、行內影像與 sh 著色繼續走它們完整的點陣色彩管線、以平常的不透明度繪製。實務上這涵蓋了預檢報告點名最多的東西:疊印規則、關鍵線、文字與平網

打樣開著時透明度照樣作用。設了 knock out 的透明度群組照樣挖空,而且該群組與疊印規則在同一趟裡合成,所以疊印黑底上有透明美工的印刷就緒檔案,兩種效果一起打樣。一種對另一種錯,只會產出另一種錯誤的打樣

這個設定也參與快取轉譯的身分識別。RenderLoadedPageToBitmapCached 以頁面、DPI 與一個由含 RenderOverprintPreview 在內的轉譯設定構成的變體作為條目鍵,磁碟層用同一個變體。在普通與打樣預覽之間切換的檢視器拿到兩個分開的快取條目,不必僅因為切換了開關就去呼叫 InvalidateRenderedPageCache

DeviceCMYK 檔案裡的 /OPM 0 陷阱

這就是讓人去找 bug 的那個案例。檔案在 DeviceCMYK 青色填色上設了 /op true、卻讓 OPM 停在預設值 0,然後打樣仍然顯示青色挖空黃色。打樣是對的。疊印模式 0 之下,§8.6.7 說 DeviceCMYK 繪料設定全部四個製程色料、零也照設,所以零的黃色分量取代黃版,與挖空毫無分別。疊印模式 0 只保護繪料沒點名的色版,對四分量 CMYK 顏色來說,那意味著特別色版、僅此而已

在合成打樣顯示的製程色版上,模式 0 與普通挖空給出相同的像素,所以 HotPDF 把那些 DeviceCMYK 物件畫成普通挖空。CMYK 物件要疊印其他製程印墨,帶 /op true 的 ExtGState 還需要 /OPM 1。當打樣與設計師的意圖在一個 DeviceCMYK 物件上不一致,先打開那個 ExtGState 找 OPM 項,再懷疑其他任何東西

反向的意外也存在。/OPM 1 之下,CMYK 白是四個零,四個零在疊印下讓每個色版保持原樣。設成疊印的白字在紙上消失,打樣也顯示它消失——這正是要跑一次打樣的全部理由

RenderLoadedPageToDevice 背後的三個裝置

THotPDF.RenderLoadedPageToDevice(PageIndex, DPI, Device) 是單一入口,目標由你傳入的裝置物件決定。nil 裝置拋出例外,低於 1 的 DPI 按 150 處理,回傳裝置 Execute 方法回報的東西。THPDFRenderDevice 是抽象基底,有兩個虛擬成員:Kind——回傳 rdkBitmap、rdkDeviceContext 或 rdkEnhancedMetafile——以及 Execute(Document, PageIndex, DPI)

三個內建裝置以不同方式擁有自己的輸出:

  • THPDFBitmapRenderDevice 把 TBitmap 放在 Bitmap 屬性裡,並在解構函式釋放它——除非 TakeBitmap 已先把它交給你
  • THPDFDeviceContextRenderDevice.Create(ADC, AWidth, AHeight) 畫進你已有的 HDC、不擁有任何東西;建構函式在 DC 為零或尺寸非正時拋出例外,且不畫背景,所以螢幕 DC 需要自己補白色填滿
  • THPDFMetafileRenderDevice 持有 TMetafile 直到 TakeMetafile 移交。文字與向量路徑在加強型中繼檔裡保持可縮放,影像 XObject 則以點陣形式抵達
HotPDF 渲染架構:單一 RenderLoadedPageToDevice 呼叫扇出到點陣圖、印表機裝置內容與加強型中繼檔渲染裝置,各帶 Kind 值與擁有權規則
一次渲染呼叫分流到點陣圖、裝置內容與中繼檔裝置,Kind 回報目標,Take 方法移交所有權

所有權遵循樸素的 Delphi 規則。物件只需要活得跟裝置一樣久,就讀 Bitmap 或 Metafile 屬性;必須活得比裝置久,就呼叫 Take 方法。拿了點陣圖、釋放裝置、忘了點陣圖,洩漏隨頁數增長:五頁的樣品上不可見,五百頁的批次上顯而易見。每個物件給自己的 try/finally,誰釋放什麼的問題就自己回答了自己

應用程式碼按 Kind 分派,不要測 is THPDFBitmapRenderDevice。Execute 與 Kind 是公開虛擬方法,你可以寫自己的裝置類別,而檢查 kind 的程式碼在你的裝置頂替內建裝置時照樣工作

為什麼印表機轉譯落在角落?

裝置內容路徑以 DPI 引數決定頁面大小,不是以你傳的寬高。轉譯器以 DPI / 72 縮放 PDF 單位,高度只用來把頁面原點錨在表面底部。寫死 300、把工作送到 600 dpi 印表機,頁面以一半尺寸出現、被推到紙張左下角。印表機解析度是驅動程式的屬性,所以要問驅動程式:

procedure PrintPage(Pdf: THotPDF; PageIndex: Integer);
var
  Device: THPDFDeviceContextRenderDevice;
  PrinterDPI, W, H, Tmp: Integer;
  L, B, R, T: Single;
begin
  if not Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T) then
    Exit;
  Printer.BeginDoc;
  try
    // 問驅動程式;300、600、1200 dpi 都很常見
    PrinterDPI := GetDeviceCaps(Printer.Handle, LOGPIXELSX);
    W := Round((R - L) * PrinterDPI / 72);
    H := Round((T - B) * PrinterDPI / 72);
    if (Pdf.GetLoadedPageRotation(PageIndex) mod 180) = 90 then
    begin
      Tmp := W; W := H; H := Tmp;            // 90 / 270 度頁面要對調
    end;
    Device := THPDFDeviceContextRenderDevice.Create(
      Printer.Canvas.Handle, W, H);
    try
      Pdf.RenderLoadedPageToDevice(PageIndex, PrinterDPI, Device);
    finally
      Device.Free;
    end;
  finally
    Printer.EndDoc;
  end;
end;

寬與高來自頁面的可見框——被 MediaBox 裁過的 CropBox——以印表機自己的解析度計算,呼叫端在建立裝置之前為旋轉頁面對調兩者。這正符合轉譯器錨定原點時的期待

疊印打樣不走這條路。裝置內容與中繼檔裝置直接把 GDI 呼叫發進目標,而色版規則需要每個繪料底下既有的像素——只有點陣圖轉譯有。要紙本打樣,先把打樣點陣圖轉譯出來、再拉伸到印表機畫布上。一個小小的自訂裝置能把這一切留在同一個入口背後:

type
  // 你自己的裝置:先打樣點陣圖,再拉伸到印表機上
  TPrinterProofDevice = class(THPDFRenderDevice)
  public
    function Kind: THPDFRenderDeviceKind; override;
    function Execute(Document: THotPDF; PageIndex, DPI: Integer): boolean;
      override;
  end;

function TPrinterProofDevice.Kind: THPDFRenderDeviceKind;
begin
  Result := rdkDeviceContext;
end;

function TPrinterProofDevice.Execute(Document: THotPDF;
  PageIndex, DPI: Integer): boolean;
var
  Proof: TBitmap;
  OldProof: Boolean;
begin
  OldProof := Document.RenderOverprintPreview;
  Document.RenderOverprintPreview := True;
  try
    Proof := Document.RenderLoadedPageToBitmap(PageIndex, DPI);
  finally
    Document.RenderOverprintPreview := OldProof;  // 讓呼叫端的設定保持原樣
  end;
  Result := Proof <> nil;
  if Result then
  try
    Printer.Canvas.StretchDraw(
      Rect(0, 0, Printer.PageWidth, Printer.PageHeight), Proof);
  finally
    Proof.Free;
  end;
end;

在 Printer.BeginDoc 與 Printer.EndDoc 之間呼叫它,DPI 取一個能讓點陣圖為紙張備足像素的值,信紙尺寸的打樣大約 200 到 300。上面的拉伸矩形填滿可列印區域;頁面與紙張形狀不同時,記得在矩形裡保持頁面長寬比

打樣在預檢旁邊的位置

打樣回答一個問題:物件重疊處哪些印墨存活。它與任何其他轉譯一樣合成在同一張 24 位元 RGB 點陣圖上,所以它的顏色顯示的是油墨組合,不是實測的紙張顏色。印刷條件是色彩管理的問題,文件透過 ConfigureRenderICCWorkflow 處理它——吃一個打樣描述檔、一個輸出描述檔與一個黑點補償旗標——ClearRenderICCWorkflow 再把它們丟掉

打樣轉譯最有生產力的位置,是在你已經在跑的檢查旁邊。預檢趟次回報黑字被設成疊印;打樣轉譯讓操作員看見它在頁面上造成的後果;兩者落進同一份報告。包裝工作常把疊印與特別色混在一起,Separation 與 DeviceN 特別色轉譯 的逐步解說涵蓋同一頁面的色料那一側,包括透過 RenderLoadedPageSeparationPlate 的單色版檢視。把 PDF 頁面轉譯成點陣圖 與 透過 TPrinter 列印已載入的 PDF 兩文以素樸形式涵蓋兩個裝置目標

這一切都在原生 Delphi 與 C++Builder 程式碼裡跑,不必在執行檔旁邊出貨任何轉譯 DLL。HotPDF Delphi 元件頁 列出轉譯與印前功能,並有試用版可對你自己的印刷檔案跑這些範例