ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VS2005 编译 PoDoFo 0.9.7:PDF 解析库移植与静态库依赖

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 和 C++11,而 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走 zlib,DCTDecode走 libjpeg,ASCIIHexDecode、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.5
opensslRC4/AES 解密、签名带口令的 PDF 打开即抛异常1.0.2u,别用 1.1+
libtiff / 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,只有带下划线的_snprintf;strtoll同样缺席。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,或者在工程设置里加/FI"podofo_compat.h"。我选后者,因为 PoDoFo 的源文件有一百多个,手改容易漏,而且以后升级源码还得再改一遍。

3.2 用脚本扫出 .cpp 清单,直接生成 vcproj 的 Files 段

VS2005 的工程文件是.vcproj,XML 格式,<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('%s<File RelativePath="%s"/>' % (pad, f)) if nested: lines.append(" </Filter>") lines.append("</Files>") with open(OUT, "w", encoding="utf-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 的编译器默认堆只有几十 MB,PoDoFo 有几个巨型翻译单元(解析器和对象模型相关的),不加这个很容易撞上fatal error C1060: 编译器堆空间不足。
  • 运行库/MD或/MT要和四个依赖库完全一致。混用的后果不是链接错误,而是运行期在free()里崩,最难查。
  • /Zc:wchar_t保持开启,同时确认所有依赖也是这个设定。wchar_t是内建类型还是 typedef,直接影响符号修饰,不一致就是一堆看不懂的LNK2001。
  • /wd4819、/wd4996、/wd4267建议加上。源码里可能有 UTF-8 的中文注释或版权头,VS2005 会刷屏式报 C4819;size_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 --prefix=d:\deps\openssl102 ms\do_ms.bat nmake -f ms\nt.mak nmake -f ms\nt.mak install

no-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 或都是 x64,VS2005 下基本都是 Win32)、同一运行库(都是/MD或都是/MT)。不满足这两条时lib.exe会给出警告,别忽略它——那个警告在链接阶段会变成运行期崩溃。

如果不想合并,那就得注意链接顺序。MSVC 的链接器是单遍扫描,被依赖的库要放在依赖它的库后面:

podofo.lib zlib.lib libjpeg.lib freetype.lib libeay32.lib ssleay32.lib ws2_32.lib crypt32.lib

顺序错了的典型症状是一部分符号能解析、一部分不能,看着像是库缺了,其实是扫描顺序问题。实在懒得排顺序,把「链接器 → 常规 → 链接库依赖

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进