用 PDFium 组件去合并 PDF,大门只有一扇:ImportPages。不管你要怎么合,套路永远是那老一套:先造个空荡荡的接收文档(destination),挨个去开源头文件,大喊一声 ImportPages 把页面全给搬过来,关上源头文件,再接着干下一个。等这个大循环跑完了,一句 SaveAs 连本带利全存硬盘上。这儿可没啥专门的“合并模式”,更没啥花里胡哨的配置开关。所有的坑全藏在那些边角料里,其中有几个,你要是不留神,绝对能被咬掉一块肉
死磕到底的核心大循环
只要手里捏着两个 TPdf 实例,就足够你打遍天下了。一个用来当装东西的箩筐(接收文档),拿 CreateDocument 把它弄成个白板。另一个就像个干粗活的,轮流去把每个源文件都撬开。下面这段代码,就是把一长串文件名单全给嚼碎了,然后吐进一个文件里的套路:
procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
PdfDest, PdfSrc: TPdf;
InsertAt, I: Integer;
begin
PdfDest := TPdf.Create(nil);
PdfSrc := TPdf.Create(nil);
try
PdfDest.CreateDocument;
InsertAt := 1; // ImportPages uses 1-based destination position
for I := 0 to FileList.Count - 1 do
begin
PdfSrc.FileName := FileList[I];
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);
PdfDest.ImportPages(
PdfSrc,
'1-' + IntToStr(PdfSrc.PageCount), // full document range
InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
end;
PdfDest.SaveAs(OutputPath);
finally
PdfSrc.Free;
PdfDest.Free;
end;
end;
粗看这段代码,绝对有俩地方容易一不留神就放跑了。头一个就是 PDFium 怎么对待那些烂文件。Active := True 可是个死哑巴,绝对不会抛半个异常出来:不管文件是不见了、烂透了,还是被密码死死锁着,PDFium 全都会把这堆破事死死地捂在肚子里,然后让 Active 老老实实地装死变成 False。你要是不像第 10 行那样亲手查岗,烂文件就会像个鬼一样,一声不吭地从合并名单里消失,连个屁的痕迹都不留。等最后拿到合并完的文件,一数页数不对,你连到底是哪个孙子掉队了都找不出来
第二个要命的就是那个 InsertAt 计数器。ImportPages 的第三个参数是个从 1 开始数的位置,代表源文件里头一页要插在哪。传个 1,那就是老老实实当排头兵。每抄完一个源文件,这计数器必须加上 PdfSrc.PageCount(源文件的页数),这样下拨人马才会乖乖接在后头。你要是敢把这步给忘了,那后头来的人全都会往第 1 页那个坑里跳,把前头的人全踩死(覆盖);到头来,你手里的成品就只剩下名单上最后那个文件的残骸
专挑自己相中的页码来抄
谁说你非得把源文件连锅端?给 ImportPages 塞的第二个参数就是个专门用来点菜的字符串,规矩简单得很:逗号和连字符(-)。写个 "1-3",就把 1 到 3 页全包了;写 "2,4,6",就是只点这三道菜;写个 "1-",那就是从头一直抄到尾。这些字符串还可以拼着玩,比如 "1-3,5,7-" 就等于是把 4 和 6 给踢出了局。不过这儿有个死规矩你必须死死记住:这些数字,永远只认源文件里的排位,统统从 1 开始,跟你打算把它们插进接收文档的哪一行八竿子打不着。你要是想把一本 200 页的画册里的第 40 到 50 页抠出来,那字符串就是雷打不动的 "40-50",绝不是啥相对于接收文档的鬼扯位置
// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active := True;
if PdfSrc.Active then
begin
// Page 1 is the cover; pages 3-5 are the summary
PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
Inc(InsertAt, 4); // 1 cover + 3 summary pages = 4 pages added
PdfSrc.Active := False;
end;
在算那个要给 InsertAt 加上去的数时,你的眼睛只能盯着你到底抄了多少页,别去看源文件的 PageCount(总页数)。要是你刚才塞的是 '1,3-5',那你就是实打实地抄了 4 页,所以就乖乖加个 4。你要是脑子一抽,直接把 PdfSrc.PageCount 加了上去,那接收文档里就会冒出一个巨大的白板黑洞,后头跟着的那拨人也会被硬生生踹到老远去,全乱套了
ImportPages 到底能留下啥,又扔了啥
被 ImportPages 抄过来的页面,所有能摆在明面上的家伙什全完好无损。文字、矢量图、光栅图、嵌里头的字体,还有那些表单里的画(form XObjects),全都在页面的内容流(content stream)里一块儿跟过来了。就连那些直接打在页面上的印记(annotations)——批注、高亮、画的圈圈——也一个没跑,因为人家全都老老实实呆在页面字典(page dictionary)里,根本不靠文档那层来管
不过要是扯到文档那一层的元数据(metadata),那可就是另一番光景了。源文件 Info 字典里那些冠冕堂皇的标题(title)、作者(author)、主题(subject)还有关键字(keyword),全都被拦在了门外。被 CreateDocument 弄出来的接收文档,肚子里可是干干净净,一个字都没有。要是你想在合并完的文件里挂上这些牌子,你必须得在按 SaveAs 之前,亲手往 PdfDest 身上贴标签。TPdf 身上的 Title、Author、Subject、Keywords 和 Creator 这些属性全收大白话字符串,存盘的时候就会一字不落地给你刻在 Info 字典里
那帮子能让人指指点点、敲敲打打的交互式表单(Interactive form fields)就更让人头疼了。AcroForm 那些表单里的小挂件,全都是挂在文档级别的字典上的,根本不跟你页面的流混在一起。当 ImportPages 去抄一页带表单的东西时,由于这表单的长相早就在内容流里画死了,所以表面上看它确实被抄过来了;但这表单底下那个让它活蹦乱跳的挂件(widgets),还死死地钉在源文件的 AcroForm 结构上,根本没挪窝。所以在普通的合并里,源文件里那个表单当时填的是啥样,搬过来还是啥样,但它已经是个死了的标本,你再想改可门都没有。你要是非得让这些表单在新文件里还能让人填,那你就得在合并之前,把每个源文件全给“压扁”(flatten)了:这等于是把那些填好的字死死地烙在内容流里,然后一把火把那些碍事的挂件全烧了,这样你在最后的文件里就能看到干干净净的画面,而不是一堆点了半天也没反应的死尸
对付带密码锁的源文件
拿带密码锁的文件跟拿普通的没啥两样,不过就是在开门之前多一道手续。只要在去碰 Active := True 这个开关之前,乖乖把密码塞进 PdfSrc.Password 里,PDFium 自己就会拿着这把钥匙去撬门:
PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.Create('Wrong password or file cannot be opened');
PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
要是密码不对,那它照样装死,连个异常都不会抛(Active = False),所以那道查岗的防线一寸都不能退。不过这加密的护身符可传不到接收文档上:不管源文件被锁得多死,抄出来的页到了新文件里,全都是没穿防弹衣的光腚子(未加密的)。要是你合出来的文件也想带把锁,那就得在按 SaveAs 存盘之前,给 PdfDest 亲手上个锁
把战果给存盘
TPdf 上的 SaveAs 是个不挑食的主,不管是丢个文件路径还是塞个 TStream 进去它都吃。不过对于绝大多数的合并活儿来说,直接写文件绝对是最顺手的:
PdfDest.SaveAs('merged-output.pdf');
它后头还能拖着个可有可无的第二个参数,也就是 TSaveOption,那是用来管存盘做派的。要是啥都不写(默认 saNone),那如果文件本来是靠旧文件改的,它就给你弄个增量更新(incremental update);要是像现在这样拿 CreateDocument 白手起家造的,那它就干脆利落地把整个身子重写一遍。所以拿这法子搞出来的成品,绝对是个既紧凑、又干净的单版本文件。至于第三个参数 TPdfVersion,那是让你在下游大爷们非得要个特定版本的 PDF 头(version header)时,用来把版本给死死钉住的;要是直接给个 pvUnknown,PDFium 就会凭着肚子里的货色自己看着办
这篇显摆的 ImportPages 和 SaveAs 招数,全都是 Delphi 和 C++Builder 手底下那个 PDFium 组件 的看家本领