技术文章

HotPDF 图像降采样核与打印抖动

HotPDF 通过 ImageDownsampleKernel 属性暴露三种图像降采样核,并通过 RenderOutputDither 提供一道独立的 Floyd-Steinberg 处理。前者控制照片在缩到体积预算之内后长什么样,后者控制页面被压成黑白后长什么样。两者默认都不开,而且 opt-in 的理由也一样:它们要花真实的时间

把人逼到这里来的压力都很熟悉。一份 60 MB 的扫描合同必须通过一个拒收 10 MB 以上邮件的网关发出去;或者一批账单要落到传真式单色设备上,那设备把每个灰像素渲染成纸或者墨粉。两个问题都是重采样问题,而且都有一个看起来很糟糕的快答案和一个看起来正确的慢答案

三种核到底差在哪

THPDFResampleKernel 有三个取值,它们确实落在速度-质量曲线的不同位置上。rkHalftone 委托给带 HALFTONE 模式的历史 GDI StretchBlt 路径——虽然名字叫 halftone,它其实是双线性级滤波:快,对线稿和截图够用,但缩小照片时会出现你一眼就能认出的硬边锯齿。rkBicubic 跑的是可分离的 Catmull-Rom 核,rkLanczos3 跑的是三瓣支撑的可分离加窗 sinc

两个可分离核都跑两遍——先水平后垂直——每个目标像素 6 到 12 个 tap,纯 Pascal 实现。这比 GDI 路径慢大约一个数量级,也正因此 rkHalftone 保持为默认值。对夜间跑几千页的批处理来说,这个差异是调度决策,不是偏好问题。对一个有人正在等着的单份文档来说,Lanczos3 几乎免费,而且肉眼可见地更好

HotPDF 三种降采样核的权重曲线:rkHalftone 委托给支撑为 1 的双线性级 GDI HALFTONE 路径,rkBicubic 跑支撑为 2 的可分离 Catmull-Rom 三次核,rkLanczos3 跑支撑为 3 的加窗 sinc,用大约一个数量级的速度换来肉眼可见更好的照片
三个核取值确实落在速度-质量曲线的不同位置:双线性级 GDI 路径、Catmull-Rom 三次核、三瓣加窗 sinc,可分离核会归一化权重,因此不会振铃越过黑或白

有两个实现性质值得知道,因为它们决定了输出能做什么、不能做什么。边界用边缘复制而不是环绕或淡出,权重按目标像素归一化。这两条加在一起,意味着结果永远不会振铃到黑以下或白以上,经典的 Lanczos 过冲光晕不会以裁剪伪影的形式出现在编码后的图像里

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // 在调用之前设置
    Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
    if Changed > 0 then
    begin
      Writeln('resampled images: ', Info.DownsampledImageCount);
      Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
      Pdf.SaveToFile('scanned-contract-150dpi.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

MinimumSavingsBytes 参数(上例中的 4096)是让这个操作保持诚实的守卫。重新编码一张本来已经压得很好的图像,可能产出比原来更大的流,一个无脑替换每张图的降采样器偶尔会把它奉命缩小的文件越弄越大。阈值的意思是:只有当替换至少省下这么多字节时才提交。PreservedCalibratedImageCount 报告的是另一个保守决定:因为携带校准色彩空间、重采样会损害它的图像保持原样不动

为什么错一个多项式系数这么难发现?

因为坏掉的插值核既不崩溃也不抛异常,它只是产出一张微妙地不对、又没人能归因的图像。Catmull-Rom 核是分段三次的,它在嵌套 Horner 形式下的外侧分支是 ((-0.5t + 2.5)t - 4)t + 2。把中间那个系数写成 -5 而不是 -4,函数照样求值、照样返回看似合理的数值、照样产出图像

损害表现为 W(1) 求出 -1 而它必须是 0。负权重累积起来,求和在零处截断,可见的症状是一段渐变左端变黑、一条阶跃边丢了中间调。失败的任何线索都不指向多项式。能在几秒内抓住它的检查是算术性的,不是视觉性的:插值核必须满足 W(0) = 1 和 W(±1) = W(±2) = 0,任何不满足这三个点的核都有系数错误,没有例外。在单元测试里断言这三个值,这一整类笔误缺陷就绝迹了

HotPDF 双三次 Catmull-Rom 外侧分支曲线图,说明错误系数为何能藏住:嵌套 Horner 形式中把 -4 写成 -5 的笔误照样求值,留下 W(1) 为 -1、W(2) 为 -2 而它们本应为零,所以断言 W(0) 等于 1 加上两个零点约束,几秒钟就能抓住它
坏核从不崩溃,只是返回看起来合理的数字,这就是眼睛抓不住系数笔误的原因。W(0) = 1 加上正负一和正负二处的零,是一个三行的单元测试

Floyd-Steinberg 抖动,以及它在流水线里的位置

抖动处理和重采样是两个问题,也待在流水线的不同位置。RenderOutputDither 在页面合成之后应用 Floyd-Steinberg 误差扩散,这对单色打印预览或传真式导出来说是唯一说得通的位置:这个操作是把一张完成的光栅降到每像素一位,跟各个图像在输入时如何被缩放无关

算法本身很短。亮度以 50% 为阈值量化,量化误差按经典的 7/16、3/16、5/16 和 1/16 权重扩散给右、左下、下、右下四个邻居。输出像素在每个通道都是 0 或 255。天真的替代方案——不带扩散的硬阈值——会把照片变成剪影,丢掉承载内容的全部中间调

HotPDF Floyd-Steinberg 抖动的渲染流水线位置:RenderOutputDither 在页面合成之后对完成的 24 位光栅运行,以 50% 对亮度做阈值,并以 7/16、3/16、5/16 和 1/16 权重把每次量化误差向右、向下扩散,经由一个必须累加的行缓冲,产出一比特单色输出
抖动属于合成之后,因为它降的是完成的光栅,与图像怎么缩放无关。扩散权重之和为一,行缓冲必须累加而不是覆盖
// 面向单色预览设备的渲染期抖动
Pdf.RenderOutputDither := True;

// 或者对你已经持有的位图应用同一道处理。位图必须是
// pf24bit;函数宁可返回 False 也不瞎猜
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// 在文档流水线之外重采样时的直接核访问
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

误差扩散里有一个实现细节,每个人都会被它咬一次。行间误差缓冲必须累加。下一行的每个像素会收到当前行三个不同像素的贡献——3/16、5/16 和 1/16 那三个 tap——如果代码用赋值而不是累加,每次写入都会丢掉前面的贡献,只有最后一个 tap 活下来。图像看起来仍然是抖动过的,这正是它难以察觉的原因,但纹理是错的,色调还原会漂移。抓住它的测试是定量式的:抖动一块均匀的中灰区域,要求内部覆盖率落在 40% 到 60% 之间

一条减体积流水线该用哪种组合?

核要跟图像的实际类型匹配,抖动则当作设备问题而不是压缩问题来对待。对必须塞进体积预算的照片扫描件,rkLanczos3 在 150 或 200 DPI 下保住人们会注意到的细节,同时把像素数砍掉四倍以上。对截图、示意图和线稿,rkHalftone 真的够用而且快得多,因为这些图像几乎没有需要保留的色调渐变。对无法逐张检查的混合批次,rkBicubic 是合理的中间项:比双线性好,tap 数大约是 Lanczos3 的一半

降采样只是好几个杠杆之一,而且未必是最大的那个。对二值扫描件,Delphi 原生 JBIG2 二值压缩覆盖的编码器通常效果要好得多,那里的收益来自符号字典而不是像素数。做决定之前,先弄清文件里到底装着什么会很有帮助——这正是 提取图像及其解码过滤器的用途:一份图像对象及其现有压缩方式的清单,能告诉你重采样还有没有油水可捞

如果你在构建展示结果的预览面,把 PDF 页面渲染成位图里记录的同一套渲染路径,正是 RenderOutputDither 生效的地方,于是抖动过的预览和抖动过的输出来自同一条代码路径,而不是两套会各自漂移的实现

两个功能背后的大原则是一致的:质量设置应该显式且可逆。HotPDF 把历史行为保留为默认值,让现有应用程序升级时不会遇到输出或耗时的意外变化,同时把更好看、更慢的路径放在一次属性赋值之外。两者都是 HotPDF Delphi PDF component 的一部分,与它们所依赖的资源优化和渲染机制在一起