HotPDF 通过 ImportSVGFormXObject 把一个 SVG 文件作为可复用的 Form XObject 导入已加载或正在编写的 PDF,该方法返回所创建 form 的资源名称。放置操作是另一个独立调用 DrawSVGFormXObject,因此一次导入即可以不同尺寸盖印到许多页面上,而不会重复哪怕一个字节的图形数据
这种拆分的意义比听起来更大。一个以 300 DPI 栅格化后贴到 400 个页面上的 logo,就是 400 份位图副本。同一个 logo 作为 Form XObject,则是一个对象被引用 400 次,它在任何缩放比例下都保持清晰,并按设备分辨率打印,而不是构建时看似足够的某个 DPI
为什么用 Form XObject,而不是每次都重新绘制 SVG?
Form XObject 是 PDF 自身用于复用内容的机制:一个自包含的内容流,拥有自己的边界框和资源字典,通过名称调用。把 SVG 导入其中,能带来直接绘制所没有的三个特性。图形数据在文件中只存在一份。每次放置只是一个小矩阵加一次调用。而且形状始终保持矢量,因此查看器放大到 800% 时绘出的是曲线,而不是像素
微妙之处在于资源。form 所用的字体和图形状态必须存放在该 form 自己的 /Resources 字典中,而不是导入时恰好当前那一页的资源里。HotPDF 会把 form 所需的 Base 14 字体和 ExtGState 条目直接写入 form 本身,因为如果 form 借用了它首个宿主页面的资源名,那么它在那一页上能正确渲染,但之后放置到其他任何页面时都会产生无法解析的资源名
两次翻转:一次用于几何形状,一次用于字形
SVG 的坐标系从左上角向下延伸;而 PDF Form XObject 使用 PDF 用户空间,从左下角向上延伸。根 viewBox 变换执行一次 Y 轴翻转,使几何形状落在正确位置。文本随后还需要第二次局部翻转,因为在已经翻转过的坐标系里绘制的字形会是倒置的
这与把 PDF 页面导出为 SVG 时所做的反向修正相同,详见 把 PDF 页面导出为 SVG。这一步出错在带文本的图形中很容易看出来,在不带文本的图形中却几乎看不出来,这也是为什么带标语的 logo 比单独的 logo 更适合作为冒烟测试
uses
HPDFDoc;
var
Pdf: THotPDF;
SvgBytes: TBytes;
FormName: AnsiString;
Stats: THPDFSVGImportStatistics;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalogue.pdf';
Pdf.BeginDoc;
SvgBytes := LoadFileBytes('brand-logo.svg');
FormName := Pdf.ImportSVGFormXObject(SvgBytes, Stats);
if FormName = '' then
raise Exception.Create('SVG rejected - see the import statistics');
// 一次导入,多次放置:图形字节只存在一份
for I := 1 to 40 do
begin
if I > 1 then
Pdf.AddPage;
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 780, 120, 40);
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
超链接如何在整个过程中保留下来?
它们无法在 form 内部保留下来,因为 Form XObject 是内容,而 PDF 链接目标是注释(annotation),注释归属于页面。因此源 SVG 中的 <a> 元素不能简单地复制进 form 的内容流
HotPDF 的解决方式是:在解析阶段把链接矩形保存为 form 空间的元数据,并在每次放置时把它们具体化为页面级的链接注释。DrawSVGFormXObject 会通过你传入的目标矩形对这些矩形做映射,因此同一个 form 以不同尺寸放置两次,就会得到两组尺寸正确的可点击区域。链接矩形只按根尺寸映射,从不进入内容流,因此无论携带多少链接,绘出的图形本身都完全一致。当你只想要图形而不需要交互性时(例如仅供打印的页面),可以传入 AddLinks = False
导入器拒绝什么,以及为什么这正是关键所在
SVG 是一种带脚本、外部引用和实体展开能力的 XML 方言,这使得任意一个 SVG 文件都可能像任意一个 HTML 文件一样具有敌意。导入配置对一切试图触及文档之外的内容采取 fail-closed(默认拒绝)策略:外部资源引用、脚本、事件处理属性、DTD,以及任何非本地片段引用的 url()
fail-closed 意味着导入直接失败,而不是悄悄丢弃有问题的结构。这一区别对批处理流水线很关键,因为悄悄丢弃会产生一个缺了某些内容却没人知道的文档,而拒绝则会产生一个你可以隔离检查的文件。如果你接受用户上传的 SVG,这正是你想要的行为,它可以与 面向不受信任图像的编解码器进程隔离 中所述的进程级隔离配合使用
如果你同时也在渲染彩色字体,有一处内部边界值得了解。用于 OpenType SVG 彩色字形的严格 SVG 子集走的是另一个独立的入口点,并保留了它原本更窄的失败行为。通用图形导入是一条独立路径,因此为导入图形新增弧线、透明度和 CSS 支持,并不会扩大字体字形所允许包含的内容范围
渐变、遮罩与不栅格化规则
渐变导入为受原始几何裁剪路径约束的有边界矢量色带,而不是栅格化图像。遮罩采用相同的处理方式:遮罩轮廓变成矢量裁剪路径。其结果是,一幅渐变密集的插图仍然保持分辨率无关且体积小巧,而一个会栅格化的导入器则会产生一个质量在导入时就已固定的大体积图像对象
代价体现在极端情况下的保真度上。用色带近似平滑渐变,在数学上并不等同于原始 SVG 渐变;而对于带有数十个重叠软遮罩的图形,如果改为渲染成高分辨率图像,效果反而会更接近原图。关于 PDF 原生支持的着色模型,以及何时应该直接使用它们,参见 渲染轴向和径向着色
// 读取导入统计信息,可以了解实际导入了哪些内容
Writeln(Format('nodes=%d css rules=%d gradients=%d masks=%d',
[Stats.NodeCount, Stats.CSSRuleCount, Stats.GradientCount,
Stats.MaskCount]));
Writeln(Format('text runs=%d links=%d form bytes=%d',
[Stats.TextRunCount, Stats.LinkCount, Stats.OutputBytes]));
// 以两种不同尺寸放置同一个 form,会得到两组正确的链接集合
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 700, 240, 80);
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 320, 700, 120, 40);
// 仅打印用途的放置:只有图形,没有可点击区域
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 60, 120, 40, False);
SVG 导入在其他矢量路径中处于什么位置
现有三条矢量导入路径,它们并不能相互替代。SVG 导入适合设计工具产出的图形,以及来自 Web 流水线的任何内容。元文件导入(在 EMF 与 WMF 矢量导入 中有介绍)适合 Windows 应用程序生成的内容,以及那些绘制到设备上下文的图表组件。而通过页面 API 直接绘制,适合由你自己的代码计算出来、完全没有中间格式的图形
就可复用性而言,SVG 导入对重复出现的元素尤其有利。信头、logo、水印、认证标记和装饰性分隔线,都是在许多页面上以相同方式绘制的,每一种都只需要一个 form 对象,加上每页一次调用即可。在断定这无关紧要之前,先在真实文档上测一测文件体积的差异:在一本每页都带矢量 logo 的 500 页产品目录中,一个 form 与 500 份内联副本之间的差距通常有好几兆字节
SVG 导入、元文件导入和直接矢量绘制,在 Delphi 与 C++Builder 中共用同一个内容流写入器;完整功能列表见 HotPDF Delphi PDF 组件页面