news 2026/9/8 6:36:37

libharu 2.3.0编译与集成:跨平台PDF生成及中文字体实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
libharu 2.3.0编译与集成:跨平台PDF生成及中文字体实战

简介: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 规范的文档。和几类常见方案放在一起看会更清楚:

方案语言/依赖擅长场景不适合的点
libharuC,依赖 zlib/libpng快速生成 PDF、报表、票据、图形化文档没有 PDF 渲染引擎,不支持 HTML 转 PDF
PDFiumC++PDF 解析、渲染、注释生成和编辑 API 偏底层,上手成本高
PoDoFoC++PDF 内容操作模板多、编译配置复杂,比较“重”
wkhtmltopdfC++ / Qt WebEngineHTML 模板转 PDF部署体积大,依赖跨平台组件多
headless ChromeChrome 浏览器核心网页截图、打印成 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/ARMgcc、make、pkg-configzlib1g-dev、libpng-dev
Windows + MSVCVisual Studio + CMake通过 vcpkg 或现成的 zlib/libpng 静态库
Windows + MinGWMSYS2 或 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.alibhpdf.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_UseCNSEncodingsHPDF_UseCNTFonts要放在加载中文字体之前,这样 libharu 才识别简体中文编码和字体列表。如果你用的是繁体中文项目,对应调用HPDF_UseCNSEncodingsHPDF_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.pdf

pdffonts能看出字体有没有真正嵌入。如果字体列表里显示的是空名称或者 not embedded,基本就是字体加载参数或路径有问题。

6. 编译和运行中的常见坑

6.1 编译阶段问题清单

报错/现象可能原因解决方法
configure 提示找不到 libpng缺少 libpng-dev,或 pkg-config 路径不对安装依赖,或设置PKG_CONFIG_PATH
CMake 找不到 zlib.h依赖库路径没加入到 CMake 搜索范围设置CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH
Windows 链接出现 unresolved external deflatezlib 没有参与链接确保 zlib.lib 被链接,且库格式和工具链匹配
DLL 编出来后HPDF_*符号缺失2.3.0 的 CMake 导出符号配置不完善改为静态库:-DBUILD_SHARED_LIBS=OFF
文件格式 not recognizedMinGW 工具链选错或混用了 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_NewHPDF_FreeHPDF_AddPageHPDF_Page_SetSizeHPDF_Page_SetFontAndSizeHPDF_Page_TextOutHPDF_Page_BeginTextHPDF_Page_EndTextHPDF_LoadTTFontFromFile2HPDF_Page_DrawImage这些。

它最大的价值就是“稳”和“准”。在确定版式的场景里,它不会给你发挥空间,但也正因如此,生成结果的确定性很高。如果你需要的是从 HTML 模板生成漂亮排版的 PDF,那就应该选 wkhtmltopdf 或者 headless Chrome,别在 libharu 里硬做复杂排版。

不同项目的边界不一样,认清工具的能力边界,比把某个库用到极致更重要。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 6:36:27

VS2019下静态编译libjsoncpp与libjson-rpc集成指南

简介&#xff1a;面向Windows平台上的C开发者&#xff0c;提供基于Visual Studio 2019环境静态编译完成的JSON解析库和JSON-RPC通信库&#xff0c;整体打包为可直接引用的静态链接库文件&#xff0c;包含头文件与导入库。针对目前网络上流行的相关编译包存在缺少依赖文件以及仅…

作者头像 李华
网站建设 2026/9/8 6:34:38

D3D11下YV12视频渲染实战:GPU加速YUV转RGB的完整方案

简介&#xff1a;面向视频显示与播放开发的 Direct3D YUV 渲染示例工程&#xff0c;支持 YV12、I420、NV12、YUY2、UYVY 及 RGB24、RGB32、RGB555、RGB565 等常见像素格式输入&#xff0c;并在画面上实现半透明文本叠加&#xff0c;便于播放器或监控客户端直接嵌入使用。工程基…

作者头像 李华
网站建设 2026/9/8 6:34:36

LLM核心机制拆解:Token、上下文窗口与采样参数实战指南

先坦白一个事儿&#xff1a;我最早做 LLM 应用时&#xff0c;最懵的不是提示词&#xff0c;也不是模型选型&#xff0c;而是一堆看着眼熟的术语——Token、上下文、温度。明明每个词单独看都认识&#xff0c;连在一起却搞不清它们怎么影响模型输出。更尴尬的是&#xff0c;我曾…

作者头像 李华
网站建设 2026/9/8 6:34:17

OpenSmith:本地化LLM流水线追踪工具的原理与应用实践

这次我们来看一个本地化 LLM 流水线追踪工具——OpenSmith。这个项目的核心价值在于让开发者能够在本地环境中完整追踪大语言模型的工作流程&#xff0c;无需依赖云端服务&#xff0c;所有数据都存储在本地 SQLite 数据库中。对于需要调试 LLM 应用、分析提示词效果或优化流水线…

作者头像 李华
网站建设 2026/9/8 6:34:16

3ds Max零基础室内小卧室建模:从搭框架到渲染出图全流程

这次我们来看一个非常适合入门的 3Dmax 场景建模练习案例&#xff1a;简单室内单间小卧室模型搭建。很多新手第一次打开 3ds Max 不知道从哪下手&#xff0c;新建一个空白场景后对着四个视图发呆&#xff0c;最后只能随便拖几个方块就当练习完了。这个案例的目的就是把“不知道…

作者头像 李华
网站建设 2026/9/8 6:32:16

Python实战:从函数图像绘制到电影短评爬取的全流程解析

头一回看到这个题目的时候我就觉得挺有意思&#xff0c;Python里最常被拿来练手的两个点——函数图像绘制和网页数据爬取&#xff0c;偏偏被塞进了同一个作业里。一个是纯本地计算加可视化&#xff0c;一个是网络请求加解析&#xff0c;看起来八竿子打不着&#xff0c;实际做下…

作者头像 李华