简介:面向具备一定 C++ 基础的 PDF 解析与文件读写开发者,这份工程把 podofo 0.9.7 在 VS2005 下完整编译通过,并将 freetype、libjpeg、libpng、libtiff、zlib、cppunit 以及加密所需的 openssl 等依赖一并整合,最终产出便于调用的 lib 库,减少开发时逐个链接静态库的配置负担。压缩包共 1382 个文件,约 42.38MB,以 372 个 h 头文件、327 个 c 源文件、279 个 obj 中间文件、102 个 cpp 源文件及 91 个 mk 构建文件为主,同时保留工程与说明文档,目录层次有助于理解模块划分和依赖关系。已有 573 人学习下载。工程下载后可直接用 VS 打开编译,适合用来研究 PDF 文档解析、读写与加密流程;若不需要文档加密,可去掉 PODOFO_HAVE_OPENSSL 宏,启用后则需补充相应 dll 并连接相关 lib。对希望跳过第三方库整合、专注业务调用的开发者,这套编译成果能提供较完整的参考与环境基础。
1. 老项目里要读 PDF,绕不开的一次选型
去年接了个活儿,一套跑了十几年的工控上位机要加两个功能:导出报表 PDF、读取上游发来的 PDF 工单。IDE 是 VS2005,编译器 VC8,整条编译链锁死,C++11 都用不上。找了一圈,能在 C++98 下编出来、又能同时覆盖读写两端的开源库不多,最后落到 podofo0.9.7 上。它是一套纯 C++ 的 PDF 解析与生成库,不依赖托管运行时,源码自带 CMake 脚本,理论上能编到很老的 MSVC 上——但"理论上"这三个字,在 VS2005 面前往往要打折扣。这篇就把我把它从头编通、再用起来的过程完整拆一遍,适合两类人:被老编译链锁住不得不硬啃的,和想拿它做 PDF 二次开发的。
2. podofo0.9.7 定位与源码结构:一个 C++ PDF 解析库的边界在哪
先说清楚它是什么、不是什么。podofo0.9.7 在定位上属于"PDF 对象层库",不是渲染引擎。它把 PDF 当成一棵对象树来读写——字典、数组、名字、流、引用,你拿到的是结构化的数据,不是一张画好的图。这个定位决定了它在文件解析、批量处理、表单填充、内容提取这类场景里非常好用,在"把 PDF 转成图片给人看"这类场景里则完全帮不上忙。
2.1 三件事:读、写、改,边界要划清
读的部分由PdfMemDocument承担。它一次性把整个文件解析进内存,构建交叉引用表、对象表、页面树,之后你能按页拿PdfPage,按页拿内容流,按字典拿字体、图像、注释。适合几十兆以内的工单、合同、报表这类文件;几百兆的扫描件全量加载会明显吃内存。
写的部分由PdfStreamedDocument承担。它是流式输出,边写边吐字节,最后落盘,内存占用和页数无关。代价是写的过程中不能回头改已经写出去的对象,也不能用它去 Load 一个已有文件。
改的部分还是PdfMemDocument,合并、删页、旋转、加解密、改表单域值都走这条路。需要注意的是页面合并和对象重编号在 0.9.x 里有一些历史遗留的坑,后面避坑章会专门讲。
它明确不做的几件事:不做光栅化渲染,没有内置的 PDF 转图片能力;不做完整的 PDF 2.0 特性支持,新版规范里的对象流、交叉引用流支持有限;CJK 字体嵌入需要你自己提供字体文件,库只负责把字形子集写进去。搞清这三条,能省掉很多"为什么它不支持 XX"的纠结。
2.2 依赖矩阵:装不装某个库,直接决定砍掉哪块功能
podofo 的依赖是"可选但影响功能"的设计,CMake 探测到就打开对应宏,探测不到就静默关掉。这个设计在 Linux 上很舒服,在 VS2005 上就得手动把路径一个个喂进去。
| 依赖库 | 是否必需 | 缺失后失去的能力 | VS2005 下的获取方式 |
|---|---|---|---|
| zlib | 必需 | 内容流解压/压缩全废,几乎无法解析正常 PDF | 源码自带makefile或 CMake 工程 |
| freetype | 强烈建议 | TrueType 字体度量与嵌入,中文输出基本别想 | 老版本builds/win32下有 VC8 工程 |
| libjpeg | 建议 | 读写内嵌 JPEG 图像 | makefile.vc,nmake 直接编 |
| libtiff | 可选 | TIFF 图像支持 | nmake.opt配置后 nmake |
| libpng | 可选 | PNG 图像支持 | 自带 VS 工程文件 |
| openssl | 可选 | PDF 加密/解密(RC4、AES) | 老版本 1.0.x 分支才认 VC8 |
关键点在于宏。编译成功后,podofo-config.h里会出现PODOFO_HAVE_FREETYPE、PODOFO_HAVE_JPEG_LIB、PODOFO_HAVE_OPENSSL这类开关。这些宏不光是编译期的#ifdef,还决定了运行时某些 API 会不会直接抛PdfError。我一般会在编译完成后打开这个头文件逐行看一眼,确认想要的功能是1而不是0,比编译成功更让人踏实。
2.3 源码目录:base / doc / auxiliary 三层结构
拿到源码包先别急着 CMake,打开src看一眼分层,后面调编译错误会快很多。
| 目录 | 职责 | 典型文件 |
|---|---|---|
src/base | 最底层的 PDF 数据类型与 IO | PdfObject、PdfVariant、PdfString、PdfName、PdfInputDevice、PdfOutputStream、PdfFilter、PdfEncrypt、PdfError |
src/doc | 文档模型与高层对象 | PdfMemDocument、PdfStreamedDocument、PdfPage、PdfPagesTree、PdfAnnotation、PdfField、PdfFont |
src/auxiliary | 面向使用者的辅助工具 | PdfContentsTokenizer、PdfPainter、PdfImage、PdfXObject、PdfFontCache |
tools | 命令行工具集 | podofotxtextract、podofoimgextract、podofopages、podofomerge、podofoencrypt |
cmake | 查找依赖的 Find 模块 | FindFREETYPE.cmake等 |
这个分层不是摆设。base层几乎不依赖外部库,doc层开始依赖 zlib,auxiliary层才用到 freetype 和图像库。如果你只是想解析对象结构,理论上可以只编base+doc。真要裁剪,改动集中在 CMakeLists 的源文件列表,而不是去删源文件——删源文件会让头文件里的PODOFO_API声明找不到实现,链接期才报错,排查成本翻倍。
头文件的统一入口是podofo/podofo.h,一行 include 把大部分类都带进来。导出宏是PODOFO_API,由PODOFO_STATIC或PODOFO_SHARED控制。用静态库时必须在工程里定义PODOFO_STATIC,否则头文件按__declspec(dllimport)展开,链接时会报一堆__imp_前缀的未解析符号。这个坑我踩过不下三次。
3. VS2005 编译实战:从 CMake 生成工程到跑通第一个命令行工具
这一章是整篇最费时间的部分。VS2005 的问题不在于难,而在于每一步都有个小前提没人告诉你。
3.1 先把依赖编出来:路径规划比编译本身重要
我的习惯是先建一个扁平的工作目录,把所有东西塞进去,路径里不要有空格和中文——VS2005 那代工具链对空格路径的处理经常出玄学问题。
D:/podofo-build/ deps/ zlib/ include/ lib/ freetype/ include/ lib/ jpeg/ include/ lib/ tiff/ include/ lib/ png/ include/ lib/ src/ podofo-0.9.7/ out/zlib 我用 CMake 单独生成一份 VS2005 工程来编,产出zlib.lib(静态)和zlib1.dll+zlib.lib(动态)。这里的关键是只选一种。如果你先编了静态库,又编了动态库,两个.lib同名,后一个会覆盖前一个,结果就是 podofo 编译时头文件按动态库展开,链接时却拿到静态库,报一堆__imp__inflate未解析。血泪经验:编完立刻把静态库改名成zlibstatic.lib再放进 deps 目录。
freetype 要注意版本。较新的 freetype 已经删掉了builds/win32/vc2005目录,得往回找还带这个工程的版本。编出来是freetype.lib,头文件目录要指到include(里面还有一层freetype2,不要多指一层也不要少指一层,#include <ft2build.h>的搜索路径正好卡在这)。
jpeg 和 tiff 走 nmake,改一下makefile.vc里的CFLAGS,加上/MT或/MD,跟后面 podofo 的运行库保持一致。这一步现在不做,后面必然 LNK2005。
3.2 CMake 配置:生成器、变量、路径三件事
VS2005 对应的是Visual Studio 8 2005生成器。新版本的 CMake 早就把这个生成器移除了,所以得用比较老的 CMake 版本(2.8 后期或 3.x 早期那一批),或者干脆不指定生成器、让它自己探测已安装的 VS。
cd D:/podofo-build/out cmake -G "Visual Studio 8 2005" ^ -DPODOFO_BUILD_SHARED=ON ^ -DPODOFO_BUILD_STATIC=OFF ^ -DPODOFO_BUILD_EXAMPLES=ON ^ -DPODOFO_BUILD_UNSUPPORTED_TOOLS=ON ^ -DCMAKE_BUILD_TYPE=Release ^ -DZLIB_INCLUDE_DIR=D:/podofo-build/deps/zlib/include ^ -DZLIB_LIBRARY=D:/podofo-build/deps/zlib/lib/zlib.lib ^ -DFREETYPE_INCLUDE_DIRS=D:/podofo-build/deps/freetype/include ^ -DFREETYPE_LIBRARY=D:/podofo-build/deps/freetype/lib/freetype.lib ^ -DJPEG_INCLUDE_DIR=D:/podofo-build/deps/jpeg/include ^ -DJPEG_LIBRARY=D:/podofo-build/deps/jpeg/lib/jpeg.lib ^ ../src/podofo-0.9.7参数逐个说。PODOFO_BUILD_SHARED和PODOFO_BUILD_STATIC我建议只开一个,两个都开会让后面的工程属性设置变成噩梦。PODOFO_BUILD_UNSUPPORTED_TOOLS打开会额外编一批实验性工具,其中有些在 VC8 上未必编得过,第一次跑建议先关掉,等主库通了再开。CMAKE_BUILD_TYPE在单配置生成器(VS 系列)里其实不起作用,真正的优化等级要在 IDE 里改,写上只是图个心安。
路径分隔符一律用正斜杠。VS2005 时代的 CMake 对反斜杠的处理会在一部分 Find 模块里出问题,导致明明路径对却报"not found"。
CMake 跑完如果看到PODOFO_HAVE_FREETYPE: 1这类输出,说明探测成功;如果是0,先去看CMakeCache.txt里对应的变量是不是空的,多数是路径写岔了一层目录。
3.3 工程属性:运行库、字符集、C++98 兼容补丁
CMake 生成的.sln打开后,第一件事不是按 F7,而是改属性。右键podofo工程,属性页里把C/C++ → 代码生成 → 运行时库统一成多线程 DLL (/MD)(Release)和多线程调试 DLL (/MDd)(Debug)。所有依赖库必须是同一套,混用就是 LNK2005 重复定义,报的符号往往还是_malloc、_free这种让人一脸问号的东西。
第二件事是字符集。项目默认可能是 Unicode,podofo 内部大量使用char*和std::string,跟wchar_t混在一起会触发大量 C4244 截断警告。我一般直接把工程改成"使用多字节字符集",比逐个函数加转换省事。
第三件事是关掉一批警告。VC8 的 STL 会刷出成片的 C4786(标识符截断到 255 字符),不影响功能但会把真正的错误淹掉。在"禁用特定警告"里填4786;4251;4355,日志立刻清爽。
第四件事,VS2005 的for循环变量作用域遵循旧规则(/Zc:forScope-是默认),for (int i = 0; ...)里的i在循环外依然可见。podofo 里有些函数在同一作用域里写了两个同名循环变量,在 VC8 上会直接报重定义。解决办法是在工程选项里加/Zc:forScope,让编译器按标准作用域处理。
// 典型的 VC8 报错场景:同一函数内两段循环都用 i for (int i = 0; i < n; ++i) { /* ... */ } // VC8 默认认为 i 还活着,下面这行报 C2374 重定义 for (int i = 0; i < m; ++i) { /* ... */ }这段不是 podofo 的代码,是我在改造它的工具源码时反复撞上的模式。加/Zc:forScope之后两段循环各自独立,报错消失。这个选项在 VS2005 上默认关闭,边界情况比想象中多。
另外 VC8 没有stdint.h,也没有标准的snprintf。podofo 内部有PdfCompilerCompat.h做了一层兼容封装,把snprintf映射到_snprintf、把定长整型用typedef补出来。这套兼容层在 VS2005 上是救命的,但它依赖 CMake 生成的配置宏,所以千万别手动去改podofo-config.h,一改就被下次 CMake 覆盖。
3.4 编译、产物与第一次验证
按 F7。第一次编译大概率不会一次过,常见的是某个 tool 编不过,主库podofo.lib和podofo.dll其实是好的。先把主库拿下,工具单独处理。
编完之后看out/bin/Release:
| 产物 | 用途 |
|---|---|
podofo.dll | 动态库,供你自己的程序链接 |
podofo.lib | 导入库,配合 dll 使用 |
podofopdfinfo.exe | 打印文档信息、页数、加密状态 |
podofotxtextract.exe | 提取纯文本,最常用的验证工具 |
podofoimgextract.exe | 抽出内嵌图像 |
podofopages.exe | 页面级操作(删除、移动、旋转) |
podofomerge.exe | 多文件合并 |
验证就一条命令,拿一个正常 PDF 喂给podofopdfinfo:
podofopdfinfo.exe D:/test/sample.pdf能打出 PageCount、PDF 版本、是否加密,说明 zlib 通了、对象解析通了。再用podofotxtextract抽一段文本出来,能出字说明字体解析也通了。这两步过了,后面写业务代码才有意义。如果podofopdfinfo直接崩,别急着调代码,先把podofo.dll和依赖的几个 dll 放到同一目录,缺 dll 在 VS2005 上的表现经常不是弹框而是静默退出。
4. 核心代码拆解:一个 PDF 从打开到取文本的完整链路
库能编通只是及格线,真正要用起来,得把它的对象模型摸清楚。这一章挑三条最常走的链路:打开解析、内容流提取、生成输出。
4.1 文档对象模型:PdfMemDocument 与 PdfStreamedDocument 的分工
PdfMemDocument走的是"全量加载 + 内存操作"路线。Load()那一刻,文件头、交叉引用表、所有间接对象全部进内存,之后每次GetPage()、GetObject()都是内存访问,速度很快。代价是内存占用和文件大小线性相关,而且它假设文件结构是完好的——遇到 xref 偏移错乱的野文件,会在 Load 阶段直接抛PdfError。
#include <podofo/podofo.h> using namespace PoDoFo; void DumpPdfInfo(const char* pszPath) { PdfMemDocument doc; doc.Load(pszPath); // 全量解析,失败抛 PdfError int nPages = doc.GetPageCount(); printf("pages = %d\n", nPages); for (int i = 0; i < nPages; ++i) { PdfPage* pPage = doc.GetPage(i); // 0-based 索引 PdfRect rect = pPage->GetMediaBox(); printf("page %d: %.1f x %.1f\n", i, rect.GetWidth(), rect.GetHeight()); } }Load接受const char*,内部按本地编码打开文件,路径里有中文要在 VS2005 上额外处理(改成短路径或者用GetShortPathName)。GetPage(int)的索引从 0 开始,这点和 PDF 规范里的 1-based 页号不同,写业务代码时最容易在这里差一页。GetMediaBox()返回的是 PDF 用户空间坐标,单位是 point(1/72 英寸),不是像素,换算成毫米要乘 0.3528。
PdfStreamedDocument则是另一套用法:构造时给文件名,之后只能CreatePage,不能Load。它适合"凭空生成一份几百页的报表"这种场景。
4.2 内容流解析:PdfContentsTokenizer 的状态机怎么用
PDF 的页面内容是后缀表达式,一堆操作数和操作符混在一起,比如BT /F1 12 Tf 100 700 Td (Hello) Tj ET。PdfContentsTokenizer做的就是把这串字节流拆成一个个 token,你自己根据关键字拼语义。
void ExtractText(const char* pszPath) { PdfMemDocument doc; doc.Load(pszPath); for (int i = 0; i < doc.GetPageCount(); ++i) { PdfPage* pPage = doc.GetPage(i); PdfContentsTokenizer tokenizer(pPage); EPdfContentsType eType; const char* pszToken = NULL; PdfVariant* pVariant = NULL; while (tokenizer.ReadNext(eType, pszToken, pVariant)) { if (eType == ePdfContentsType_Keyword) { // Tj / TJ 后面跟的是待绘制的字符串 if (strcmp(pszToken, "Tj") == 0 || strcmp(pszToken, "TJ") == 0) { // 此处从上一轮缓存的操作数里取字符串 } else if (strcmp(pszToken, "Tf") == 0) { // 字体切换,缓存当前字体名和字号 } else if (strcmp(pszToken, "ET") == 0) { // 文本块结束,补一个换行 } } else if (eType == ePdfContentsType_Variant) { // 操作数:数字、字符串、数组都在这里 if (pVariant->IsString()) { PdfString str = pVariant->GetString(); // str.GetString() 拿 UTF-8 前的原始字节 } } } } }ReadNext是流式推进的,返回false表示内容流读完。三个出参里,eType区分"这是关键字"还是"这是操作数",pszToken只在关键字时有意义(且指向内部缓冲区,不能长期持有),pVariant只在操作数时有意义。
真实场景里最麻烦的不是拆 token,是字符编码。PDF 里的字符串可能是单字节编码、可能是 Identity-H 的双字节 CID,要正确还原成可读文本,必须结合当前Tf设置的字体和它的ToUnicode映射。podofo 在PdfFont上提供了GetEncoding(),但要拿到人可读的 Unicode,podofotxtextract里那套"按字体查 ToUnicode CMap 再查表"的逻辑基本得照抄一遍。这是整个库最不"开箱即用"的地方,别指望三行代码抽出中文。
4.3 生成 PDF:PdfStreamedDocument + PdfPainter 的组合
生成侧比解析侧友好得多,坐标系、字体、图像都被PdfPainter包了一层。
void CreateReport(const char* pszOut) { PdfStreamedDocument doc(pszOut); PdfPage* pPage = doc.CreatePage( PdfPage::CreateStandardPageSize(ePdfPageSize_A4)); PdfPainter painter; painter.SetPage(pPage); // 注册字体:第二参数传 true 表示嵌入子集 PdfFont* pFont = doc.CreateFont("Arial"); painter.SetFont(pFont); painter.SetFontSize(14); painter.DrawText(60, 780, "Monthly Report"); painter.SetFontSize(10); painter.DrawText(60, 750, "generated by podofo 0.9.7"); painter.FinishPage(); doc.Close(); // 流式文档必须 Close,否则文件不完整 }几个参数要说清楚。CreateStandardPageSize(ePdfPageSize_A4)生成的 MediaBox 是 595 × 842 point,DrawText的坐标原点在左下角,y 值往上增大,这一点和屏幕坐标相反,画页眉的时候 y 要给大数(780 左右)。FinishPage()把当前页的内容流刷出去,Close()才是真正写完 xref 和文件尾,少调任何一个,产出的文件都是坏的。
CreateFont在不指定编码的情况下会用字体的默认编码,输出英文没问题,输出中文会变成一堆问号。要输出中文,常见做法是先CreateFont("SimSun", new PdfIdentityEncoding())走 Identity-H,再确保系统里能找到对应字体文件,让 podofo 做子集嵌入。这一步在 VS2005 环境下还要注意字体文件的路径必须用短路径,长中文路径在fopen那一层就可能失败。
4.4 异常与内存:PdfError 和引用计数
podofo 的异常体系很统一,所有失败都抛PdfError,继承自std::exception,带一个错误码枚举。
try { PdfMemDocument doc; doc.Load("broken.pdf"); } catch (PdfError& e) { // PrintError 会往 stderr 打一段可读描述 e.PrintError(); // GetError() 拿枚举值做分支判断 if (e.GetError() == ePdfError_InvalidPassword) { // 加密文件没给密码 } }ePdfError_InvalidPassword这个分支一定要单独处理。加密 PDF 在Load阶段就会抛异常,如果不捕获,程序直接终止;如果只捕获PdfError不区分错误码,用户看到的就是一句没头没脑的"加载失败"。正确做法是先提示输入密码,用doc.SetPassword()再 Load。
内存方面,PdfMemDocument析构时释放所有对象,PdfPage*的指针归属文档,不要自己去delete,也不要跨文档持有。PdfFont*、PdfImage*虽然由文档创建,但它们的生命周期绑定在被PdfPainter使用的那一页上,跨页复用同一个字体指针在 0.9.7 里是允许的,跨文档就是未定义行为。这类"谁 owns 谁"的问题,文档里写得不全,只能靠读doc/下的析构链自己捋。
5. 避坑排查:VS2005 下五个高频翻车点
这一章按阶段分,每条都按"现象 → 原因 → 解决"写,都是我实际撞过的。
5.1 生成阶段:CMake 根本不给 VS2005 的机会
现象:执行cmake -G "Visual Studio 8 2005"直接报Generator: Visual Studio 8 2005 could not find any instance of Visual Studio,或者更直接地报"generator not supported"。
原因:VS2005 对应的生成器在新版 CMake 里已经被移除了。CMake 3.x 后期开始逐步淘汰老生成器,装机器上那个版本可能压根不认识Visual Studio 8 2005这个名字,也可能认识但找不到注册表里的安装信息。
解决:换一个还带这个生成器的老版本 CMake,别贪新。装的时候注意把老版本装到独立目录,不要覆盖系统里新版本,用的时候用绝对路径调用。如果实在拿不到老版本,退一步用 CMake 生成NMake Makefiles,再拿nmake编,绕开 IDE 生成器这一层,代价是要手动维护编译参数。
5.2 编译阶段:stdint、snprintf、for 作用域三连
现象:编译PdfString.cpp、PdfParserObject.cpp时报Cannot open include file: 'stdint.h',或者snprintf未声明,或者报C2374: 'i' : redefinition。
原因:VC8 的 C 运行时停留在 C89 附近,没有 C99 的stdint.h和snprintf;for循环变量作用域也还是旧规则。podofo 内部有兼容层,但它依赖 CMake 生成的配置宏,如果 CMake 探测阶段把某些PODOFO_HAVE_*判成了 0,兼容分支就不会启用。
解决:先检查podofo-config.h里PODOFO_HAVE_SNPRINTF之类的宏是否为 0,如果是,手动在 CMake 里把它置 1 后重新生成。for作用域问题在工程属性里加/Zc:forScope即可。stdint.h如果兼容层没兜住,最省事的办法是把一份msinttypes的stdint.h丢进 include 目录,别去改 podofo 源码。
5.3 链接阶段:满屏 LNK2005 和 _imp未解析
现象:链接podofo.dll时报几十条LNK2005: _malloc already defined,或者链接自己的程序时报unresolved external symbol __imp__inflate。
原因:两种情况。第一种是运行库混用,主库用/MD,某个依赖库用/MT,两套 CRT 各带一份malloc,撞在一起。第二种是自己程序里没定义PODOFO_STATIC(用静态库时)或者没链接导入库(用动态库时),头文件按错误的导入导出方式展开。
解决:先用dumpbin /directives zlib.lib | findstr /i "DEFAULTLIB"看一眼每个依赖库声明的默认库,全部统一成/MD或全部/MT,我一般选/MD。然后检查自己工程的预处理器定义:静态库必须写PODOFO_STATIC,动态库不能写。这两步之后 LNK2005 基本能消。
5.4 依赖阶段:freetype 明明编了,CMake 还是说找不到
现象:FREETYPE_LIBRARY和FREETYPE_INCLUDE_DIRS都填了,CMake 输出里PODOFO_HAVE_FREETYPE还是 0。
原因:FindFREETYPE.cmake会去include目录下找ft2build.h,而 freetype 的头文件实际在include/freetype2/ft2build.h(有些版本是include/ft2build.h)。目录层级差一层,就判成找不到。另外FREETYPE_INCLUDE_DIRS是复数形式,和单数的FREETYPE_INCLUDE_DIR是两个变量,填错了不生效。
解决:把FREETYPE_INCLUDE_DIRS指到能直接#include <ft2build.h>的那一层,通常是deps/freetype/include。填完删掉CMakeCache.txt重新跑,缓存里残留的失败结果会一直骗你。
5.5 运行阶段:加载 PDF 直接抛异常,日志只有一行
现象:程序跑起来,doc.Load("xxx.pdf")抛PdfError,PrintError()打出来的信息很短,看不出是文件坏了还是密码没给。
原因:PDF 加载失败的原因分三类——文件本身 xref 损坏(裁剪过的、下载不完整的)、文件加密但没设置密码、文件用了 podofo 0.9.x 不完全支持的新特性(比如交叉引用流里带特殊过滤器)。异常描述本身不区分这几种。
解决:先用podofopdfinfo单独跑一遍这个文件,能正常输出说明是程序里的密码或配置问题,打不开说明是文件问题。加密文件先doc.SetPassword("")试空密码,再试用户提供的密码。如果确实是文件结构损坏,podofo 0.9.x 没有宽松模式下修复 xref 的开关,只能先用别的工具重写一遍文件结构再喂给它。这个限制挺硬,业务上要提前跟上游约定好 PDF 产出规范。
6. 二次开发:裁剪模块、封装接口、验证解析结果
把库编通只是起点,真正要落地,通常要做三件事。
第一件是裁剪编译单元。如果你的程序只需要写 PDF、不需要读 PDF,PdfContentsTokenizer、PdfImage那一批可以整个不编,二进制能从十几兆砍到三四兆。改法是在CMakeLists.txt的源文件列表里删对应.cpp,同时把头文件里的导出声明注释掉。别去物理删文件,用 CMake 的条件判断包一层,方便以后回滚。
第二件是封装一层自己的门面。podofo 的 API 是对象层的,业务代码直接调它会到处散落PdfVariant、PdfObject这类类型,一旦将来换库或者升级版本,改起来就是灾难。我的做法是抽一个IPdfReader接口,只暴露Open、PageCount、ExtractText(pageIndex)、RenderInfo这几个方法,podofo 的实现类放在.cpp里,头文件不 includepodofo.h。这样编译依赖也小了,VS2005 那种编译速度,少 include 一个大头文件能省好几秒。
// PdfReader.h —— 业务侧只看到这个 class IPdfReader { public: virtual ~IPdfReader() {} virtual bool Open(const char* pszPath, const char* pszPassword) = 0; virtual int PageCount() const = 0; virtual bool ExtractText(int nPage, std::string& outText) = 0; };第三件是验证。文本提取这类功能,最怕的是"看着对、实际少字"。我的验证方式是准备一组固定样本,包含纯英文、简中、繁中、表格、竖排、带表单域六类,每次改完代码跑一遍,把输出文本和人工标注的基准做逐字比对,差异率超过千分之五就回头看编码分支。这套样本我在第一个项目里花了半天做,后面三年一直在用,属于最划算的投入。
改造过程中最容易后悔的地方是过早优化内存。PdfMemDocument全量加载在几十兆文件上看起来很浪费,但换成流式解析要自己接管对象生命周期,代码量翻三倍。我的经验是先用全量加载跑通,遇到真实的大文件瓶颈再回来改,别一上来就设计一个通用架构,最后发现根本没那么多大文件。
从那以后我每次从源码编一个老库,都先把依赖清单和运行库设置在纸上写一遍再动手,比在 IDE 里反复试错快得多。希望帮到你。
本文还有配套的精品资源,点击获取