HotPDF 只需一次调用 BuildLoadedPageSVG,即可将已加载 PDF 文档的任意一页导出为独立的 SVG 标记,该调用以字符串形式返回完整的 SVG 文档。导出的标记包含页面几何信息、以真正的 SVG text 元素表示的文本、嵌入的位图图像,以及 PDF 操作符在每次绘图操作时建立的描边状态
最后这一点正是大多数自研转换器悄然崩溃的地方。把 PDF 页面转成 SVG 看起来像是坐标问题,实际上却是状态问题。PDF 是一台栈式机器,其图形状态会随内容流的解释而不断变化;SVG 则是一棵声明式的树,其中每个元素都各自携带自己的呈现属性。解释器在生成某个元素的那一刻若未能捕获相应状态,该状态就会在输出中彻底丢失,而且这种失败是无声的:你得到的是一份合法的 SVG,渲染出的却是一个细微出错的页面
为什么 PDF 页面不能直接转换为 SVG?
三处不匹配让这项转换变得不简单,而且这三处在你把结果与原文逐一对比之前都显得合情合理。第一处是 y 轴。PDF 用户空间从页面左下角向上增长;SVG 则从左上角向下增长。仅在页面层面做一次翻转能修正绘图坐标,却会同时把每个字形都翻转过来,因为翻转整个画布也会把字母形状一并镜像
第二处不匹配是继承关系。在 PDF 中,q 和 Q 会压入和弹出一个图形状态,其中包含线宽、线端点样式、线连接样式、斜接限制、虚线数组、虚线相位和透明度。在 SVG 中,一个未指定某属性的元素会从祖先分组继承该属性,这是完全不同的作用域规则。若导出器只跟踪当前变换矩阵而遗漏描边状态,Q 之后恢复的状态就会泄漏进后续元素
第三处在于 PDF 有不少内容是靠约定而非显式数值来表达的。线端点样式和线连接样式是整数,零线宽在设备空间里意味着一条细线而非不可见的线,填充操作符的星号变体改变的是缠绕规则而非颜色。这些都需要翻译,而不是简单照搬
覆盖常见场景的单次调用
对于为 Web 查看器、比对工具或设计交付导出页面这类日常任务,API 表面只有一个函数。BuildLoadedPageSVG 接受当前已加载文档中从零开始的页面索引,并以 AnsiString 形式返回 SVG 文档:
var
Pdf: THotPDF;
I: Integer;
Svg: AnsiString;
Output: TFileStream;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
Exit; // LoadFromFile 返回页数
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
Svg := Pdf.BuildLoadedPageSVG(I);
if Length(Svg) = 0 then
Continue;
Output := TFileStream.Create(Format('page-%d.svg', [I + 1]), fmCreate);
try
Output.WriteBuffer(Svg[1], Length(Svg));
finally
Output.Free;
end;
end;
finally
Pdf.Free;
end;
end;
同样的导出功能也以 export-svg 命令的形式暴露在 HotPDF 命令行工具中,这在构建管线和回归脚本里很有用,让你无需写一行 Pascal 代码就能得到一份可文本比对的页面表示。由于 SVG 是文本,它天然可以和 将 PDF 页面渲染为位图 中描述的位图路径搭配使用:位图告诉你页面看起来是什么样,SVG 则告诉你页面是由什么构成的
PDF 文本是如何映射到 SVG 文本元素上的?
HotPDF 按前缀矩阵乘以 CTM 再乘以文本矩阵再乘以字形翻转的顺序组合文本矩阵链,其中字形翻转是右乘 matrix(1,0,0,-1,0,0)。这个右侧因子的唯一作用就是抵消页面层面对字形形状造成的垂直翻转,否则在翻转的局部坐标系中绘制的 SVG 文本会呈现倒立状态。把这一修正放进矩阵而非放进特殊分支代码里,意味着旋转、镜像和倾斜的文本都能不加额外分支地正确呈现
水平定位使用 SVG text 元素的多值 x 语法,每个字符对应一个坐标,由每个字形前进量加上当时生效的字符间距 Tc 与词间距 Tw 累加而成。水平缩放 Tz 被折叠进文本矩阵的 a 列和 c 列,而不是单独输出,因此即便查看器忽略了这些特殊的文本属性,每个字形仍会落在 PDF 原本设定的位置上。经过复杂排版处理产生的文本,详见 复杂文字排版塑形,也会沿同一条路径处理,因为在内容流被解释时,排版器早已把字符簇解析成了带定位信息的字形
旋转与图像:两处容易搞反的翻转
/Rotate 项非零的页面需要一个预变换,它由针对旋转后画布高度的翻转和以 y 轴向上的显示空间表达的旋转组合而成。三种旋转矩阵分别是 90 度对应 (0,-1,1,0,0,W)、180 度对应 (-1,0,0,-1,W,H)、270 度对应 (0,1,-1,0,H,0),其中 W 和 H 是旋转前的页面尺寸。手工推导这些矩阵极易在恰好三处出现符号错误,因此导出器统一通过处理其他所有变换的同一个矩阵乘法例程来生成它们
嵌入图像需要自己的一次翻转,因为 PDF 图像空间把第一行采样放在单位正方形的顶边,而 SVG image 元素采用的是 y 轴向下的局部坐标系。因此输出的变换是 CTM 右乘 matrix(1,0,0,-1,0,1)。这一步做错会在其他方面完美无缺的页面上产生垂直镜像的照片,这类缺陷审阅者一眼就能看出,而自动化测试往往发现不了
图形状态设备究竟保留了什么?
HotPDF 通过一个独立的可选设备接口分派描边状态操作符 w、J、j、M 和 d,因此描边保真度是在不改变现有内容设备虚表、也不破坏针对旧版本构建的代码的二进制兼容性的前提下加入的。具体来说,导出的 SVG 收到的是翻译后的关键字,而不是原始的 PDF 整数:
// PDF 整数枚举值变成 SVG 关键字属性
// 线端点样式 0, 1, 2 -> butt, round, square
// 线连接样式 0, 1, 2 -> miter, round, bevel
//
// PDF 中零线宽意味着设备空间里的一条细线,因此导出器
// 会输出 vector-effect="non-scaling-stroke",以便在经过
// CTM 变换后依旧保持描边可见、宽度接近一个设备像素
//
// f* B* b* 选用奇偶规则,输出 fill-rule="evenodd"
// f B b 则保留 SVG 默认的非零缠绕规则
Q 恢复状态时会一并涵盖不透明度、线宽、线端点、线连接、斜接限制、虚线数组和虚线相位。嵌套的 Form XObject 也会在其边界处快照并恢复同一整套状态,因此定义在图章内部的虚线边框不会把自己的样式泄漏进后续的页面内容。如果你出于其他原因已经在跟踪裁剪和 CTM 行为,这与 EMF 和 WMF 矢量导入 中出现的是同一套状态模型,只是方向相反
上线前应了解的边界
导出器对自身的能力范围很坦诚,提前了解这些边界远比在生产环境中才发现要划算。颜色是通过 rg、RG、g 和 G 操作符传给 SVG 设备的。通过颜色空间加 scn 建立的填充,也就是 Separation、DeviceN 和 ICCBased 颜色的绘制方式,不会以已解析的 RGB 三元组形式到达设备,因此以这种方式使用专色的页面会导出其几何信息,却不会导出那些颜色。对于面向印刷的源文件,请改为栅格化,或者先把专色扁平化;这套上色模型本身在 渲染 Separation 和 DeviceN 专色 中有介绍
两处细节值得留意,能节省不少调试时间。十六进制颜色字面量以大写形式输出,因此一个断言 #ff0000 的测试会在完全正确的 #FF0000 面前失败。另外,SVG 设备是通过其接口进行引用计数的,这意味着释放它只需让接口引用超出作用域即可,而不是对该对象调用 Free,如果你要扩展该设备以在页面内容旁输出自己的标记,这一点差异就很关键
当你需要判断某份生成文档在两次构建之间是否真的发生了变化时,SVG 导出与结构化比较是天然的搭档。围绕已加载文档的更完整工具集,从渲染到编辑再到导出,记录在 HotPDF Delphi PDF 组件页面