简介面向 Windows 平台 C PDF 解析与文件解析开发的 podofo 0.9.7 编译包基于 VS2005 工程整理适合具备一定 C 与 Visual Studio 基础、希望直接复用 PDF 读写库的开发者。包内已将 freetype、libjpeg、libpng、libtiff、zlib、openssl、cppunit 等依赖预先编译并整合为 lib减少逐项链接静态库的配置负担同时保留 crypto 相关头文件便于核对加密模块的引用关系VS 工程下载后可直接打开编译便于在文档解析、PDF 生成与加密等场景中二次开发。需要注意涉及文档加密时启用的 PODOFO_HAVE_OPENSSL 宏需配合两个 dll 放入程序目录并连接相应 lib另有少数依赖 Linux 环境的用例保持禁用。压缩包共 1382 个文件约 42.38MB以 h 头文件、c 与 cpp 源码、obj 中间文件、mk 构建脚本及 vcproj/sln 工程文件为主另含 lib、dll 和少量示例与说明文档目录保留了完整源码与工程结构。已有 572 人学习适合需要快速搭建 podofo 编译环境、排查依赖链接问题并对照宏开关与库配置的开发者参考。1. 一个不许换编译器的老项目怎么把 PDF 读写塞进去接手那套归档系统时开发机是一台跑着老系统的虚拟机装着的编译器只有 VS2005现场机器连运行库都是手工拷的。业务新提的需求很直接把归档里的 PDF 拆页、抽文本、加水印、再合并回去。升级工具链这句话在会上说出来是要被否掉的所以只能在 VS2005 里找一条路。翻了一圈PoDoFo 0.9.7 算是纯 C PDF 解析这块依赖最轻、源码能整包塞进老工程的选择不依赖平台运行时不要求 C11 之外的东西太多读写两条链路都齐全。这篇就是我把它从一个源码包变成一个能在 VS2005 里#include podofo/podofo.h的静态 lib 的全过程——配置头缺失、语言特性降级、CRT 混链这些坑都写进去了照着抄能跑通改的时候也知道该动哪。2. 技术选型与模块分层PoDoFo 0.9.7 到底给了什么在动手建工程之前得先弄清楚这个库的边界在哪。PoDoFo 不是那种“一句话转 PDF”的封装层它更接近一个 PDF 文件格式的实现词法分析、xref 解析、对象树、流过滤器、字体、页面内容流一层一层都是暴露出来的。这既是好事也是坏事——好在你对文件解析有完全控制权坏在你要自己决定用哪一层。2.1 三块代码base、doc 和你要用的那几十个类源码按职责大致分成三块。最底下是基础对象层PdfString、PdfName、PdfVariant、PdfObject、PdfReference、PdfDictionary、PdfArray这些类负责把 PDF 的八种基本类型表达出来PdfVecObjects是所有间接对象的容器PdfParser、PdfTokenizer、PdfXRef、PdfWriter负责字节流与对象树之间的双向翻译。中间是文档层PdfDocument是抽象基类PdfMemDocument把整个文件读进内存后再解析PdfStreamedDocument则是边写边输出PdfPage、PdfPageTree、PdfFont、PdfImage、PdfContents、PdfAnnotation都挂在这一层。最上面是工具层PdfPainter提供画线画字这类绘图接口PdfFontManager管字体缓存PdfEncoding管编码转换。真正做业务时用得最多的其实只有五个类PdfMemDocument读、PdfStreamedDocument写、PdfPage页、PdfPainter画、PdfObject兜底任何解析器没覆盖到的字段都从这儿拿。其余的类你可以先当它们不存在等碰到具体需求再翻。2.2 依赖裁剪zlib 必须有freetype 和 libjpeg 看你场景PoDoFo 的外部依赖不多但每一个都会在 VS2005 里给你制造麻烦所以建工程前先想清楚裁剪策略。依赖作用能不能关关闭后的影响zlibFlateDecode 流压缩/解压几乎所有现代 PDF 的内容流都靠它基本不能关了之后大部分 PDF 的内容流读不出来freetype字体度量与 TTF 嵌入写 PDF 时用能只能用手写的标准 14 字体中文别想了libjpegDCTDecode 图像解码处理扫描件时用能图片对象读出来是原始 JPEG 字节得自己解opensslPDF 加密解密能加密 PDF 打不开会抛异常我的建议是只读不写、或者只处理文本型 PDF 的场景zlib 加进来就够freetype 先关掉编译速度快很多符号冲突也少。要往 PDF 里写中文的freetype 必须开不然你只能靠嵌入整个 TTF 文件这种笨办法。判断开关的方式不是改代码而是改稍后要手写的那份配置头——这一点在 3.2 节会展开。2.3 VS2005 的能力边界C98 加一点点扩展这是整件事最难受的地方。PoDoFo 0.9.7 的源码里已经零散混进了一些较新的写法而 VS2005 的编译器对标准的支持停在 C98/03 附近加上少量厂商扩展。具体来说下面这些你在源码里一定会撞上nullptrVS2005 完全不认只能换NULL或0。auto做类型推导不认必须写完整类型。cstdint和stdint.hVS2005 的 CRT 里没有得自己补一份 typedef。snprintf、vsnprintf只有带下划线的_snprintf而且行为略有差异。long long能当扩展用但和标准类型混用时会出警告尽量统一成__int64。override、final、 delete、右值引用全部不支持。TR1 里的std::shared_ptr库本身用的是自己实现的引用计数PdfRefCountedBuffer这块不用管但你自己的代码别往里塞 TR1。我的处理办法不是全局改源码而是先编一遍让编译器把所有不兼容的点全部报出来再按报错文件逐个改。这样改动的范围最小将来换新版本重新走一遍流程也快。别一上来就全局替换nullptr那会把注释和字符串里的内容一起改掉后面 diff 惨不忍睹。3. 源码到工程把 PoDoFo 0.9.7 搬进 VS2005 的完整流程这一章是纯操作按顺序做完就能得到一个podofo.lib。3.1 解压之后先看目录哪些 .cpp 要进工程源码包解开后根目录下一般能看到src、tools、test、cmake或build-aux这几块。真正要编的只有src下面的东西tools里是一些命令行小工具比如打印 PDF 结构的、导出图片的可以后面单独建工程test直接跳过。src里再往下分base、doc、main三个子目录头文件统一放在src/podofo下面用#include podofo/podofo.h引入的那个总头就在这儿。文件清单大概是这个规模# 只列出需要进工程的 .cpp其余按实际解压结果补 src/base/PdfArray.cpp src/base/PdfDictionary.cpp src/base/PdfName.cpp src/base/PdfObject.cpp src/base/PdfParser.cpp src/base/PdfParserObject.cpp src/base/PdfReference.cpp src/base/PdfStream.cpp src/base/PdfString.cpp src/base/PdfTokenizer.cpp src/base/PdfVariant.cpp src/base/PdfVecObjects.cpp src/base/PdfWriter.cpp src/base/PdfXRef.cpp src/base/PdfFilter.cpp src/base/PdfEncrypt.cpp # doc 目录下是页面、字体、图像、注释等 src/doc/PdfDocument.cpp src/doc/PdfMemDocument.cpp src/doc/PdfStreamedDocument.cpp src/doc/PdfPage.cpp src/doc/PdfPageTree.cpp src/doc/PdfPainter.cpp src/doc/PdfFont.cpp src/doc/PdfImage.cpp src/doc/PdfContents.cpp # 其余文件按目录逐个添加不要漏 PdfDefines.h 相关的实现文件实际操作上不用手写这段清单VS2005 的“添加现有项”支持一次多选把src/base和src/doc下的.cpp全选加进去就行。加完之后编译一次链接器会告诉你哪些文件是必须的——哪些符号没解析对应的.cpp就是漏掉的。3.2 手写 podofo_config.h 和工程配置这是整个流程里最容易卡住的一步。PoDoFo 的源码里会#include podofo_config.h有些版本叫config.h打开podofo.h看一眼就知道这份头文件本来是构建脚本在配置阶段根据探测结果生成的手工建工程时它根本不存在编译器第一句就报fatal error C1083: 无法打开包括文件。解决办法是自己写一份放在src/podofo/下面。内容不用多把版本号和探测宏补上即可// src/podofo/podofo_config.h —— 手工补的配置头不是源码自带的 #ifndef PODOFO_CONFIG_H #define PODOFO_CONFIG_H // 版本号源码里 PdfVersion.h 之类的地方会引用 #define PODOFO_VERSION_MAJOR 0 #define PODOFO_VERSION_MINOR 9 #define PODOFO_VERSION_PATCH 7 #define PODOFO_VERSION_STRING 0.9.7 // 有 zlib 就打开 FlateDecode 支持这是必选项 #define PODOFO_HAVE_ZLIB 1 // 字体管理器依赖 freetype只读场景可以注释掉 // #define PODOFO_HAVE_FONTCONFIG 1 #define PODOFO_HAVE_FREETYPE 1 // 有 libjpeg 才打开 DCTDecode // #define PODOFO_HAVE_JPEG_LIB 1 // Windows 平台标识源码里有几处用它区分路径分隔符和字符集 #define PODOFO_IS_WIN32 1 // 老编译器没有 stdint.h用这里的 typedef 顶上 #if defined(_MSC_VER) _MSC_VER 1600 typedef __int64 int64_t; typedef unsigned __int64 uint64_t; typedef __int32 int32_t; typedef unsigned __int32 uint32_t; typedef __int16 int16_t; typedef unsigned __int16 uint16_t; typedef __int8 int8_t; typedef unsigned __int8 uint8_t; #endif #endif // PODOFO_CONFIG_H这段的关键点有三个。第一PODOFO_HAVE_ZLIB必须定义否则PdfStream里解码 FlateDecode 的分支会被预处理器整段裁掉你能编译通过但运行时读不出内容流。第二_MSC_VER在 VS2005 里是 1400这里用 1600判断是为了顺带兼容 VS2008你只编 VS2005 的话写死 1400也行。第三PODOFO_HAVE_FREETYPE如果定义了却没链 freetype 的库会在链接期爆一堆LNK2001所以定义和库文件必须成对出现。工程配置部分新建一个“静态库”类型的 Win32 工程然后改这几项配置属性 → C/C → 常规 → 附加包含目录加src、src/podofo以及 zlib、freetype 的头文件目录。配置属性 → C/C → 预处理器 → 预处理器定义加WIN32、NDEBUGRelease、_LIB、PODOFO_STATIC。最后这个宏决定源码里那套导入导出修饰会不会展开成__declspec(dllexport)静态库必须定义它。配置属性 → C/C → 代码生成 → 运行时库Release 选“多线程 (/MT)”Debug 选“多线程调试 (/MTd)”。这一项和后面所有链接这个库的程序必须完全一致不一致就是第 5 章要讲的堆损坏。配置属性 → C/C → 语言 → 将 wchar_t 视为内置类型选“否”。PoDoFo 内部有wchar_t和pdf_utf16be之间的转换打开这个选项会引起重载解析歧义。配置完直接生成命令行方式更省事:: 在 VS2005 的命令行环境里执行先初始化编译器环境 vcvars32.bat :: 生成 Release 静态库 devenv PoDoFo.sln /build Release|Win32第一条命令把cl.exe、lib.exe的路径和INCLUDE、LIB环境变量都设好VS2005 时代这两个脚本还在VC\bin下面路径别写错。第二条命令走的是 VS 的解决方案构建比逐个cl调用稳错误信息也更全。3.3 一个最小可运行的 PdfMemDocument 例子库编出来之后先别急着接业务写一个十行的程序验证一下读链路是通的。#include podofo/podofo.h #include iostream using namespace PoDoFo; int main(int argc, char** argv) { if (argc 2) { std::cerr usage: pdfprobe file.pdf std::endl; return 1; } // 打开日志出问题时能看到解析器走到哪一步挂的 PdfError::EnableLogging(true); PdfError::EnableDebug(false); try { PdfMemDocument doc; doc.Load(argv[1]); // 整个文件读进内存并解析 PdfPageCollection pages doc.GetPages(); std::cout pages: pages.GetCount() std::endl; // 对象表大小能直观反映解析是否完整 PdfVecObjects objs doc.GetObjects(); std::cout objects: objs.GetSize() std::endl; } catch (PdfError e) { e.PrintError(); // 打印错误码和出错位置 return 2; } return 0; }doc.Load()这一句背后做了三件事把文件读成PdfInputDevice、定位文件尾的startxref、按偏移把 xref 表和 trailer 解出来然后惰性解析对象。所谓惰性是指它只把对象的位置记下来你真正GetObject的时候才去解那一段字节——所以GetSize()返回的是对象数量不代表内容都解完了。PdfPageCollection这个引用要小心它挂在doc身上doc一旦析构、或者你调了doc.Save()重新组织对象表这个引用就悬空了。我一般不在函数之间传它传doc的引用更安全。catch (PdfError e)是必须的PoDoFo 内部几乎所有错误都靠抛PdfError传播而且它抛的路径比你想的多——不只是文件损坏连字体找不到、编码不支持这类“正常业务分支”也会抛。PrintError()会把错误码、出错文件和行号一起打出来比e.what()有用得多。3.4 编译产物自检先跑 tools 再上业务静态库生成出来后建议先把tools目录下那个打印 PDF 结构的命令行工具也编出来。它的价值在于当你怀疑库本身有问题时用它跑一遍样本文件能立刻区分是“库编错了”还是“业务代码用错了”。建工程的方式和静态库一样区别是类型选“控制台应用程序”链接时把podofo.lib、zlib.lib、freetype.lib加进去。跑通以后输出大概长这样第一行是文件版本号比如%PDF-1.4接着是 trailer 里的/Root、/Info、/Size然后是逐个对象编号和它们的类型/Type /Page、/Type /Font、/Filter /FlateDecode这些。如果输出里对象数量明显少于预期或者一堆/Type /XRef找不到基本可以确定是 xref 解析走偏了先回去检查 3.2 节里的PODOFO_HAVE_ZLIB有没有漏定义。4. 核心模块拆解一个 PDF 从字节流到对象树的完整链路到这一步库能跑了接下来是看懂它内部怎么走。这块决定了你后面改的时候是能在合适的层次插入还是只能在外围打补丁。4.1 从 PdfTokenizer 到 PdfParser词法和语法是分开的PdfTokenizer只做一件事从输入设备里往外吐 token。它认得的 token 类型很有限——数字、名字/开头、字符串(或者开头、字典的开始和结束、、数组的开始和结束、以及关键字obj、endobj、stream、endstream、R。它不关心中间有没有空格、注释怎么跳过、字符串里的转义怎么处理这些都在这一层做掉。PdfParser拿到 token 之后才开始做语法它先根据文件尾的startxref偏移Seek到 xref 表的位置读出一张“对象号 → 文件偏移”的映射再把这个映射交给PdfXRef管理。之后每次你要某个对象PdfXRef查表、PdfParser跳到那个偏移、PdfTokenizer重新从那个位置开始吐 token直到读到endobj为止。这条链路里有个细节值得知道有相当一部分 PDF 的 xref 表是坏的生成工具写错了偏移或者文件被截断过。PdfParser在 xref 读失败的时候会尝试重建——它退化成从头扫一遍整个文件靠正则式的模式匹配找数字 数字 obj这样的片段把对象重新收进PdfVecObjects。这个重建逻辑不算特别健壮碰到加密文件或者流里恰好有类似片段的情况会误判所以线上跑的解析代码一定要包在try/catch里并且对“解析出来的页数为 0”这种情况做兜底。4.2 对象模型PdfObject、PdfVariant 和 PdfReference 的关系这三者的关系第一次看会有点绕。PdfVariant是一个“值”能装下 PDF 的基本类型——布尔、数字、字符串、名字、数组、字典、引用、空值。PdfObject是“间接对象”它继承自PdfVariant额外带上了对象号和代数而且它可以挂一个PdfStream。PdfReference则是“指向某个间接对象的引用”本质是一对(对象号, 代数)。理解这个结构之后遍历就有了套路你从doc.GetObjects()拿到的每一项都是PdfObject*想看它是什么类型就GetDataType()想当字典用就GetDictionary()想拿流就HasStream()加GetStream()。要取解码后的内容用GetFilteredCopy()而不是GetBuffer()前者会按/Filter里的过滤器链依次解压后者给你的还是原始压缩字节。PdfVecObjects objs doc.GetObjects(); for (int i 0; i objs.GetSize(); i) { PdfObject* pObj objs.GetObjectAt(i); if (pObj NULL || !pObj-HasStream()) continue; // 只关心内容流其他流图片、字体文件跳过 PdfStream* pStream pObj-GetStream(); if (pStream-HasFilter()) { PdfRefCountedBuffer buffer; pStream-GetFilteredCopy(buffer); // 解压后的明文 // buffer.GetBuffer() 拿首地址buffer.GetSize() 拿长度 } }遍历的时候有两个地方容易翻车。一是GetObjectAt(i)有可能返回NULL尤其是重建过 xref 的文件空槽位就是这么来的一定要判空。二是GetFilteredCopy返回的PdfRefCountedBuffer是引用计数的它的生命周期跟着buffer变量走出了作用域数据就没了别把裸指针存下来跨函数用。4.3 写入链路PdfStreamedDocument 加 PdfPainter 就够了写 PDF 比读简单得多因为不用猜别人的格式。PdfStreamedDocument是流式的构造时传一个文件名之后每加一页就立刻往文件里写对象最后Close()的时候补 xref 和 trailer。这套流程内存占用低几百页的文档也不会爆。PdfStreamedDocument doc(output.pdf); // A4 纸单位是点1 点 1/72 英寸 PdfPage* pPage doc.CreatePage(PdfPage::CreateStandardPageSize(ePdfPageSize_A4)); PdfPainter painter; painter.SetPage(pPage); // 内嵌字体第三个参数控制是否子集化只嵌入用到的字形 PdfFont* pFont doc.CreateFont(simhei, true, true); painter.SetFont(pFont, 12.0); painter.DrawText(72.0, 720.0, 第一行文字); // 画一条横线坐标原点是左下角 painter.DrawLine(72.0, 700.0, 500.0, 700.0); painter.FinishPage(); // 必须调用否则这一页的内容流不会落盘 doc.Close(); // 必须调用否则文件没有 xref等于废文件FinishPage()和Close()这两个调用是硬性要求少任何一个产出的都是坏文件而且坏得很隐蔽——有些阅读器能打开有些直接报错排查起来很费劲。这是我在项目上返回去重建过一次的东西血泪经验。CreateFont的第二个参数是“是否嵌入”第三个是“是否子集化”。做中文文档时两个都要开不嵌入的话接收方机器上没有这个字体就会显示成方块不做子集化的话一个中文字体动辄十几兆全塞进 PDF 里传输成本受不了。子集化的代价是生成的字体只包含你实际用到的字所以同一份文档里别在写完Close()之前反复改文字——改不了流已经写出去了。5. 避坑与排查VS2005 编 PoDoFo 的 5 个真实翻车现场下面这五条是我从“解压源码”到“业务代码上线”这段时间里实际卡过的每一条都按现象、原因、解决写清楚。5.1 编译期头文件和语言特性现象一第一句编译就报fatal error C1083: 无法打开包括文件:podofo_config.h但你在源码目录里能搜到这个文件名。原因是这份文件在原始工程里是构建脚本生成的源码包里只有模板没有实体。解决就是 3.2 节那份手写配置头放到src/podofo/下面确保“附加包含目录”里有这个路径。补完还会顺带报一批int64_t未定义的错误那是模板里本来就靠stdint.h的东西配置头里的 typedef 块一起解决掉。现象二改完配置头编译器开始批量报error C2065: nullptr: 未声明的标识符还有一些error C2143: 语法错误 : 缺少,定位到auto那一行。原因是源码里混了少量 C11 写法VS2005 的编译器不认。解决不是全局替换而是按报错文件逐个改nullptr换成NULLauto x ...展开成完整类型。改的时候注意别动注释和字符串里的同名内容用 IDE 的“替换”限定当前文档并区分大小写比全局替换安全。改完这一批override和 delete还会再冒一批同样处理一个文件一个文件过。现象三error C2039: snprintf: 不是 std 的成员。原因是 VS2005 的 CRT 只提供_snprintf标准名要等到后面的版本才有。解决是在配置头里加一个兼容宏或者干脆在报错文件里把snprintf换成_snprintf并手动给缓冲区末尾补\0——_snprintf在不截断的情况下不保证结束符这一点和标准行为不同不加会读到缓冲区外的内存。5.2 链接期与运行期CRT、依赖库和坏文件现象四静态库编过了业务程序链接时报一堆LNK2001: 无法解析的外部符号 _inflate、_deflateEnd符号名前面还带下划线。原因是 zlib 没链或者链了但运行时库选项不一致——你的工程是/MTzlib 那份是/MD符号修饰就对不上。解决办法是把 zlib 的源码也按/MT重新编一份或者在工程里统一改运行时库。判定方式很简单看LNK2001的符号名纯 C 符号带下划线前缀说明是 C 库没链上C 修饰过的长名字说明是某个.cpp漏进了工程。现象五程序能跑但读某几份 PDF 的时候随机崩溃崩溃点在free或者memcpy堆栈看起来毫无规律。原因有两个都很典型。一是 Debug 版库配 Release 版程序或者反过来两边 CRT 不一致new在一个堆上分配、delete在另一个堆上释放。二是同一份PdfMemDocument实例在catch之后被继续复用——PoDoFo 抛异常的时候对象表可能处于半解析状态继续调GetPages()会读到已经释放的内存。解决前者靠统一运行时库第三 章 3.2 节那条解决后者靠“一次解析一个实例”异常一出立刻析构当前 doc重新构造一个新的再来。这条规则后来被我写进了封装层的注释里用大红字标着。现象六顺带记一笔属于业务层中文画上去变成一堆方块。原因是字体没嵌入或者文字编码和字体的编码没对上。CreateFont的第二个参数传false时字体不嵌入接收方没有这个字体就渲染不了而编码这块内部走的是 UTF-8 到 UTF-16BE 的转换如果你传进去的字符串本身是本地编码的窄字符串转出来就是错的。做法是统一在边界处把文本转成 UTF-8 再往里传同时确认CreateFont的嵌入和子集化都开着。6. 二次开发把 PoDoFo 包一层别让业务代码看见它库跑通之后真正的工程问题才刚开始业务代码里到处#include podofo/podofo.h意味着将来换版本、或者给不支持 C 的平台做一套备选实现你都得回头改几十个文件。我在第二次迭代的时候把它包了一层收益比预期大。核心思路是 pimpl 加异常转换。对外暴露的头文件里只有一个前置声明和一个句柄类PoDoFo 的所有类型都藏在实现文件的struct Impl里所有PdfError在跨越这一层的时候转换成项目自己的错误码和错误消息业务层不需要知道PdfError是什么。// PdfFacade.h —— 业务代码只看得见这个文件 class PdfFacade { public: PdfFacade(); ~PdfFacade(); // 返回 0 表示成功非 0 是错误码错误消息通过 outMsg 带出 int Open(const std::string path, std::string outMsg); int GetPageCount(int outCount); int ExtractText(int pageIndex, std::string outText); private: struct Impl; // 所有 PoDoFo 类型都在 Impl 里 Impl* m_pImpl; PdfFacade(const PdfFacade); // 禁拷贝老编译器没有 delete PdfFacade operator(const PdfFacade); };这套结构配合三个改造点一起用效果最好改造点具体做法解决什么问题生命周期隔离每次Open内部new一个PdfMemDocumentClose/析构时删掉解析异常后对象表状态不可信复用必崩异常转错误码catch (PdfError e)里取错误码转成int返回业务层不需要理解 PoDoFo 的异常体系页级懒加载只在ExtractText时按页取对象不预加载全部大文档几百页带图内存占用从几百兆降到几十兆验证方式也很直接准备一份“坏样本集”里面放截断的 PDF、xref 写错的 PDF、加密的 PDF、一堆扫描件然后写一个跑批程序每份文件跑一遍OpenGetPageCount 前三页抽文本看错误码分布和内存增长曲线。这套回归在我这里是每次改封装层都强制跑一遍的因为它能抓到一类特别难查的问题——单份文件跑没事连着跑两百份才慢慢泄漏。有一次为了赶进度我跳过这套回归直接把封装层推上测试环境结果跑了半天之后进程内存一路涨到几个 G 被系统杀掉回头查是某个分支里PdfMemDocument构造了但没删。从那以后我每次动这一层哪怕只改一行错误码映射也强制把坏样本集跑一遍看内存曲线是平的才敢提交。希望这些能帮到你少走两趟弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?