尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

libharu实战:用C/C++轻量生成PDF文档与中文字体集成指南

发布时间:2026/9/8 11:32:26

资讯中心
01
ARTICLE

libharu实战:用C/C++轻量生成PDF文档与中文字体集成指南

libharu实战:用C/C++轻量生成PDF文档与中文字体集成指南
简介libharu是一套免费开源的PDF生成库面向需要在C/C项目中动态创建PDF文档的开发者可解决文本、图形、图像及元数据处理等常见需求。压缩包内含完整源码与Visual Studio解决方案共100个文件以57个c源文件和36个h头文件为主体覆盖核心对象、页面管理、字体嵌入、编码器及PDF解析等模块配套vcxproj、sln工程配置与CMake辅助文件便于在VS2013或类Unix环境中构建整体仅506KB。已有370人学习下载适合希望掌握PDF生成API或离线集成libharu的开发者。通过包内代码可快速熟悉hpdf_doc_create、hpdf_page_create、hpdf_page_draw_text等关键接口的调用流程同时参考其模块化组织方式为后续扩展自定义PDF工具提供实用基础。需注意libharu不支持交互式表单等高级特性但用于常规文档生成已相当高效。1. libharu是什么一个C/C项目里很好用的PDF生成库先说结论libharu是一个用纯C编写的开源PDF生成库核心功能就是让你在代码里直接创建和输出PDF文件。我最初接触它是因为一个嵌入式项目需要在Linux服务器上批量生成产品质检单和订单回执不想在目标环境里装一堆运行时依赖也不想为了一个小功能硬套一个重型文档框架。后来顺着libharu.rar这个资源找到源码包才发现这个库的体量比我想象中小很多整个源码编译完后静态库也就几百KB级别集成非常干净。适合谁看两类人比较典型一类是在C/C服务端或嵌入式环境里需要程序化生成PDF的开发者另一类是嫌弃现有方案太重、想找一个纯C、跨平台、可控性强的PDF输出底层库的技术选型者。如果你用过PHP的FPDF、Python的ReportLab或Java的iText再看libharu会觉得很眼熟但它的定位更底层——C语言直接操作PDF对象模型没有任何脚本解释层性能更好内存占用也更稳定。要理解libharu最好先理解PDF本身。PDF文件本质上是一系列对象的集合包括页面、字体、图像、内容流等每个对象按类型写入文件最终形成一个完整的文档结构。libharu干的事情就是把你脑子里的画一条线、写一段文字、插入一张图片变成符合PDF规范的对象写入流程。你不需要懂PDF规范的细节只需要调用API它负责把对象按规范写进文件里。这就好比你去打印店打印文件你只需要把Word文档丢给店员后台的排版、纸张、墨色管理都是打印店的流程。1.1 为什么我选择libharu而不是其他库有时候我得先泼盆冷水libharu不是功能最全的PDF库如果你的需求涉及复杂表单填写、富文本排版、PDF模板渲染它可能不是最佳选择。但如果你的需求是程序化生成结构化文档——比如报告、清单、票据、数据导出它有四个别人不太好替代的优势。第一依赖极轻。libharu本身只依赖zlib和libpng用于压缩流和PNG图片解码而且源码包里带了可选的内部实现编译时甚至可以剥离PNG支持仅保留zlib。这在嵌入式部署场景下非常关键。我试过一个裸机Linux环境里直接把libharu的静态库链接进业务程序整个二进制增加不到1MB。第二API风格直接没有复杂的对象生命周期。几乎所有操作都围绕HPDF_Doc、HPDF_Page这两个核心句柄展开。你创建文档、添加页面、画东西、保存文件、释放句柄逻辑清晰不容易写出悬垂指针。第三对中文支持比想象中好。只要准备一个TTF或TTC字体文件通过HPDF_UseTTFont2注册进去就能写入中文。这一点是很多轻量级PDF生成方案做不到的。第四线程模型友好。这个库没有隐式全局状态每个文档实例独立所以在多线程场景下你可以给每个工作线程分配独立的文档句柄互不干扰。相比之下某些PDF库在并发环境下需要加锁或者使用线程局部存储。我画过一个简单对比表格帮团队做选型时参考用方案语言依赖中文支持体积适用场景libharuCzlib/libpng需嵌入TT字体很小C/C嵌入、服务端批量出PDFCairoC较多系统库强较大跨平台图形绘制、渲染管线PDFiumC较多强大Chrome系渲染、RPA类需求ReportLabPythonPython环境强中数据分析报表、Python生态1.2 拿到libharu.rar之后先看这四样东西如果你是从网上下载的libharu.rar压缩包第一件事不是急着编译而是先确认包内容是否完整。通常一个标准release压包里会包括这样几部分include/目录放头文件src/目录或CMake工程文件用于编译库本体lib/目录放的是预编译产物demo/目录则会提供几十个示例程序这部分对新手非常有用。我建议你把demo目录完整过一遍。libharu的demo写得相当直白从最基础的text_demo.c写文字、rectangle_demo.c画矩形到复杂一点的font_demo.c字体加载、image_demo.c嵌入图片基本覆盖了90%的日常用法。很多人拿到库喜欢直接开写业务代码这没错但我经验是花二十分钟跑一遍官方demo能帮你省下几个小时的排查时间——因为你会发现很多坑官方示例里早就避开了。还有一点要注意先确认压缩包里的版本号和编译平台。libharu从2.0开始API基本稳定但旧版本对新字体格式的支持有差异如果你要嵌入新版TrueType字体尽量选择2.3.0以上的版本。如果压缩包内有CMakeLists.txt优先用CMake编译如果只有老式Makefile说明可能是很老的版本需要小心API兼容性问题。2. 编译与接入把libharu跑起来的完整记录这块看起来简单实际踩坑的人不少。我先讲Linux下的编译因为大多数人型后端环境都跑在Linux上再说Windows分支。下面是我在一台干净服务器上的实测流程照着做基本一次通过。2.1 Linux环境从源码编译libharu首先确认系统安装了编译工具链和zlib、libpng开发包。Ubuntu/Debian系执行sudo apt-get install build-essential cmake zlib1g-dev libpng-dev然后解压源码包并进入目录tar -xf libharu.rar # 假设你下载的是rar包需要unrar或7z先解压 cd libharu mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DLIBHPDF_EXAMPLESON make -j4 sudo make install这里我建议把LIBHPDF_EXAMPLES打开因为编译完成后build/examples目录里会生成一堆可执行demo你可以直接执行./text_demo看效果确认库能正常工作。编译完成后标准安装路径是/usr/local/lib/libhpdf.a头文件在/usr/local/include/hpdf.h和/usr/local/include/hpdf_*.h。一个比较隐蔽的坑是某些发行版的libpng是libpng16版本libharu依赖头文件的写法如果CMake报cannot find PNG多半是libpng-dev没装或者安装后仍找不到。这时可以手动指定-DPNG_LIBRARY和-DPNG_INCLUDE_DIR两个参数指向libpng.so所在目录和png.h所在目录问题就解决了。2.2 Windows环境下用Visual Studio编译Windows下编译libharu足够顺利。如果你的压缩包里自带sln工程文件直接用Visual Studio打开对应版本选Release x64构建即可。如果没有就用CMake生成cmake -S . -B build -G Visual Studio 17 2022 -A x64 cmake --build build --config Release构建完成后你会得到hpdf.dll和hpdf.lib动态库版本用起来方便一点。需要注意的是libharu依赖的zlib和libpng在Windows下如果没装CMake会在生成阶段报错。这时候最好在压缩包内找找是否有thirdparty目录否则直接用微软自家vcpkg装依赖即可这条路径验证过最稳。注意Windows下如果你把libharu编译成静态库同时你的项目又链接了其他也用zlib的静态库有极小概率出现符号冲突。这种情况建议改用动态库版libharu或者在编译libharu时改掉zlib符号前缀——但这会带来维护成本一般情况下不用碰。2.3 在自己的项目里引用libharuLinux下最简单的方式是直接链接系统安装的静态库并带上依赖库gcc -o myapp myapp.c -lhpdf -lz -lpng如果你用CMake管理项目可以这样写find_package(LibHaru REQUIRED) add_executable(myapp myapp.c) target_link_libraries(myapp LIBHPDF::LIBHPDF)这个find_package模块需要CMake能够找到libharu的配置文件如果你用make install安装到/usr/local通常在系统路径里能找到。如果找不到手动设置环境变量LibHaru_DIR指向/usr/local/lib/cmake/LibHaru即可。我自己更推荐的做法是把libharu作为CMake子目录直接编译进工程根本不用安装到系统路径这样版本锁定在项目里换机器也不怕依赖缺失。操作也简单把libharu整个源码目录放进third_party/libharu然后在根CMakeLists.txt里加一句add_subdirectory(third_party/libharu) include_directories(${CMAKE_SOURCE_DIR}/third_party/libharu/include)然后将hpdf库目标加到你所需的target_link_libraries中。唯一要小心的是libharu项目中可能有多个库目标名比如hpdf、libhpdf建议在CMake里实际打印一下目标名再引用。3. 核心API实操从零生成一份PDF的完整代码现在进入重头戏用libharu写代码生成PDF。我会分两个梯次第一梯段先做一个最简单的Hello PDF让你跑通全链路第二梯段再做一个带中文、带表格、带线条的正式版页面。每一步都会解释坐标系统、API调用的底层逻辑。3.1 最简单的Hello World#include stdio.h #include stdlib.h #include hpdf.h void error_handler(HPDF_STATUS error_no, HPDF_STATUS detail_no, void *user_data) { fprintf(stderr, libharu error: error_no%04X detail_no%d\n, (unsigned int)error_no, (int)detail_no); abort(); } int main(void) { HPDF_Doc pdf HPDF_New(error_handler, NULL); if (!pdf) { fprintf(stderr, cannot create HPDF_Doc object\n); return 1; } 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_BeginText(page); HPDF_Page_SetFontAndSize(page, font, 20); HPDF_Page_MoveTextPos(page, 100, 700); HPDF_Page_ShowText(page, Hello, libharu!); HPDF_Page_EndText(page); HPDF_SaveToFile(pdf, hello.pdf); HPDF_Free(pdf); printf(PDF generated: hello.pdf\n); return 0; }这段代码的逻辑很好理解HPDF_New创建文档句柄HPDF_AddPage增加一页HPDF_GetFont选用内置基础字体BeginText/EndText包裹文本绘制区域SaveToFile输出文件Free释放内存。核心点是坐标系统libharu的坐标原点在页面左下角单位是点point1点等于1/72英寸所以A4纸宽约595点、高约842点。上面代码里MoveTextPos(100, 700)就是让文字从页面左侧100点、距底部700点的位置开始输出——实际显示在页面中上方。3.2 画线和矩形理解内容流PDF的绘图和人画画很接近从某一个点开始用路径勾勒轮廓再填充或描边。libharu封装了这套绘图API你看代码就懂HPDF_Page_SetLineWidth(page, 2); HPDF_Page_SetRGBStroke(page, 0.2, 0.4, 0.8); HPDF_Page_MoveTo(page, 50, 50); HPDF_Page_LineTo(page, 545, 50); HPDF_Page_Stroke(page); HPDF_Page_SetRGBFill(page, 0.9, 0.9, 0.9); HPDF_Page_Rectangle(page, 100, 100, 200, 100); HPDF_Page_Fill(page);每一条线、每一个矩形最终都会写入该页的content stream里。你可能会问直接调用Stroke和Fill时为什么有的路径要手动Stroke有的Rectangle不需要MoveTo因为Rectangle本身就是一个完整的路径封装它内部做了MoveTo和多个LineTo所以画完直接填充即可。了解这个机制后写复杂图形时思路就清晰了先定义路径再决定是描边还是填充。3.3 中文字体接入和编码这是libharu使用中最高频的痛点。默认的14种标准字体Helvetica、Times、Courier等都不支持中文字符你直接ShowText传中文生成的PDF会显示成乱码或空白。解决办法是嵌入一个支持中文的TTF/TTC字体文件。Windows系统自带黑体一般位于C:\Windows\Fonts\simhei.ttf或C:\Windows\Fonts\msyh.ttcLinux服务器则需要单独上传一个字体文件比如Noto Sans CJK SC或者思源黑体。注册方式如下HPDF_Font chinese_font; const char *font_path path/to/simhei.ttf; HPDF_FontDef font_def HPDF_UseTTFont2(pdf, font_path, 1); chinese_font HPDF_GetFont(pdf, font_def-base_font, NULL);在较新的libharu版本里推荐使用HPDF_UseUTFEncodings它会注册UTF-8的编码方式然后你直接ShowText传UTF-8编码的中文字符串即可HPDF_UseUTFEncodings(pdf); HPDF_Font chinese_font HPDF_GetFont(pdf, Helvetica, UTF-8);但注意HPDF_GetFont第二个参数传Helvetica只能识别内置字体。要使用中文字体仍必须先通过HPDF_UseTTFont2或HPDF_UseTTFont注册字体文件然后通过返回的HPDF_FontDef拿到字体名再交给HPDF_GetFont。说得直白点第一步是让libharu知道去哪里读取字形数据第二步是让libharu知道文本内容按照什么编码去映射字形。我第一次踩坑时就犯了这个错误注册完了直接写HPDF_GetFont(pdf, SimHei, NULL)结果返回的字体句柄是空的程序在写文本时直接崩溃。正确写法应该拿到font_def-base_font来查询。3.4 一个能用的中文表格案例下面是我在实际项目中常用的一个模板综合了字体、表格、线条、列对齐这几个高频需求void draw_table_header(HPDF_Page page, HPDF_Font font, const char *titles[], int col_x[], int y) { HPDF_Page_BeginText(page); HPDF_Page_SetFontAndSize(page, font, 10); for (int i 0; i 3; i) { HPDF_Page_TextOut(page, col_x[i], y, titles[i]); } HPDF_Page_EndText(page); HPDF_Page_SetLineWidth(page, 0.5); HPDF_Page_MoveTo(page, 40, y - 5); HPDF_Page_LineTo(page, 555, y - 5); HPDF_Page_Stroke(page); }配合表格数据的绘制就能快速生成带表头、带数据行的结构化页面。这里有个细节如果你希望表格线严格贴合文字不要靠眼睛估坐标而应该用HPDF_Page_TextWidth测量文本宽度计算列宽后再画线。libharu提供文本宽度测量接口这就是我前文提到的自己实现简单排版的基础工具。float w HPDF_Page_TextWidth(page, 订单号A-1001);有了文本宽度你可以精确计算每列宽度动态决定是否换行或压缩字号。这在生成长文本时尤其重要否则内容会溢出页面边缘。4. 中文字体、排版与编码避坑容易被忽略的细节4.1 字体文件格式的坑TTF和TTC差别很大libharu对.ttf和.ttc支持程度不一样。.ttc是TrueType字体集合里面可能包含多个字体族比如Windows的msyh.ttc里至少包含微软雅黑常规和粗体两个字体。如果你只用HPDF_UseTTFont2传一个index0拿到的通常只是第一个字体粗体就加载不出来。而.ttf一般只包含一个字体族处理起来简单很多。我在一个票据项目里被这个坑折腾过很久想用msyh.ttc同时显示普通和粗体文字结果粗体一直显示成默认黑体。后来换成网上下载的独立msyhbd.ttf粗体文件分别注册两个HPDF_FontDef问题立刻解决。所以经验是需要多种字重时尽量准备单独的.ttf文件别依赖ttc内的多字体索引。4.2 换行和自动排版需要自己实现libharu没有提供文本自动换行APIShowText只会原样输出一整行。如果你的文本超过页面宽度它会直接溢出到看不见的地方。解决办法是在业务层处理用HPDF_Page_TextWidth测量当前行宽度如果超过页面可打印宽度就在最近的空格或中文字符边界插入换行符\n或者拆成多个TextOut调用分段输出。每次测量比较消耗性能吗实测下来还好几百段文本没问题但如果生成几千页PDF频繁调用TextWidth仍然可能让耗时从毫秒级变成秒级。我的优化方案是预先把固定模板段的字符宽度缓存起来动态文本段才实时测量。4.3 压缩模式与文件体积优化默认情况下libharu保存的PDF是不压缩的一个没什么内容的页面也可能生成几十KB体积。想变小在保存前调用HPDF_SetCompressionMode(pdf, HPDF_COMP_ALL);——这个枚举值会同时压页面内容流、压缩对象流等实测能把纯文本页面从几十KB压到几KB收益很明显。不过注意压缩模式必须在保存前设置且对已经通过SaveToStream写入缓冲区的内容不生效。如果你在内存里做二次开发比如转成字节数组发给前端提前确认这一点避免缓存数据一直不压缩。5. 常见问题与排查技巧实录5.1 PDF打不开或报文件损坏遇到这种情况首先检查是否在HPDF_SaveToFile之前就调用了HPDF_Free。这个错误新手容易犯保存完文件直接释放句柄看起来没问题实际上如果程序在保存过程中发生异常或你用了自定义SaveToStream回调但回调实现不完整生成的就是残缺文件。另一个原因是内存不足或文件路径不可写。libharu的错误处理是回调机制你必须在HPDF_New时传入错误处理函数否则出错后程序会直接abort()退出没有任何日志。我的习惯是错误回调里打印错误码同时记录到日志文件排查起来效率高得多。5.2 内嵌图片失败或显示空白libharu支持JPEG和PNG图片但两种格式的处理路径不一样。JPEG图片直接读取文件流PNG则依赖libpng解码然后转成原始像素数据再压缩写入PDF。如果你编译libharu时禁用了PNG支持又强行加载PNG图片多半会返回HPDF_FAULT_INVALID_IMAGE_DATA之类错误。我的建议是部署环境中放置图片文件时优先用JPEG格式减少一个动态库依赖。即使要用PNG也确认你的libharu构建版本带了libpng。5.3 有些字符显示成方框或问号这基本可以断定字体文件不含对应字形。市面上免费中文TTF覆盖范围参差不齐有的精简字体只有GB2312字符集遇到生僻字自然显示成方框。解决方法是换用更完整的字体或者使用思源黑体、Noto Sans CJK这类开源字体覆盖GB18030全字符集实测基本不会缺字。5.4 内存泄漏和长驻服务稳定性如果你用libharu跑长驻服务一定要保证每个文档处理结束后调用HPDF_Free。再加上libharu错误回调里abort会导致整个进程退出正确的错误处理应该是记录错误后跳过当前文档而不是直接abort。新版代码里我会在错误回调中设置一个全局或线程局部的错误标志主流程检测到标志后释放本次句柄并返回错误码以保证服务不中断。6. 遇到瓶颈后还能怎么扩展libharu的边界很明显它不太适合做复杂动态排版也不支持直接在已有PDF上编辑。但作为生成器角色它的扩展空间很大。我在生产环境里做过一个相对复杂的方案用libharu生成纯数据型PDF再把中间产物交给另一套工具做页码标记和数字签名。也见过同事把libharu封装成C类库上层又套一层JSON配置驱动的模板引擎这样业务方不用写代码只需修改JSON模板就能调整PDF输出格式稳定性还不错。如果你以后遇到既想要libharu的轻量又需要模板化的需求可以往这个方向走把模板解析、样式计算放在业务层渲染层始终只做对象-页面-文本/图形这些原子操作。这个分层思路让维护成本低很多。最后说一个我从实际项目里沉淀下来的小技巧在项目早期就把libharu的demo跑通并把它生成的PDF放到多平台PDF阅读器里检查一遍格式兼容性。因为PDF虽然理论上跨平台一致但不同阅读器对某些对象的处理有细微差异尽早发现这些差异能避免后期在兼容性问题上反复翻车。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。