PDFium Component 在 Delphi 里经 TPdf.SaveAsShadingCreated 创建轴向与径向 PDF 渐变。你在 TPdfShadingCreate 记录里描述每个渐变(类型、色彩空间、坐标、颜色或停靠点、目标页),方法写出一版经过验证的副本——其中每一次填充都是真正的 ISO 32000-1 shading 字典,注册进页面 /Resources /Shading 字典、由 sh 操作符绘制,PDFium 于是原生渲染它
需求出现在报表与证书生成器里。设计师交来小样:一条柔和的蓝到白页头横带,或印章背后一圈径向光晕——而覆盖文本、路径和图像的 page-object API 对此无能为力。你可以拿一百个颜色略有差别的细矩形伪造渐变,但放大一看条带毕现,内容流膨胀,印出来也不行。PDF 从 1.3 起就有平滑 shading;难的是把它们写成渲染器真的会画的样子
PDFium Component 为什么把渐变做成一次单独保存?
TPdf.SaveAsShadingCreated 是保存操作而不是 page-object 调用,因为 PDFium 的公开 ABI 没有任何创建 shading 字典或发出 sh 操作符的函数。用 PDFium Component 从零创建 PDF时用的 page-object 层构建路径、文本和图像,到那里就停了。shading 住在更低一层:对象图和内容流语法里
所以方法分阶段工作。PDFium 先原生保存打开的文档,保住你经正常 API 做的每一次编辑。PDFium Component 随后重写那份暂存输出:为每个 shading 分配一个间接对象,给每个目标页加资源条目,向页面 /Contents 追加新内容流,再写一份全新的交叉引用表。在结果到达你的流或文件之前,会用严格的交叉引用检查重新加载一遍,加载失败则方法返回 False 并在报告里给出消息。文件重载先把输出写到临时文件,保存失败不会砸掉既有的目标文件
构造双色轴向渐变
axial shading(ISO 32000-1 §8.7.4.5.3,Type 2)沿从 x0 y0 到 x1 y1 的直线混色,所以 Coords 收四个值。radial shading(§8.7.4.5.4,Type 3)在两个圆之间混色,收六个:x0 y0 r0 x1 y1 r1。给普通 StartColor 和 EndColor 时,PDFium Component 发出带 /N 1 的 Type 2 指数函数(§7.10.3)——即线性插值——并设 /Extend [true true],端色延续到轴线两端之外
uses
System.SysUtils, PDFium, FPdfCompress;
procedure AddHeaderBand(const InFile, OutFile: string);
var
Pdf: TPdf;
Fills: TPdfShadingCreateShadings;
Report: TPdfShadingCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
SetLength(Fills, 1);
Fills[0] := Default(TPdfShadingCreate);
Fills[0].ShadingType := pstAxial;
Fills[0].ColorSpace := pscDeviceRGB;
Fills[0].Coords := [0, 792, 0, 640]; // x0 y0 x1 y1,从上到下
Fills[0].StartColor := [0.12, 0.35, 0.75]; // 顶边的深蓝
Fills[0].EndColor := [1, 1, 1]; // 下方变白
Fills[0].PageNumber := 1; // 从 1 起计
Fills[0].ResourceName := 'HeaderBand'; // 可选,默认 Sh1、Sh2...
if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
每个颜色的分量数必须匹配色彩空间:pscDeviceGray 和 pscCalGray 一个,pscDeviceRGB、pscCalRGB 和 pscLab 三个,pscDeviceCMYK 四个。pscICCBased 的分量数来自 IccProfile 里传入的 profile 字节,必须暴露 1、3 或 4 个分量;profile 作为带匹配 /N 的独立 ICC 流嵌入。校准空间接受可选的 CalWhitePoint(三个值,Y 为正)、CalGamma(CalGray 一个值、CalRGB 三个值),Lab 另有六值的 LabRange。每个坐标和分量在写出任何字节之前都查 NaN 和无穷
注意坐标在默认用户空间里,原点在页面左下。记住轴线只定义混色发生在哪:画出来的区域是裁剪允许的范围——这比听起来要紧,下面几节会展示
多停靠点渐变编译成 Type 3 stitching 函数
PDFium Component 把 Stops 数组变成 PDF 阅读器期待的函数:两个停靠点变成单个 Type 2 函数,3 到 256 个停靠点变成 Type 3 stitching 函数(ISO 32000-1 §7.10.4),由每对相邻停靠点一个线性 Type 2 段组成。每个 TPdfShadingCreateStop 带 Position 和 Color。Stops 非空时完全取代 StartColor 和 EndColor
stitching 函数需要三个手工很容易弄微错的数组。/Functions 装段,/Bounds 只装内部停靠点位置(k 段需要 k-1 个界),/Encode 把每个子域映回 [0 1],让每段见到完整的输入范围。组件从你的停靠点推导这三者,所以它对输入很严格:位置必须有限、严格递增、从 0 起到 1 止(容差 1e-6)。起点 0.1 的停靠点列表会让部分定义域没有函数,方法拒绝而不是猜
function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
I: Integer;
begin
Result.Position := Position;
SetLength(Result.Color, Length(Rgb));
for I := 0 to High(Rgb) do
Result.Color[I] := Rgb[I];
end;
procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
Fill := Default(TPdfShadingCreate);
Fill.ShadingType := pstRadial;
Fill.ColorSpace := pscDeviceRGB;
// x0 y0 r0 x1 y1 r1:印章中心的点长成 90 pt 的圆
Fill.Coords := [450, 160, 0, 450, 160, 90];
Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
Stop(0.4, [0.95, 0.75, 0.30]),
Stop(1.0, [1.00, 1.00, 1.00])];
Fill.PageNumber := 1;
end;
三停靠点渐变产出带两个 Type 2 段的 /Functions、/Bounds [0.4] 和 /Encode [0 1 0 1]。段边界是生硬的颜色接缝,停靠点列表不会跨缝抹平;接缝看着太利落就加停靠点,别指望阅读器替你插值出弧度
PDFium 为什么无声地什么也不画?
PDFium 只有两个细节都做对时才渲染新建的 shading:/ShadingType 必须是整数,sh 调用必须待在裁剪路径里。错任何一个,文件照开、结构照过、页面显示原样。没有错误、没有警告、没有一行日志
陷阱一:/ShadingType 是数字,不是名字
ISO 32000-1 表 78 把 /ShadingType 定义为整数:轴向 2,径向 3。这个写入器的第一版发出 /ShadingType /Axial,因为这样在十六进制转储里更好读。PDFium 用整数 getter 读这个键,对名字得到 0,把 shading 判为无效、加载失败、跳过 sh 操作符且毫无怨言。页面渲染成空白,PDFium 什么都不说。直到手工写了个最小 PDF、再用另一个独立渲染器打开——它报出未知的 shading type 0——原因才现形。教训可以推广到每个 PDF 写入器:规范把枚举定为整数,就写整数,自述性的名字再诱人也不写
陷阱二:只装 sh 的内容流没有包围盒
sh 操作符(ISO 32000-1 §8.7.4.2)把 shading 画在当前裁剪区域上,所以纸面上由页面自身范围兜底。但 PDFium 构建 shading 页面对象时,矩形要么来自活动裁剪路径、要么来自同流里更早的绘制操作符累积出的包围盒,而渲染器会剔除空矩形的对象。刚追加、只含 /Sh1 sh 的流两者皆无,渐变在光栅化之前就被丢掉。TPdf.SaveAsShadingCreated 因此读取每页的 crop box(回退到 media box,再回退到页面尺寸),并把每次调用包进显式 re W n 裁剪。原点为零的 US Letter 页面上,追加的流长这样:
q
0 0 612 792 re W n
/HeaderBand sh
Q
q/Q 对把裁剪和任何变换与既有页面内容隔离开,页面上后画的东西不会继承它们。你自己在别的工具里写 shading 调用时,照抄这个形状:同一流里的显式裁剪很便宜,还免除了对前文任何东西的依赖
自定义渐变矩阵怎么与裁剪互动?
PDFium Component 的渐变矩阵作为 cm 操作符施加,放在裁剪之后、sh 之前,所以裁剪保持整页,只有渐变几何被变换。把 HasMatrix 设为 True,填 MatrixA 到 MatrixF——与任何 PDF 变换矩阵相同的六个数。值必须有限、行列式非零,因为奇异矩阵会把 shading 压到一条线上。组件也把这六个值作为 /Matrix 记进 shading 字典,但对直接 sh 调用而言,定下 PDFium 所画坐标系的是内容流里的 cm
顺序在这里要紧,这是第三个陷阱。re 操作数按当前变换矩阵解释。把 cm 放前面,页面大小的裁剪矩形就跟着渐变一起缩放、旋转或斜切,压成椭圆的矩阵把可见区域也压成椭圆,页面局部失去填充。先在未变换的用户空间里裁剪、再拼接矩阵,才是预期结果:一个盖满整页的变换后渐变。Form XObject 印章及其六数矩阵指南逐项讲了矩阵每个分量的作用,需要自己拼矩阵时值得一读
procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
BuildSealGlow(Fill); // 以 450,160 为中心的径向填充
Fill.Coords := [0, 0, 0, 0, 0, 90]; // 圆在原点
Fill.HasMatrix := True;
Fill.MatrixA := 1.6; // 水平拉伸
Fill.MatrixB := 0;
Fill.MatrixC := 0;
Fill.MatrixD := 0.8; // 垂直压缩
Fill.MatrixE := 450; // 再移到印章中心
Fill.MatrixF := 160;
end;
读报告,知边界
TPdfShadingCreateReport 告诉你写了什么:SourceByteCount 和 OutputByteCount、CreatedShadingObjectNumbers(每个输入条目一个对象号,按顺序)、AttachedShadingCount、Succeeded 和 ErrorMessage。校验错误会点名条目和字段——比如不严格递增的停靠点列表——值得原样记日志
if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
Writeln(Format('%d shading(s) attached, %d -> %d bytes',
[Report.AttachedShadingCount, Report.SourceByteCount,
Report.OutputByteCount]))
else
Writeln('Rejected: ' + Report.ErrorMessage);
profile 刻意有界。只创建轴向与径向 shading;基于函数和网格的 shading(Type 1 与 4 到 7)和 pattern 色彩空间被拒。一次调用收 1 到 4096 个填充,页码必须存在,资源名限 127 个字符的字母、数字、下划线、减号和句点。加密输入在 trailer 阶段被拒。已签名文档需要 AllowSignedDocument := True,而且输出是完整重写而非增量更新,既有签名将不再通过验证——所以先加渐变、后签名。每次填充画在页面内容末尾、盖在已有内容之上;要背景效果就先建渐变、后加文本和图像,填充需要可切换时按 在 Delphi 里控制 PDF 可选内容层一文所述使用可选内容
FAQ
SaveAsShadingCreated 支持多停靠点渐变吗?
支持。向 Stops 传 2 到 256 个条目,位置从 0 到 1 严格递增。两个停靠点产出 Type 2 函数,三个及以上产出带推导 /Bounds 与 /Encode 数组的 Type 3 stitching 函数
文件看起来有效,渐变在 PDFium 里却不可见,为什么?
检查 /ShadingType 是整数 2 或 3 而不是名字,且 sh 操作符在同一内容流的 re W n 裁剪里运行。PDFium 无声跳过无效 shading 类型,并剔除空矩形的 shading 对象
创建的 shading 能用哪些色彩空间?
TPdfShadingCreate 接受 DeviceGray、DeviceRGB、DeviceCMYK、CalGray、CalRGB、Lab,以及配 1、3 或 4 分量 profile 的 ICCBased。Pattern 与 Indexed 色彩空间不在创建 profile 之内
渐变创建、可选内容和其余暂存写入器都随 PDFium Component for Delphi and C++Builder 发布,它为 Delphi、C++Builder 与 Lazarus 项目封装 PDFium