简介面向仍在使用 VS2005 的 C 开发者这份工程给出 podofo0.9.7 开源 PDF 读写库的完整可编译版本重点解决第三方依赖繁多、手工配置静态库耗时的问题。压缩包共 1382 个文件、约 42.38MB以 .h 头文件、.c 源文件、.cpp 实现和 .obj 中间文件为主另含 vcproj 工程、lib 静态库、dll 动态库及 readme、html 说明下载后可直接打开解决方案编译。freetype、libjpeg、libpng、libtiff、zlib、lua、openssl、cppunit 等依赖已预先编译并封装为 lib 以减少逐个链接的负担文档加密由 PODOFO_HAVE_OPENSSL 宏控制启用后需补充两个 dll 并连接对应库两个依赖 Linux 的用例保持禁用。已有 572 人学习适合具备一定 C 基础、需要研究 PDF 解析与生成的读者快速搭建实验环境并借助目录结构排查依赖与编译问题。1. 一条锁死在 VS2005 的产线为什么还要用 podofo 0.9.7某设备上位机的源码树从 2007 年就没换过编译器VC8 的 vcproj 一开就是十几个最近要加一个功能读 PDF 参数表把里面的型号、阈值抽出来入库。第三方 PDF 控件要授权费新版本的库又基本是 C11 起步评估了一圈podofo 0.9.7 是少数源码结构还算规整、能靠降级改造塞进 VS2005 的 C PDF 解析库。它同时给读写两条链路读取侧做文件解析xref、对象树、内容流、文本抽取写入侧能做页面生成和文字绘制。适合手里压着老工程、又不想为了一个 PDF 模块整体升工具链的人。下面按我实际走通的顺序把 CMake 配置、C11 降级、调用代码和几个翻车点摊开讲。2. 依赖链先摸清zlib 必需jpeg、freetype、openssl 按需裁剪2.1 podofo 0.9.7 的模块边界与四个可选依赖podofo 在 0.9.x 这一代是分层写的最底下是 PdfParser PdfObject/PdfVariant负责词法分析和对象树还原往上是 PdfStream处理流对象的压缩与解压再往上是 PdfMemDocument把整个 PDF 读进内存形成文档模型最上层才是 PdfPage、PdfContentsTokenizer、PdfPainter、PdfFont、PdfImage 这些面向业务的类。这个分层决定了你裁依赖的时候哪些能关、哪些关了就残废。模块依赖是否必需关掉之后的实际影响核心读写PdfMemDocument / PdfStreamzlib必需绝大多数 PDF 的页面流是 FlateDecode没有 zlib 直接打不开图像解码PdfImagelibjpeg可选内嵌 JPEG 的图片解不出来纯文字页不受影响字体嵌入PdfFont / PdfFontCachefreetype可选只能用标准 14 字体不能嵌入 TTF/OTF中文排版基本没戏加密解密PdfEncryptopenssl可选打不开 RC4 / AES 加密的 PDF打开直接抛异常我第一次配的时候图省事全开了结果光 freetype 在 VC8 下就折腾掉一晚上最后发现当期需求只是抽文本压根用不到字体嵌入。血泪经验是第一次编译只开 zlib把 podofo.lib 先产出来跑通调用侧再按需要往回加依赖。每多一个第三方库就多一次运行库不匹配、头文件版本冲突的机会而 VC8 时代没有包管理器全靠手工编成本是叠加的。需要提前说明的是不同小版本的 CMake 选项名可能差一个前后缀比如 freetype 相关的开关在有的版本里叫PODOFO_HAVE_FREETYPE在另一些配置里挂在 fontconfig 下面。判断办法不是翻文档而是配置完直接打开build_vc8\CMakeCache.txt搜PODOFO_HAVE_前缀看它到底识别成了 ON 还是 OFF。这比任何教程都准。2.2 生成器选型VS2005 工程要怎么从 CMake 出来CMake 对 VS2005 的支持是有版本窗口的新版本早就把Visual Studio 8 2005这个生成器从列表里拿掉了所以第一步不是急着 configure而是先确认手上的 CMake 还认不认这个生成器cmake --help在输出的「Generators」段落里找Visual Studio 8 2005。找得到就用现成的找不到退回 3.10.x 一类的老版本用 zip 免安装包解压即用别去动系统里已有的新 CMake两个版本共存反而更省事。生成器确认之后配置命令长这样Windows 命令行用^续行PowerShell 换成反引号mkdir build_vc8 cd build_vc8 cmake -G Visual Studio 8 2005 ^ -DPODOFO_BUILD_LIB_ONLYON ^ -DPODOFO_BUILD_STATICON ^ -DPODOFO_BUILD_SHAREDOFF ^ -DPODOFO_HAVE_JPEG_LIBOFF ^ -DPODOFO_HAVE_OPENSSLOFF ^ -DZLIB_INCLUDE_DIRE:/third_party/zlib-1.2.11 ^ -DZLIB_LIBRARYE:/third_party/zlib-1.2.11/zlib.lib ^ -DCMAKE_CXX_FLAGS/D_CRT_SECURE_NO_WARNINGS /D_SCL_SECURE_NO_WARNINGS /wd4996 ^ ../podofo-0.9.7逐项说一下为什么这么设。PODOFO_BUILD_LIB_ONLYON是关键它跳过 tools 和 tests 目录那里面用了大量 C11 写法的示例程序编不过会让你误以为库本身有问题。PODOFO_BUILD_STATICONPODOFO_BUILD_SHAREDOFF是为了拿一个静态 lib老工程部署时少带一个 DLL少一个版本对不上的隐患。PODOFO_HAVE_JPEG_LIB和PODOFO_HAVE_OPENSSL先关掉理由见上一节。ZLIB_INCLUDE_DIR/ZLIB_LIBRARY直接给绝对路径绕开 find_package 在自己机器上乱找系统 zlib 的问题路径里的斜杠统一用正斜杠CMake 认。CMAKE_CXX_FLAGS里那三个宏是 VC8 时代的标配。/D_CRT_SECURE_NO_WARNINGS 压掉 sprintf、strcpy 这类安全警告/wd4996 直接屏蔽弃用函数告警不然一份编译日志几千行警告真正有用的错误会被埋掉。配置阶段只负责生成 sln真正的报错全在编译阶段别看到「Configuring done」就以为成了。2.3 第三方库先编zlib 的 nmake 与目录布局zlib 是最省事的它自带win32\Makefile.msc在 VS2005 的命令行环境里直接 nmake 就行REM 先初始化 VC8 的编译环境32 位 call C:\Program Files\Microsoft Visual Studio 8\VC\vcvarsall.bat x86 cd E:\third_party\zlib-1.2.11 nmake -f win32\Makefile.msc zlib.lib编完在目录下会得到zlib.lib和zlib.h、zconf.h。1.2.8 到 1.2.11 这几个版本我都用过接口没变化随便挑一个稳定的就行。目录布局建议固定成下面这样后面 CMake 和 VS 工程的路径才好写E:\ ├─ third_party\ │ ├─ zlib-1.2.11\ (zlib.h zconf.h zlib.lib) │ └─ vc8_compat\ (自造的 stdint.h、compat 头第 3 章用) ├─ podofo-0.9.7\ (源码未改动前的原始副本另存一份) └─ build_vc8\ (CMake 生成目录可随时删掉重来)还有一件事必须在编译 podofo 之前确认zlib.lib 用的是哪套 C 运行库。用 dumpbin 查一下它写进目标文件的默认库指令dumpbin /directives E:\third_party\zlib-1.2.11\zlib.lib | findstr /i DEFAULTLIB输出里如果出现LIBCMT说明是静态运行库/MT出现MSVCRT或MSVCRTD说明是动态运行库/MD。这个结果决定了 podofo 的CMAKE_CXX_FLAGS里要加/MT还是/MD两边必须一致否则链接期或者运行期一定出问题。这一步花三十秒能省掉后面半天。3. 把 C11 降回 C03VC8 下必须动刀的四个位置3.1 先盘点用 grep 把 C11 用法列出来动手改之前先做一次全量扫描别凭印象改。在 Git Bash 里进源码根目录跑grep -rnE std::unique_ptr|std::make_unique|std::to_string|nullptr|override|static_assert|enum class|va_copy|std::int[0-9]_t|std::uint[0-9]_t src include cxx11_report.txt wc -l cxx11_report.txt拿到报告之后按类型分类处理方式完全不同nullptr最省事全局替换成NULL即可注意别误伤字符串字面量。override/finalVC8 不认这两个关键字直接删掉不影响语义。static_assert源码里如果有改成typedef char xxx[(cond) ? 1 : -1]这种编译期断言或者干脆注释掉。std::unique_ptr不能无脑替换见 3.3。va_copy、snprintf、std::to_string属于「标准库没给」靠兼容层补见 3.2。cstdint/std::int64_t属于「头文件没给」同样靠兼容层补。顺序上建议先做机械替换nullptr、override再做兼容层头文件和宏最后处理智能指针这种涉及所有权语义的因为前两步做完就能看到真实剩下的报错而不是被几百条 nullptr 未定义淹掉。3.2 补头文件与宏stdint.h、va_copy、snprintfVC8 没有stdint.h这是最密集的一类报错来源。自己造一个放在vc8_compat目录下注意文件名必须就叫stdint.h因为源码里是#include stdint.h或者通过cstdint间接包含的/* E:\third_party\vc8_compat\stdint.h VC8(MSVC 14.00) 没有标准 stdint.h这里补一份最小实现。 只覆盖 podofo 用到的定长整型不做完整实现。 */ #ifndef VC8_STDINT_H #define VC8_STDINT_H typedef signed char int8_t; typedef short int16_t; typedef int int32_t; typedef __int64 int64_t; typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef unsigned __int64 uint64_t; /* 顺带补上 min/max 宏需要的极限值有些头文件会引用 */ #define INT8_MAX 127 #define INT16_MAX 32767 #define INT32_MAX 2147483647 #define UINT32_MAX 4294967295U #endif /* VC8_STDINT_H */编译器的 include 搜索顺序是按/I给出的先后走的所以这个目录必须排在 podofo 源码目录和系统目录之前。在 2.2 的CMAKE_CXX_FLAGS里补上/IE:/third_party/vc8_compat位置放在最前面。同样的思路再做一份podofo_vc8_compat.h专门补宏和函数/* E:\third_party\vc8_compat\podofo_vc8_compat.h 在 podofo 的公共头之前强制包含/FI 编译选项 或者手工加到 PdfCompilerCompat.h 顶部。 */ #ifndef PODOFO_VC8_COMPAT_H #define PODOFO_VC8_COMPAT_H #if defined(_MSC_VER) _MSC_VER 1600 #include stdarg.h #include stdio.h #include sstream #include string /* VC8 没有 va_copy32 位下直接赋值在绝大多数调用场景可用 */ #ifndef va_copy #define va_copy(d, s) ((d) (s)) #endif /* VC8 只有 _snprintf且返回值语义与 C99 不同不保证补 0 */ #ifndef snprintf #define snprintf _snprintf #endif /* 用 ostringstream 顶上 std::to_string */ template typename T inline std::string podofo_to_string(T v) { std::ostringstream oss; oss v; return oss.str(); } #define PODOFO_TO_STRING(v) podofo_to_string(v) #endif /* _MSC_VER 1600 */ #endif /* PODOFO_VC8_COMPAT_H */逻辑说明用_MSC_VER 1600VS2010 之前做条件编译是个好习惯将来哪个同事把这套代码挪到新编译器上兼容层会自动失效不会污染新环境。参数上唯一需要留意的是va_copy的替换在 x86 上va_list就是个指针直接赋值没问题如果哪天要出 x64 版本这里必须换成__va_copy或者重新设计因为 x64 的va_list是结构体。宏定义好了之后还要让 podofo 的源文件真的包含到它。最省事的做法是用编译器的强制包含选项/FI在CMAKE_CXX_FLAGS里追加/FIE:/third_party/vc8_compat/podofo_vc8_compat.h这样每个 .cpp 都会先吃这个头不用逐个文件改。3.3 智能指针与容器unique_ptr 换 auto_ptr 的边界这是整个降级过程里唯一有语义风险的地方。std::auto_ptr看起来能直接顶替std::unique_ptr但它的拷贝构造就是所有权转移一旦放进std::vector这类要求元素可拷贝的容器原对象会被置空后面一解引用就是空指针。所以先看 grep 报告里 unique_ptr 出现在哪只在函数内的栈上局部变量换成std::auto_ptr没问题。作为类的成员变量、且类本身不可拷贝换成std::auto_ptr也能用。出现在std::vector、std::map的值类型里不能换 auto_ptr必须改成裸指针 手动管理或者引入 boost 的shared_ptr。对第三种情况我一般写一个极简的 RAII 壳禁止拷贝行为上等价于 unique_ptr/* VC8 下的 unique_ptr 替身独占所有权禁止拷贝。 C03 里禁用拷贝的惯用法是私有声明 不实现。 */ template typename T class ScopedPtr { public: explicit ScopedPtr(T* p 0) : m_p(p) {} ~ScopedPtr() { delete m_p; } T* get() const { return m_p; } T* operator-() const { return m_p; } T operator*() const { return *m_p; } bool IsNull() const { return m_p 0; } void Reset(T* p 0) { if (p ! m_p) { delete m_p; m_p p; } } /* 释放所有权模拟 unique_ptr::release() */ T* Release() { T* tmp m_p; m_p 0; return tmp; } private: ScopedPtr(const ScopedPtr); /* 故意不实现 */ ScopedPtr operator(const ScopedPtr); /* 故意不实现 */ T* m_p; };逻辑说明Reset里先比较指针再删是为了防住Reset(p.get())这种自赋值场景。Release是必要的因为 podofo 内部有些地方会把指针交给父对象管理如果只有析构函数删除会出现双重释放。参数上没什么可调的唯一的坑是delete m_p对不完整类型是未定义行为所以这个模板只能用在头文件里已经看到完整定义的类上。容器那边对应的改法是std::vectorstd::unique_ptrPdfObject 改成std::vectorPdfObject*然后在持有这个 vector 的类的析构函数里统一delete或者干脆存ScopedPtr的指针。改完一定要跑一遍原来的样例因为这类问题编译期不报错只在运行期崩。3.4 源文件编码BOM、C4819 与 /utf-8 的替代方案VC8 没有/utf-8这个编译选项那是很后面的版本才加的源文件如果没有 BOM编译器会按系统本地代码页简体中文环境下是 GBK去解析结果就是两种表现一是满屏warning C4819二是更糟的——某些字节序列被当成非法多字节字符直接error C2001: 常量中有换行符而且报错行号经常是错的找起来很玄学。podofo 的源码里非 ASCII 字符主要出现在版权头注释和少量字符串字面量里处理办法是批量给含非 ASCII 字节的源文件补 UTF-8 BOM# add_bom.py —— 跑之前先备份一份源码目录 import os, codecs ROOT rE:\podofo-0.9.7 for dirpath, _, files in os.walk(ROOT): for name in files: if not name.endswith((.cpp, .h, .hpp)): continue path os.path.join(dirpath, name) with open(path, rb) as f: data f.read() # 纯 ASCII 文件不用动加了 BOM 反而可能在别处出问题 if not any(b 127 for b in data): continue if data.startswith(codecs.BOM_UTF8): continue with open(path, wb) as f: f.write(codecs.BOM_UTF8 data) print(bom added:, path)逻辑说明先判断文件里有没有大于 127 的字节纯 ASCII 文件跳过这样改动面最小。BOM 加上之后VC8 会正确按 UTF-8 读源文件C4819 和 C2001 基本消失。但要注意一个边界BOM 只解决「源文件怎么被解析」不解决「字符串字面量运行时是什么编码」。窄字符串字面量在 VC8 下仍然按执行字符集本地代码页落地所以你在源码里写的 UTF-8 中文运行时拿到的可能是 GBK 字节也可能是一串问号。真正要输出 UTF-8 的地方得在代码里显式转换别指望源文件编码这一点在第 4 章抽文本那段会再碰一次。3.5 编译出 podofo.lib从 sln 到 lib 的检查点配置和代码都改完之后用命令行编本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?