技术文章

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

HotPDF 通过一个入口点 RenderLoadedPageToDevice 渲染已加载的 PDF 页面,你交给它的设备决定了结果是位图、画到打印机画布之类的外部设备上下文(device context)上的绘图,还是矢量增强型图元文件(metafile)。把 RenderOverprintPreview 设为 True,同一次调用就会模拟 CMYK 四色油墨叠印,让操作员在屏幕上看到原本只会在印刷张上才出现的油墨相互作用

这两个特性解决的是不同的问题,恰好在同一段代码路径上汇合。设备抽象去掉了那样一条分支:预览、打印和导出各自有自己的渲染调用、各自产生各自的偏差。叠印打样去掉了一类生产错误——文档在每个阅读器里看起来都对,下了印刷机却出错了

为什么打印出来和预览不一样

因为叠印是对成像设备的一项指令,不是一次绘制操作。当页面在图形状态里把 /OP/op 设为真,它是在告诉 RIP 不要挖空底下的油墨——一个青色对象画在黄色之上会保留黄色,纸张上就显示绿色。一个忽略叠印的阅读器按正常方式挖空,显示的是青色。两者在各自的语境里都没有错,而这恰恰就是问题所在:屏幕和印刷机各执一词,等到打样回来才发现

RenderOverprintPreview 让 HotPDF 认真对待受 /OP/op/OPM 1 控制的 DeviceCMYK 绘画的这项指令。结果是一份打样预览而不是阅读器预览:黑色叠印一个淡色时保留为富黑叠合而不是挖出一个洞,设计师在白色文字上无意设的叠印会作为即将消失的文字而显现出来

var
  Pdf: THotPDF;
  Device: THPDFBitmapRenderDevice;
  Proof: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('cover-cmyk.pdf');
    Pdf.RenderOverprintPreview := True;    // proof, not plain preview
    Device := THPDFBitmapRenderDevice.Create;
    try
      if Pdf.RenderLoadedPageToDevice(0, 150, Device) then
      begin
        Proof := Device.TakeBitmap;        // ownership moves to the caller
        try
          Image1.Picture.Assign(Proof);
        finally
          Proof.Free;
        end;
      end;
    finally
      Device.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

这一设置参与渲染缓存标识(cache identity),无论是在内存里还是磁盘上,所以普通预览和打样预览绝不会共用一张位图。切换这个属性不需要你手工让任何东西失效——一个返回这两者中错误那一个的缓存,比根本没有缓存更糟

三种设备,一次渲染调用

THPDFRenderDevice 是一个抽象类,有两个值得关注的成员:Kind 把目标报告为 rdkBitmaprdkDeviceContextrdkEnhancedMetafileExecute 由库调用。HotPDF 附带三种具体设备,各自以不同方式持有其输出

THPDFBitmapRenderDevice 持有一个 TBitmap,直到 TakeBitmap 把所有权转交给你。THPDFDeviceContextRenderDevice 接收一个既有的 HDC 加上宽度和高度并直接画进去,这正是无需位图往返就能渲染到打印机画布的方式。THPDFMetafileRenderDevice 持有一个 TMetafile,直到 TakeMetafile 把它转交,让矢量内容对需要矢量的消费方仍保持矢量

var
  Device: THPDFDeviceContextRenderDevice;
begin
  Printer.BeginDoc;
  try
    Device := THPDFDeviceContextRenderDevice.Create(
      Printer.Canvas.Handle, Printer.PageWidth, Printer.PageHeight);
    try
      Pdf.RenderLoadedPageToDevice(PageIndex, 300, Device);
    finally
      Device.Free;
    end;
  finally
    Printer.EndDoc;
  end;
end;

Kind 而不是测试运行时类,这是刻意的。按设备类型分发的应用程序代码,在设备被包装、装饰或替换时仍然能工作,而测试 is THPDFBitmapRenderDevice 的代码则不能

所有权转移在实践中意味着什么

TakeBitmapTakeMetafile 之前,设备拥有这个对象并在自己的析构函数里释放它。调用之后,你拥有它,设备不再拥有。两种用法都正当:当对象只需比渲染调用本身活得更久时使用 BitmapMetafile 属性;当对象要比设备活得更久时拿走所有权

失败模式是 Delphi 里最普通的那种。拿走位图、释放设备、忘了释放位图,就有了一个随页数增长的内存泄漏——在五页的测试上看不见,在五百页的批量上显而易见。把这两个对象各自包进各自的 try/finally 而不是共用一个,所有权问题就自己解答了

叠印打样与同一页面内的透明度

叠印预览打开时,透明度组的挖空(knockout)仍然有效,两者在同一条有界绘制快照(bounded paint snapshot)路径里合成。这一点很重要,因为真实的印刷就绪文件不断混用两者:一个容纳图稿的透明度组坐在一个黑色被设为叠印的背景上,只模拟其一不模拟其二,得到的是一份以新的方式出错的打样,而不是一份正确的打样

也要看清局限。叠印预览模拟的是受上述叠印控制项约束的 DeviceCMYK 绘画的四色油墨行为。它是油墨相互作用的打样,不是色彩管理的合同样张:它不能替代 ICC 工作流,也不能告诉你某台具体印刷机和某种具体承印物会得到什么结果。把它当成印前操作员在专业阅读器里看待叠印预览的方式——一道能抓出谁也看不出(看普通预览时)的错误的那种检查

把打样放进预检步骤

它的合适位置,是在你已经在跑的那些检查旁边。一道预检扫描报告黑色文字被设为叠印;一次打样渲染向操作员展示这在页面上意味着什么;两者都进同一份报告。对于包装印活里常常伴随叠印出现的专色,Separation 与 DeviceN 专色渲染的详解覆盖了同一页面的色料那一面,而 把 PDF 页面渲染到位图通过 TPrinter 打印已加载的 PDF的笔记覆盖了这两种设备目标的普通非打样形态

HotPDF 用面向 Delphi 和 C++Builder 的原生 VCL 代码渲染、打样并打印已加载的 PDF 页面,无需随应用程序部署任何外部渲染 DLL——HotPDF 组件页有渲染功能清单和试用构建