PDFium 满江湖的声名都是靠着当“阅读引擎”打下的,它是 Chrome 那个 PDF 标签页背后的无名英雄。所以咱们得先澄清一件事:PDFium 组件绝对有能耐凭空捏出一个以前压根不存在的文档来。它的这套创作班底,说白了就是给 PDFium 底层那套页面对象 API 穿了件马甲:你先建个空壳文档,往里头塞几张指定好长宽的白纸,然后指哪打哪地把文字、矢量路径和图片按着坐标糊上去。这儿没什么见鬼的页面描述语言要你去学,更没哪个磨洋工的打印驱动横在中间碍事。你只管掉方法,库在底下吭哧吭哧地替你把 PDF 对象给攒起来,最后 SaveAs 大手一挥,完事儿
但丑话说在前头,你在这儿绝对捞不着半个排版引擎。这事大到必须得提前甩到桌面上来说清楚,因为底下那堆代码全是被这个现实给逼出来的。PDFium 组件只会把你塞给它的货,死死地钉在你给定的那个绝对坐标上,别的地方它去都不去。它不懂什么叫段落换行,也不会好心地帮你把多出来的文字踢到下一页去,更别指望着它能用几行几列的数据凭空变出个带框的表格来。这些脏活累活全都是你的。要是你跑这儿来是盼着能找到个像 Word 那个文档处理器一样,能自动帮你把排版搞得服服帖帖的玩意儿,那你最好现在就清醒清醒:这套 API 就是个极其低级但精准得吓人的搬砖工,它骨子里更像是在块光秃秃的画布上作画,而不是在那儿正儿八经地排版。但要是换成那些你早就把每个坑位都算得死死的发票、证书、标签或者报表,这份指哪打哪的精准,恰恰就是你做梦都想要的
能憋出一个文件的最少代码
从一个空荡荡的 TPdf 到一个安安稳稳躺在硬盘上的 PDF,中间就隔着这三招:建文档、塞白纸、存盘。至于剩下的,全都是你往这夹心饼干里塞的肉
uses
Vcl.Graphics, // for clBlack and TColor
PDFium; // TPdf lives here
procedure CreateBlankPdf(const FileName: string);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // empty in-memory document
Pdf.AddPage(0, 595, 842); // A4 portrait, in points
Pdf.AddText('First page', 'Arial', 18, 50, 780);
Pdf.SaveAs(FileName); // serialize to disk
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
有个小坑,专门坑那些看过点老黄历代码的倒霉蛋:在喊完 CreateDocument 之后,千万别画蛇添足地去搞什么 Pdf.Active := True。Active 这属性纯粹就是个探子,用来通风报信说“这会儿到底有没有个活着的文档句柄”,而 CreateDocument 早就把这活干完了,所以只要这方法一回头,这属性早就自己变成 True 了。你再死乞白赖地设一遍,往好了说是脱裤子放屁,往坏了说就是给后头接盘看代码的兄弟下绊子。Active 真正发光发热的时候是在你拍屁股走人的时候:在 Free 之前给它塞个 False,它就能干干净净地把底下的文档给超度了,这才是这帮句柄该有的体面死法。记住,CreateDocument 和那些用来加载文件的 open 调用,俩人是不共戴天的。你要是敢在 TPdf 的肚子里已经装了个活物的时候,硬逼着它再建个新的,这库绝对能跟你翻脸,所以你要是想拿它再干一票,必须得先把前头那个活物给掐死(关闭)
坐标系的规矩:从脚底板开始量
AddText 里的第二对参数,也是所有用来把东西拍到纸上的调用的参数,那是 PDF 用户空间里实打实的一个坐标点。这破坐标系的原点死死地钉在页面的左下角,X 轴往右跑,而 Y 轴是 往天上去 的。这里的一个单位就是一磅,不多不少正好是一英寸的七十二分之一,所以一张 A4 纸的身板就是 595 乘 842 个单位,美式信纸(US Letter)则是 612 乘 792。这个死不悔改往上爬的 Y 轴,绝对是逼疯无数好汉、让他们哭喊着“我的字跑到页面外头去了”的第一元凶,因为屏幕上和位图世界里的坐标,从来都是把原点顶在脑门上、Y 轴一路往下砸的。在一张 842 磅高的纸上,你想把个大标题拍在脑门附近,那它的 Y 坐标差不多得是个 780,绝对不是啥 60。哪天你要是发现你拍上去的东西飞到了一个找不着北的地方,拿页面的大高个减去你那个倒霉的 Y 值,十有八九就是你脑子里真正想填的那个数
AddPage 第一个吃下的参数是你要加塞的位置。这玩意儿的规矩是从 1 开始数数,但它好心地留了个 0 当“给我滚到最前面去”的后门。你要是喂它个 0 或者 1,这页纸就乖乖地插在最前头;你要是喂个跟当前页数一模一样的数字,它就老老实实地跑到最后面排队去。这刚塞进去的新一页,会立刻咸鱼翻身变成“当前页”,也就是后头那些画画的调用排着队去霍霍的主儿,所以你加完页之后,压根不用再脱裤子放屁地来一句“给我选中这页”。你要是一口气塞了好几页,后来又反悔了想跑回前头某页去画两笔,那就去动 PageNumber 来挪位置;但要是你就这么老老实实地建一页画一页,那这属性你这辈子都不用拿正眼看它
写几个字,顺带踩踩那不吭声的字体雷
AddText 的家当(参数表)里装着写几个字所需要的一切破烂:字符串本身、字体的大名、字号有几磅、X 和 Y 坐标这俩定海神针,后头还跟着选配的颜色、用来管透明度的 alpha 字节,还有一个用来转着圈玩的角度
procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
// Title in black, default opacity, no rotation
Pdf.AddText(Title, 'Arial', 20, 50, 780);
// A lighter byline 24 points below it
Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
// A faint diagonal draft stamp across the page
Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;
那个 alpha 字节的势力范围从 $00(见鬼去吧,隐形)一路管到 $FF(挡得严严实实),这就是为啥上面那个草稿章看起来是个正儿八经的水印,而不是一块黑漆漆的狗皮膏药:$30 算下来差不多只有百分之十九的透明度,刚刚好能让你透过它看清底下的字。那个角度则是让字绕着它那定海神针(锚点)逆时针转圈圈,所以塞个 45 度进去,就能得到个原汁原味的、从这个角斜杀到那个角的水印大印。这堆破事压根就不需要啥专门的水印功能来伺候。水印说白了,也就是个撑破了天、半明半暗、还被拧了一把的 AddText 调用罢了,至于它是躲在正文背后还是糊在人脸上,全看你是在画正文之前喊它,还是之后喊它
字体这事,必须得掰开了揉碎了好好说道说道,因为这雷炸起来连个响儿都没有。当你扔个字体的大名过去时,PDFium 组件会跑去找操作系统要这种字体的 TrueType 数据,然后死死地把它给塞(嵌入)进文档的肚子里,这就是为啥你在自己机子上捏出来的文件,拿去别人那台连这字体见都没见过的破电脑上,还能长得一模一样的原因。但坑就坑在,万一这大名没对上号怎么办:你手抖拼错了,或者是干活的这台机子上压根就没这号字体。它绝对不会好心地弹个报错异常出来救你。这库只会直接摆烂,给你造个光挂着这大名当招牌的文字对象,肚子里连个字体的影儿都没塞进去,然后甩手掌柜一样把这烂摊子扔给后头的阅读器,让阅读器自己去看着办(找个长得差不多的来凑合)。于是,这字在你自己的测试里看着人模狗样的,结果一到了装了别家字体的电脑上,不管它是个啥,只要它一睁眼,字距和字形当场就能给你来个大变活人。听好了,只用那些你百分之百确信生成这玩意的机子上有的字体大名,把字体列表当成是个比命还重的部署依赖,而且在你有胆子拍着胸脯打包票之前,最好先在一个一穷二白的干净系统上找个阅读器验验货
矢量形状:先把路铺好,再一锤定音
不管你是画线、画框子,还是填那些花里胡哨的色块,全都得经过“路径(path)”这道关卡。你得拿 CreatePath 来打头阵,这兄弟一出马,不仅把起点给你定死了,还一口气把你那些臭规矩全给定下来了:该怎么填色、填色和描边分别穿啥颜色的马甲还得挂着各自的 alpha 透明度、线得有多粗、线头线尾是个啥死样、拐个弯又是个啥德行。接着,你就拿 LineTo、BezierTo 和 ClosePath 这帮家伙去开疆拓土。最后,你必须得甩出一个 AddPath,把这费了半天劲才搞完的路径一板砖拍到页面上。这最后拍板的一步最容易被人忘在脑后,你要是敢漏了它,你前头忙活的全得打水漂,页面上连根毛的痕迹都不会有
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
// A thin horizontal rule. The rectangle overload sets a box directly:
// X, Y, Width, Height, then fill mode and colors.
Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
True, clBlack, $FF, 1.0);
Pdf.AddPath;
end;
procedure DrawTriangle(Pdf: TPdf);
begin
// Point overload: start at the first vertex, line to the rest, close.
Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
Pdf.LineTo(300, 300);
Pdf.LineTo(250, 400);
Pdf.ClosePath;
Pdf.AddPath; // nothing is drawn until this runs
end;
这俩重载的家伙能把平时的糙活都给包圆了。那个吞四个坐标的家伙,吃下 X、Y、宽和高,一招就能给你拍出个跟轴平行的方块来,你要是画个横线、弄个表格的边框,或者搞个带颜色的背景板,找它准没错。而那个只吞俩坐标的家伙,就只管给你找个起点安营,剩下的边界线全得靠你自己拿着 LineTo 和 BezierTo 一点点去描。填色规矩(Fill mode)管的是那些互相打架重叠的地盘最后到底涂个啥颜色:fmWinding(非零环绕)对付那些个实心疙瘩最顺手,fmAlternate(奇偶规则)则是专门收拾那些带窟窿眼或者自己咬自己尾巴的奇葩形状的,至于 fmNone,那就是个狠心不管填色、只留下个光秃秃描边轮廓的狠角色,上面那条横线就是拿它搞出来的
表格就是一堆乱窜的路径和文字,全是手搓出来的
因为这世上压根就不存在啥现成的表格大招(primitive),所以表格说穿了,就是个让你头皮发麻的死循环。你得去当那个拍板的祖宗:这列的 X 坐标该挪多少,这行的高度得留多少,然后拿着 AddText 挨个格子去塞字,最后再用画方块的路径把边框给描出来。这里的算术活全是你一个人的,但好在它也就是些没技术含量的糙活,一旦你把这套公式写出来了,随便换啥网格它都能照吃不误
procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
ColX: array[0..2] of Double = (0, 110, 210); // column offsets
RowH = 20;
var
Y: Double;
Row: Integer;
begin
// Header row
Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);
// Rule under the header
Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
Pdf.AddPath;
// Data rows, stepping Y downward each iteration
Y := Top;
for Row := 1 to 3 do
begin
Y := Y - RowH;
Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
end;
end;
看仔细了,那 Y 每次循环都是拿着行高在那儿往下做减法,还是那句老话,因为在这里头,往上爬才是正道。这也恰恰是没法去量字有多宽这事儿最让人脑壳疼的地方:要是哪项货物的名字长得要死,它绝对会不要脸地直接窜进下一列的地盘去,因为这破库压根就不知道你那串字画出来到底有多宽。你要是接手的都是些被你捏得死死的老旧格式,那你大可以阔气点,把列宽给得足足的,然后闭眼往前走。但凡你要对付的是那种想怎么长就怎么长的妖魔鬼怪,你要么在源头就把它们给卡死,要么就得在把它们拍上去之前,自己亲自去量量那些字到底有多宽,而恰恰到了这种节骨眼上,那些专门干排版的老爷库才真正体现出它们骗钱的底气来
图片和那永无止境的多页地狱
那些个光栅(位图)破烂全都是靠着一帮专门的图片小弟给抬进来的。AddPicture 接过一个早就装填好的 TPicture 把它往坑里一扔,还允许你顺带报个数来把它给拉扯成指定的长宽;AddImage 更接地气,不管是直接扔个文件路径还是塞个 TBitmap 它都敢接;AddJpegImage 更是个狠角色,直接拿着 JPEG 的字节流就开干,连绕道位图去投胎这步都省了。和前面的这堆破规矩一样,用来定坑位的坐标永远都是用户空间里这张图的左下角,而你报的那串宽和高,是在纸上拿磅量出来的地盘大小,跟那图本来有几个破像素半毛钱关系都没有
procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
Pdf: TPdf;
P: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
for P := 1 to PageCount do
begin
Pdf.AddPage(P, 595, 842); // append; the new page becomes current
Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
'Arial', 10, 50, 30); // footer near the bottom edge
// ... draw this page's body here ...
end;
Pdf.SaveAs(FileName);
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
搞个多页文档,说白了就是把单页的那套把戏塞进个死循环里去。每个 AddPage 都会在屁股后头加塞一页,然后顺理成章地把它捧成当前页,所以你接下来不管是画正文还是糊页脚,全都会结结实实地落在你刚拉来的那张白纸上。在这个循环里你压根不需要去管啥 PageNumber,因为加新页的时候光标自己长腿就跑那儿去了;只有当你脑子抽了,非要跑回前头你早就盖完的烂尾楼里去画两笔的时候,那破玩意儿才派得上用场。最后等所有的纸都画满了,喊一嗓子 SaveAs 就能全剧终。你要是想搞个能进博物馆(存档)的货色,而不是个普通的破文件,同一个文档对象肚子里还揣着 SaveAsPdfA 和其它几个符合乱七八糟标准的兄弟,所以你要换啥输出标准,大不了就是去调个不同的保存调用罢了,压根用不着去换啥建文档的套路
这玩意儿到底该用在哪个烂摊子上
把话敞开了说吧,PDFium 组件的这套创作 API,它就是个老老实实、规规矩矩地披在 PDFium 那个页面对象模型上的一层薄皮:它干的都是真刀真枪的建文档、嵌字体、画矢量和倒光栅图的活儿,最后给你序列化出一个挑不出刺的标准文件来。但它绝对不是,也从来没把自己打扮成一个能给你自动折腾排版的文档引擎。那条楚河汉界,就是文字排版。如果你的活儿是有模有样的模板——不管是发票、证书、标签,还是死死钉在网格里的仪表盘报表——那这种靠绝对坐标来指哪打哪的套路,绝对是既干脆又快当,代码看着也清爽。要是你的活儿是一坨老太婆的裹脚布,非得让它自己知道什么时候换行、什么时候滚到下一页去,那你绝对会发现自己其实是在这些简陋调用的头顶上,硬生生地重新捏一个排版引擎出来,毫无疑问,这玩意儿绝不该是你在这个节骨眼上干这活的首选兵器。搞清楚你自己到底站在这条界的哪一边,这仗你就已经打赢一大半了
这篇用来揭短的创建方法,全都是为了那套在 Delphi 里头打拼的 PDFium 组件 写的,它把这套创作的手艺,跟 PDFium 真正在道上扬名立万的渲染和抓文字(文本提取)手艺,完美地绑在了一条船上