VS2005 编译 PoDoFo 0.9.7:PDF 解析库移植与静态库依赖 简介面向需要在 Windows 平台使用 Visual Studio 处理 PDF 的中初级 C 开发者此工程提供 podofo 0.9.7 在 VS2005 下编译成功的完整版本适合希望快速集成 PDF 解析、读取与生成能力又不想被大量第三方依赖拖慢进度的人员。工程已将字体解析、图像编解码、压缩、加密及单元测试相关依赖一并整理并编译成库减少逐个链接静态库的配置负担。压缩包共 1382 个文件约 42.38MB主体为头文件、C 与 C 源码以及编译产生的中间文件并配有工程文件、解决方案、静态库、动态库、脚本和说明文档覆盖源码、示例与构建配置便于按模块查看和二次开发。目前已有 573 人学习下载。下载后可获得可直接打开的 VS 工程、已配置的依赖库和示例代码加密宏的启用方法也有说明开启后需要把两个动态库放到程序目录并链接对应库依赖 Linux 环境的用例则保持禁用。整体适合用来减少开源库整合与排错时间把精力放在 PDF 读写功能开发上。1. VS2005 里编 PoDoFo 0.9.7为什么有人还在做这件逆流的事老项目维护到第七个年头工具链被固定在 VS2005 上编译机、静态库、第三方控件全部是那个年代的东西。需求本身不复杂把用户上传的合同 PDF 打开、读出页数和文本、再把几页拼成一份新的带页眉的文件。控件报价谈不下来于是选型落到 podofo0.9.7——一个纯 C 的 PDF 解析与写入库源码完全摊在自己手里读写改一条链路能走完。麻烦在于它默认面向现代 CMake 和 C11而 VS2005 的编译器停在_MSC_VER 1400。下面的内容就是一次完整的移植记录配置头怎么手写、文件清单怎么生成、四个依赖按什么顺序编、以及那几处能让编译停在原地反复翻车的具体故障。适合同样被老工具链绑死、又必须做文件解析的人。2. 拆开 PoDoFo 0.9.7 的骨架解析器、文档对象与依赖矩阵动手改代码之前得先知道自己在改什么。PoDoFo 的源码目录结构其实很规整src/podofo下面按职责分了几个子目录base放基础类型和工具函数object放 PDF 对象模型parser放词法与语法解析doc放文档级 API 和绘制接口。搞清楚这三层的边界后面加文件、砍依赖才不会连锁出错。2.1 对象模型与解析器PdfObject、PdfVariant、PdfParser 三层一份 PDF 文件在磁盘上就是「头 一堆间接对象 交叉引用表 trailer」的线性结构。PoDoFo 把它拆成三层PdfParser负责读 xref、定位对象偏移、解压对象流PdfObject表示一个带对象号的间接对象PdfVariant表示具体的值类型包括PdfName、PdfString、PdfArray、PdfDictionary、PdfReference、数字、布尔和 null。这一层最值得记住的是延迟解析。PdfMemDocument::Load()并不会把每个对象都完整解出来很多对象只是记了偏移量真正访问时才由PdfObject::DelayedLoad()触发解析。好处是打开大文件快坏处是异常会从你意想不到的调用点抛出来——遍历对象树时崩了根因往往在某个几层调用之外的对象读取上。我一般会在调试构建里把PdfError::EnableLogging(true)打开让内部错误直接打到 stderr比一层层追栈快得多。另一层是过滤器。PDF 里的流基本都是压缩或编码过的PdfFilterFactory按/Filter里的名字分派FlateDecode走 zlibDCTDecode走 libjpegASCIIHexDecode、ASCII85Decode、LZWDecode、RunLengthDecode是库内自实现。这也解释了为什么 zlib 是硬依赖、libjpeg 是强烈建议——现实中的 PDF 几乎不可能没有 Flate 流扫描件几乎不可能没有 JPEG 图。2.2 文档与绘制层PdfMemDocument 与 PdfStreamedDocument 的分工文档层有两个入口选错了后面会很别扭。PdfMemDocument把整个文档读进内存可以随意增删页、改对象、重新保存适合「读进来改一改再写出去」的场景PdfStreamedDocument是只写一次的流式文档边写边落盘内存占用低适合「从零生成一份新 PDF」。两者的基类都是PdfDocument创建页面、创建字体、设置版本号这些接口是共用的。再往上是绘制层PdfPage管页面尺寸和内容流PdfPainter提供DrawText、DrawLine、DrawImage、DrawRect这类绘图原语PdfFont管字体与编码PdfImage管图像嵌入PdfContentsTokenizer则是反向的——把页面内容流拆成操作符和操作数抽文本就靠它。表单域PdfField、注释PdfAnnotation、数字签名PdfSignature是外围模块不用的化可以在编译期砍掉能省不少编译时间和最终体积。2.3 依赖矩阵每个第三方库关掉之后会丢什么这一步决定编译工作量。PoDoFo 0.9.7 的依赖都是可选的除 zlib关掉就要接受功能缺失。我把实际用到的整理成表依赖用途关掉之后的后果VS2005 下的选择zlib 1.2.xFlateDecode 压缩流绝大多数正常 PDF 直接读不了1.2.11必选libjpegDCTDecode 图像扫描件、含 JPEG 图的页面取不到图8d最省事freetype 2.x字体度量与嵌入子集嵌入字体排不出来中文尤其明显2.6.5opensslRC4/AES 解密、签名带口令的 PDF 打开即抛异常1.0.2u别用 1.1libtiff / libpng少数图像的透传极少数文档的图像提取失败直接关掉表里最需要注意的两行是 libjpeg 和 openssl。VS2005 的编译器对 C99 的支持很有限jpeg 9 系里有些写法要改openssl 则是 API 断代问题——1.1.0 之后EVP_CIPHER_CTX变成不透明类型栈上分配全部失效0.9.7 的源码是按 1.0.x 写的强行上高版本会得到一片编译错误。老工具链配老依赖这个组合反而最省时间。3. 把源码塞进 VS2005配置头、文件清单与编译开关真正开始动手。这一章的目标是让podofo.lib能在 VS2005 里编译通过产出物是一个静态库不依赖运行时 DLL。整个流程分四步补头文件、生成文件清单、手写配置头、调编译开关。顺序不能乱因为第一步的垫片头要在后面所有编译单元里生效。3.1 先补 VS2005 缺的那几样东西stdint、snprintf 与整型格式化VS2005 没有stdint.h这个头是 VS2010 才进的标准库也没有snprintf只有带下划线的_snprintfstrtoll同样缺席。PoDoFo 源码里这几样用得不算密集但足够致命随便一个文件撞上就是 C1083 或者 C3861。与其一个个文件改不如做一个强制预包含的垫片头// podofo_compat.h —— 放在 include 路径最前面再用 /FI 强制预包含 #pragma once #if defined(_MSC_VER) _MSC_VER 1600 #include stddef.h typedef signed char int8_t; typedef unsigned char uint8_t; typedef short int16_t; typedef unsigned short uint16_t; typedef int int32_t; typedef unsigned int uint32_t; typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #ifndef _INTPTR_T_DEFINED typedef _W64 int intptr_t; typedef _W64 unsigned int uintptr_t; #endif /* VS2005 只有带下划线的版本宏替换最省事 */ #define snprintf _snprintf #define vsnprintf _vsnprintf #define strtoll _strtoi64 #define strtoull _strtoui64 #define atoll _atoi64 #endif代码里的关键点有三个。第一int64_t用__int64而不是long long虽然 VS2005 认long long这个扩展但和第三方头混用时类型匹配更容易出岔子。第二intptr_t用_W64修饰这是 MSVC 在 32/64 位之间切换指针宽度的老写法不加会在 64 位编译时报警告。第三_snprintf在截断时不会自动补\0和标准的snprintf行为不一致如果 PoDoFo 里有依赖「截断后仍是合法 C 字符串」的代码得单独补一个包装函数不能只靠宏。加这个头的方式有两种每个.cpp顶部#include或者在工程设置里加/FIpodofo_compat.h。我选后者因为 PoDoFo 的源文件有一百多个手改容易漏而且以后升级源码还得再改一遍。3.2 用脚本扫出 .cpp 清单直接生成 vcproj 的 Files 段VS2005 的工程文件是.vcprojXML 格式Files节点下平铺或者按Filter分组列出所有源文件。PoDoFo 0.9.7 的src/podofo下有一百多个.cpp手动一个个加进去不现实而且漏一个的后果是链接期才报出来的LNK2001排查成本远高于写个脚本。思路很简单遍历源码目录按一级子目录分组输出可以直接粘进.vcproj的 XML 片段。# gen_vcproj_files.py —— 生成 vcproj 的 Files 段 import os SRC_ROOT r..\src\podofo # 相对 .vcproj 所在目录路径必须写对 OUT files.fragment.xml def collect(root): groups {} for dirpath, dirnames, filenames in os.walk(root): dirnames.sort() # 保证每次生成的顺序一致 rel os.path.relpath(dirpath, root) key . if rel . else rel.split(os.sep)[0] for fn in sorted(filenames): if fn.lower().endswith((.cpp, .c)): groups.setdefault(key, []).append(os.path.join(dirpath, fn)) return groups def main(): groups collect(SRC_ROOT) lines [Files] for key in sorted(groups): nested key ! . if nested: lines.append( Filter Name%s % key) pad if nested else for f in groups[key]: lines.append(%sFile RelativePath%s/ % (pad, f)) if nested: lines.append( /Filter) lines.append(/Files) with open(OUT, w, encodingutf-8) as fp: fp.write(\n.join(lines)) print(files%d - %s % (sum(len(v) for v in groups.values()), OUT)) if __name__ __main__: main()几个参数值得说清楚。SRC_ROOT必须相对于.vcproj文件所在目录来写因为RelativePath是相对于工程文件的写成绝对路径工程能编但换台机器就全红。key取一级子目录名是为了让 VS 的解决方案视图里自动出现base、doc、object、parser这几个筛选器找文件时不用在一百多项里翻。dirnames.sort()和sorted(filenames)保证同一个源码树每次生成的顺序一致否则每次重新生成都会产生一大片 diff代码评审时没法看。生成之后新建一个空的 Win32 静态库工程把Files整段替换进去即可。VS2005 打开时会有一次工程转换提示选「否」——不要让它升级格式升级完就不是 VS2005 工程了。注意如果源码目录里有.c文件比如某些滤镜实现一定也要收进来只扫.cpp会在链接期才发现缺实现。3.3 手写 podofo_config.h把 CMake 的活接过来PoDoFo 原本靠 CMake 探测环境生成podofo_config.h。VS2005 这条路上 CMake 走不通新版 CMake 已经没有 Visual Studio 8 2005 的生成器老版本 2.8 系列还能用但配置脚本本身也会因为编译器探测失败而中断所以这个头得手写。做法是打开源码里的podofo_config.h.in逐条对照愿意支持的宏留着并置 1不支持的注释掉/* podofo_config.h —— 手工版宏名以源码的 podofo_config.h.in 为准 */ #ifndef PODOFO_CONFIG_H #define PODOFO_CONFIG_H #define PODOFO_VERSION_MAJOR 0 #define PODOFO_VERSION_MINOR 9 #define PODOFO_VERSION_PATCH 7 /* 按实际编好的依赖开关注释掉就等于关闭该功能 */ #define PODOFO_HAVE_JPEG_LIB 1 #define PODOFO_HAVE_FREETYPE 1 #define PODOFO_HAVE_OPENSSL 1 /* #define PODOFO_HAVE_TIFF_LIB 1 */ /* #define PODOFO_HAVE_PNG_LIB 1 */ #define PODOFO_HAVE_WIN32_FLOAT_H 1 #endif /* PODOFO_CONFIG_H */最容易出错的是开关和实际链接的库不一致。PODOFO_HAVE_JPEG_LIB打开但工程里没链libjpeg.lib表现是一串LNK2001: 无法解析的外部符号 _jpeg_read_header反过来库链了但宏没开则表现为运行期取不出图而且不报错只是PdfImage拿到的数据是空的。我一般先只开PODOFO_HAVE_JPEG_LIB一项编译链接跑通再逐个打开其余开关出问题时能立刻定位到是哪个依赖引入的。3.4 编译开关与运行库/EHsc、/Zm、wchar_t 与字符集工程属性里有几项必须显式设定默认值在这个场景下不够用。/EHsc必须开。PoDoFo 全程用异常传递错误关掉异常处理后throw会直接终止进程。/Zm500建议加上。VS2005 的编译器默认堆只有几十 MBPoDoFo 有几个巨型翻译单元解析器和对象模型相关的不加这个很容易撞上fatal error C1060: 编译器堆空间不足。运行库/MD或/MT要和四个依赖库完全一致。混用的后果不是链接错误而是运行期在free()里崩最难查。/Zc:wchar_t保持开启同时确认所有依赖也是这个设定。wchar_t是内建类型还是 typedef直接影响符号修饰不一致就是一堆看不懂的LNK2001。/wd4819、/wd4996、/wd4267建议加上。源码里可能有 UTF-8 的中文注释或版权头VS2005 会刷屏式报 C4819size_t到int的窄化警告在 64 位下尤其多实际无害。预编译头建议直接关掉。PoDoFo 的文件碎、单文件不大PCH 带来的收益有限反而在老机器上把内存吃掉一大块更容易触发 C1060。4. 依赖的编译顺序zlib、libjpeg、freetype、openssl 怎么排依赖编译是有顺序的不是随便编。合理的顺序是 zlib → libjpeg → freetype → openssl因为 freetype 可以选择依赖 zlib也可以关掉而 openssl 完全不依赖前三个。先把无依赖的编出来后面的配置才不会有「找不到 xxx.lib」的循环。4.1 zlib 与 libjpeg半小时能出的两个库zlib 用自带的win32\Makefile.msc最稳一个命令出库cd zlib-1.2.11 nmake -f win32\Makefile.msc zlib.lib这里有一个非常隐蔽的坑zconf.h里有个ZLIB_WINAPI开关打开后所有函数变成__stdcall调用约定。PoDoFo 的源码是按__cdecl调的一旦 zlib 这边开了这个宏链接期就是满屏的符号不匹配而且错误信息里的符号名看起来一模一样很容易看漏。除非你的整个工程都统一到__stdcall否则这个宏一定不要定义。libjpeg 推荐 8d 版本makefile.vc直接可用cd jpeg-8d copy jconfig.vc jconfig.h nmake -f makefile.vc libjpeg.lib第一步的copy不能少。jconfig.vc是给 MSVC 准备的配置文件模板不改名成jconfig.h编译时就会去找一个不存在的头。jpeg 9 系里有一些更接近 C99 的写法VS2005 下可能要多改几处8d 能省掉这些功夫功能上也完全够用。4.2 freetype 与 openssl 1.0.2版本被卡死的两个点freetype 相对友好老一点的版本比如 2.6.5在builds\win32\vs2005目录下还保留着 VS2005 的解决方案。直接用 VS2005 打开编译产出一个freetype.lib然后把include目录加到 PoDoFo 工程的附加包含路径里。如果手里的版本已经没有这个目录了就退回到手工建工程把src/base、src/cff、src/truetype、src/sfnt、src/raster这几个目录的.c加进去也不算麻烦。freetype 自己的 zlib 支持建议关掉避免和 PoDoFo 链接的 zlib 重复。openssl 是这条路线上最需要克制的一环。必须用 1.0.2 系列1.0.2u 是最后的维护版本1.1.0 之后 EVP 相关结构体变成不透明类型EVP_CIPHER_CTX不能再在栈上分配0.9.7 的源码整片编译不过。编译命令perl Configure VC-WIN32 no-asm no-shared --prefixd:\deps\openssl102 ms\do_ms.bat nmake -f ms\nt.mak nmake -f ms\nt.mak installno-asm是为了绕开 NASM。VS2005 环境下配一套能用的 NASM 反而比不用汇编更费时间而 PDF 解密里的 AES 性能瓶颈不在这一层。no-shared出静态库和老工程的链接方式一致。产出的libeay32.lib和ssleay32.lib之外链接时还要带上ws2_32.lib、advapi32.lib、user32.lib、crypt32.lib少一个就是几个符号解析不了。4.3 静态库合并与链接顺序把三十个符号错误压到零四个依赖加 PoDoFo 本身是五个.lib。每次新建一个用到 PDF 功能的工程都要配一遍附加依赖项容易漏。我的做法是先用lib.exe合并成一个lib /OUT:podofo_all.lib podofo.lib zlib.lib libjpeg.lib freetype.lib libeay32.lib ssleay32.lib合并能省事但有两个前提所有库必须是同一架构都是 Win32 或都是 x64VS2005 下基本都是 Win32、同一运行库都是/MD或都是/MT。不满足这两条时lib.exe会给出警告别忽略它——那个警告在链接阶段会变成运行期崩溃。如果不想合并那就得注意链接顺序。MSVC 的链接器是单遍扫描被依赖的库要放在依赖它的库后面podofo.lib zlib.lib libjpeg.lib freetype.lib libeay32.lib ssleay32.lib ws2_32.lib crypt32.lib顺序错了的典型症状是一部分符号能解析、一部分不能看着像是库缺了其实是扫描顺序问题。实在懒得排顺序把「链接器 → 常规 → 链接库依赖本文还有配套的精品资源点击获取