简介libharu.rar 是一份面向 C/C 开发者的开源 PDF 生成库源码包适合需要在项目中自定义输出 PDF 文档、又不想依赖商业组件的编程人员。资源围绕 libharu 的核心能力展开涵盖文本、图形、图像与元数据处理支持 TrueType、Type1 等多种字体并提供页面布局、颜色、线条样式等 API 控制跨平台特性使其可在 Windows、Linux、Mac OS 上运行。压缩包共 100 个文件约 506KB以 57 个 c 源文件和 36 个 h 头文件为主体另含 cmake、vcxproj、sln 等构建配置及少量说明文件便于在 Visual Studio 2013 等环境下编译集成。内容预览可见编码器、字体定义、页面操作、文档管理等模块源码结构完整。目前已有 372 人学习下载适合希望深入理解 PDF 底层生成机制、按需裁剪依赖并快速上手 API 的中高级开发者参考。1. 从一份 libharu.rar 说起C/C 项目里怎么把 PDF 生成这件事做扎实如果你正在用 C 或 C 写服务端程序突然接到一个需求把查询结果导出成 PDF 报表或者把一批图片打包成带页码的 PDF 文档你大概率会先愣一下——这活儿在 Java 里有 iText在 Python 里有 ReportLab在 Node 里有 pdfkit可到了 C/C 这一层能选的库其实不多。libharu 就是其中一个绕不开的名字。它是一套纯 C 实现的 PDF 生成库不依赖外部渲染引擎编译出来就是一个静态库或动态库塞进你的工程里就能直接调用。这份 libharu.rar 里打包的正是它的源码、头文件、编译脚本和示例程序拿到手之后你需要做的不是“安装”而是把它编进自己的项目里。适合谁用做嵌入式报表、桌面端打印模块、服务端批量出 PDF 的 C/C 开发者以及那些不想为了生成一个 PDF 就引入一整条 Java 或 Python 运行时链路的人。2. libharu 的编译与工程接入从源码到第一个能跑的目标文件libharu 的源码结构不算复杂但第一次编译的人经常在 zlib 和 libpng 这两个依赖上卡住。它的核心逻辑是纯 C但为了支持压缩流和嵌入 PNG 图片默认会去链接 zlib 和 libpng。如果你只是生成纯文本 PDF理论上可以裁掉这两个依赖但实际操作中不建议这么做因为 PDF 内部的流对象默认就会走 Flate 压缩没有 zlib 的话文件体积会大得离谱。2.1 源码目录里哪些文件真正需要编进工程解压 libharu.rar 之后你会看到类似这样的目录结构src/放的是核心实现include/放的是对外头文件demo/里是一堆示例script/里是各平台的编译脚本。真正需要编进你工程的是src/下的那些.c文件加上include/里的hpdf.h、hpdf_types.h等几个头文件。demo/里的东西不要直接拖进工程它们只是用来验证库能不能跑通的。我一般会先把源码单独编成一个静态库再让业务工程去链接它。这样做的好处是编译参数干净不会跟业务代码的宏定义打架。下面是一个在 Linux 下用 gcc 编静态库的最小命令序列# 进入源码目录 cd libharu/src # 先确认 zlib 和 libpng 的头文件路径 # 常见做法是用 pkg-config 查没有的话手动指定 pkg-config --cflags --libs zlib libpng # 编译所有 .c 文件为目标文件-fPIC 是为了后面能编动态库 gcc -c *.c -I../include -I/usr/include/libpng16 -fPIC -O2 # 打包成静态库 ar rcs libhpdf.a *.o # 如果你要动态库再加一步 gcc -shared -o libhpdf.so *.o -lz -lpng这段命令里-I../include指向 libharu 自己的头文件目录-I/usr/include/libpng16是 libpng 的常见安装路径不同发行版可能不一样用pkg-config查最稳。-fPIC这个参数很多人会漏结果编出来的静态库在链接动态库时直接报 relocation 错误。-O2是常规优化libharu 的代码量不大开不开优化对编译时间影响很小。编完之后你会得到libhpdf.a和libhpdf.so接下来在业务工程里链接的时候顺序很重要你的目标文件在前libhpdf.a在中-lz -lpng在后。链接器是从左往右解析符号的顺序反了就会报一堆 undefined reference。2.2 第一个能生成 PDF 的 C 程序最小可用骨架光编出库不算完得写一个能跑通的程序验证。下面这段代码是我每次接入新库时都会先跑一遍的“冒烟测试”它只做一件事生成一个只有一行文字的 PDF 文件。#include hpdf.h #include stdio.h /* 错误处理回调libharu 出错时会调这里 */ void error_handler(HPDF_STATUS error_no, HPDF_STATUS detail_no, void *user_data) { printf(ERROR: error_no%04X, detail_no%u\n, (unsigned)error_no, (unsigned)detail_no); } int main() { HPDF_Doc pdf HPDF_New(error_handler, NULL); if (!pdf) { printf(cannot create pdf object\n); return 1; } /* 设置压缩模式默认就是开启的这里显式写出来 */ HPDF_SetCompressionMode(pdf, HPDF_COMP_ALL); /* 新增一页A4 尺寸 */ HPDF_Page page HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); /* 设置字体Helvetica 是 PDF 内置字体不需要嵌入字体文件 */ HPDF_Font font HPDF_GetFont(pdf, Helvetica, NULL); HPDF_Page_SetFontAndSize(page, font, 12); /* 在坐标 (50, 750) 处写一行字坐标原点在左下角 */ HPDF_Page_BeginText(page); HPDF_Page_TextOut(page, 50, 750, Hello, libharu.); HPDF_Page_EndText(page); /* 保存文件 */ HPDF_SaveToFile(pdf, smoke_test.pdf); /* 释放资源不调这个会内存泄漏 */ HPDF_Free(pdf); return 0; }编译命令是gcc smoke_test.c -I../include -L. -lhpdf -lz -lpng -o smoke_test。跑完之后如果当前目录下出现了smoke_test.pdf并且用阅读器打开能看到那行字说明库的编译和链接都没问题。这里有几个参数值得展开说。HPDF_New的第二个参数是用户数据指针libharu 不会碰它只是原样传回给你的错误回调一般传NULL就行。HPDF_SetCompressionMode接受HPDF_COMP_NONE、HPDF_COMP_TEXT、HPDF_COMP_IMAGE、HPDF_COMP_ALL这几个值默认是HPDF_COMP_ALL除非你在调试时想直接看 PDF 内部流的内容否则不用改。HPDF_Page_TextOut的坐标单位是点point1 点等于 1/72 英寸A4 纸的尺寸是 595×842 点所以(50, 750)大概在页面左上角偏下的位置。这个坐标系跟很多 GUI 框架是反的第一次用容易把文字写到页面外面去。3. 用 libharu 做中文 PDF字体嵌入、编码转换与排版控制英文 PDF 跑通之后下一个绕不开的问题就是中文。libharu 内置的 14 种标准字体里没有中文字形直接写中文进去要么显示成空白要么显示成乱码。解决办法只有一个嵌入一个支持中文的 TTF 字体文件并且把字符串编码转成 libharu 能识别的格式。3.1 字体嵌入的两种方式与编码选择libharu 提供两个加载字体的接口HPDF_GetFont用于内置字体HPDF_LoadTTFontFromFile用于加载外部 TTF 文件。中文场景下必须用后者。加载的时候有一个embed参数传HPDF_TRUE表示把字体子集嵌入 PDF传HPDF_FALSE表示只引用字体名、不嵌入数据。做中文报表一定要传HPDF_TRUE否则在没装对应字体的机器上打开就是乱码。编码方面libharu 支持HPDF_ENCODING_GB_EUC、HPDF_ENCODING_GBK、HPDF_ENCODING_UTF8等几种。我的习惯是统一用 UTF-8因为业务层拿到的字符串大概率就是 UTF-8少一次转换少一个坑。但要注意libharu 对 UTF-8 的支持依赖于字体本身的 cmap 表不是所有 TTF 都能正常工作选字体的时候尽量用思源黑体、文泉驿微米黑这类 cmap 完整的开源字体。/* 加载中文字体embed 传 HPDF_TRUE 表示嵌入子集 */ HPDF_Font cn_font HPDF_LoadTTFontFromFile(pdf, /path/to/SourceHanSans.ttf, HPDF_TRUE); /* 设置编码为 UTF-8 */ HPDF_SetCurrentEncoder(pdf, UTF-8); /* 在页面上使用这个字体 */ HPDF_Page_SetFontAndSize(page, cn_font, 10.5); /* 写中文字符串直接传 UTF-8 字节流 */ HPDF_Page_BeginText(page); HPDF_Page_TextOut(page, 50, 700, 这是一段中文测试文本); HPDF_Page_EndText(page);HPDF_SetCurrentEncoder这个调用必须在HPDF_GetFont或HPDF_LoadTTFontFromFile之前执行否则编码设置不会生效。这个顺序问题在官方文档里写得比较隐蔽我第一次用的时候调换了顺序结果中文全是问号排查了半天。3.2 多行文本与自动换行的实现思路libharu 本身没有提供自动换行的接口HPDF_Page_TextOut只负责在指定坐标画一行字。做报表的时候一页要放几十行文字手动算坐标不现实。常见的做法是自己写一个简单的换行函数核心逻辑是用HPDF_Font_GetUnicodeWidth测量每个字符的宽度累加到超过行宽就换行。/* 简易换行输出返回下一个可用的 y 坐标 */ float draw_wrapped_text(HPDF_Page page, HPDF_Font font, float size, float x, float y, float max_width, float line_height, const char *text) { float cur_x x; float cur_y y; const char *p text; while (*p) { /* 取出一个 UTF-8 字符的字节数这里简化处理只处理 ASCII 和常见中文 */ int len 1; unsigned char c (unsigned char)*p; if (c 0xE0) len 3; /* 中文通常是三字节 */ else if (c 0xC0) len 2; /* 测量这个字符的宽度单位是 1/1000 em */ HPDF_UINT16 code 0; /* 这里需要先把 UTF-8 转成 Unicode 码点省略转换细节 */ float w HPDF_Font_GetUnicodeWidth(font, code) * size / 1000.0f; if (cur_x w x max_width) { cur_x x; cur_y - line_height; } HPDF_Page_TextOut(page, cur_x, cur_y, /* 单个字符 */); cur_x w; p len; } return cur_y - line_height; }这段代码省略了 UTF-8 到 Unicode 码点的转换实际用的时候需要补一个解码函数。HPDF_Font_GetUnicodeWidth返回的是千分之一 em 为单位的宽度乘以字号再除以 1000 才是实际点数。line_height一般取字号的 1.2 到 1.5 倍中文报表里 1.5 倍读起来比较舒服。这个函数每输出一个字符就调一次HPDF_Page_TextOut性能上不是最优但胜在逻辑简单几百行的报表完全够用。如果文本量特别大可以先把整行拼成一个字符串再一次性输出。4. 图片嵌入与页面布局把 PNG 和 JPEG 塞进 PDF 的正确姿势报表里只有文字是不够的图表、截图、二维码这些都得放进去。libharu 支持 JPEG 和 PNG 两种格式的图片嵌入但两者的处理路径不一样踩的坑也不一样。4.1 JPEG 与 PNG 的加载差异JPEG 的加载最简单HPDF_LoadJpegImageFromFile直接把文件读进来不需要额外依赖。PNG 就麻烦一些HPDF_LoadPngImageFromFile依赖 libpng而且对 PNG 的色彩模式有要求——带 alpha 通道的 PNG 在某些版本上会加载失败。如果遇到 PNG 加载报错先用工具把图片转成不带 alpha 的 RGB 模式再试。/* 加载 JPEG 图片 */ HPDF_Image jpg HPDF_LoadJpegImageFromFile(pdf, chart.jpg); /* 加载 PNG 图片注意 alpha 通道问题 */ HPDF_Image png HPDF_LoadPngImageFromFile(pdf, logo.png); /* 把图片画到页面上指定左上角坐标和宽高 */ HPDF_Page_DrawImage(page, jpg, 50, 500, 200, 150); HPDF_Page_DrawImage(page, png, 300, 500, 100, 100);HPDF_Page_DrawImage的坐标是图片左下角的位置宽高单位是点。如果传入的宽高比跟原图不一致图片会被拉伸变形所以最好先用HPDF_Image_GetWidth和HPDF_Image_GetHeight拿到原始尺寸再按比例算目标尺寸。4.2 页面坐标系与常见布局参数libharu 的页面坐标系原点在左下角x 轴向右y 轴向上。这个跟很多前端框架是反的做布局的时候容易搞混。A4 纵向的尺寸是 595×842 点横向是 842×595 点。页边距我一般设 50 点也就是大约 1.76 厘米打印出来比较美观。参数常用值说明页边距50 点约 1.76 cm适合大多数报表正文字号10.5 点中文报表常用对应五号字标题字号16 点一级标题加粗需换字体行高字号 × 1.5中文阅读舒适区图片间距10 点图片与上下文字的间隔表格里的这些值不是硬性规定但如果你没有特别的排版要求照着设基本不会出错。字号方面要注意libharu 的HPDF_Page_SetFontAndSize接受的是浮点数10.5 这种带小数的值完全合法。5. 避坑与排查libharu 实际使用中的五个血泪教训这一章记录的是我在不同项目里用 libharu 时真实翻过的车每一条都按“现象 → 原因 → 解决”写清楚希望能帮你省下几个小时的排查时间。5.1 生成的 PDF 打开是空白页现象程序跑完没报错PDF 文件也生成了但用阅读器打开是一片空白。原因最常见的是忘了调HPDF_Page_EndText或者HPDF_Page_BeginText和HPDF_Page_EndText没有配对。libharu 在BeginText和EndText之间的绘图指令才会被写入内容流少了EndText的话内容流不完整阅读器直接忽略。解决检查每一对BeginText/EndText是否闭合。另外如果用了HPDF_Page_SetFontAndSize但没在BeginText之前调用文字也不会显示。5.2 中文显示成方框或问号现象英文正常中文全是方框或者问号。原因三种可能——字体没嵌入、编码没设对、字体本身不含中文字形。HPDF_LoadTTFontFromFile的第三个参数传了HPDF_FALSE是最常见的失误。解决确认第三个参数是HPDF_TRUE确认HPDF_SetCurrentEncoder在加载字体之前调用确认用的 TTF 文件确实包含中文字形可以用字体查看器打开看看。5.3 链接时报 undefined reference todeflate现象编译 libharu 静态库的时候没问题链接业务程序时报一堆 zlib 相关的符号找不到。原因libharu 的压缩功能依赖 zlib静态库本身不包含 zlib 的代码链接时必须显式加上-lz。如果用了 PNG还要加-lpng。解决链接命令末尾加上-lz -lpng并且确保这两个库的开发包已经安装。顺序上-lz要放在-lhpdf后面。5.4 内存泄漏与HPDF_Free的调用时机现象长时间运行的服务每生成一批 PDF 内存就涨一截最后 OOM。原因HPDF_New创建的文档对象必须用HPDF_Free释放包括中间加载的字体、图片资源都会挂在文档对象上HPDF_Free会一并回收。如果只调了HPDF_SaveToFile就返回内存就漏了。解决把HPDF_Free放在所有操作之后、函数返回之前。如果中间有多个返回分支用goto或者封装成一个函数统一处理释放逻辑。5.5 大文件生成时内存占用过高现象生成几百页的 PDF 时进程内存占用飙升到几个 GB。原因libharu 默认把所有页面内容缓存在内存里最后HPDF_SaveToFile的时候才一次性写盘。页数多了之后内存自然扛不住。解决libharu 提供了HPDF_SaveToStream和分页写入的接口但更简单的做法是分批生成——每 50 页存一个 PDF 文件最后用其他工具合并。如果必须单文件可以考虑用HPDF_NewDoc配合自定义的内存分配器把页面内容及时刷到临时文件。这个方案实现起来复杂非必要不建议走这条路。6. 进阶技巧用 libharu 做带目录和链接的交互式 PDF基础功能跑通之后可以试试 libharu 的一些进阶能力。它支持创建 PDF 大纲也就是阅读器左侧的目录树和页面内的超链接跳转做产品手册或者电子书的时候很有用。创建大纲的接口是HPDF_CreateOutline每个大纲项可以绑定一个目标页面和坐标。下面这段代码演示了怎么给三页文档建一个两级目录/* 创建一级大纲项 */ HPDF_Outline root HPDF_CreateOutline(pdf, NULL, 第一章, NULL); HPDF_Outline_SetOpened(root, HPDF_TRUE); /* 给大纲项绑定跳转目标第 1 页坐标 (0, 842) 即页面顶部 */ HPDF_Destination dst1 HPDF_Page_CreateDestination(page1); HPDF_Destination_SetXYZ(dst1, 0, 842, 1.0); HPDF_Outline_SetDestination(root, dst1); /* 创建二级大纲项父节点是 root */ HPDF_Outline sub HPDF_CreateOutline(pdf, root, 1.1 节, NULL); HPDF_Destination dst2 HPDF_Page_CreateDestination(page1); HPDF_Destination_SetXYZ(dst2, 0, 700, 1.0); HPDF_Outline_SetDestination(sub, dst2);HPDF_Destination_SetXYZ的三个参数分别是左、上、缩放比例。缩放比例传 1.0 表示保持当前缩放传 0 表示适应窗口。HPDF_Outline_SetOpened控制大纲项默认是否展开做多级目录的时候把根节点设为展开体验更好。页面内链接用HPDF_Page_CreateLinkAnnot创建绑定一个矩形区域和目标页面点击矩形区域就会跳转。这个功能做“点击目录跳转到对应章节”很实用但要注意矩形区域的坐标是相对于页面的别算错了位置。验证生成结果的时候我一般会用pdfinfo和pdftotext这两个命令行工具快速检查页数、元数据和文字内容是否正确比每次都打开阅读器快得多。pdfinfo output.pdf能看到页数、页面尺寸、是否加密pdftotext output.pdf -能把文字内容直接打到终端检查中文有没有乱码很方便。从那以后我每次接入 libharu都会先跑一遍冒烟测试再单独验证中文和图片最后才往业务代码里集成。这个习惯帮我省下了不少返工的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?