简介:libharu源码包是免费开源的PDF生成库,面向需要在C/C++项目中自行实现PDF导出能力的开发者,尤其适合在Visual Studio 2013环境下从源码编译并集成的初中级程序员,可满足报表、票据与文档类PDF输出的定制需求。压缩包共100个文件、约506KB,以57个.c源文件与36个.h头文件为主体,涵盖文档、页面、字体、编码、图像及页面操作符等模块,另含CMake配置、VS工程文件与说明文本,便于直接编译或按需裁剪。已有374人学习下载。读者可获取完整的库源码与头文件接口,结合hpdf_doc_create、hpdf_page_create、hpdf_page_draw_text等API实践文档、页面与文本绘制流程,理解TrueType/Type1字体加载与中日韩编码处理,并参考工程配置完成zlib等依赖的排错与集成,为二次开发和跨平台移植打下基础。
1. 服务端批量出 PDF 的另一条路:几百 KB 的 C 库,而不是塞一个浏览器
去年接了个对账单导出的活,第一版用无头浏览器渲染 HTML 再打印成 PDF,镜像直接胖了 700 多兆,冷启动两秒起步,压测时 CPU 全耗在解析 CSS 上。后来换成 libharu pdf 这条路线,一个纯 C 写的 PDF 生成库,编译产物只有几百 KB,跟主进程一起加载,在代码里直接画页面、写文字、贴图,中间不经过 HTML 这一层。这份 libharu.rar 就是它的源码包。它适合版式固定的批量输出:票据、标签、对账单、检测报告;不适合拿去改别人给过来的 PDF。下面按我接活儿的顺序,从编译到成品校验拆一遍。
2. 搞清能力边界:它只写不读,对象都挂在 Doc 上
选型阶段最容易犯的错,是把它当成“PDF 处理库”而不是“PDF 生成库”。libharu 里没有任何解析器:打不开已有 PDF、读不出表单域、也没法把两个文件合并。凡是需求里出现“在客户提供的模板上填数”“把扫描件和报表拼一起”,这个库就不合适,硬上只会做到一半推翻。它强的地方是从零构造:页面、文字、矢量线、图片、书签、链接、加密,全都能在 C 层直接控制,没有中间格式转换,输出结果可预测。
2.1 HPDF_Doc 是唯一的根,Page 和 Font 都不归你释放
整个库的对象模型只有三层:HPDF_Doc是文档根,HPDF_Page、HPDF_Font、HPDF_Image、HPDF_Outline全部由它持有,生命周期跟着 Doc 走。这意味着你不需要、也不应该去单独释放一个 Page;调用HPDF_Free(pdf)时这些东西一起回收。我见过有人在循环里对每页做手工释放,结果堆直接损坏——这种崩溃现场很难查,因为崩的位置和出错的位置隔着几百行。
第二件事是错误处理模型。libharu 的所有接口返回HPDF_STATUS,HPDF_OK(值为 0)表示成功,出错不抛异常、不中断流程。你在HPDF_New时传一个回调进去,错误会实时打到回调里;也可以在事后用HPDF_GetError/HPDF_GetErrorDetail取最后一次错误码。这套设计和大多数 C 库一致,代价是每个调用点理论上都该判返回值。实际写业务代码时不必每个都判,但凡是涉及文件读写、字体加载、图片解码的调用,必须判——这几类是运行时最容易失败的地方,而失败后往往不报错,只是默默生成一份内容缺失的 PDF。
还有一点值得提前知道:所有坐标和尺寸的单位都是点(point),1 点 = 1/72 英寸,A4 是 595.27 × 841.89 点。原点在页面左下角,y 向上为正。这个设定跟前端、跟 Canvas、跟大多数截图工具的左上角原点完全相反,是后面排版出错的最大来源。
2.2 构建:先确认 zlib 和 libpng 有没有被探测到
拿到源码包先看根目录有没有CMakeLists.txt。有就走 CMake,没有就是更早的 Makefile 体系,那就得手工改makefile里的编译器和依赖路径,不建议折腾,直接换包里另一份构建脚本更省事。主线版本走 CMake,两个外部依赖是可选的:zlib 决定能不能压缩流,libpng 决定能不能读 PNG;缺了前者文件会明显变大,缺了后者所有 PNG 加载接口在链接期就找不到。JPEG 是内置解码的,不依赖 libjpeg。
# 依赖:zlib 负责流压缩,libpng 负责 PNG 解码 sudo apt-get install -y build-essential cmake zlib1g-dev libpng-dev cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DLIBHPDF_SHARED=ON \ -DLIBHPDF_STATIC=OFF \ -DLIBHPDF_EXAMPLES=ON cmake --build build -j"$(nproc)" cmake --install build --prefix "$PWD/install"-DLIBHPDF_SHARED=ON出动态库,服务端常驻进程用这个,多个模块共享一份;-DLIBHPDF_STATIC=ON出静态库,适合要单文件分发或者要塞进移动端的情况,但注意静态链接时许可证虽然宽松,仍要随产品附上声明。-DLIBHPDF_EXAMPLES=ON会把官方示例一起编出来,这些示例价值很高——它是唯一能对照“正确调用顺序”的参考,比翻头文件快得多。--prefix指定安装目录,装完是include/hpdf.h加lib/libhpdf.*的结构。
不同版本的开关名可能略有出入,configure 阶段的输出里能看到实际探测结果,重点确认三行:zlib 找到了没有、libpng 找到了没有、共享库还是静态库。这三行看错,后面所有问题都是连锁的。
装完先验一下 UTF 编码支持有没有编进去,这一步能省掉后面半小时的乱码排查:
# 有输出说明 UTF-8 编码器可用;没有输出就是构建时被裁掉了 nm -D install/lib/libhpdf.so | grep -i UTFEncoding2.3 二十行跑通第一份 PDF
不要一上来就写业务,先跑通最小闭环:建文档、加一页、写一行字、存盘。
#include <stdio.h> #include "hpdf.h" /* 所有 API 都不抛异常,出错走这个回调 */ static void on_error(HPDF_STATUS err_no, HPDF_STATUS detail_no, void *user_data) { (void)user_data; fprintf(stderr, "[libharu] err=0x%04X detail=%u\n", (unsigned)err_no, (unsigned)detail_no); } int main(void) { HPDF_Doc pdf = HPDF_New(on_error, NULL); if (!pdf) { fprintf(stderr, "HPDF_New failed\n"); return 1; } HPDF_SetCompressionMode(pdf, HPDF_COMP_ALL); /* 文本流和图片流一起压 */ HPDF_Page page = HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); HPDF_Font font = HPDF_GetFont(pdf, "Helvetica", NULL); HPDF_Page_SetFontAndSize(page, font, 18); HPDF_Page_BeginText(page); HPDF_Page_MoveTextPos(page, 60, 760); /* 原点在左下角 */ HPDF_Page_ShowText(page, "first page from libharu"); HPDF_Page_EndText(page); if (HPDF_SaveToFile(pdf, "out.pdf") != HPDF_OK) fprintf(stderr, "save failed: %u\n", (unsigned)HPDF_GetError(pdf)); HPDF_Free(pdf); /* Page/Font/Image 随 Doc 一起回收 */ return 0; }编译命令里参数顺序有讲究,静态链接时库的顺序错了就是一堆 undefined reference:
gcc -O2 main.c \ -I"$PWD/install/include" \ -L"$PWD/install/lib" \ -lhpdf -lz -lpng -lm \ -o mkpdf ./mkpdf && ls -l out.pdf-I指向hpdf.h所在目录,-L指向库目录。-lhpdf必须放在源文件之后,这是链接器的单遍扫描规则决定的:它从左往右处理,遇到-lhpdf时还不知道有哪些未解析符号,就会丢掉这个库。-lz -lpng是 libharu 的传递依赖,静态链接时必须显式列出,动态链接时通常可以省,但写上更稳。-lm是数学库,部分平台上的字体度量代码会用到。
文字部分用BeginText/MoveTextPos/ShowText/EndText这套,不要漏EndText——漏了之后 PDF 结构里文本对象不闭合,某些阅读器会直接渲染空白,而 libharu 本身不认为这是错误。这也是为什么我建议第一版就跑通再往上加功能:出问题时能立刻分清是“基础链路坏了”还是“新加的功能坏了”。
3. 中文不乱码:字体嵌入、编码器与折行度量
英文跑通之后,下一个必然撞上的墙是中文。这一段的坑密度最高,因为它同时涉及三个独立的东西:字体里有没有字形、字符串编码对不对、PDF 里的编码器声明得对不对。三者任意一个错,现象都是“中文不见了”,但排查方向完全不同。热词里“libharu pdf 中文乱码”常年挂着,说明这不是个别现象。
3.1 内置字体只有 14 款,字形表里根本没有汉字
HPDF_GetFont(pdf, "Helvetica", NULL)取的是 PDF 标准的 14 款基础字体:Helvetica、Times、Courier 三族各四种字形,加 Symbol 和 ZapfDingbats。这些字体在 PDF 里只写名字不嵌文件,由阅读器用本机字体替代渲染,好处是文件极小,坏处是编码只有单字节的 WinAnsi、MacRoman 这类,字符集覆盖到 Latin-1 就没了。你把一段 UTF-8 中文喂进去,libharu 会按单字节逐个取字形,取不到就是空白或者问号。
所以中文这件事只有一条路:加载一个包含汉字的 TrueType 字体文件并嵌入。不要试图用“把中文字符串转成 GBK 再塞进内置字体”——编码器对不上,结果只会更乱。挑字体时看两点:覆盖范围要够(GB18030 常用字全覆盖,别拿只做了 6763 个字的精简版去排人名地名),文件别太大(一个全量中文字体动辄十几兆,靠子集嵌入能把最终 PDF 压到几百 KB)。
3.2 编码器 + 嵌入字体,顺序不能反
正确的调用顺序是:新建 Doc → 打开 UTF 编码支持 → 设置当前编码器为 UTF-8 → 加载 TTF → 写文本。前三步必须在加载任何字体、写入任何文本之前完成。
/* 1) 打开 UTF 编码支持,必须在加载字体之前 */ HPDF_UseUTFEncodings(pdf); HPDF_SetCurrentEncoder(pdf, "UTF-8"); /* 2) 嵌入字体:第三个参数 HPDF_TRUE 表示做子集嵌入 */ HPDF_Font cn = HPDF_LoadTTFontFromFile2(pdf, "fonts/zh.ttf", 0, HPDF_TRUE); if (!cn) { fprintf(stderr, "font load failed: 0x%04X\n", (unsigned)HPDF_GetError(pdf)); return 2; } /* 3) 写中文:传进去的必须是 UTF-8 字节序列 */ HPDF_Page_SetFontAndSize(page, cn, 12); HPDF_Page_BeginText(page); HPDF_Page_MoveTextPos(page, 60, 700); HPDF_Page_ShowText(page, "对账单 2024 年 03 月"); HPDF_Page_EndText(page);HPDF_LoadTTFontFromFile2的第三个参数是字体集合里的索引,普通.ttf传 0 就行,.ttc这种一个文件装多套字形的才需要换索引。第四个参数HPDF_TRUE是子集嵌入,只把页面里实际用到的字形打进 PDF——这个开关千万别图省事关掉,关掉之后每个 PDF 都会把整份字体塞进去,十几兆起步。HPDF_TRUE的代价是生成时要做一次字形收集,速度慢一点,但换来的是几百 KB 的成品。
如果链接时提示找不到HPDF_UseUTFEncodings,说明构建时把 UTF 编码支持裁掉了,回到 2.2 节重新检查编译开关,别想着绕过去——没有这个函数,中文在这条路线上基本走不通。
顺带说一个跨平台的坑:字体文件路径里带中文,在 Windows 上大概率加载失败。libharu 内部用的是窄字符fopen,走系统 ANSI 代码页,路径里的中文会被截断或转义失败。解决办法有两个,把字体放到纯英文路径下,或者自己用宽字符接口读进内存,再调HPDF_LoadTTFontFromMem传字节数组和长度——后者更稳,但要自己管这块内存的生命周期,必须在HPDF_Free之后才能释放。
3.3 折行得自己算,TextWidth是唯一可信的尺子
本文还有配套的精品资源,点击获取