HotPDF 渲染以非 Normal Blend mode 绘制的 PDF 透明度组时,让组自己计算混合:Cb(1 − α) + α·B(Cb, Cs),所用组 alpha 来自它自己跟踪的值,或从黑底、白底两次绘制恢复。自 v2.770.0 起,THotPDF.RenderColorimetricLuminosityMasks 属性还决定亮度软蒙版按常见查看器那样以加权 RGB 读色,还是按 ISO 32000-1 描述的那样以 CIE Y 读色
两处修改源于同一类文件:设计工具导出的页面,一张产品照片配着柔和的投影。投影是一个以透明度组绘制的 Form XObject,带 /BM /Multiply,它的渐隐则是一个亮度软蒙版。在其它查看器里,这道投影是浅淡的中性灰;在旧版 HotPDF 里却几乎全黑,还向背景色偏色。两个独立的 bug 叠在一起,值得拆开讲,因为任何从“先画不透明对象”设计长出来的渲染器都会两个都撞上
Multiply 投影为什么渲染得过暗
Multiply 投影渲染过暗,是因为渲染器拿组的合成结果去混合,而不是拿组自己的颜色和 alpha。那个结果里半透明的像素已经含着背景,再拿它们与背景混合,等于让背景乘以它自己。带不透明度的 Normal 混合是线性的,犯这个错也看不出来;Multiply、Screen、Darken 以及所有其它非线性模式就不是了
HotPDF 渲染管线最初用一个简单快速的模型按绘制对象处理 Blend mode:绘制对象前,渲染器快照受影响的像素;绘制后,比较“前后”两张位图,把“后”的颜色当作不透明源 Cs,计算 B(Cb, Cs) 再以当前不透明度合成。对填充路径、字形或不透明图像,这完全正确:源在其形状内部确实不透明,形状也正是像素变化的地方
透明度组打破了这个假设。ISO 32000-1 §11.4 把组定义成先合成进自己的结果——带自己的颜色和自己的 alpha——然后才作为单个对象与背景混合。投影不透明度为 30% 的地方,“后”那张位图里大约是 0.3 的投影颜色加 0.7 的底下页面。把这个当成不透明源喂给 Multiply,公式就成了背景乘以一个已经含着背景的值:投影变暗,还染上背景的色调。什么都不崩溃,没有诊断报警,页面只是看着不对——这是渲染器最糟糕的失败方式
HotPDF 如何让透明度组自己完成混合
自 v2.769.3 起,HotPDF 把以非 Normal Blend mode 绘制的组的混合,从通用的按对象路径挪进了组渲染器本身。组直接从自己的颜色、自己的 alpha 和为 Do 操作符拍下的背景快照计算 Cb(1 − α) + α·B(Cb, Cs),然后把结果标记为已混合,外层 Do 于是只用当前不透明度做合成,不再混第二次。普通对象保留快速的前后对比模型,Normal 混合下的组不受影响,因为线性情形从来就没错过
难的是 alpha。孤立组(/I true)从完全透明的背景起步,渲染器本来就为它跟踪一个单独的 alpha 平面,可以直接读 α。非孤立组合成到页面上已有的内容上,没有自己的 alpha 平面。HotPDF 的恢复办法是把组画两遍,一遍黑底、一遍白底:
- 黑底上,每个像素最终是
α·Cs - 白底上,同一像素最终是
α·Cs + (1 − α) - 相减得
α = 1 − (white − black),再得Cs = black / α
之所以可行,是因为渲染器执行的每一次 Normal 合成对背景都是线性的,包括对带软蒙版和抗锯齿边缘图像使用的 GDI AlphaBlend。但这个戏法有个前提:组内容本身必须按 Normal 混合。如果组自己的 ExtGState 资源设置了非 Normal 的 /BM,两次绘制就不再相差一个干净的线性项,HotPDF 便不拆组,退回原行为。这个检查只覆盖组自己的资源,不含嵌套 Form。代价也是真实的:这类页面渲染稍慢,因为组要画两遍。组内构建的软蒙版会保留到两次绘制结束,第二遍直接命中蒙版缓存,不用重建
// 逐通道 alpha 恢复的示意(取值 0..1)。
// Black = 组画在黑底上,White = 画在白底上。
procedure RecoverGroupPixel(Black, White: Double;
out Alpha, Source: Double);
begin
Alpha := 1.0 - (White - Black);
if Alpha < 0.0 then Alpha := 0.0;
if Alpha > 1.0 then Alpha := 1.0;
if Alpha > 0.0 then
Source := Black / Alpha // 去预乘
else
Source := 0.0; // 全透明:颜色无所谓
end;
// Multiply 混合,然后组合成 Cb(1 - a) + a * B(Cb, Cs)
function CompositeMultiply(Backdrop, Source, Alpha: Double): Double;
begin
Result := Backdrop * (1.0 - Alpha) + Alpha * (Backdrop * Source);
end;
上面的代码是数学示意,不是 HotPDF 的 API;渲染器在 THotPDF.RenderLoadedPageToBitmap 内部按通道、按像素做的就是这件事。实践中,从两次 8 位绘制恢复的 alpha 在接近零时会损失精度,所以极淡的边缘带一点量化噪声——这正是除法只在 alpha 明显大于零处才有意义的原因
组内部 Blend mode 重置为 Normal
Blend mode 和常量 alpha、软蒙版一样,作用于透明度组整体,而不是组内每个对象,所以组内容必须从 Normal 混合起步(ISO 32000-1 §11.6.6)。HotPDF 以前在进入组时会重置不透明度和软蒙版,却对 Blend mode 不闻不问。孤立组里的结果非常戏剧化:内容与组的空初始背景——黑色——做 Multiply,整个组画成一片黑
构建软蒙版时适用同样的重置。如果一个 ExtGState 同时设置 /BM /Multiply 和 /SMask,蒙版组必须以 Normal 混合绘制,否则白色的蒙版组会变黑,恰好把蒙版本该显露的东西藏掉。两项重置都是 v2.769.3 的一部分,且在简单文档上都看不出来——它们也因此活到了那么久
中灰的亮度软蒙版该取什么值
按 ISO 32000-1,混合空间为 CIE 基的亮度软蒙版应取 CIE 1931 Y,而 sRGB 中灰 0.5 对应的 Y 只有约 0.21,因为 Y 是线性光。常见 PDF 查看器则把组颜色解析成 RGB,按大致 0.30 / 0.59 / 0.11 加权编码值,同样的灰得到约 0.5。自 v2.770.0 起,HotPDF 默认采用与查看器一致的加权,并为规范读法提供 THotPDF.RenderColorimetricLuminosityMasks
这个差距不是学术问题。设计工具导出里的渐隐通常是 ICCBased 或校准组里的一个渐变,而中间调恰是线性 Y 与伽马编码灰度分歧最大的地方。用 CIE Y,中灰蒙版只放过约五分之一的投影而不是一半,于是投影和暗角明显比客户用其它任何查看器打开时都淡。按标准字面正确、在人眼里全错,作为默认是笔坏买卖,所以默认跟着查看器走,严格模式只差一个属性
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
begin
// 默认(False):亮度蒙版对解析出的颜色
// 按 DeviceRGB 方式加权,与常见查看器一致
// True:蒙版组为 CIE 基空间时取 CIE Y
Pdf.RenderColorimetricLuminosityMasks := False;
Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
if Bmp <> nil then
try
Bmp.SaveToFile('catalog-page-viewer.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
只有蒙版组的混合空间是 CIE 基时,这个属性才起作用:CalGray、CalRGB、Lab 或 ICCBased。组用 DeviceGray、DeviceRGB 或 DeviceCMYK 的蒙版在两种设置下都和从前渲染得完全一样,因为设备空间转灰度在两种模式下是同一条路
陷阱:/BC、/CS 与渲染缓存
亮度软蒙版的一致性取决于它的背景:填充组外蒙版区域的 /BC 颜色,必须与组内着色用同一条规则转换。HotPDF 在 v2.770.0 里把两者一起改了。着色用加权 RGB、背景却用 CIE Y,组包围盒结束处就会出现一道可见接缝——背景与内容走了不同辅助函数时,这个 bug 很容易引入
这一切发生之前,组属性还必须合法。HotPDF 从组的 /CS 条目读混合空间,并要求 /BC 为该空间的每个分量恰好携带一个数:RGB 基空间三个,CMYK 四个,灰度一个。/CS 缺失或 /BC 分量数不对,蒙版会在组执行前就失败,着色随后不加蒙版直接画出,而不是套着一个猜出来的背景。Separation、DeviceN 和 NChannel 不是合法的混合空间,所以蒙版组里的专色着色会先经 tint transform 转到替代空间,再计算亮度
最后,这个设置改变像素,所以它是缓存键的一部分。开与关 RenderColorimetricLuminosityMasks 渲染出的位图,在内存和磁盘渲染缓存里各自独立存放,两次 THotPDF.RenderLoadedPageToBitmapCached 调用之间切换属性,绝不会返回另一模式的位图。一条运维提示:v2.769.3 引入过一个仅 Win64 的回归——带透明度组、但组自身未设软蒙版的页面渲染失败;已在 v2.770.1 修复,64 位应用请用该版本或更新
// 并排比对:两种模式各渲染同一页。
// 每种模式有自己的缓存键,重复调用很便宜。
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);
Pdf.RenderColorimetricLuminosityMasks := True;
StrictBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);
// ... 两张都显示后释放:缓存调用返回归调用方所有的副本
ViewerBmp.Free;
StrictBmp.Free;
更完整的渲染 API——包括 DPI 选择、页面缓存和渲染器仍会近似的地方——见用 HotPDF 把 PDF 页面渲染成 Delphi TBitmap。如果你的问题是反方向的,不是画透明度而是为印刷或归档去掉文件里的透明度,PDFlibPas 的软蒙版与透明度扁平化讲的是那一侧
FAQ
HotPDF 能正确渲染透明度组内的 PDF Blend mode 吗
能,自 v2.769.3 起。以 Multiply 或其它非 Normal Blend mode 绘制的组,会拿自己的颜色和 alpha 与背景混合,组内容也按 ISO 32000-1 §11.6.6 的要求从 Normal 混合起步
HotPDF 为什么把非孤立组渲染两次
非孤立组没有自己的 alpha 平面。HotPDF 把它画在黑底和白底上,按 1 − (white − black) 恢复 alpha,牺牲一点速度,换来 Multiply 投影的正确
该把 RenderColorimetricLuminosityMasks 设为 True 吗
保持 False 即可与常见 PDF 查看器的显示一致。只有当你需要 ISO 32000-1 对 CIE 基蒙版组的 CIE Y 读法时才设 True,比如做符合性比对
Blend mode 透明度组、亮度软蒙版以及绘制它们的页面渲染器,都是 HotPDF Delphi Component 的一部分——一个面向 Delphi 和 C++Builder 的原生 VCL 库,无需外部 DLL 即可创建、编辑和渲染 PDF 文档