简介:面向需要在VS2005环境下处理PDF读写的C++开发者,这是一套已完成移植与联调的PoDoFo 0.9.7工程包。原库依赖freetype、libjpeg、libpng、libtiff、openssl、zlib等第三方组件,逐个链接静态库颇为繁琐,此处已将依赖编译为lib库,用VS打开即可编译通过,省去环境搭建的反复试错。包内共1382个文件,以h头文件、c与cpp源码、obj中间产物为主,另有vcproj与sln工程文件、lib与dll二进制、mk与jamfile构建脚本,以及readme、html说明文档,整体约42.38MB,兼顾源码查阅与直接复用。文档加密由PODOFO_HAVE_OPENSSL宏控制,启用后需补充两个dll并链接对应lib;两个依赖Linux接口的用例已置为禁用状态。已有572人学习,适合具备一定C++基础、希望在旧版VS上实现PDF解析与生成的中高级开发者参考。
1. 为什么在 2024 年还有人用 VC8 编 PoDoFo 0.9.7:一个 PDF 解析库的老工具链落地
某公司的老 MFC 报表系统还锁在 VS2005 工具链上,客户新提的需求里有一条:把系统生成的 PDF 按章节拆开、给每页加水印、再合并回去。选型时试过几个库,最后落在 PoDoFo 0.9.7——C++ 接口干净、能读写加密文档、源码量不大,静态链进 exe 不额外带 DLL。真正动手才发现 0.9.7 是照着 C++11 写的,VC8 编译器不认 nullptr、override、enum class,新版本 CMake 也把 Visual Studio 8 2005 生成器删了。这篇记录从依赖库编译、CMake 版本对齐、C++11 语法降级到中文 PDF 乱码排查的完整路径,适合同样被老工具链绑住、又必须把 PDF 文件解析塞进现有工程的 C++ 开发者。
2. 拆开 PoDoFo 0.9.7 的依赖与编译链路:选型、裁剪与 CMake 生成器适配
2.1 PoDoFo 0.9.7 在 PDF 库坐标系里的位置
先把选型说清楚,否则后面每一步裁剪都没有依据。PDFium 渲染最强,但它是 Chromium 的一部分,拉下来就是几百 MB,还要求 C++14 以上;MuPDF 渲染快、许可证是 AGPL,商业闭源项目基本不能碰;Poppler 的 C++ 接口带一堆 Glib 依赖,在 Windows 上编过的人都懂那种酸爽;libharu 只写不读,满足不了拆分的需求。PoDoFo 的定位在中间:读写都支持,原生 C++ 面向对象接口,LGPL 许可,静态库体积能压到几 MB,正好适合塞进一个老 MFC 工程。
0.9.7 这个版本能覆盖的能力包括:PDF 1.7 文档的解析与生成、RC4 40/128 位和 AES-128 位加密文档的读写、增量更新、AcroForm 表单字段访问、字体嵌入与子集化、页面内容流的解析与重写。拆分、合并、加水印、提取文本、填表单这几类需求都在射程内。不足也明显:官方文档薄,出错时抛的 PdfError 只给一个错误码,字体管理在 Windows 上要靠自己注册 ttf,中文支持需要额外做功课。
我选它还有一个现实原因——源码结构清晰。core 目录负责对象模型和解析器,base 目录放流、字符串、编码这些基础设施,doc 目录是文档和页面,font 目录处理字体。真出问题时能顺着调用栈找到具体文件,不像某些库一层套一层模板,报错信息全是 instantiating 某某。
2.2 依赖裁剪:zlib、libjpeg、libtiff、freetype 的取舍
PoDoFo 0.9.7 的依赖分成“必须”和“可选”两档,先把表列出来,再逐个说为什么这么选。
| 依赖 | 作用 | 是否必须 | 建议版本 | VC8 下的编法 |
|---|---|---|---|---|
| zlib | FlateDecode 流压缩/解压 | 必须 | 1.2.11 | nmake + win32/Makefile.msc |
| freetype | 字体嵌入、子集化、字形度量 | 必须 | 2.8 以前 | CMake 生成 VS2005 工程 |
| libjpeg | DCTDecode 图片解码 | 按需 | 6b 或 9 | nmake + makefile.vc |
| libtiff | CCITTFaxDecode 之外的 TIFF 图片 | 按需 | 4.0.x | nmake + nmake.opt |
| fontconfig | Linux 下系统字体查找 | Windows 关闭 | - | 不编 |
| OpenSSL | AES 加密的底层实现 | 可关 | 1.0.2 | 不编 |
裁剪原则很直接:如果只做文本页面的拆分合并和水印,zlib + freetype 就够;文档里有扫描件图片才需要 jpeg/tiff。fontconfig 是 Unix 那套 fontconfig 库,Windows 上不存在,CMake 里必须显式关掉,否则 configure 阶段就会报找不到。OpenSSL 我一般也先关,0.9.7 在没有 OpenSSL 时会退回内置的加密实现,先让编译链路跑通,等加密需求真正压上来再补。
对应到 CMake 变量,就是PODOFO_HAVE_JPEG_LIB、PODOFO_HAVE_TIFF_LIB、PODOFO_HAVE_PNG_LIB、PODOFO_HAVE_FONTCONFIG、PODOFO_HAVE_OPENSSL这几个开关,取值 0 或 1。另外PODOFO_BUILD_LIB_ONLY=1一定要开,它会把 podofo-tools 那堆命令行工具排除掉,少编十几个工程,也少踩一批 main 函数里用到的 C++11 语法。
2.3 CMake 与 VS2005 生成器的版本对齐
这是很多人第一步就翻车的地方。CMake 对 Visual Studio 8 2005 的生成器名字是Visual Studio 8 2005,注意不是VS2005,也不是Visual Studio 2005。从 CMake 4.x 开始,这个生成器被彻底移除,装最新版连 configure 都过不去。稳妥做法是锁 3.20 到 3.27 之间的版本,装完先跑一遍确认:
cmake --version cmake --help | findstr /C:"Visual Studio 8 2005"第二条命令能打印出生成器名字,才说明这个 CMake 可用。我一般会在工程根目录放一个cmake-version.txt记下具体版本号,换机器时照装,省得又去猜。
还有一点容易忽略:VS 是多人称生成器,CMAKE_BUILD_TYPE在这种生成器下不生效,Debug/Release 是在编译命令里用/p:Configuration=或者devenv.com /build "Release|Win32"指定的。CMake 阶段写-DCMAKE_BUILD_TYPE=Release不会报错,但也不会起作用,第一次编完发现是 Debug 版库,多半就是这个原因。
2.4 目录与构建脚本落位
依赖库、源码、构建目录、输出目录分开放,后面排查问题时能一眼看出用的是哪份头文件。我用的结构是这样:
pdfkit/ deps/ zlib-1.2.11/ jpeg-6b/ tiff-4.0.10/ freetype-2.8/ src/ podofo-0.9.7/ build/ vs2005/ # CMake 生成 .sln 的位置 out/ include/ # 汇总头文件 lib/ # 汇总 .lib vc8_compat.h构建入口脚本只做三件事:进 VS2005 环境、切到构建目录、调 CMake。写成build.bat放在根目录:
@echo off call "C:\Program Files\Microsoft Visual Studio 8\VC\vcvarsall.bat" x86 if not exist build\vs2005 mkdir build\vs2005 cd build\vs2005 cmake -G "Visual Studio 8 2005" ^ -DPODOFO_BUILD_LIB_ONLY=1 ^ -DPODOFO_BUILD_STATIC=1 ^ -DPODOFO_BUILD_SHARED=0 ^ -DPODOFO_HAVE_FONTCONFIG=0 ^ -DPODOFO_HAVE_OPENSSL=0 ^ -DCMAKE_INCLUDE_PATH="%CD%\..\..\out\include" ^ -DCMAKE_LIBRARY_PATH="%CD%\..\..\out\lib" ^ ..\..\src\podofo-0.9.7vcvarsall.bat x86是为了让 nmake 和 cl 都在 PATH 里,32 位是因为老 MFC 工程几乎都是 Win32 目标。CMAKE_INCLUDE_PATH和CMAKE_LIBRARY_PATH指向提前编好的依赖,比在 CMake GUI 里一个个填路径可靠得多。
3. 用 VS2005 工具链把依赖库先编出来:zlib / libjpeg / libtiff / freetype 逐个过
3.1 zlib 的 VC8 编译与符号导出
zlib 的源码里自带win32/Makefile.msc,这是给 nmake 用的,比 CMake 更省事,也不会引入新版本 CMake 的语法问题。进 VS2005 命令行环境后直接编:
call "C:\Program Files\Microsoft Visual Studio 8\VC\vcvarsall.bat" x86 cd deps\zlib-1.2.11 nmake -f win32\Makefile.msc编完会得到zlib.lib(静态库)、zdll.lib(导入库)和zlib1.dll。PoDoFo 静态链接时用zlib.lib,不要错拿zdll.lib,否则运行时要带 DLL,客户机器上少一个文件就是启动报错。
Makefile.msc里默认的CFLAGS含/MD,这个必须和后面 PoDoFo 的运行时库保持一致。如果老工程是/MT,要么改 Makefile 里的/MD为/MT重编,要么在 PoDoFo 侧也改成/MD。我一般选后者,因为/MT下静态库重复链接会让 exe 体积翻倍,还容易在跨 DLL 传指针时出问题。
3.2 libjpeg 与 libtiff 的静态库配置
libjpeg 6b 的编译入口是makefile.vc,但编之前得先把jconfig.vc复制成jconfig.h,并确认里面这几个宏是打开的:
#define HAVE_STDDEF_H #define HAVE_STDLIB_H #define HAVE_UNSIGNED_CHAR #define HAVE_UNSIGNED_SHORT少一个HAVE_STDLIB_H,后面 PoDoFo 引用 jpeg 头文件时就可能报size_t未定义,这种报错出现在第三方头文件里,第一眼很难联想到是 jconfig 的问题。命令是:
cd deps\jpeg-6b copy jconfig.vc jconfig.h nmake -f makefile.vc nodebug=1nodebug=1会编 Release 版本,体积小一半。产物是libjpeg.lib。
libtiff 稍微麻烦一点,得先改nmake.opt:OPTFLAGS里的/MD保持一致,INCLS加上 zlib 和 jpeg 的头文件路径,LIBDIR加库路径。改完再:
cd deps\tiff-4.0.10 nmake -f makefile.vc如果只是做文本 PDF 的拆分合并,libjpeg 和 libtiff 都可以先不编,CMake 里把对应开关置 0,编译时间能省一大截。等真的遇到带扫描图的文档再补,避免一开始就被第三方库的错误拖住。
3.3 freetype 的 win32 字体方案
freetype 的源码里没有现成的 VS2005 工程,用 CMake 生成最省事,但要挑版本。2.8 之后的源码开始用一些 C99 的写法,VC8 编起来报错会多不少,我一般锁在 2.8 以前。
cd deps\freetype-2.8 mkdir build && cd build cmake -G "Visual Studio 8 2005" ^ -DCMAKE_INSTALL_PREFIX=..\..\..\out ^ -DFT_WITH_ZLIB=0 ^ -DFT_WITH_BZIP2=0 ^ -DFT_WITH_PNG=0 ^ -DFT_WITH_HARFBUZZ=0 ^ -DFT_WITH_BROTLI=0 .. devenv.com freetype.sln /build "Release|Win32"把 zlib、png、harfbuzz 全关掉,是因为 PoDoFo 只用 freetype 做字形度量和子集化,用不到这些高级特性,关掉能少一堆依赖。编完在objs\release下拿到freetype.lib,头文件在include目录。
Windows 上没有 fontconfig,PoDoFo 的字体查找会退化成“你得自己告诉它 ttf 在哪”。常见做法是在初始化时把C:\Windows\Fonts\simsun.ttc这类字体路径注册进去,或者干脆把字体文件随程序发布,用相对路径加载。我倾向后者,客户机器上字体缺失或版本不一致时,至少程序行为是确定的。
3.4 依赖目录结构与环境变量
四个库编完后,把头文件和 lib 汇总到out下,CMake 找起来最省心:
out/ include/ zlib.h, zconf.h jpeglib.h, jconfig.h tiff.h, tiffio.h ft2build.h, freetype/ lib/ zlib.lib libjpeg.lib libtiff.lib freetype.lib汇总这一步别偷懒用xcopy全拷,只拷需要的头文件。freetype 的include下面有一堆内部头,全拷进去会让编译器的搜索路径变长,偶尔还会和系统里的同名头文件撞车。
4. PoDoFo 0.9.7 主体编译:从 CMake 配置到 .sln 落地的每一步
4.1 CMake 参数清单与缓存变量
依赖齐了之后,configure 这一步的核心是把每个依赖的开关和路径喂给 CMake。完整命令如下:
cmake -G "Visual Studio 8 2005" ^ -DPODOFO_BUILD_LIB_ONLY=1 ^ -DPODOFO_BUILD_STATIC=1 ^ -DPODOFO_BUILD_SHARED=0 ^ -DPODOFO_HAVE_JPEG_LIB=0 ^ -DPODOFO_HAVE_TIFF_LIB=0 ^ -DPODOFO_HAVE_PNG_LIB=0 ^ -DPODOFO_HAVE_FONTCONFIG=0 ^ -DPODOFO_HAVE_OPENSSL=0 ^ -DCMAKE_INCLUDE_PATH="%CD%\..\..\out\include" ^ -DCMAKE_LIBRARY_PATH="%CD%\..\..\out\lib" ^ -DCMAKE_CXX_FLAGS="/D_CRT_SECURE_NO_WARNINGS /FI\"vc8_compat.h\"" ^ ..\..\src\podofo-0.9.7每个参数的作用:PODOFO_BUILD_LIB_ONLY只编库不编工具;PODOFO_BUILD_STATIC出静态库;PODOFO_HAVE_*控制可选依赖;CMAKE_INCLUDE_PATH/CMAKE_LIBRARY_PATH是 CMake 的查找提示路径,比FindXXX.cmake里写死路径灵活;CMAKE_CXX_FLAGS里的/FI是强制包含,把兼容头塞进每个编译单元,省得手工改几十个 cpp 的 include。
configure 完成后,看CMakeCache.txt里的PODOFO_HAVE_ZLIB和PODOFO_HAVE_FREETYPE是不是 1,这两个是必须项,如果是 0 说明路径没找对,后面必然链接失败。
4.2 头文件与 C++11 差异:stdint.h、override、nullptr 的补丁策略
VC8 缺的东西集中在两处:C99 的<stdint.h>和 C++11 的关键字/库设施。前者好办,后者要写兼容头。我的vc8_compat.h大致长这样:
// vc8_compat.h:VC8 缺失的 C99/C++11 符号兜底 #if defined(_MSC_VER) && _MSC_VER < 1600 // 1. stdint.h 的替代,VC8 自带的是 Windows SDK 里的版本,不一定在默认路径 #ifndef _STDINT_H typedef signed __int64 int64_t; typedef unsigned __int64 uint64_t; typedef signed int int32_t; typedef unsigned int uint32_t; typedef signed short int16_t; typedef unsigned short uint16_t; typedef signed char int8_t; typedef unsigned char uint8_t; #endif // 2. C++11 关键字降级 #ifndef nullptr #define nullptr NULL #endif #ifndef override #define override #endif // 3. snprintf / va_copy 在 VC8 里不存在 #ifndef snprintf #define snprintf _snprintf #endif #ifndef va_copy #define va_copy(dst, src) ((dst) = (src)) #endif #endif // _MSC_VER < 1600nullptr在 VC8 里不是关键字,也不是保留字(除非开/clr,PoDoFo 不开),所以#define nullptr NULL是安全的。override同理,它只是一个标识符,宏定义成空之后所有void f() override;都能正常编译。_snprintf和snprintf的返回值语义不同,_snprintf在截断时不保证以\0结尾,但 PoDoFo 里这个函数主要用在日志和错误信息拼接,风险可以接受;如果介意,可以自己包一个保证结尾的podofo_snprintf。
真正麻烦的是enum class和std::unique_ptr。enum class在 VC8 里直接报 C2059,只能改成普通 enum 再手工限定作用域,0.9.7 里用到的地方不算多,一处处改,半天能过。std::unique_ptr没有现成替代,auto_ptr的语义接近但不支持数组和移动,我一般是把成员变量里的unique_ptr换成裸指针加析构 delete,局部变量里的换成auto_ptr。这一步没有捷径,改完编译一遍,看链接错误定位漏网之鱼。
4.3 编译错误逐类排查:从 time_t 到 snprintf
第一次编报几百个错是正常的,按错误码归类能快很多。下面这张表是我实际遇到过的几类:
| 错误码 | 典型信息 | 原因 | 处理 |
|---|---|---|---|
| C2065 | nullptr未声明 | 兼容头没生效 | 检查/FI路径,或直接在项目属性里加强制包含 |
| C2059 | enum后跟class | C++11 的 enum class | 改普通 enum + 手工限定 |
| C3861 | 找不到va_copy | VC8 无此宏 | compat 头里用赋值替代 |
| C2039 | unique_ptr不是std成员 | C++11 库设施 | 换裸指针或auto_ptr |
| C4996 | strcpy不安全 | VC8 的 CRT 安全检查 | 加/D_CRT_SECURE_NO_WARNINGS |
| LNK2019 | __imp__inflate未解析 | zlib 没链或 CRT 不一致 | 检查 zlib.lib 的编译选项 |
C4996 这类警告不用一个个改代码,/D_CRT_SECURE_NO_WARNINGS一关就行,老代码里这种调用成百上千,改是改不完的。LNK2019 集中在__imp__开头的符号时,基本可以断定是“静态库当导入库用”或者“运行时库不一致”,回到 3.1 节检查 zlib 的编译选项。
4.4 生成 lib 与头文件整理
configure 通过、语法错误清完之后,用 devenv 命令行编译:
devenv.com podofo.sln /build "Release|Win32" /project podofo/project podofo只编库工程,跳过依赖的其他子工程,速度快很多。产物在build\vs2005\src\Release\podofo.lib(具体路径看 CMakeLists 里的输出目录设置,不同小版本可能差一层)。头文件在src\podofo-0.9.7\src下,按base、core、doc、font分目录,用的时候把整个src目录加到包含路径,代码里写#include <podofo/podofo.h>。
我一般还会把podofo.lib、podofo_d.lib(如果编了 Debug)和头文件一起拷到out下,再写一个podofo.pc或者简单的readme.txt记录版本和编译选项。过三个月再回来查“这个 lib 到底带不带 jpeg”,没有记录就只能重新编一遍试。
5. 避坑与排查:VC8 编译 PoDoFo 常见的五个翻车现场
5.1 LNK2019:CRT 与符号导出不一致
现象:主工程链接podofo.lib时,报一串__imp__inflate、__imp__jpeg_read_header未解析,但库文件明明在附加依赖里。
原因:依赖库和主工程的运行时库不一致,或者把导入库zdll.lib当成了静态库zlib.lib。__imp__前缀是导入库的特征,说明链接器认为这些符号要从 DLL 导入。
解决:统一所有库的/MD或/MT;在附加依赖里按podofo.lib → freetype.lib → zlib.lib的顺序排,被依赖的放后面;用dumpbin /symbols zlib.lib | findstr inflate确认拿到的是静态符号而不是__imp__。
5.2 C2059 / C2065:C++11 语法在 VC8 里的连锁报错
现象:一编译几百个错误,集中在nullptr、override、enum class上,改完一个文件又冒出十个。
原因:/FI"vc8_compat.h"没生效,或者兼容头被放在了stdafx.h之后,预处理顺序不对;enum class的报错则会连锁触发后面的语法解析失败。
解决:在项目属性 → C/C++ → 高级 → 强制包含里直接加文件名,比 CMake 的CMAKE_CXX_FLAGS更可靠;enum class集中改完再编,不要边改边编,否则报错数量会一直误导你。
5.3 生成 PDF 后 Acrobat 报文件损坏
现象:用PdfStreamedDocument生成的 PDF,PoDoFo 自己读没问题,Acrobat 打开提示“文件已损坏”,换 Foxit 也一样。
原因:流式写入要求对象按严格顺序落盘,先写页面再回头补资源、或者跨页共享对象,都会让 xref 的偏移算错;另一种情况是对象没Close()程序就退出了,文件尾部缺startxref。
解决:几十 MB 以内的文档改用PdfMemDocument,内存里组织完一次性Save(),不依赖写入顺序;必须流式时,保证对象创建顺序和写入顺序一致,析构前显式Close(),并在Close()之后检查文件大小是否非零。
5.4 加密 PDF 打开抛 PdfError
现象:PdfMemDocument::Load()构造时抛PdfError,GetError()返回InvalidPassword,但密码明明是对的。
原因:0.9.7 只支持 Standard Security Handler 的 R2 到 R4(RC4 和 AES-128),PDF 2.0 的 AES-256(R6)不在支持范围;另一种情况是密码字符串编码不对,中文密码按 UTF-8 传进去会被当成 Latin-1。
解决:先用别的工具确认文档的加密算法版本;R6 的文档在上游解密或换库;密码用PdfPassword对象构造,明确指定编码,不要直接传const char*。
5.5 中文提取乱码与字体注册
现象:PdfPage::ExtractText()拿到的中文全是问号或方块,英文正常。
原因:文档里的 CID 字体没有/ToUnicodeCMap,PoDoFo 只能按字体编码回退,Identity-H 编码下拿到的就是字形索引而不是 Unicode。
解决:先看PdfFont能不能拿到 ToUnicode 流,拿不到就自己解析/ToUnicode的 CMap 做映射;生成端则要保证嵌入字体时写入 ToUnicode,PdfFont创建时把 `ePdfFont_W
本文还有配套的精品资源,点击获取