页面拼接将现有页面的内容组合到一个共享的画布上,这与合并文件是不同的操作;losLab PDF Library 为 Delphi 实现了 Form XObject 页面拼接和可组合的水印模板:StitchPageOverlay、StitchPagesSideBySide 和 StitchPagesVertically 能够以完全的矢量保真度在任何缩放比例下放置捕获的页面,而 TPDFlibCompositeWatermark 可以在一次调用中跨页面范围堆叠文本和图像水印;如果您实际需要的是将多个文档的整页追加到一个文件中,这属于合并系列,在关于快速字节级 PDF 合并的文章中进行了介绍;本文介绍的是在单个坐标空间内组合页面内容
引导至此的需求是平凡且顽固的;某所学校希望将两张试卷页面拼在一张横向版面上,从而节约一半的纸张;一位本地化审查员希望英文页面及其翻译在单个页面上并排,并一起滚动;一个法律团队希望每一份草案一次性盖印上三个图层:角落里的徽标、中间呈对角线的 CONFIDENTIAL,以及一个小页脚行(仅限第 1 到 3 页);这三者都是同一个底层问题,即将已经排版好的页面内容以其他尺寸放置在其他地方,且不降级为位图
为什么级联 PDF 内容流会失效?
天真的拼接做法是将源页面的 /Contents 流复制到目标页面中,但这会由于三个结构性原因而失效;首先,坐标会发生冲突:两个内容流都假定它们自己的原点位于自己 MediaBox 的左下角,因此粘贴的内容会以错误的位置落在现有内容的顶部;其次,资源名称会发生冲突:每个页面都会对照自己的 /Resources 字典解析像 /F1 Tf 和 /Im0 Do 这样的操作符,两个页面经常将相同的名称绑定到不同的字体或图像,导致其中一个静默渲染了错误的资源;第三,原始流复制根本无法提供缩放,您能做的顶多是平移
Form XObjects 一举解决了这三个问题,这也是 losLab PDF Library 将其拼接建立在其之极点的原因;ISO 32000-1 §8.10 将 form XObject 定义为一个独立的、拥有自己 /Resources 字典和自己坐标空间的内容流,通过在任何当前变换矩阵处于活动状态下调用其名称来放置在页面中;CapturePage 将整个现有页面(内容加资源)包裹到一个 /Subtype /Form XObject 中,然后拼接函数使用 cm 矩阵对其进行定位:随心所欲地缩放、平移和旋转;因为源保持矢量,文本在任何缩放级别下仍是文本,路径也仍是路径,这与使矢量图形和渐变模式分辨率无关的属性相同;一切都没有被光栅化,而且隔离的资源字典意味着捕获的页面不会与目标页面因名称 /F1 产生争用
如何将两个 PDF 页面组合到一个页面上?
StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) 是直接的答案:它捕获两个源页面,创建一个新的更宽的页面,并在左右两侧放置两个捕获的内容(它们之间留有装订线),每个都经过缩放以适应 TargetHeight;该函数返回新页码(基于 1)或在失败时返回 0,两个源页面都会作为操作的一部分被捕获并隐藏起来;每页两卷的考试拼版只需三行代码:
var
Lib: TPDFlib;
NewPage: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('exam.pdf', '');
// Pages 1 and 2 on one new wider page with an 18pt gutter,
// both scaled to a 595pt output height (A4 landscape)
NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
if NewPage > 0 then
Lib.SaveToFile('exam-2up.pdf');
finally
Lib.Free;
end;
end;
对于垂直组合,StitchPagesVertically(PageRanges, Gap, TargetWidth) 将整个页面范围堆叠为一个高大的、海报风格的页面;PageRanges 接受标准范围语法(如 '1-3,5');每个源页面都经过缩放,使其宽度与 TargetWidth 匹配,因此每个页面贡献的高度是其自身高度乘以其自身缩放因子,而输出页面高度是这些高度之和,外加每对页面之间的一个 Gap;这正是您针对连续滚动审查文档或从信纸大小源装配的长收据所想要的形态
如何将一个页面叠加到另一个页面上?
StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) 将一个现有页面绘制到另一个现有页面上,而不是创建新页面;源页面被捕获为 form XObject,并在您给定的矩形处绘制在目标页面上, Opacity 从 0 到 1(1 为不透明);宽度和高度是自由参数,因此如果您的比例与源页面自身的宽高比不同,放置的内容会拉伸;当您需要源页面的无失真缩略图时,请传递等比例的宽高对;典型的用法是其审批块作为独立页面维护并盖印在封面上:
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('report.pdf', '');
// Page 5 holds the approval block; place it on page 1
// in a 260 x 180 box at 45% opacity. Page 5 is consumed:
// it is captured and hidden, so pages 6..n become 5..n-1
if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
Lib.SaveToFile('report-approved.pdf');
finally
Lib.Free;
end;
end;
捕获后页码会发生什么变化?
CapturePage 会消耗源页面:其内容移入 XObject,现在为空的页面将被隐藏,因此源页面之上的每个页面都会立即向下移动一位;这是整个拼接 API 中最重要的语义;losLab PDF Library 在每次拼接调用内部处理数学计算,当目标位于源之上时,StitchPageOverlay 会调整目标索引,且 StitchPagesSideBySide 先捕获高页码的页面,使得第二次捕获的索引仍有效,但您自己的外围代码必须说明调用之间的移位;两条规则可以保持多重拼接代码正确:在第一次捕获*之前*记录任何您需要的页面大小或页码,因为之后它们会改变;而且当您在循环中自己捕获多个页面时,请以递减的页面顺序进行迭代,这样隐藏后面的页面绝不会扰乱您尚未捕获的前面页面的索引
如何将多层水印应用于页面范围?
PDFlibWatermark.pas 中的水印模板层将“一次定义,应用于多页”转为对象;TPDFlibWatermarkBase 携带每个水印共享的内容:九点 Position(wpTopLeft 至 wpBottomRight,外加使用原始 X/Y 坐标的 wpCustom)、角度(以度为单位)、Opacity (从 0 到 1),以及 PageRange 字符串(空表示所有页面);TPDFlibTextWatermark 添加了文本、字体 ID(默认为 Helvetica)、大小和 RGB 颜色;TPDFlibImageWatermark 添加了来自先前添加图像的图像 ID,以及放置的宽度和高度;TPDFlibCompositeWatermark 持有子水印列表并递归应用它们, TPDFlib.ApplyWatermark 是单次调用入口点,它尊重模板自己的页面范围并返回被盖印的页面数量;三层法律图章如下所示:
var
Lib: TPDFlib;
Logo: TPDFlibImageWatermark;
Diagonal, Footer: TPDFlibTextWatermark;
Stack: TPDFlibCompositeWatermark;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('draft.pdf', '');
Stack := TPDFlibCompositeWatermark.Create;
try
Logo := TPDFlibImageWatermark.Create;
Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
Logo.Position := wpTopRight;
Logo.Width := 90;
Logo.Height := 32;
Stack.Add(Logo);
Diagonal := TPDFlibTextWatermark.Create;
Diagonal.Text := 'CONFIDENTIAL';
Diagonal.Position := wpCenter;
Diagonal.Angle := 45;
Diagonal.FontSize := 60;
Diagonal.Opacity := 0.15;
Stack.Add(Diagonal);
Footer := TPDFlibTextWatermark.Create;
Footer.Text := 'Internal review copy';
Footer.Position := wpBottomCenter;
Footer.FontSize := 9;
Footer.Opacity := 0.6;
Stack.Add(Footer);
Stack.PageRange := '1-3'; // children inherit this range
Lib.ApplyWatermark(Stack);
Lib.SaveToFile('draft-marked.pdf');
finally
Stack.Free; // composite frees its children
end;
finally
Lib.Free;
end;
end;
值得了解的边界限制
这里有两个真实的限制,它们都是由机制的工作原理决定的;首先,捕获仅包裹了页面的内容流和资源;批注、链接、表单域和注释是作为单独的对象存在于页面字典中,而不是在内容流中,因此它们不会进入 XObject;拼接后的布局带走了可见的页面内容,但并没有带走其数据源的交互层;其次,九点文本定位是一种近似值:在没有完整排版引擎的情况下,库无法获知字符串的确切渲染宽度,因此在解析诸如 wpCenter 这样的位置时,它将文本框估计为页面宽度的 40%;这对于居中短标签已经足够精确了,但对于长字符串的像素级精准放置,请切换到 wpCustom 并自行计算坐标
规模在这里并不是实际的限制,因为捕获和拼接操纵的是对象引用,而不是重新编码页面内容;如果您的源文件有几百兆字节,在进行拼接前,可以应用直接访问的大文件合并和拆分指南中描述的相同方法;当您需要的布局超出了三个预建拼接调用所覆盖的范围时,此处展示的拼接函数和水印模板类,以及更底层的 CapturePage、DrawCapturedPage 和 DrawCapturedPageMatrix 原语,都在当前的适用于 Delphi、C# 和 VB.NET 的 losLab PDF Library 中随附提供