ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

libharu实战:用C/C++轻松生成规范PDF,解决中文乱码与批量性能

libharu实战:用C/C++轻松生成规范PDF,解决中文乱码与批量性能 简介面向需要在C/C项目中生成PDF文档的开发者libharu开源库资源包提供了完整的库源码与Visual Studio 2013工程文件涵盖文档创建、文本绘制、图形图像处理等核心功能实现。资源共100个文件以57个c源文件和36个h头文件为主另有cmake构建脚本、解决方案及说明文档便于直接编译或按需裁剪压缩包仅506KB轻量便携已有370人学习下载。包内包含hpdf_doc、hpdf_pages、hpdf_fontdef_tt等核心模块对应文档对象管理、页面布局、字体嵌入等关键机制可帮助读者理解PDF创建的完整流程并掌握在VS2013下配置zlib依赖等注意事项。对于希望深入定制PDF生成逻辑、学习库内部结构的初中级开发者这份资源能提供清晰完整的代码参考与实践入口。 上个月给业务系统加后端PDF生成能力客户不接受HTML打印的模糊效果必须直接输出规范的多页PDF。我翻了一圈开源方案最后把目光落在一个叫libharu.rar的源码包上。libharu 是 C 语言写的 PDF 生成库不依赖庞大的渲染内核API 简洁编译出来体积很小特别适合嵌入到 C/C 后端模块或桌面工具里。用它做报表、票据、合同存档这类“方方正正”的文档顺手又可靠。这篇文章从一个 rar 压缩包说起把 libharu 的选型、编译、第一个程序、中文字体乱码、以及批量生成时的性能问题一次讲清楚。适合正在做 PDF 生成功能、对 C/C 有基本了解、又不想引入笨重框架的朋友。1. 解压 libharu.rar 前先搞清 PDF 库该怎么选1.1 我为什么没选 ReportLab、TCPDF、Puppeteer做 PDF 生成技术选型最容易犯的错是“看哪个酷就上哪个”。我当时的需求很明确C 后端模块离线批量生成用户机器上没有 Python 环境也不允许为了一个 PDF 功能塞一个 Chromium 进去。市面上常见的方案我拉了个表方案语言/环境依赖体积适合场景ReportLabPythonPython 解释器较大服务端批量报表、排版灵活TCPDF / FPDFPHPComposer一般Web 项目、虚拟主机Puppeteer ChromiumNode/ChromeChrome 内核几百 MB复杂网页排版、截图libharuC/Czlib/libpng/libjpeg 可选极小嵌入 C/C 模块、轻量服务在“生产环境直接生成 PDF”这个场景里Python 方案虽然爽但部署环境要保持纯净Puppeteer 出图效果最好可内存和体积都是问题。libharu 没有这些包袱编译完就是一个静态库想扔到哪个模块里都行。这是它能从一堆方案里胜出的首要原因。1.2 libharu 到底能干什么、不能干什么libharu 提供一套 C 接口HPDF_*核心能力包括创建多页文档、绘制文字、画线框矩形等基本图形、嵌入 JPEG/PNG 图片、设置文档权限和密码、压缩输出。PDF 1.4 规范里最常见的功能它基本都覆盖了。但要清楚它的边界libharu 不是排版引擎没有 HTML 解析、没有自动换行、也没有流式文档对象模型。所有文字坐标、页面尺寸、表格线位置都得自己算。刚开始可能觉得麻烦但一旦封装好反而能精确控制每个元素的落点生成的 PDF 很“板正”。如果想做自适应网页那种连续排版libharu 不合适。1.3 什么时候该坚定选它我的判断标准很简单如果你的功能是“把已有结构化数据变成固定版式的 PDF 文件”比如订单、发票、合同、报告而且开发环境是 C/C那 libharu 工程成本最低、运行最可控。下载回来的libharu.rar里最多几十个源码文件翻一遍就能看懂出了问题也可以直接调源码这是黑盒商业库给不了的底气。2. 从 rar 包到编译完成源码安装没有想象中麻烦2.1 先看压缩包里有什么解压libharu.rar后目录结构大致是src/核心实现所有 C 源文件都在这里include/hpdf.h总头文件后面写代码只需要 include 它examples/官方示例比如text_demo.c、font_demo.c强烈建议先跑这些CMakeLists.txt现代版本用 CMake 构建configure/Makefile一些历史版本也提供 autotools 构建如果压缩包里面带win32/或vs/目录说明还附带了 Visual Studio 工程文件。老版本确实常用工程方式分发但新版本更推荐 CMake。2.2 Linux 下编译几行命令搞定unrar x libharu.rar # 如果系统没装 unrar用 7z x libharu.rar 或 bsdtar 也可 cd libharu mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIX/usr/local make -j$(nproc) sudo make installlibharu 的编译依赖是可选的如果系统里有 zlib、libpng、libjpegCMake 会自动检测并开启对应的压缩和图片功能。只想快速跑通文字版 PDF 的话没有这些库也能编译只是生成的 PDF 不压缩、不支持图片嵌入。在生产环境我建议装齐因为开启压缩后文件体积差距非常明显。2.3 Windows 下编译用 CMake Visual StudioWindows 下用 rar 包分发更常见但流程和 Linux 差不多解压源码用 CMake GUI 指定源码目录和构建目录点击 Configure选择 Visual Studio 版本如果需要 zlib/libpng/libjpeg用 vcpkg 安装依赖后设置CMAKE_TOOLCHAIN_FILE点击 Generate然后打开生成的.sln编译Windows 上最容易踩的坑是运行库不一致应用工程用/MT静态运行库而 libharu 用/MD编译链接时就会报一堆libcmt.lib和msvcrt.lib冲突。所以 libharu 的 CMake 生成项里CMAKE_MSVC_RUNTIME_LIBRARY一定要和主工程保持一致。2.4 版本和 UTF-8 编码支持的关系编译前确认一下版本号。libharu 2.3.0 以后增加了HPDF_UseUTFEncodings这类接口对多字节编码的支持才算完善。如果拿到的是老版本1.x后面处理中文会非常痛苦。可以在代码里调用HPDF_GetVersion()验证也可以直接在include/hpdf.h里找HPDF_VERSION宏。3. 第一个能跑的 PDF核心 API 的使用逻辑3.1 一个完整的 C 语言示例直接上代码这个程序会创建一个 A4 页面输出一行中文并且画一条红色横线#include stdio.h #include string.h #include setjmp.h #include hpdf.h static jmp_buf env; static void error_handler(HPDF_STATUS error_no, HPDF_STATUS detail_no, void *user_data) { fprintf(stderr, libharu error: %04X, detail %04X\n, (unsigned int)error_no, (unsigned int)detail_no); longjmp(env, 1); } int main(void) { HPDF_Doc pdf; HPDF_Page page; HPDF_Font font; const char *font_path /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf; const char *font_name; pdf HPDF_New(error_handler, env); if (!pdf) return 1; /* 开启 UTF-8 编码支持否则中文文本会被按字节拆散 */ HPDF_UseUTFEncodings(pdf); /* 加载 TrueType 字体HPDF_TRUE 表示把字体嵌入 PDF */ font_name HPDF_LoadTTFontFromFile(pdf, font_path, HPDF_TRUE); font HPDF_GetFont(pdf, font_name, UTF-8); HPDF_NewDoc(pdf); page HPDF_AddPage(pdf); HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT); /* 写文字 */ HPDF_Page_BeginText(page); HPDF_Page_SetFontAndSize(page, font, 14); HPDF_Page_MoveTextPos(page, 72, 720); HPDF_Page_ShowText(page, Hello libharu 你好); HPDF_Page_EndText(page); /* 画一条红色横线 */ HPDF_Page_SetRGBStroke(page, 0.8, 0.2, 0.2); HPDF_Page_SetLineWidth(page, 1.5); HPDF_Page_MoveTo(page, 60, 600); HPDF_Page_LineTo(page, 540, 600); HPDF_Page_Stroke(page); HPDF_SaveToFile(pdf, hello.pdf); HPDF_FreeDoc(pdf); HPDF_Free(pdf); return 0; }这段代码基本展示了 libharu 的核心骨架HPDF_New创建文档对象HPDF_AddPage加页BeginText和EndText之间写文字MoveTo/LineTo/Stroke画线最后SaveToFile落盘。无论做多复杂的 PDF最终都会归结成这些操作的排列组合。3.2 PDF 坐标系和 HTML 是反的通篇要注意libharu 的坐标系原点在页面左下角x 向右增大y 向上增大单位是磅point1 英寸等于 72 磅。A4 页面尺寸是 595 x 842 磅。示例里MoveTextPos(page, 72, 720)的意思是文字基线起点在距左边 1 英寸、距底部 10 英寸的位置。这个坐标习惯和 HTML/SVG 的原点在左上角完全相反。我第一次写画表格逻辑时一直把 y 坐标当屏幕坐标算结果整页内容上下颠倒。封装 Pager 类时建议统一用“距离顶部多少”转换成 PDF 坐标比如double y_from_top 100; double pdf_y HPDF_Page_GetHeight(page) - y_from_top;3.3 字体句柄为 NULL 时先检查这两件事HPDF_GetFont返回NULL通常只有两个原因字体名不对。HPDF_LoadTTFontFromFile返回的字体名不一定是文件名而是字体内部 PS 名所以最好不要自己拼名字直接用返回值。没有调用HPDF_UseUTFEncodings导致传入UTF-8编码时查找不到合适的字体映射。调试时把font_name打印出来比瞎试字体名省时间得多。4. 中文字体与乱码绕不开的坎4.1 乱码现象和根因libharu 最大的新手拦路虎就是中文。现象通常是文字输出空白或变成一堆乱码。根因有两个层面第一PDF 标准不强制内置中文字体必须自己嵌入 TrueType 字体。如果你直接用HPDF_GetFont(pdf, Helvetica, NULL)Helvetica 只覆盖 Latin-1 字符中文自然显不出来。第二libharu 老版本对多字节编码支持不好文本内部的字节会被拆开解释。即使嵌入了字体如果没开启 UTF-8 支持也会乱码。这就是必须调用HPDF_UseUTFEncodings(pdf)的原因而且要在HPDF_NewDoc之前或之后都行只要在加载字体之前调用即可。4.2 字体文件怎么选TTF 优先TTC 要小心各平台常见的字体路径Linux:/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc或wqy-microhei.ttcmacOS:/System/Library/Fonts/PingFang.ttcWindows:C:/Windows/Fonts/msyh.ttc或C:/Windows/Fonts/simhei.ttf我个人的建议是优先准备单独的.ttf文件。libharu 对.ttcTrueType Collection的支持在不同版本有差异TTC 里包含多个字体老版本通常只取第一个 face而且字体内部索引可能对不上。如果你手头只有 TTC可以用字体工具拆成单 TTF 再喂给 libharu能少踩一半的坑。生产环境还要注意字体授权Windows 自带字体不能随便随软件分发。很多商用项目最终会选择思源黑体这类开源字体把字体文件放在指定目录通过配置项传入路径。4.3 换行和对齐得自己写但实现并不复杂libharu 没有段落排版一切换行手动处理。最简单的方式是逐字测量宽度超过容器宽度就换行。核心函数是HPDF_Page_MeasureText它能计算一段文本在当前字体字号下占多少磅double text_width HPDF_Page_TextWidth(page, text);“测量宽度、决定换行”的逻辑可以写成一个工具函数对中文字符逐字累加宽度遇到超宽就在上一个字符处断行用HPDF_Page_TextOut把每一行画到指定位置。这个函数一旦写好所有表格单元格、标题、段落都能复用。对齐也同理。文字居中时先用HPDF_Page_TextWidth测出本行宽度然后用容器宽度减去文本宽度除以 2得到起始 x 坐标。不要靠肉眼估位置最终打印出来的 PDF 会有像素级差异。5. 高效生成一个像样的 PDF表格、图片、页眉页脚与文件保护5.1 表格内容是用图形和文字拼出来的libharu 没有表格组件但表格本质就是线框加文字。我封装的画表工具简单可靠根据列宽和行高计算每个单元格的矩形用HPDF_Page_RectangleHPDF_Page_Stroke画边框用HPDF_Page_SetRGBFillHPDF_Page_Fill画表头底色用HPDF_Page_TextOut逐格写入文字画填充矩形时注意顺序先填充后描边或者填充时用HPDF_Page_FillStroke。如果先描边再填充描边会被覆盖线的颜色会变脏。这个细节我做第一版时没注意导出后所有表格边框都被底色盖住了。5.2 嵌入图片JPEG 最稳PNG 要挑格式用图片做签名或 Logo 时走这个流程HPDF_Image image; image HPDF_LoadJpegImageFromFile(pdf, signature.jpg); HPDF_Page_DrawImage(page, image, x, y, width, height);PNG 用HPDF_LoadPngImageFromFile但兼容性没有 JPEG 好。部分带 alpha 通道或特殊色彩空间的 PNG 会加载失败。如果遇到“图片加载成功但显示黑块”的问题多半是颜色空间解析的问题。生产环境里需要贴照片的场景我会先约定用 JPEG透明背景用 PNG并且用图像处理库统一转换。大图嵌入前尽量先压缩比如把 3000x4000 的手机照片缩放到 300x400 转成 JPEG。libharu 嵌入图片时是把像素数据放进 PDF 的缩放的只是显示尺寸文件体积和内存占用仍然跟原始像素量有关。5.3 页眉页脚和页码每页都要重新画一遍libharu 没有“母版页”概念每个页面互相独立。页眉页脚必须在产生每一页时画一遍。画页脚页码的逻辑可以封装成一个函数在HPDF_AddPage之后调用。页码用格式化字符串生成char page_label[16]; snprintf(page_label, sizeof(page_label), %d, page_no); HPDF_Page_TextOut(page, 280, 30, page_label);如果担心最后一页页脚压到内容需要根据当前页面的 y 坐标判断剩余空间。还有一种简单策略是先用少量数据测试所有页面的内容高度再确定页脚安全线。5.4 加密和权限控制文档要设置密码时在HPDF_SaveToFile之前调用HPDF_SetPassword(pdf, owner_password, user_password); HPDF_SetPermission(pdf, HPDF_ENABLE_PRINTING | HPDF_ENABLE_EDIT_ALL);owner 密码是管理密码user 密码是打开密码。权限位控制打印、复制、修改等操作可以用位或组合。注意一点libharu 的加密容器是标准 RC4 加密现在的审阅标准看已经不算高强度。如果涉密等级高应该在业务层再做一层加密PDF 密码只用来防普通用户误操作。5.5 压缩文件体积缩一半以上生成前加一行HPDF_SetCompressionMode(pdf, HPDF_COMP_TEXT | HPDF_COMP_IMAGE);文本内容压缩率非常高实测一段带表格的报告从 150KB 降到 60KB 很常见。代价是 CPU 消耗变大批量场景里需要权衡。如果磁盘和带宽都不敏感也可以只压文本不压图片。6. 批量生成 PDF 时的性能与内存实测和最终选择6.1 循环生成多个 PDF 时的“僵尸文档”批量业务最常见的形式是“一个订单生成一个 PDF”。最直接的写法是循环里反复HPDF_New、HPDF_NewDoc、HPDF_SaveToFile、HPDF_FreeDoc、HPDF_Free。我第一次跑 1000 个订单时发现内存只增不降排查后发现是有几个异常分支直接return了没走HPDF_FreeDoc。libharu 的文档对象一旦不释放字体资源、页面树、图像对象会全部驻留内存。后来我统一在函数末尾做清理并用 RAII 封装才算根治。另一个坑是每次HPDF_New后都重新HPDF_LoadTTFontFromFile嵌入字体。因为每个 PDF 是独立文档字体必须随文件嵌入这个开销省不掉。实测下来文件数量过千时字体文件的大小比正文内容还影响耗时和体积。如果字体文件很大建议在系统层面打包字体子集。libharu 本身没有子集化接口需要借助外部工具先做一套精简 TTF再交给 libharu。6.2 大量文本的测量性能如果一页里有几十段文本而且每段都做换行测量HPDF_Page_TextWidth的调用次数会非常可观。这个函数走的是 FreeType 字体宽度映射单次很快但循环里高频调用还是会成为热点。我的优化方法是给常用字符宽度做一层内存缓存。比如中文常用字就那几千个首次测量后把字符宽度存进哈希表后面直接查表。对文本换行这种场景速度提升很明显。如果不在乎字体宽度精度也可以统一用“1 个中文字符宽度 当前字号”近似计算省去大量测量调用。6.3 保存到流控制超大文件内存峰值HPDF_SaveToFile实现简洁但如果某个 PDF 包含几千张图片一次性把整个文件内容倒进内存再写盘峰值会很高。libharu 提供了流式接口HPDF_SaveToStream将 PDF 写入内存流然后用HPDF_ReadFromStream分块读出来写文件。常规场景我不建议一上来就用流代码复杂度会高很多。先跑一次性能测试如果单个 PDF 超过 100MB 且内存峰值不可接受再切换到流式保存。我实际测下来几百页纯文字 PDF 用SaveToFile完全没问题瓶颈基本在磁盘 IO而不是内存。6.4 最终工程里我保留了哪几个关键设计兜兜转转踩了一圈坑后我的工程结构固化成了这样封装PdfBuilder类构造函数里完成HPDF_New、启用 UTF-8、加载共用字体、设置压缩和权限页面元素全部用“锚点 偏移”的坐标而不是写死数字字体路径通过配置文件传入方便不同环境切换每个保存操作都检查HPDF_STATUS错误回调里记录日志后跳转退出所有HPDF_Doc销毁逻辑放在一个统一的清理入口杜绝中途 return 导致资源泄漏这几点看起来简单但足以把 libharu 从“能用”变成“稳用”。最后再分享一个调试技巧生成的 PDF 不要只看预览器可以用pdftotext导一遍文本确认中文是否正确提取。这个步骤能同时验证字体嵌入和编码设置是否真的没问题。回头再看那个libharu.rar里面装的其实是一个靠谱的老伙计只是需要你花点时间摸熟它的脾气。本文还有配套的精品资源点击获取
返回列表