简介面向Delphi 7与CBuilder开发者的PDF处理控件包提供页面创建、文本图像插入、页面增删复制移动、表单字段、水印与权限控制等完整能力也支持页面尺寸/方向/边距设置与自定义字体颜色大小位置适合文档管理、报表导出、电子签章等桌面应用软件开发场景。压缩包共收录617个文件约4.43MB除cpp/pas/h源文件外还含dfm窗体、res资源、bpr/cbproj工程及pdf示例、chm帮助与txt说明便于查看组件结构或直接编译集成。已有637人学习下载。同时可借助完整源码与示例来了解组件注册、控件放置、属性设置到API调用的完整流程水印位置/透明度/旋转角度及多文件合并与PDF解析等模块也能快速对照参考。对需要快速加入文档生成与编辑能力的项目开发团队具备直接工程复用价值。1. Delphi 项目里要 PDF 格式控件选型决定今晚加不加班老 Delphi 项目被塞了个需求把业务单据直接导出成 PDF客户点一下按钮就要拿到能打印、能存档的文件。你在编辑器里搜了一圈“delphi-pdf 控件”跳出几十个名字有免费的有收费的有的十年没更新有的连 Demo 都跑不起来。这个标题指向的正是 Delphi 生态里做 PDF 的选型与实践覆盖从手工拼 PDF 文件到控件 API 生成、从中文乱码到分页重叠的完整链路。这篇不打算写成产品说明书而是把三条路线摊开讲清楚你对着自己的项目情况就能判断哪条值得投入。适合两类人老项目里被临时塞了导出任务的开发者以及正在评估“自己写还是引库”的桌面端负责人。读完你至少能确定一件事——今晚要不要加班。2. 三条技术路线手写 PDF、开源库、商业控件怎么选2.1 手写 PDF 格式对象、引用、交叉表得自己拼PDF 文件不像 Word 那种二进制私有格式它本质上是一个带索引的文本文件。一个最小可打开的 PDF 由四部分组成文件头、若干对象、交叉引用表xref、文件尾trailer。对象之间靠/Type /Catalog、/Pages 2 0 R这种标签和间接引用来串联阅读器拿到/Root 1 0 R之后顺着引用往下找最终找到每一页的内容流。内容流里是BT /F1 24 Tf 72 720 Td (Hello) Tj ET这样的绘图指令它们决定这页画了什么文字、放在什么位置。我见过有人真的这么干过产品只输出一种固定版式的报告数据量不大于是直接在代码里拼字符串几十行就搞定了。这样做的好处是零依赖、发布包只有一个 exe缺点是所有细节都得自己负责。偏移量算错一个字节文件就打不开内容流里多一个空格某些严格解析器直接报错。常见做法是拿它当学习工具不建议直接上生产除非你的版式比 A4 纸还固定。手写还有一个被低估的工作量压缩。PDF 规范允许用 FlateDecode 压缩内容流也可以不压缩、直接明文写指令但不压缩的文件体积会明显偏大。真要走手写路线我一般会先把“对象模型”装进脑子目录对象、页面树对象、页面对象、内容流对象、字体对象这五个是起步配置。关系是 Catalog 指向 PagesPages 指向 PagePage 引用 Contents 和 Fontxref 表登记每个对象在文件里的字节偏移量。想清楚这张图后面用控件时遇到怪问题也能自己排查。2.2 开源库选型维护状态、API 风格、打包方式三看Delphi 生态里流传最广的 PDF 方案其实是那批免费库靠论坛帖子一代代传下来。选它们和选商业控件逻辑不一样免费意味着没有售后你需要自己看源码、自己修。我选型时只看三件事维护状态、API 风格、打包方式。维护状态看最后更新时间和支持的 Delphi 版本。有些库停在 Delphi 7 时代你拿 XE 或新版本编译会报一堆类型不匹配因为 string 已经从 AnsiString 变成了 UnicodeStringTFileStream 的构造函数签名也变了。判断标准很朴素库作者自己还在用它才能跟着新 IDE 走超过五年没动的代码再漂亮也当遗产看。API 风格决定你写起来顺不顺。比如添加页面的方法叫 AddPage 还是 NewPage画文字叫 TextOut 还是 DrawText字体加载是全局设置还是页面级绑定。这些命名差异直接体现在你每天的代码里。我一般会把候选库的 Demo 样例跑一遍重点看三个 API加页、画中文、保存。这三个能跑通说明基础路径是通的剩下细节都能查。打包方式直接影响交付体积。Delphi 习惯静态链接一个 exe 带走所有功能但有些库依赖 DLL 或运行时包发布时要附带额外文件。如果客户对“绿色单文件”有执念这个条件一票否决。顺带一提免费库里老的居多对 PDF/A、数字签名、表单域这些现代特性的支持普遍较弱需求里有这几个词基本可以直接跳到商业或者外部进程。2.3 商业控件与外部进程花钱买的是确定性与边界商业控件适合的场景很清晰项目要表单域、水印、加密、PDF/A 归档这些硬能力或者你公司的法务对第三方开源许可证有洁癖。付费换来的不只是功能还有“出了问题能找到人问”的确定性以及适配新操作系统版本时的持续更新。代价是授权费以及老控件在新系统上偶尔的兼容性抽风——装上去屏幕闪烁、打印偏移这种事遇到一次就够你记两年。外部进程是另一条务实路线Delphi 这边负责传参数真正的渲染交给一个独立工具完成。常见做法是把 HTML 转成 PDF前端排版能力强中文支持天然好表格样式也不受 PDF 控件绘画模型的限制。代价是部署时要多带一个 exe 或一组 DLL还得处理对方进程的退出码和超时。适合对格式美观度要求高的项目。三条路线的取舍我的习惯是画一条线需求是“生成固定版式的单据、报表”优先开源库和手工需求是“表单、签名、复杂交互”买商业控件需求是“排版自由、前后端一致”进程调度 HTML 转 PDF。选型没有绝对正确只有合不合适。一旦选了路线接下来的实现细节都围绕一个目标展开——用最小代码跑通一个能看的 PDF。3. 跑通最小 PDF从手工写格式到控件 API3.1 全手工生成一个能打开的 PDF 需要哪些零件先亲手拼一个最小 PDF你会发现控件替你干了多少脏活。下面这段 Delphi 代码生成一个 A4 页面上面写一行“Hello Delphi PDF”不依赖任何第三方库。核心是五个对象加一张交叉引用表每条记录都必须精确到字节。procedure TForm1.ExportMinimalPdf(const AFileName: string); var FS: TFileStream; Buff: AnsiString; Content: AnsiString; Offsets: array[0..4] of Int64; XRefPos: Int64; I: Integer; begin FS : TFileStream.Create(AFileName, fmCreate); try // 文件头第二行用不可打印字符防止被文本工具误判截断 Buff : %PDF-1.4#10%#$B5#$C3#$D5#$AA#10; FS.WriteBuffer(Buff[1], Length(Buff)); // 1号对象文档目录 Offsets[0] : FS.Position; Buff : 1 0 obj#10 /Type /Catalog /Pages 2 0 R #10endobj#10; FS.WriteBuffer(Buff[1], Length(Buff)); // 2号对象页面树只包含一个页面3号 Offsets[1] : FS.Position; Buff : 2 0 obj#10 /Type /Pages /Kids [3 0 R] /Count 1 #10endobj#10; FS.WriteBuffer(Buff[1], Length(Buff)); // 3号对象页面本身A4尺寸引用内容流和字体 Offsets[2] : FS.Position; Buff : 3 0 obj#10 /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources /Font /F1 5 0 R /Contents 4 0 R #10endobj#10; FS.WriteBuffer(Buff[1], Length(Buff)); // 4号对象内容流BT/Tf/Td/Tj 是一组文字绘制指令 Content : BT /F1 24 Tf 72 720 Td (Hello Delphi PDF) Tj ET; Offsets[3] : FS.Position; Buff : 4 0 obj#10 /Length AnsiString(IntToStr(Length(Content))) #10stream#10 Content #10endstream#10endobj#10; FS.WriteBuffer(Buff[1], Length(Buff)); // 5号对象字体Helvetica 是 PDF 基础字体阅读器自带 Offsets[4] : FS.Position; Buff : 5 0 obj#10 /Type /Font /Subtype /Type1 /BaseFont /Helvetica #10endobj#10; FS.WriteBuffer(Buff[1], Length(Buff)); // 交叉引用表登记每个对象的字节偏移 XRefPos : FS.Position; Buff : xref#100 6#100000000000 65535 f #13#10; FS.WriteBuffer(Buff[1], Length(Buff)); for I : 0 to 4 do begin Buff : AnsiString(Format(%.10d 00000 n , [Offsets[I]])) #13#10; FS.WriteBuffer(Buff[1], Length(Buff)); end; // trailer 指向目录对象startxref 指向交叉引用表 Buff : trailer#10 /Size 6 /Root 1 0 R #10startxref#10 AnsiString(IntToStr(XRefPos)) #10%%EOF#10; FS.WriteBuffer(Buff[1], Length(Buff)); finally FS.Free; end; end;这段代码的逻辑主线是先写五类对象每写一个就把当前文件位置记录到 Offsets 数组最后把这些偏移量按编号填进 xref 表。阅读器打开文件时先找到 startxref 指向的 xref 位置拿到每个对象的偏移后直接跳到对应位置解析所以偏移量差一个字节都会导致文件损坏。注意/Length必须和 stream 里的实际字节数完全一致我用Length(Content)动态生成避免手数出错——常见做法里手数长度是最容易翻车的点。参数说明$B5#$C3#$D5#$AA是四个高位字节注释防止文件被传输链路当纯文本处理MediaBox 的0 0 595 842是 A4 尺寸的磅值内容流里的24 Tf设置字号为 24 磅72 720 Td表示文字左下角坐标Tj负责真正画出文字。这个文件用浏览器打开几乎没问题但用 Acrobat 打开时对格式严格程度完全不同下面避坑章会展开讲。3.2 用控件 API 出一页报表最小调用链手工方式看懂格式之后日常开发就该交给控件了。不同库的类名会有差异但调用链高度相似下面用常见写法演示一页报表的最小命令var Doc: TPdfDocument; Page: TPdfPage; begin Doc : TPdfDocument.Create; // 新建文档默认 A4 try Page : Doc.AddPage; // 追加一页拿到页面对象 Page.LoadFont(msyh.ttc, 12); // 加载字体文件设置基础字号 Page.TextOut(30, 800, Hello PDF); Doc.SaveToFile(demo.pdf); finally Doc.Free; end; end;代码逻辑很直白创建文档、加一页、加载字体、画一行字、保存。顺序上有两个讲究。第一LoadFont必须在TextOut之前大部分控件在保存时才真正渲染字体没加载文字就按默认字体画中文直接变方块。第二SaveToFile只需要一个路径参数控件内部会处理页面的结束标记和对象索引不需要你手动调 ClosePage 这类收尾方法。参数说明里最容易踩的是单位。有的控件默认单位是磅pt有的默认是毫米mm30, 800在不同控件里的含义完全不同。判断方法很简单把 A4 宽度传成 595如果画出来的文字超出或者只在角落说明单位是磅传 210 才正常说明单位是毫米。这个差异藏得深等你把坐标从界面换算逻辑搬过来时才会发作。我习惯在封装层统一转成磅所有业务代码只认磅底层控件单位再乱也只影响封装函数内部。3.3 调用顺序和单位两个默认值暗藏的坑Delphi 控件普遍存在“设置延迟生效”的行为这是封装 PDF 绘图模型的代价页面对象只是一个记录器真正排版发生在文档关闭时。所以设置属性和画内容的顺序会直接影响输出。比如先TextOut再LoadFont字体设置会被部分控件忽略或者只在下一段文本生效。类似情况还有页边距、纸张方向、默认字体最好都在AddPage之后立刻设完再开始画具体内容。单位问题再补一个具体换算。磅pt是 PDF 的原生单位1 磅 1/72 英寸1 毫米 2.834645 磅。A4 宽 595 磅、高 842 磅就是由此算出来的。如果你的业务逻辑来自屏幕像素或者数据库里的毫米值建议在封装层写两个函数function MmToPt(AMm: Double): Double; begin Result : AMm * 72 / 25.4; end; function PtToMm(APt: Double): Double; begin Result : APt * 25.4 / 72; end;这两个函数能挡住一半以上的坐标玄学问题。屏幕坐标和 PDF 坐标的差异属于另一层换算下一章展开讲。先记住一件事所有控件在保存时做最终排版所以“设置”要在“绘制”之前绘制要在“保存”之前中间不要穿插改动页面尺寸的调用否则某些控件会直接抛异常或者静默忽略。4. 中文、坐标、分页三个必调参数一次说清4.1 中文不乱码字体嵌入、子集化还是转曲线Delphi 生成的 PDF 里中文变方块根因不是编码而是字体。PDF 阅读器在渲染文字时需要拿到对应的字形数据如果你的文件里只写了“微软雅黑”这个名字而没有嵌入字体文件阅读器去自己系统里找找不到就画方块。解决办法有三条路。第一嵌入字体。这是最正规的做法控件读取 TTF 或 OTF 文件把字形数据打包进 PDF。绝大多数控件提供类似LoadFont加一个参数控制是否嵌入的选项。嵌入又分完整嵌入和子集嵌入完整嵌入会把整个字体文件塞进去一个中文字体动辄几 MB子集嵌入只包含你用到的那些字形页面上的文字基本能覆盖全部字形时体积小很多。我一般默认开子集嵌入文件能小 80% 以上。// 常见控件写法加载字体后设置子集嵌入 Doc.FontManager.Load(msyh.ttc); Doc.FontManager.Subset : True; // 只嵌入用到的字形 Page.Font : Doc.FontManager.Use(微软雅黑); Page.TextOut(60, 700, 中文报表标题);第二转曲线。把文字当成矢量图形画进页面每个字变成一组路径指令。好处是不依赖任何字体文件也不依赖阅读器端有没有这个字体坏处是文字不可选中、不可搜索、体积可能变大。适合 Logo、印章、封面标题这类少量装饰性文字。如果需求是“客户必须能看到中文字但不能复制”转曲线反而是最优解。三条路的取舍用一张表就能看清方案体积文字可搜索依赖阅读器适合场景不嵌入字体最小是但可能乱码高仅限纯英文嵌入完整字体最大是低小册子、正式文档子集嵌入小是低日常业务报表转曲线中否极低标题、印章、形状4.2 坐标换算PDF 原点在左下角你的 Canvas 在左上角PDF 的坐标系规定原点在页面左下角X 向右、Y 向上而 Delphi 的 Canvas 原点在左上角Y 向下。这个差异让所有从界面代码迁移过来的绘制逻辑集体翻车。你在窗体上画线条用Canvas.TextOut(10, 100)到了 PDF 控件里同样的坐标文字会跑到页面顶部甚至超出页面之外。常见做法是维护一个换算函数把界面坐标转成 PDF 坐标。公式很简单YPdf PageHeight - YTop - LineHeight。YTop 是你脑子里“从页面顶部往下”的界面坐标LineHeight 是文字行高不减的话文字会整体偏上。封装成函数后其他地方一律调用function ConvertY(const AYTop, ALineHeight: Double): Double; begin Result : 842 - AYTop - ALineHeight; // 842 为 A4 高度 end;A4 高度 842 磅这个值不需要背用页面对象的 Height 属性拿更稳。有些控件直接提供了从顶部开始定位的模式能省掉换算但要注意它能省的前提是控件内部做了补偿补偿的基准通常还是左下角所以混合使用时容易两边各错一次看起来对实际偏差抵消了。我见过最典型的错误是把屏幕坐标直接搬过去文字画到页面上方之外整页内容整体下移一段距离——问题出在“画面外的部分被裁掉而不是报错”等发现时整份报告已经生成几百份了。4.3 分页与页脚换页后必须重置 Y 坐标PDF 不像 TCanvas 那样天然带滚动和分页一切分页逻辑都要自己算。画长表格或者连续单据时最常见的错误是内容画到页面底部之外或者第二页内容叠在第一页上。根因只有一个换页之后Y 坐标没有重置。正确流程是每次循环前检查当前 Y 是否到了页脚边界到了就加新页然后重新初始化 Y 和每页的表头。下面这段代码是典型的多行报表渲染骨架procedure RenderReport(Doc: TPdfDocument; Rows: TStrings); var Page: TPdfPage; Y: Double; I: Integer; const TopMargin 40; BottomMargin 40; RowHeight 18; begin Page : Doc.AddPage; Y : 842 - TopMargin; // 第一页顶部起始坐标 for I : 0 to Rows.Count - 1 do begin if Y BottomMargin then // 到页脚了换页 begin Page : Doc.AddPage; // 换页后重置 Y Y : 842 - TopMargin; DrawPageHeader(Page); // 每页重新画表头 end; Page.TextOut(50, Y, Rows[I]); Y : Y - RowHeight; // 行高以磅为单位向下递减 end; end;代码核心是Page : Doc.AddPage之后立刻重置Y并且把表头绘制放在换页后面。如果漏了重置第二页的内容会从Y残留值开始画和第一页的尾部重叠或者直接画到页面外。另一个细节是行高RowHeight的取值中文字体行高通常比字号大 30% 左右12 磅字号建议行高给 16 到 18 磅给太小会让多行文字贴在一起。每页表头这种东西单独抽一个DrawPageHeader(Page)过程换页时调一次避免在循环里复制粘贴一堆表头绘制语句。5. 避坑PDF 控件最常见的 5 个翻车现场5.1 中文方块 / 乱码不是编码问题是字体没进去现象生成的 PDF 在本地预览正常发给客户后打开全是方块或问号。原因你的机器上装了微软雅黑控件生成时只写了字体名字没把字体文件嵌入。客户机器没装这个字体阅读器找不到字形数据只能用默认字体替代。解决检查控件加载字体时是否默认嵌入。多数控件有一个EmbedFonts或Subset开关打开它。如果用的是转曲线方案不存在这个问题。验证方法也简单用文本编辑器打开生成的 PDF搜字体文件的二进制内容搜不到msyh说明没嵌入。这个动作应该写进每次发布的检查清单等客户截图发过来再处理一来一回半天就没了。5.2 Acrobat 提示文件损坏浏览器却预览正常现象同一个 PDFChrome 和 Edge 预览没问题Acrobat 打开直接弹“文件已损坏”。有些老客户只认 Acrobat这就成了必须解决的问题。原因PDF 生成端交叉引用表算错比如 xref 条目的偏移量没更新、对象列表数量对不上、startxref 指向的位置不对。浏览器的 PDF 解析器容错性强会扫描整个文件重建索引Acrobat 则严格按照 xref 表定位对象表错了就报损坏。解决先用十六进制编辑器打开文件跳到 startxref 标注的位置确认那里是不是xref四个字节开头。如果是手工拼文件重点查每个对象写入后是否立即记录FS.Position如果是控件检查是否有“在保存前又往页面画了内容”的操作导致对象顺序和索引不一致。这条血泪经验告诉我任何对 PDF 文件的手工拼接都必须假设阅读器比你想的更严格。5.3 文字画到页面外或上下颠倒坐标系算错了现象第一页内容整体偏高文字跑到页面边缘外或者文字在垂直方向上位置正确但上下颠倒。原因Y 坐标没做换算或者换算时漏了行高。PDF 原点在左下角界面约定是 top-left 起点两者相差一个页面高度。上下颠倒的情况一般不是坐标系问题而是某个控件把Tf指令解释成了基线位置而你当成顶部位置用。解决封装统一的坐标换算函数所有绘制调用都走它不要在界面现场手写842 - y。手写一次没问题手写十次必然有一处漏减行高。另一个有效手段是把常用的页面尺寸先打出来看一眼A4 高度确实 842你画的文字 Y 在 800那它应该在页面偏上的位置——用这个直觉反推哪里算错了。5.4 第二页内容叠在第一页换页后没重置画布现象多页单据生成后第二页顶部的内容和第一页底部的内容重叠或者第二页内容直接画到第一页页脚位置。原因换页后继续用旧的 Y 坐标绘制而新页面的坐标系是从左下角重新开始的。部分控件在AddPage之后之前的Page对象句柄会失效继续用旧对象画内容可能被静默忽略或画到旧页面上。解决换页后重新获取Page : Doc.AddPage并且立刻重置 Y 坐标和当前绘制对象。封装渲染循环时把“换页 重置 画表头”绑成一个函数不允许在循环体里直接AddPage。这个习惯能挡住一大半分页相关 bug尤其当循环嵌套多层时人肉维护 Y 状态几乎必错。5.5 文件体积翻倍同一个字体被重复嵌入现象一个 10 多页的 PDF纯文本内容却生成出 8 MB 文件。打开文件看字体资源同一个字形列表出现了十几次。原因部分控件在每次LoadFont时都执行一次字体嵌入如果你在循环里为每页重新加载字体等于把同一个字体文件拷贝了 N 份。中文字体普遍在 2 MB 以上重复嵌入直接爆炸。解决字体加载和页面创建分离。在文档生命周期内只加载一次字体后续页面通过引用复用。具体到代码把LoadFont从页面循环里挪到Doc.Create之后如果控件字体管理器提供全局注册表优先往里面注册。还有一个判断技巧直接查看 PDF 里出现的字体对象数量超过页数的一半就要警惕了。体积问题虽然不致命但客户拿 8 MB 的文件发给外部系统时被拒收是迟早的事。6. 进阶把体积压下来以及从生成走向解析6.1 压缩内容流与对象流控制最终体积的关键开关控制 PDF 体积除了字体子集化还有两个被忽略的开关内容流压缩和对象流。内容流指页面绘指令默认可能是明文打开压缩后能小 50% 以上对象流则是把多个小对象打包进一个流减少文件内部的碎片和 overhead。部分控件把这两个能力合并成一个枚举或一个布尔组合比如Doc.Compression : cpFlateDecode这种写法。这个方案有一个兼容性取舍非常老版本的 PDF 阅读器不认识对象流打开会空白或提示格式错误。如果你面向的客户内部有十年前的归档系统建议只开内容流压缩对象流先关掉等确认下游系统支持再说。另一个相关技巧是嵌入字体前先精简字形范围只保留用到的字符集能进一步压缩。我一般在最终交付前跑一个脚本对比压缩开关前后的体积和打开兼容性用数据做决定而不是凭感觉。6.2 解析已有 PDF生成之外的另一半需求PDF 控件不只用来生成还经常被问“能不能把客户发来的 PDF 数据提出来”。这个需求比生成难一个量级因为生成是你控制一切解析是你要还原别人控制一切的布局。文本提取、表格还原、坐标定位每一层都可能出兼容性问题。控件能提取的通常只是文本内容本身表格线和排版结构往往需要额外分析复杂表单甚至要配合 OCR 才能覆盖扫描件。如果你遇到解析需求我的第一个建议不是选控件而是确认输入来源程序生成的 PDF 提取相对稳定扫描件则完全是图像识别问题坐标系和字体信息全部丢失。第二个建议是优先用外部成熟工具做脏活Delphi 这边只负责调度和结果整理比自己从零写解析器省太多时间。解析这个方向值不值得投入取决于你手头是否有持续的 PDF 数据流如果只是偶尔一次外部工具配脚本就够了。现在接到任何 PDF 相关需求我都会先问一句是生成还是解析。这个问题决定后面所有选型。生成就走这篇的路线先定字体嵌入和分页策略解析就直接评估外部工具链。这个习惯帮我少加了不少班希望帮到你。本文还有配套的精品资源点击获取