简介libharu.rar 是一份面向 C/C 开发者的开源 PDF 生成库源码包适合需要在项目中自定义生成 PDF 文档、又希望避开商业库授权限制的编程人员尤其对刚接触 PDF 底层 API 的初学者有参考价值。压缩包共 100 个文件约 506KB以 57 个 .c 源文件和 36 个 .h 头文件为主体覆盖文档、页面、字体、编码、图像等核心模块另含 CMake 配置、VS 工程文件与示例代码便于在 Visual Studio 2013 下编译集成。资源围绕文本、图形、图像与元数据处理展开提供页面布局、颜色、线条样式等控制接口并支持 TrueType、Type1 字体跨平台特性使其可在 Windows、Linux、Mac OS 上运行。目前已有 371 人学习下载读者可借此掌握 hpdf_doc_create、hpdf_page_create 等 API 的调用方式理解依赖库配置与排错思路为后续 PDF 功能开发打下基础。1. libharu.rar 里到底装了什么一个被低估的 PDF 生成方案如果你在搜索框里敲下libharu.rar大概率不是想听一段 PDF 格式发展史而是手里正卡着一件具体的事服务端要批量出 PDF又不想为了一个导出功能把整个浏览器内核或者几 GB 的运行时拖进来。libharu就是干这个的——一个用 C 写的、跨平台的 PDF 生成库编译出来体积小、没有外部依赖能直接在 C/C 项目里调用也能通过绑定给 Python、Go、Node 用。它不负责把 HTML 渲染成 PDF而是让你用 API 一行行“画”出页面文字、线条、图片、表格、字体嵌入都是显式调用。适合谁适合那些对内存和启动时间敏感、需要长时间稳定跑批量任务的场景比如后台报表、电子票据、日志归档。代价是它不像浏览器方案那样“所见即所得”你得自己算坐标、管字体、处理分页。这篇就按我实际落地的顺序把从拿到libharu.rar到跑通第一个中文 PDF 的路径拆开讲。2. 从压缩包到可编译libharu 的构建路径与依赖取舍拿到libharu.rar之后第一件事不是急着写业务代码而是先确认它能不能在你的目标平台上干净地编译出来。libharu 本身依赖极少核心只依赖 zlib 和 libpng前者用于压缩流后者用于把 PNG 图片嵌进 PDF。如果你不需要插图libpng 可以关掉但 zlib 基本绕不开因为 PDF 内部的内容流默认就要 Deflate 压缩关掉会让文件体积翻好几倍。常见做法是先用 CMake 走一遍默认配置确认工具链没问题再按需裁剪。2.1 解压后先看目录哪些是源码哪些是绑定解压libharu.rar后目录结构通常长这样src/放的是 C 源码include/是公开头文件demo/里有一堆按功能拆开的示例bindings/下是其他语言的封装CMakeLists.txt或Makefile在根目录。我一般先翻demo/因为里面text_demo.c、image_demo.c、font_demo.c这几个文件基本覆盖了最常用的调用路径比读文档快。注意一点不同来源的压缩包可能已经被人改过目录名如果根目录没有构建脚本先找CMakeLists.txt在哪一层别在错误的层级执行命令。2.2 用 CMake 编出静态库命令与三个关键开关下面是我在 Linux 上常用的构建流程Windows 下用 CMake GUI 或 vcpkg 也类似。关键是把LIBHPDF_SHARED、LIBHPDF_EXAMPLES、LIBHPDF_USE_PNG这三个开关按需设置。# 进入解压后的根目录 cd libharu # 创建独立的构建目录避免污染源码树 mkdir build cd build # 配置生成静态库关闭示例开启 PNG 支持 cmake .. \ -DLIBHPDF_SHAREDOFF \ -DLIBHPDF_EXAMPLESOFF \ -DLIBHPDF_USE_PNGON \ -DCMAKE_BUILD_TYPERelease # 编译-j 后面跟 CPU 核心数 cmake --build . --config Release -j 8LIBHPDF_SHAREDOFF表示产出.a静态库部署时不用带.so适合塞进容器镜像。LIBHPDF_EXAMPLESOFF能省掉一堆示例可执行文件的编译时间调试阶段可以打开。LIBHPDF_USE_PNGON会去链接系统的 libpng如果系统没装开发包这一步会直接报找不到png.h那就先装libpng-dev或对应平台的包。编译完成后产物一般在build/src/下头文件在include/里链接时把这两个路径加进你的工程即可。2.3 最小验证十行代码生成一个空白 PDF在写业务之前先用最小代码确认库能跑通。下面这段 C 代码只做一件事创建一个 A4 页面并保存。#include hpdf.h #include stdio.h int main(void) { HPDF_Doc pdf HPDF_New(NULL, NULL); if (!pdf) { fprintf(stderr, HPDF_New failed\n); return 1; } // 添加一页默认 A4 尺寸 HPDF_Page page HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); // 保存到文件 HPDF_SaveToFile(pdf, blank.pdf); // 释放所有内部资源 HPDF_Free(pdf); return 0; }HPDF_New的两个参数分别是错误处理回调和用户数据传NULL表示用默认行为——出错时直接返回空指针或设置内部错误码。HPDF_AddPage返回的HPDF_Page是后续所有绘制操作的句柄。HPDF_SaveToFile会一次性把内存里的对象树序列化成 PDF 文件。最后必须调HPDF_Free否则整个文档占用的内存不会释放批量任务里这是内存泄漏的重灾区。编译命令大致是gcc blank.c -I include -L build/src -lhpdf -lz -lpng -o blank如果链接报错优先检查 zlib 和 libpng 的库名是否匹配。3. 中文不乱码的关键字体嵌入与编码处理libharu 默认只认标准 14 种 PDF 字体这些字体里没有中文字形直接写中文会输出一堆问号或者空白。要输出中文必须嵌入一个支持中文的 TTF 字体并且用正确的编码方式把字符串传进去。这一步是新手翻车最集中的地方我见过不少人卡在“英文正常、中文乱码”上耗掉一整天。3.1 为什么必须嵌入字体标准字体的边界PDF 标准 14 字体Helvetica、Times 等是阅读器内置的文件里只存字符编码不存字形数据。中文字符不在这些字体的编码表里所以阅读器找不到对应字形只能显示替代符号。解决办法是把 TTF 字体文件的数据子集嵌进 PDF这样任何阅读器打开都能正确渲染。代价是文件体积会增加一个完整中文字体动辄十几 MB但 libharu 支持子集嵌入只把用到的字符打包进去实际增量通常只有几十 KB。3.2 加载 TTF 并设置编码代码与参数说明下面这段代码演示如何加载一个中文字体、设置编码、写出一行中文。#include hpdf.h int main(void) { HPDF_Doc pdf HPDF_New(NULL, NULL); HPDF_Page page HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); // 加载 TTF 字体第二个参数是字体在 PDF 内部的标识名 const char *font_path /usr/share/fonts/truetype/some-cjk-font.ttf; HPDF_Font font HPDF_LoadTTFontFromFile(pdf, font_path, HPDF_TRUE); if (!font) { // 加载失败时HPDF_GetError 能拿到具体原因 fprintf(stderr, font load failed: %s\n, HPDF_GetError(pdf)); HPDF_Free(pdf); return 1; } // 设置页面字体和字号 HPDF_Page_SetFontAndSize(page, font, 12); // 关键设置文本编码为 UTF-8 HPDF_Page_SetTextEncoding(page, HPDF_ENCODING_UTF8); // 从左上角偏移一段距离开始写 HPDF_Page_BeginText(page); HPDF_Page_MoveTextPos(page, 50, 750); HPDF_Page_ShowText(page, 第一行中文测试); HPDF_Page_EndText(page); HPDF_SaveToFile(pdf, cjk.pdf); HPDF_Free(pdf); return 0; }HPDF_LoadTTFontFromFile的第三个参数HPDF_TRUE表示启用子集嵌入只打包用到的字形如果传HPDF_FALSE整个字体文件都会被嵌进去文件会大很多。HPDF_Page_SetTextEncoding必须在写文本之前调用传HPDF_ENCODING_UTF8后HPDF_Page_ShowText接收的就是 UTF-8 字节串。注意字体文件路径必须是实际存在的 TTF不能是 TTC 集合文件libharu 对 TTC 的支持有限遇到 TTC 先用工具拆成单个 TTF。如果输出仍然是乱码先确认源文件本身是不是 UTF-8 编码再确认字体是否真的包含对应汉字字形。3.3 字体子集化的体积与兼容性权衡子集嵌入虽然省体积但有一个边界如果 PDF 后续要被编辑子集字体可能无法支持新增字符。我一般对归档类、只读类文档开子集对需要二次编辑的模板则嵌入完整字体。另外不同阅读器对子集字体的解析严格程度不一样遇到过某些老版本阅读器显示缺字的情况换用完整嵌入就正常了。所以如果目标环境不可控宁可多花点体积换兼容性。4. 批量生成时的内存与分页把单页逻辑扩成生产任务单页 PDF 跑通之后真正的考验是批量。后台任务往往要在一个进程里连续生成成百上千个 PDF或者一个 PDF 里塞几百页表格。这时候内存管理、分页坐标、错误恢复这三件事会同时压过来。4.1 一个文档多页页面句柄的生命周期libharu 里每个HPDF_Page都归属于文档对象页面句柄在HPDF_Free之前一直有效。常见做法是循环里反复HPDF_AddPage每页画完不需要手动释放页面文档释放时会统一回收。但要注意不要在多页之间复用同一个页面句柄去改内容页面一旦添加就固定了后续绘制都是追加操作。下面是一个生成 100 页、每页带页码的骨架。#include hpdf.h #include stdio.h int main(void) { HPDF_Doc pdf HPDF_New(NULL, NULL); HPDF_Font font HPDF_LoadTTFontFromFile(pdf, cjk.ttf, HPDF_TRUE); for (int i 1; i 100; i) { HPDF_Page page HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); HPDF_Page_SetFontAndSize(page, font, 10); HPDF_Page_SetTextEncoding(page, HPDF_ENCODING_UTF8); char buf[64]; snprintf(buf, sizeof(buf), 第 %d 页, i); HPDF_Page_BeginText(page); HPDF_Page_MoveTextPos(page, 500, 30); HPDF_Page_ShowText(page, buf); HPDF_Page_EndText(page); } HPDF_SaveToFile(pdf, batch.pdf); HPDF_Free(pdf); return 0; }snprintf用来拼页码字符串注意缓冲区大小要够中文按 UTF-8 算一个汉字占 3 字节。HPDF_Page_MoveTextPos的坐标原点在页面左下角单位是点1 点约等于 1/72 英寸A4 宽度约 595 点、高度约 842 点所以(500, 30)是右下角附近。如果页码位置偏了先检查是不是把坐标算成了像素。4.2 内存增长排查什么时候该复用文档对象如果每个 PDF 都HPDF_New再HPDF_Free内存会周期性涨落这是正常的。但如果发现内存只涨不落常见原因是HPDF_Free没被调用或者错误分支里提前 return 跳过了释放。我的习惯是用一个统一的清理标签所有失败路径都 goto 到那里。另外字体对象如果每个文档都重新加载CPU 开销会比较大可以考虑把字体数据缓存在进程级别但 libharu 的字体句柄是绑定到文档的跨文档复用需要重新加载这一点没有捷径。4.3 错误处理别让一个坏字符毁掉整批任务libharu 的错误处理分两层HPDF_New可以传自定义错误回调库内部出错时会调用同时每个可能失败的函数返回空指针或错误码。批量任务里我一般对单个 PDF 的生成做 try-catch 式的封装某个文档失败就记录日志跳过不让整个批次挂掉。HPDF_GetError和HPDF_GetErrorDetail能拿到最近一次错误的描述排查时先看这两个。5. 避坑与排查libharu 落地时最容易翻车的五件事这一章是我自己踩过、也见别人踩过的坑按“现象 → 原因 → 解决”整理。每一条都对应一个具体的排查动作不是泛泛而谈。5.1 中文显示为方框或问号现象英文正常中文全是方框或问号。原因没有嵌入中文字体或者嵌入的字体本身不含对应字形或者文本编码没设成 UTF-8。解决先用HPDF_LoadTTFontFromFile加载一个确认包含目标汉字的 TTF再调HPDF_Page_SetTextEncoding(page, HPDF_ENCODING_UTF8)最后确认源文件保存为 UTF-8。三步缺一不可。5.2 链接时报 undefined reference现象编译通过链接阶段报一堆undefined reference to HPDF_xxx。原因库路径或库名不对或者静态库链接顺序有问题。解决确认-L指向的目录里有libhpdf.a-l后面的名字和实际库文件名匹配静态库链接时把-lhpdf放在依赖它的目标文件之后同时补上-lz -lpng。5.3 生成的 PDF 体积异常大现象几十页纯文字 PDF 超过几十 MB。原因字体用了完整嵌入而不是子集嵌入或者图片没有压缩。解决HPDF_LoadTTFontFromFile第三个参数传HPDF_TRUE开启子集图片尽量用 JPEG 或压缩后的 PNG检查是否误把大图重复嵌入。5.4 批量任务跑一段时间后内存居高不下现象进程内存持续增长最终被系统杀掉。原因错误分支里HPDF_Free没执行或者每轮循环都新建文档但旧文档没释放。解决用统一的清理路径确保每个HPDF_New都有对应的HPDF_Free用内存分析工具确认增长点是在 libharu 内部还是业务代码。5.5 某些阅读器打开报“文件损坏”现象本地阅读器正常发给别人或上传系统后提示损坏。原因保存时文件流没正确关闭或者磁盘写满导致文件截断。解决HPDF_SaveToFile之后检查返回值确认写入成功如果是流式输出确保HPDF_SaveToStream和HPDF_GetStreamSize配合正确最后把流完整写出。6. 进阶技巧用模板与流式输出把 libharu 用出生产级效率走到这里基本功能已经能跑了。但如果要把它放进真正的生产链路还有两个技巧值得花时间一是用模板减少重复绘制二是用流式输出避免大文件占内存。模板的思路是预先创建一个只含固定元素表头、logo、页脚的 PDF后续每次生成时加载它、在指定位置追加内容。libharu 支持HPDF_LoadPdfFromFile加载已有 PDF但要注意它主要是读取结构不是完整的编辑能力复杂模板还是建议用代码参数化生成。流式输出则适合把 PDF 直接推给 HTTP 响应不用先落盘。// 流式输出骨架把 PDF 写到内存流再一次性取出 HPDF_Doc pdf HPDF_New(NULL, NULL); // ... 绘制内容 ... HPDF_SaveToStream(pdf); HPDF_UINT32 size HPDF_GetStreamSize(pdf); const void *data HPDF_GetStreamBuf(pdf); // 此时 data 指向内部缓冲区size 是字节数 // 可以 write(socket, data, size) 或 memcpy 到自己的缓冲 HPDF_Free(pdf);HPDF_SaveToStream把文档序列化到内部流HPDF_GetStreamSize拿长度HPDF_GetStreamBuf拿指针。注意这个指针在HPDF_Free之后失效所以要么在释放前把数据拷走要么直接写出去。我一般会在写之前先检查size是否合理避免空文档或异常大的流把下游打挂。最后说一个我自己的习惯每次升级 libharu 版本或换字体都先跑一遍包含中文、图片、多页、流式输出的回归用例确认没有回归再上生产。这个习惯帮我省过好几次“后悔药”。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?