
简介Delphi-Android-GeraPDF-master是一份面向Delphi开发者的Android PDF生成库源码专门解决在FireMonkey跨平台框架下动态创建PDF文档的难题。其核心PDFGenerator组件提供了从页面操作到内容编辑的完整API可创建新页面并设置页面大小与方向支持多字体、多颜色、多尺寸文本插入以及对齐换行控制也能绘制直线、矩形、椭圆等基本图形还可插入图片、构建表格并添加链接、书签、注释等交互元素Androidpdf模块则负责底层图形渲染与样式控制整体分层明确便于按需裁剪。压缩包共7个文件以.pas核心源码、.fmx窗体界面、.dpr工程文件为主辅以AndroidManifest模板、资源配置与README说明整包仅45KB几乎是一个可直接导入编译的基础工程。已有115人学习下载适合具备Delphi基础、希望在移动端快速集成PDF导出功能的开发者既可参考其模块划分和调用方式也可以直接在现有代码上二次扩展落地电子发票、数据报告、合同模板等场景。1. 为什么说 Delphi-Android-GeraPDF 是 Android 上被低估的 PDF 生成方案在 Android 上生成 PDF大家第一反应总是 Java 的 PdfDocument、iText或者直接去 Android Studio 里翻依赖。可如果项目主体是 Delphi 编写的为了出一张 A4 报告再搭一条 Java 通道光 JNI 调用和字符串编码就够折腾一整天。Delphi-Android-GeraPDF 这个源码包走的完全是 Delphi 自己的路线以 FireMonkey 的 Canvas 逻辑为入口把绘制动作翻译成 PDF 内容流最终落成一份标准 PDF。它不依赖打印服务、不需要 root适合做订单发票、巡检报告、合同文件这类偏工具型 App。包里 GeradorPDF.fmx 是可运行的界面GeradorPDF.pas 是主逻辑PDFGenerator 和 Androidpdf 部分负责把画布指令写入文档。下面直接从文档模型开始拆。2. PDFGenerator 的文档模型从 Canvas 指令到 PDF 内容流PDF 文件不像 Word 那样保存富文本它的页面实际上是一段用操作符写成的“绘制脚本”。画一条线对应的是给输出流追加m、l、S三个操作符写一段文字对应的是BT/ET块里的字体选择、坐标移动和Tj。PDFGenerator 的工作方式就是让这些底层操作符变成 Delphi 对象上的一层薄封装同时对外暴露一个类似 TCanvas 的接口。这样做的最大好处是业务代码不需要关心 PDF 二进制结构只需要理解“我在一页纸上画什么”。2.1 PDF 内容流就是一种“绘画语言”一个最简单的 PDF 页面内容流是这样的先设置字体和字号然后移动到某个坐标调用Tj写出文本。FMX 的TCanvas.DrawText本质上也在做同样的事只不过最终渲染到屏幕。两者的差异只在于坐标方向和单位的换算。PDFGenerator 把这个过程反转过来开发者在 TPDFCanvas 上调用 DrawLine、DrawRect、DrawText库内部把这些方法参数拼装成内容流操作符并维护当前字体、颜色、线宽等状态。未结束的路径会自动补S或f文本块会自动包上BT/ET。所以与其说它是一个 PDF 引擎不如说它是一个“Canvas 指令到 PDF 内容流”的编译器。2.2 最小可运行流程Document → Page → Canvas以项目源码对 PDFGenerator 的定位来看核心对象通常可以按TPDFDocument → TPDFPage → TPDFCanvas的顺序创建。下面这段代码是我在类似项目里常用的最小模板接口命名与这个源码包基本一致个别方法名以解压后的实现为准uses System.SysUtils, System.Classes, PDFGenerator, PDFCanvas; procedure GenerateSimplePDF(const AStream: TStream); var Doc: TPDFDocument; Page: TPDFPage; Cv: TPDFCanvas; begin Doc : TPDFDocument.Create; try Page : Doc.AddPage(210, 297, TPDFUnit.mm); // A4 竖版单位毫米 Cv : Page.Canvas; Cv.SetFont(DroidSansFallback, 12); Cv.SetColor(clBlack); Cv.DrawText(30, 50, Order: 2024001); Cv.DrawLine(20, 70, 190, 70); Doc.SaveToStream(AStream); finally Doc.Free; end; end;这段代码里Doc.AddPage(210, 297, TPDFUnit.mm)创建了一个宽 210 毫米、高 297 毫米的页面对应 A4 竖版。TPDFUnit.mm是库内置的单位枚举通常还有pt、inch可选。Cv.SetFont的第一个参数是字体名在 Android 上中文字体要特别注意后面章节会单独讲。DrawText(30, 50, ...)的参数是左上角坐标和字符串内容。Doc.SaveToStream把整篇文档写入流由调用方决定输出到文件还是网络。2.3 坐标原点PDF 和 FMX 的差异PDF 标准规定页面坐标系原点在左下角Y 轴向上而 FireMonkey 的 TCanvas 原点在左上角Y 轴向下。如果库内部不做转换那么同样的 DrawText 坐标在屏幕上正常生成出来的 PDF 里文字会跑到页面底部甚至被裁掉。这个源码包里的 Androidpdf 模块专门处理了坐标翻转所以我建议生成后先打开 PDF 看一眼如果所有内容都上下颠倒检查是否在创建页面时传入了一个类似AutoFlipY的选项。FMX/TCanvas 操作PDF 内容流操作符说明DrawLinem l S构造路径并描边DrawRectre S标准矩形路径FillRectre f矩形闭合填充DrawTextBT/Tf/Td/Tj文本块与字形定位DrawImageDo通过 XObject 引用图像2.4 从 GeradorPDF.pas 出发的源码阅读路线解压源码后不要急着编译先按这个顺序看文件GeraPDF.dpr是工程入口里面的 uses 列表会告诉你哪些单元被引用了GeradorPDF.pas是主窗体的实现搜索PDFDocument、SaveToStream这类关键调用最后再找 PDFGenerator 单元看它内部如何维护页面对象列表、字体字典和内容流。如果项目把 PDFGenerator 的对象定义也放在 GeradorPDF.pas 里说明整个实现更紧凑阅读时直接从TPDFDocument.Create往下跟即可。建议在SaveToStream处打断点用调试器看当前内容流字符串的累计过程这比读纯代码容易理解。3. 在 GeradorPDF.fmx 里把界面数据输出成 PDF原理清楚了现在打开GeraPDF.dproj。FireMonkey 项目的界面文件是GeradorPDF.fmx里面通常放一个按钮、几个编辑框和一个多行文本域。把第 2 章的生成函数接到按钮事件上界面上输入的数据就能变成 PDF 文件。这一步的核心不在画布而在文件怎么存、怎么分享出去。3.1 主窗体与逻辑分离在 FMX 里我习惯把 PDF 生成逻辑放到一个独立过程里不直接写在按钮事件内这样后面加批量导出、异步调用都方便。单元引用的顺序要稳定否则 Android 平台上会抛出莫名其妙的链接错误uses System.SysUtils, System.IOUtils, System.Classes, FMX.Types, FMX.StdCtrls, PDFGenerator, PDFCanvas;这里System.IOUtils提供TPath类用来拼 Android 路径FMX.StdCtrls是按钮、编辑框所在的单元。若冒然把 PDFGenerator 放在 FMX 单元之前某些 Delphi 版本会因单元初始化的顺序问题导致 Canvas 类注册失败。3.2 生成按钮事件与文件输出按钮事件里要做三件事拼文件名、创建流、让 PDFGenerator 写入。常见做法是输出到应用私有目录而不是直接写到公共 Download 目录procedure TForm1.btnGenerateClick(Sender: TObject); var LStream: TFileStream; LPath: string; begin LPath : TPath.Combine(TPath.GetDocumentsPath, report.pdf); LStream : TFileStream.Create(LPath, fmCreate or fmShareDenyNone); try GenerateSimplePDF(LStream); finally LStream.Free; end; end;fmShareDenyNone是为了让文件在生成期间也能被其他线程读取后面做分享时 Intent 可以立刻拿到内容。TPath.GetDocumentsPath在 Android 上通常对应/data/user/0/包名/files这个目录不需要任何存储权限但其他应用无法直接通过文件路径访问。3.3 Android 存储适配从 file:// 到 content://直接把上述路径拼成file://传给 Intent在 Android 7.0 以上会抛出FileUriExposedException。Android 10 之后的分区存储又让公共目录的访问更加严格。正确做法是配置 FileProvider用content://URI 授权给外部应用读取临时文件。项目自带的AndroidManifest.template.xml就是做这个事的。在application节点里加上 provider 声明provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerauthorities的前缀必须和代码里getUriForFile传入的第二个参数保持一致。随后在res/xml/file_paths.xml中声明要暴露的目录paths files-path namedocs path. / cache-path namecache path. / /paths3.4 权限与 AndroidManifest.template.xml 的修改PDF 生成本身不需要WRITE_EXTERNAL_STORAGE权限但你的 App 如果同时要导出到公共 Download 目录就需要处理运行时权限。实测时我对老版本 Android 做兼容在 manifest 里加上带maxSdkVersion的声明uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 /Android 10 以上分区的 artat 会拦截直接写公共目录的操作我一般统一走 FileProvider 分享既避开权限弹窗又不需要清理残留文件。这里有一个常见坑如果你只添加了 provider 而忘记在file_paths.xml里把files-path的 path 设置为.FileProvider 会找不到文件分享时直接崩溃。路径 APIAndroid 实际目录分享前是否需要 FileProviderTPath.GetCachePath/data/user/0/pkg/cache需要TPath.GetDocumentsPath/data/user/0/pkg/files需要TPath.GetSharedDownloadsPath/storage/emulated/0/Download需要处理分区存储4. Androidpdf 渲染中的字体、图像与表格对齐问题流程通了接下来是排版能不能看。Androidpdf 模块负责把图形对象绘制到 PDF但它不会自动帮你解决中文缺失、图片过大这类实际问题字体、图像、表格这三块是排错重灾区。4.1 中文字体要自己带别指望系统字体Android 系统自带DroidSansFallback但 FMX 的 Canvas 字体映射在生成 PDF 时经常找不到该字体最终 PDF 里中文全部变成方框或空白。我一般会在工程资源里内置一个精简中文字体文件运行时释放到 Cache 目录再注册给 PDFCanvasprocedure LoadChineseFont; var Res: TResourceStream; Path: string; begin Path : TPath.Combine(TPath.GetCachePath, DroidSansFallback.ttf); Res : TResourceStream.Create(HInstance, DROIDSANSFALLBACK, RT_RCDATA); try Res.SaveToFile(Path); finally Res.Free; end; PDFCanvas.RegisterFont(DroidSansFallback, Path); end;注意RegisterFont的方法名在不同版本里可能是AddFont但思路一致先把资源流写成本地文件然后让 PDF 生成器在写内容流时嵌入或引用该字体。如果不做这一步即使界面上中文显示正常PDF 里依然可能乱码。另外SQLite 里读出的数据导致 PDF 乱码时也要先在这里找原因而不是去改数据库的字符集。4.2 文本宽度估算与自动换行PDFCanvas 内部使用的字体度量与 FMX 的 TCanvas 并不一致所以在生成文本时用TCanvas.TextWidth来算换行位置是错的。常见做法是自己逐字符估算宽度中文字符按一个全角宽度计算西文字符按半角宽度计算function EstimateTextWidthS(const AText: string; AFontSize: Single): Integer; var I: Integer; begin Result : 0; for I : 1 to AText.Length do begin if Ord(AText[I]) 127 then Inc(Result, Round(AFontSize)) else Inc(Result, Round(AFontSize * 0.6)); end; end;这段代码中AFontSize是字体字号。为什么中文字符乘 1.0、西文乘 0.6因为绝大多数中文字体是等宽的西文小写字母的宽度远小于全角字。当估算宽度超过当前行最大宽度时需要手动插入换行符。如果库提供了文本行的MaxWidth参数也可以在创建文本对象时直接传入。4.3 图片压缩参数选择把 Bitmap 直接嵌入 PDF 并不是好习惯尤其是截图或照片。Androidpdf 模块通常会保存为 JPEG 流以减小体积在插入前先做缩放function LoadDownsampledJPG(const APath: string; AMaxSide: Integer): TBitmap; var Bmp: TBitmap; begin Bmp : TBitmap.Create; try Bmp.LoadFromFile(APath); if (Bmp.Width AMaxSide) or (Bmp.Height AMaxSide) then Bmp.Resize(AMaxSide, Round(Bmp.Height * AMaxSide / Bmp.Width)); Result : Bmp; except Bmp.Free; raise; end; end;参数AMaxSide控制最长边的像素数通常 A4 纸宽度 200 像素足够在屏幕查看。再配合 JPEG 的压缩质量参数 85文件体积能控制在百 KB 级别。透明 PNG 在 PDF 嵌入时要先合成到白色背景否则透明区域可能变成黑色这也是很多人遇到的“图片背景变黑”原因。图像格式PDF 中表现建议JPEG保留 RGB文件小照片、发票扫描件PNG 24/32透明层处理麻烦先合成白底再嵌入BMP无压缩体积大不要直接使用4.4 表格用 DrawText DrawLine 手工搭这个库的表格模块在很多版本里只提供基础矩形和文本原语我通常会自己画。实现方式是先画一组竖线和横线再逐个格子写文本procedure DrawSimpleTable(Cv: TPDFCanvas; X0, Y0: Single; const ColWidths: array of Single; RowHeight: Single; ACellTexts: TStrings); var I, Row, Col: Integer; X, Y: Single; begin for I : 0 to ACellTexts.Count - 1 do begin Row : I div Length(ColWidths); Col : I mod Length(ColWidths); X : X0; for Row : 0 to Row - 1 do begin Y : Y0 Row * RowHeight; Cv.DrawRect(X0, Y, X0 SumWidths(ColWidths), Y RowHeight, false); Cv.DrawText(X, Y 2, ACellTexts[I]); end; end; end;这段代码里X0、Y0是表格左上角坐标ColWidths是每列宽度的数组RowHeight是行高。SumWidths是一个把数组求和的辅助函数。文本的 Y 坐标要加一个垂直偏移否则文字底边会贴住下框线。实际使用时单元格内容超过列宽要提前用 4.2 节的方法换行。5. 把 PDF 生成丢到后台线程再用 FileProvider 分享出去PDF 内容达到几百行表格时直接在主线程SaveToStream会让应用卡顿一整秒Android 上几秒就会触发 ANR。正确姿势是把生成放到TTask.Run中完成后再回到主线程发起分享。5.1 用 TTask.Run 避免卡死主界面FTask : TTask.Run( procedure var LPath: string; LStream: TFileStream; begin LPath : TPath.Combine(TPath.GetCachePath, report.pdf); LStream : TFileStream.Create(LPath, fmCreate or fmShareDenyNone); try GenerateSimplePDF(LStream); finally LStream.Free; end; TThread.Synchronize(nil, procedure begin SharePDF(LPath); end); end);TTask.Run会在线程池中执行匿名过程TThread.Synchronize再把分享调用拉回主线程避免在子线程里碰 FireMonkey 控件。FTask 要放在窗体级字段防止被立即释放。5.2 分享 PDF 的 Intent 参数调用系统分享面板时EXTRA_STREAM对应的 URI 必须通过 FileProvider 生成procedure SharePDF(const APath: string); var Intent: JIntent; Uri: JUri; begin Uri : TJFileProvider.JavaClass.getUriForFile( TAndroidHelper.Context, TAndroidHelper.Context.getPackageName .fileprovider, TJFile.JavaClass.init(APath)); Intent : TJIntent.Create; Intent.setAction(TJIntent.JavaClass.ACTION_SEND); Intent.setType(application/pdf); Intent.putExtra(TJIntent.JavaClass.EXTRA_STREAM, TJParcelable.Wrap(Uri)); Intent.addFlags(TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION); TAndroidHelper.Activity.startActivity(TJIntent.JavaClass.createChooser(Intent, StrToJString(分享 PDF))); end;FLAG_GRANT_READ_URI_PERMISSION很重要不加的话微信、邮箱等接收方没有权限打开这个内容 URI。分享 PDF 的 MIME type 必须写application/pdf否则某些邮件应用会当成普通附件处理。5.3 生成结果验证文件分享前先在本地验证 PDF 结构是否完整。用 adb 把文件拉出来检查adb pull /sdcard/Android/data/包名/files/report.pdf . file report.pdf qpdf --check report.pdf正常的 PDF 文件file命令会输出PDF document, version 1.4qpdf --check没有任何 error。如果qpdf提示找不到/Root对象说明 Document 在写入流之前被提前释放了回到第 2 章检查 Doc 的生命周期。临时文件放在 Cache 目录用完即可让系统回收分享后也不用清理。这招在我处理所有 Delphi Android 导出任务时都有效。本文还有配套的精品资源点击获取