技术文章

HotPDF 的 CMYK 叠印打样与渲染设备

一个青色框叠在黄色背景上,绘制它的图形状态带着 /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;

印版规则取决于叠印颜料的色彩空间,值得弄懂它们,因为每一条都解释了样张上的一种不同意外:

  • DeviceCMYK 配 /OPM 1:为零的分量不动其印版,非零分量替换之。黄背景上青 100 / 黄 0 保住黄色,变成绿色
  • DeviceGray:颜料只点名黑版,底下青、品红、黄全部幸存。于是设成叠印的白色 DeviceGray logo 在 CMY 背景上直接消失
  • Separation 与 DeviceN:名为 Cyan、Magenta、Yellow 或 Black 的色料直接替换对应工艺印版,/None 什么都不画,/All 写每一块工艺印版
  • 专色色料:其他任何名称按专色油墨处理;其淡色变换给出一个替代色,乘到下方各工艺印版上,所以专色压暗它覆盖的区域而不是替换它

打样作用于填充与描边路径以及文本,包括描边渲染模式下的文本——那里由描边标志 OP 而不是 op 决定。图像、内联图像和 sh 着色继续走各自完整的光栅颜色管线,以普通不透明度绘制。实践中这已覆盖预检报告最常标记的内容:叠印规则、键线、文字和平色块

打样开着时透明度依然存活。设了 knockout 的透明组照样挖空,且该组与叠印规则在同一步合成,所以一件透明素材叠在叠印黑色背景上的印刷就绪文件,两种效果一起打样。只对一半就能产出的,是另一种错误的样张

这个设置也参与缓存渲染的身份。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 时抛异常,DPI 低于 1 按 150 处理,返回值即设备 Execute 方法报告的结果。THPDFRenderDevice 是抽象基类,带两个虚成员:返回 rdkBitmap、rdkDeviceContext 或 rdkEnhancedMetafile 的 Kind,以及 Execute(Document, PageIndex, DPI)

三个自带设备各按不同方式持有输出:

  • THPDFBitmapRenderDevice 在 Bitmap 属性里持有一份 TBitmap,析构时释放它——除非 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 参数决定页面尺寸,而不是你传的宽高。渲染器把 PDF 单位乘以 DPI / 72,高度只用来把页面原点锚在表面底部。写死 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;

宽高来自页面可见框(CropBox 裁剪到 MediaBox),按打印机自身分辨率计算,调用方在创建设备前为旋转页面交换两者。这正是渲染器锚定原点时期待的输入

叠印打样不走这条路。设备上下文与图元文件设备把 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 给到位图足以覆盖纸面的像素量,letter 尺寸样张大约 200 到 300。上面的拉伸矩形填满可打印区域;页面与纸张形状不同时,记得在里面保持页面纵横比

打样在预检旁边的位置

打样回答一个问题:对象重叠时哪些油墨活下来。它与其他任何渲染一样合成在同一张 24 位 RGB 位图上,所以它的颜色显示的是油墨组合,不是实测的纸张颜色。印刷条件是色彩管理问题,文档经 ConfigureRenderICCWorkflow 处理——它接受打样 profile、输出 profile 和一个黑点补偿标志——ClearRenderICCWorkflow 则把它们撤掉

打样渲染最有产出的位置,是在你已在跑的检查旁边。预检一轮报告黑色文字被设成叠印;打样渲染向操作员展示这在页面上的效果;两者落进同一份报告。包装活儿常把叠印和专色混在一起,Separation 与 DeviceN 专色渲染演练覆盖同一页面的色料侧,包括经 RenderLoadedPageSeparationPlate 的单版视图。把 PDF 页面渲染到位图和通过 TPrinter 打印已加载的 PDF两文覆盖两种设备目标的朴素形态

这一切跑在原生 Delphi 和 C++Builder 代码里,可执行文件旁边不需要随附渲染 DLL。HotPDF 组件页列出渲染与印前特性,并提供试用版让你对着自己的印件跑这些示例