简介:libharu2.3.0开源PDF写入库的完整编译成果,面向需要轻量级PDF生成能力的C/C++开发者,尤其适合在文档生成、报表导出等场景中快速集成PDF输出。该库仅依赖libpng与zlib,结构简洁;针对原生Unicode支持不足的问题,通过增加Unicode转GBK函数并调整部分读写代码,即可实现中英文、简繁体混合文本的正确写入。资源共361个文件,涵盖c/h源码、Visual Studio工程文件、lib与dll库、可执行示例、生成的PDF及PNG预览等,整体约2.66MB。除静态库与DLL动态库四个编译版本外,还编译了全部官方实例,并额外添加一个中文输出Demo,推荐直接使用DLL版本以省去额外静态库链接。已有1605人浏览学习,适合需要对PDF生成库进行二次开发或希望获得中文完整支持的开发者参考。 如果你接手过那种“没有浏览器但必须生成 PDF 对账单”的嵌入式项目,或者想在 C/C++ 服务端里直接输出票据、报告、条码标签,八成会碰到 libharu 这个名字。它是一个纯 C 写的开源 PDF 写入库,2.3.0 是 2018 年发布的版本——虽然这几年官方仓库基本处于“静默维护”状态,但因为这个库代码量小、依赖少、跨平台,依然是很多工控、医疗、物流、报表场景里的常客。很多你见过的 PDF 编辑器和转换工具,后端能力里就有它的一份贡献。
这篇文章不打算只讲“装好之后怎么用”就完事,而是把 2.3.0 从源码编译、跨平台集成、中文字体处理、常见坑位整体过一遍,目标就一个:让你拿到这份记录之后,不用再重复我踩过的那些坑。
1. 项目背景与技术选型思考
1.1 这个库到底适合解决什么问题
libharu 的核心能力是“写入 PDF”,不是“读取 PDF”,也不是“渲染 PDF”。它能做的,是我在代码里直接创建页面、写文字、画线、填色、贴图片、建书签,最后产出一个符合 PDF 规范的文档。和几类常见方案放在一起看会更清楚:
| 方案 | 语言/依赖 | 擅长场景 | 不适合的点 |
|---|---|---|---|
| libharu | C,依赖 zlib/libpng | 快速生成 PDF、报表、票据、图形化文档 | 没有 PDF 渲染引擎,不支持 HTML 转 PDF |
| PDFium | C++ | PDF 解析、渲染、注释 | 生成和编辑 API 偏底层,上手成本高 |
| PoDoFo | C++ | PDF 内容操作 | 模板多、编译配置复杂,比较“重” |
| wkhtmltopdf | C++ / Qt WebEngine | HTML 模板转 PDF | 部署体积大,依赖跨平台组件多 |
| headless Chrome | Chrome 浏览器核心 | 网页截图、打印成 PDF | 需要独立的浏览器进程,资源占用高 |
所以如果你和我一样,是在嵌入式设备、工控机或者后端服务里生成 PDF,libharu 的优势非常明显:编译简单、体积小、API 稳定、对资源受限环境友好。它不擅长 HTML 转 PDF 那种“所见即所得”的排版,但如果你的版式是可控的、手写代码能表达的,它反而是最省事的选择。
1.2 版本 2.3.0 有什么特殊之处
选择 2.3.0 不是因为它新,而是因为它“稳”。这一版发布后,libharu 官方仓库基本进入慢速维护状态,之后很长一段时间都没有大的功能变更,这也意味着 API 冻结,不会出现今天写的代码明天因为升级就崩掉的情况。很多 Linux 发行版内置的 libharu 其实就是基于这版打的包,社区里的使用经验、坑位记录也大多集中在这版上。
“完美编译”这件事的关键点在于:2.3.0 是一个比较老的版本,代码本身很规矩,但现代工具链、新版本 zlib/libpng、Windows 平台下 CMake 工程,都需要我们额外处理。我把这套流程拆开讲透,后面你换到任何平台都能少走弯路。
2. 依赖解析与工具链准备
2.1 三层依赖关系要理清
libharu 自身其实很克制,核心逻辑不依赖外部库。但它支持嵌入 PNG 图片,这部分依赖 libpng;libpng 又依赖 zlib。所以完整依赖链是这样的:
- libharu:负责 PDF 对象、页面、字体、图形等核心逻辑
- libpng:解析 PNG 图片数据
- zlib:提供压缩能力,用于压缩 PDF 内部流
如果你的应用场景不需要 PNG,理论上可以关闭相关功能,但实际编译时几乎所有人都会把所有依赖装齐,省得后面要加图片功能时又重新折腾。
另外,如果需要做编码转换,可能还会用到 libiconv。不过在现代 Linux 上,glibc 已经内置 iconv 接口;Windows 上则可以用 Win32 API 里的 MultiByteToWideChar/WideCharToMultiByte 替代,也不是必须依赖。
2.2 按目标平台准备工具链
编译之前,先把工具链准备好:
| 平台 | 工具链 | 需要提前装的依赖 |
|---|---|---|
| Linux x86/ARM | gcc、make、pkg-config | zlib1g-dev、libpng-dev |
| Windows + MSVC | Visual Studio + CMake | 通过 vcpkg 或现成的 zlib/libpng 静态库 |
| Windows + MinGW | MSYS2 或 Qt 自带工具链 | mingw-w64-x86_64-zlib、libpng |
| 嵌入式 Linux | 交叉编译工具链 | 目标平台对应的 zlib/libpng 源码包 |
装依赖时建议多用系统包管理。比如 Ubuntu 上执行:
sudo apt-get install -y zlib1g-dev libpng-dev如果是在内网离线环境,没有系统包,那就提前把 zlib 和 libpng 源码也准备好,交叉编译时统一打进前缀目录,这个后面会提到。
3. 四套编译方案实测
3.1 Linux 下用 autotools 一条龙
Linux 上的常规路径最顺,因为 2.3.0 自带 configure 脚本。我实际编译的步骤:
tar xf libharu-2.3.0.tar.gz cd libharu-2.3.0 ./configure --prefix=/opt/libharu make -j$(nproc) make install这里有一个容易忽略的点:configure 脚本会生成hpdf_config.h,这个头文件会决定某些宏定义是否开启,例如是否有 PNG 支持、是否启用 libiconv 等。如果你的系统里装了多版本 libpng,configure 可能因为 pkg-config 路径不对找不到库,这时先手动确认:
pkg-config --exists libpng && echo "libpng ok" pkg-config --cflags --libs libpng如果 pkg-config 找不到,试试安装pkg-config工具,或者通过环境变量补路径:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH编译完成后,/opt/libharu/include下会有hpdf.h,/opt/libharu/lib下会有libhpdf.a或libhpdf.so。这一步基本没有坑,属于“顺利到让人怀疑是不是遗漏了什么”的那种顺利。
3.2 Windows 下用 MSVC 编译
Windows 上稍微麻烦一点。libharu 2.3.0 自带的 win32 工程文件年代比较久,直接打开不一定能在新版 Visual Studio 里生成,所以我建议用 CMake 从源码生成 VS 工程。
mkdir build cd build cmake .. -G "Visual Studio 17 2022" -A x64 ^ -DCMAKE_PREFIX_PATH=C:/deps ^ -DCMAKE_INSTALL_PREFIX=C:/libharu ^ -DBUILD_SHARED_LIBS=OFF cmake --build . --config Release cmake --install .CMAKE_PREFIX_PATH指向 zlib 和 libpng 的安装位置。如果直接使用 vcpkg 安装依赖,也可以让 CMake 通过 toolchain 文件自动找到:
cmake .. -G "Visual Studio 17 2022" -A x64 ` -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake ` -DBUILD_SHARED_LIBS=OFF有一点要提醒:2.3.0 的 CMake 对 DLL 导出符号的处理不算完善,直接用BUILD_SHARED_LIBS=ON可能会遇到部分HPDF_*函数没有导出、链接时找不到符号的问题。如果项目不强制要求 DLL,优先编成静态库,省心很多。
3.3 Windows 下用 MinGW 编译
如果你习惯用 Qt 自带的 MinGW 工具链,或者用 MSYS2,流程也简单。以 MSYS2 为例:
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-zlib mingw-w64-x86_64-libpng mkdir build && cd build cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX=C:/libharu .. mingw32-make mingw32-make install这里有个容易踩的小坑:如果同时装了多个 MinGW 工具链,CMake 可能选错编译器,导致链接时出现“file format not recognized”之类的报错。解决方案是在PATH环境变量里把目标编译器的 bin 目录放在最前面,或者在 CMake 命令里显式指定编译器:
cmake -G "MinGW Makefiles" \ -DCMAKE_C_COMPILER=C:/Qt/Tools/mingw1310/bin/gcc.exe \ -DCMAKE_CXX_COMPILER=C:/Qt/Tools/mingw1310/bin/g++.exe \ ..3.4 嵌入式交叉编译与静态库
很多嵌入式项目要用 libharu,常规做法是交叉编译。以 ARM Linux 和arm-linux-gnueabihf-gcc为例:
./configure \ --host=arm-linux-gnueabihf \ --prefix=/opt/libharu-arm \ CC=arm-linux-gnueabihf-gcc \ CXX=arm-linux-gnueabihf-g++ \ CPPFLAGS=-I/path/to/zlib/include \ LDFLAGS=-L/path/to/zlib/lib注意交叉编译时,zlib 和 libpng 也需要先用同一套交叉编译器编出来,不能直接拿主板上的 .so 来链接。如果目标板没有动态库,或者不想在部署时额外放依赖,就改成全静态编译:
./configure \ --host=arm-linux-gnueabihf \ --enable-static \ --disable-shared \ LDFLAGS="-static -L/path/to/zlib/lib" make -j$(nproc)编译完检查一下产物:
file src/.libs/libhpdf.a确认架构正确再往工程里集成,这一步能省出不少调试时间。
4. 中文字体与编码的实战细节
4.1 字体文件要显式加载
libharu 默认自带一组 PDF 基础字体,例如 Helvetica、Times、Courier,只支持 Latin 字符。想显示中文,必须把系统里的中文字体文件加载进去。常见做法:
const char *font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"; HPDF_Font font = HPDF_GetFont( pdf, HPDF_LoadTTFontFromFile2(pdf, font_path, 0, HPDF_TRUE), "UniGB-UCS2-H" );HPDF_LoadTTFontFromFile2的第三个参数是字体索引,面对.ttc(TrueType Collection)文件时可以选第几个字体;第四个参数是是否嵌入字体。这里建议永远传HPDF_TRUE,不然换一台没有同样字体的机器,PDF 里的中文就可能变成方框或空白。
Windows 下直接加载系统字体也是可以的,比如:
C:/Windows/Fonts/simhei.ttf C:/Windows/Fonts/simsun.ttc路径里尽量用正斜杠,避免转义和平台差异。
4.2 简体中文的编码姿势
这是 libharu 中文乱码的重灾区。很多人直接往HPDF_Page_ShowText里塞 UTF-8 字节流,结果生成的 PDF 打开全是“□□□□”。
原因是 libharu 的文本接口本质是按字节流工作的,它会根据当前字体关联的编码来解释这些字节。对于简体中文,最稳妥的搭配是:
- 字体编码使用
GBK-EUC - 传入的字节流必须是 GBK 编码
如果你的业务逻辑内部全是 UTF-8 字符串,用 iconv 转成 GBK 再传给 libharu:
#include <iconv.h> #include <string.h> static size_t utf8_to_gbk(const char *in, char *out, size_t out_sz) { iconv_t cd = iconv_open("GBK", "UTF-8"); if (cd == (iconv_t)-1) { return 0; } char *src = (char *)in; char *dst = out; size_t in_len = strlen(in); size_t ret = iconv(cd, &src, &in_len, &dst, &out_sz); iconv_close(cd); if (ret == (size_t)-1) { return 0; } return (size_t)(dst - out); }在 Linux 的 glibc 环境下,直接调用iconv不需要额外链接;如果是 Windows 的 MSVC,没有 iconv 可用,就换成 Win32 的MultiByteToWideChar先转成 UTF-16,再用WideCharToMultiByte转 GBK,逻辑一样。
为什么不推荐UniGB-UCS2-H? 因为ShowText系列接口以空字符作为字符串终止判断,而 UCS-2BE 编码里 ASCII 字符的高字节是0x00,很容易被当成结束符,导致文本被截断。虽然有些 hack 能绕过去,但远不如直接用 GBK 字节流省心。
4.3 多页与页码处理
生成多页 PDF 时,页脚“第 x / y 页”里的总页数 y 往往要等所有页面创建完才知道。我的习惯是两段式处理:先创建完全部页面,再通过HPDF_GetPageByIndex拿到每页对象补写页脚。
int total = HPDF_GetPageCount(pdf); for (int i = 0; i < total; i++) { HPDF_Page page = HPDF_GetPageByIndex(pdf, i, 0); // 这里复用字体,写上页脚 HPDF_Page_BeginText(page); HPDF_Page_SetFontAndSize(page, cn_font, 10); char footer[64]; snprintf(footer, sizeof(footer), "第 %d / %d 页", i + 1, total); char gbk_footer[128]; utf8_to_gbk(footer, gbk_footer, sizeof(gbk_footer)); HPDF_Page_TextOut(page, 280, 40, gbk_footer); HPDF_Page_EndText(page); }这样就不需要提前猜总页数,也避免了“生成一半发现页数变了”的问题。
5. 最小可运行示例与产物验证
5.1 完整代码解读
下面是一个能跑通的完整示例,创建三页 A4 页面,每页写一行正文,最后补页脚:
#include <hpdf.h> #include <iconv.h> #include <stdio.h> #include <string.h> static size_t utf8_to_gbk(const char *in, char *out, size_t out_sz) { iconv_t cd = iconv_open("GBK", "UTF-8"); if (cd == (iconv_t)-1) { return 0; } char *src = (char *)in; char *dst = out; size_t in_len = strlen(in); size_t ret = iconv(cd, &src, &in_len, &dst, &out_sz); iconv_close(cd); return (ret == (size_t)-1) ? 0 : (size_t)(dst - out); } int main(void) { HPDF_Doc pdf = HPDF_New(NULL, NULL); if (!pdf) { fprintf(stderr, "create pdf handle failed.\n"); return 1; } HPDF_SetCompressionMode(pdf, HPDF_COMP_ALL); HPDF_UseCNSEncodings(pdf); HPDF_UseCNTFonts(pdf); const char *font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"; HPDF_Font cn_font = HPDF_GetFont( pdf, HPDF_LoadTTFontFromFile2(pdf, font_path, 0, HPDF_TRUE), "GBK-EUC" ); for (int i = 0; i < 3; i++) { HPDF_Page page = HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); HPDF_Page_SetFontAndSize(page, cn_font, 14); char line[64]; snprintf(line, sizeof(line), "第 %d 页正文内容", i + 1); char gbk_line[128]; utf8_to_gbk(line, gbk_line, sizeof(gbk_line)); HPDF_Page_BeginText(page); HPDF_Page_TextOut(page, 72, 700, gbk_line); HPDF_Page_EndText(page); } int total = HPDF_GetPageCount(pdf); for (int i = 0; i < total; i++) { HPDF_Page page = HPDF_GetPageByIndex(pdf, i, 0); HPDF_Page_SetFontAndSize(page, cn_font, 10); char footer[64]; snprintf(footer, sizeof(footer), "第 %d / %d 页", i + 1, total); char gbk_footer[128]; utf8_to_gbk(footer, gbk_footer, sizeof(gbk_footer)); HPDF_Page_BeginText(page); HPDF_Page_TextOut(page, 280, 40, gbk_footer); HPDF_Page_EndText(page); } HPDF_SaveToFile(pdf, "demo.pdf"); HPDF_Free(pdf); return 0; }注意HPDF_UseCNSEncodings和HPDF_UseCNTFonts要放在加载中文字体之前,这样 libharu 才识别简体中文编码和字体列表。如果你用的是繁体中文项目,对应调用HPDF_UseCNSEncodings和HPDF_UseCNTFonts这组 API 同样适用。
5.2 编译链接与验证命令
Linux 下链接:
gcc demo.c -I/opt/libharu/include -L/opt/libharu/lib \ -lhpdf -lpng -lz -o demo如果是在 Windows 的 MSVC 环境:
cl demo.c /I C:\libharu\include /link \ /LIBPATH:C:\libharu\lib libhpdf_static.lib zlib.lib libpng16.lib生成demo.pdf后,强烈建议用工具验证一下文件结构,而不是直接拿肉眼去看:
qpdf --check demo.pdf pdfinfo demo.pdf pdffonts demo.pdfpdffonts能看出字体有没有真正嵌入。如果字体列表里显示的是空名称或者 not embedded,基本就是字体加载参数或路径有问题。
6. 编译和运行中的常见坑
6.1 编译阶段问题清单
| 报错/现象 | 可能原因 | 解决方法 |
|---|---|---|
| configure 提示找不到 libpng | 缺少 libpng-dev,或 pkg-config 路径不对 | 安装依赖,或设置PKG_CONFIG_PATH |
| CMake 找不到 zlib.h | 依赖库路径没加入到 CMake 搜索范围 | 设置CMAKE_INCLUDE_PATH、CMAKE_LIBRARY_PATH |
| Windows 链接出现 unresolved external deflate | zlib 没有参与链接 | 确保 zlib.lib 被链接,且库格式和工具链匹配 |
DLL 编出来后HPDF_*符号缺失 | 2.3.0 的 CMake 导出符号配置不完善 | 改为静态库:-DBUILD_SHARED_LIBS=OFF |
| 文件格式 not recognized | MinGW 工具链选错或混用了 MSVC 库 | 用同一套编译器统一编依赖和 libharu |
6.2 运行阶段问题清单
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 中文全是方框 | 编码不匹配,或字体未嵌入 | 确认使用GBK-EUC且传入的是 GBK 字节流 |
| 中文字体在别的机器上丢失 | HPDF_LoadTTFontFromFile2的嵌入参数传了HPDF_FALSE | 改为传HPDF_TRUE |
| 文本被截断 | 使用了UniGB-UCS2-H,字节流里的0x00被当成结束符 | 改用 GBK 编码;或自己处理 UCS-2 长度问题 |
| 加载 PNG 时崩溃 | 个别 PNG 灰度位数、色深不是 libharu 喜欢的格式 | 先用工具转成标准 8bit RGB/RGBA PNG |
| 生成的 PDF 在某些软件里显示异常 | 缺少压缩、页面属性没设置完整 | 使用qpdf --check检查结构,并补全页面尺寸等属性 |
| 错误回调为空导致进程异常 | HPDF_New传了 NULL 错误处理函数,出错时没有回调 | 建议传入一个日志回调函数,便于定位问题 |
6.3 一些值得养成的小习惯
第一,每次编译完先file查看库文件的架构,尤其交叉编译时,这一步能立刻发现工具链不匹配的问题。第二,链接时把依赖顺序排好,静态库链接时-lhpdf -lpng -lz的顺序不能乱,否则可能出现 undefined reference。第三,生成 PDF 后跑一遍qpdf --check,以低成本方式确认文档结构正常。
7. 在自己的工程中集成时的取舍
7.1 集成方式怎么选
libharu 的集成方式大概有三种:源码直接纳入、编译成动态库、使用系统包管理。
嵌入式项目或对交付体积敏感的项目,我建议源码级集成,把 libharu 源码放进工程树里一起编译,最后静态链接。这样运行时不需要额外传 .so/.dll,目标机上的依赖问题最少。
服务端项目则可以考虑编译成独立动态库,由多个模块共享。Windows 上建议用静态库编译,避免 DLL 导出符号遗漏的问题。Linux 上如果系统包里的版本和代码兼容,直接用apt install libharp-dev之类的包管理方式也不是不行,但要注意版本锁定,避免升级后行为变化。
7.2 几点个人经验
我实际用 libharu 时,绝大多数业务场景只用到不到二十个 API:HPDF_New、HPDF_Free、HPDF_AddPage、HPDF_Page_SetSize、HPDF_Page_SetFontAndSize、HPDF_Page_TextOut、HPDF_Page_BeginText、HPDF_Page_EndText、HPDF_LoadTTFontFromFile2、HPDF_Page_DrawImage这些。
它最大的价值就是“稳”和“准”。在确定版式的场景里,它不会给你发挥空间,但也正因如此,生成结果的确定性很高。如果你需要的是从 HTML 模板生成漂亮排版的 PDF,那就应该选 wkhtmltopdf 或者 headless Chrome,别在 libharu 里硬做复杂排版。
不同项目的边界不一样,认清工具的能力边界,比把某个库用到极致更重要。
本文还有配套的精品资源,点击获取