libharu 从源码编译到 PDF 生成:C/C++ 轻量级 PDF 库实战指南 简介libharu.rar 是一份面向 C/C 开发者的开源 PDF 生成库源码包适合需要在项目中自定义生成 PDF 文档、又希望避开商业库授权限制的编程人员尤其对刚接触 PDF 底层接口的初学者较为友好。压缩包共 100 个文件约 506KB以 57 个 .c 源文件和 36 个 .h 头文件为主体覆盖文档、页面、字体、编码、图像等核心模块另含 CMake 配置、VS 工程文件.sln、.vcxproj及说明文本便于在 Visual Studio 2013 下直接编译集成。资源围绕文本、图形、图像与元数据处理展开提供创建文档、添加页面、绘制文字等 API 示例并涉及 TrueType、Type1 字体及 zlib 等依赖库的配置思路可帮助读者理解跨平台 PDF 生成的完整流程与常见排错方法。目前已有 372 人学习下载适合作为 PDF 功能开发的入门与参考素材。1. 拿到 libharu.rar 先别急着解压它到底能帮你省下哪类工作量如果你正在找一个不依赖 Office、不依赖浏览器内核、能在 C/C 工程里直接吐出 PDF 的库那 libharu 大概率就是你绕了一圈之后会回头看的那个选项。它是一套开源的 PDF 生成库核心能力是把文字、线条、图像、矢量图形按 PDF 规范写进文件编译出来就是一个静态库或动态库塞进你的工程就能用。我见过太多项目为了导出一张报表硬生生拖进一个几百兆的运行时最后部署时各种缺 DLL而 libharu 的体量小到可以忽略不计。这份 libharu.rar 解压后是一套完整的源码包包含库源码、头文件、示例程序和构建脚本。它适合三类人做嵌入式或桌面端 C/C 工具、需要在服务端批量生成 PDF 报表、以及想搞懂 PDF 底层对象结构的学习者。不适合想拖个控件就出图的人它没有可视化设计器每一行内容都得你自己写代码画上去。下面我按实际拆包和编译的顺序把这份资源怎么落地讲清楚。2. 拆包与编译从源码到能链接的库文件2.1 目录结构里哪些是必须的哪些可以扔解压后你会看到几个顶层目录我一般先扫一遍再决定动不动手。src是库的核心实现所有.c和.h都在这里这是必须保留的。include放对外暴露的头文件你的工程只需要包含这个目录。demo里是一堆示例从画线到嵌入字体都有建议留着后面调 API 时直接翻比查文档快。script或build目录放的是 Makefile、CMakeLists 之类的构建脚本平台不同名字略有差异。有几个目录是可以直接忽略的.git如果存在删掉不影响编译docs里的 HTML 文档可以留着查函数签名但别指望它讲清楚参数怎么填win32或msvc这类平台专用目录如果你用 CMake 统一构建里面的工程文件基本用不上。我习惯先把src和include单独拷到一个干净目录避免构建时被示例代码的依赖干扰。提示解压前先确认压缩包完整性我遇到过下载中断导致src里某个.c文件只有一半的情况编译时报一堆莫名其妙的符号缺失查了半天才发现是包坏了。2.2 Linux 下用 CMake 构建三条命令跑通在 Linux 上最省事的路径是 CMake。进入源码根目录依次执行下面三条命令。第一条生成构建目录第二条编译第三条安装到系统路径。注意-DCMAKE_INSTALL_PREFIX可以改成你自己的目录避免污染系统环境。# 生成构建目录指定安装路径到本地 cmake -B build -DCMAKE_INSTALL_PREFIX/opt/libharu -DLIBHPDF_SHAREDON # 编译-j 后面跟 CPU 核心数加快速度 cmake --build build -j8 # 安装头文件和库文件到指定路径 cmake --install build这里有几个参数值得说清楚。LIBHPDF_SHAREDON表示生成动态库.so如果你要做成单文件可执行程序改成OFF生成静态库.a链接时把静态库一起打包进去。CMAKE_INSTALL_PREFIX决定make install把文件放哪默认是/usr/local没有 root 权限时会失败所以建议指定到用户目录。编译完成后/opt/libharu/lib下会出现libhpdf.so/opt/libharu/include下是hpdf.h等头文件。如果你不想用 CMake源码里通常带了一个Makefile直接make也能编但生成的库文件位置不固定而且默认可能只编静态库。我一般优先用 CMake因为跨平台一致换到 Windows 上同样的命令结构能复用。2.3 Windows 下用 MSVC 或 MinGW 编译的差异Windows 上分两条路。用 Visual Studio 的话源码包里一般有.sln文件直接打开编译就行但要注意配置选Release且平台选对x64 还是 Win32。编译产物是libhpdf.lib和libhpdf.dll你的工程要同时链接.lib并把.dll放到可执行文件旁边。用 MinGW 的话走 CMake 更顺。命令和 Linux 几乎一样只是生成器要指定cmake -B build -G MinGW Makefiles -DCMAKE_INSTALL_PREFIXC:/libharu -DLIBHPDF_SHAREDOFF cmake --build build -j8 cmake --install build这里我特意用了LIBHPDF_SHAREDOFF因为 MinGW 生成的动态库在分发时容易缺libgcc_s_seh-1.dll之类的运行时静态链接能省掉一堆部署麻烦。编译出来的libhpdf.a直接链进你的 exe拷到别的机器上也能跑。注意Windows 下路径不要带空格和中文我见过C:\Users\张三\Desktop\libharu这种路径导致 CMake 找不到编译器换成C:\dev\libharu立刻就好了。3. 第一个 PDF用代码画出一页带文字和线条的文档3.1 最小可运行示例从创建文档到保存编译出库之后先别急着往工程里集成写一个最小示例跑通链路。下面这段代码创建一个 PDF加一页写一行字画一条线然后保存。你把它存成test_hpdf.c用刚才编出来的库链接。#include hpdf.h #include stdio.h /* 错误处理回调PDF 操作出错时会调到这里 */ static void error_handler(HPDF_STATUS error_no, HPDF_STATUS detail_no, void *user_data) { printf(ERROR: error_no0x%04X, detail_no%u\n, (unsigned)error_no, (unsigned)detail_no); } int main(void) { /* 创建文档对象传入错误回调 */ HPDF_Doc pdf HPDF_New(error_handler, NULL); if (!pdf) { printf(无法创建 PDF 对象\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 */ HPDF_Font font HPDF_GetFont(pdf, Helvetica, NULL); HPDF_Page_SetFontAndSize(page, font, 24); /* 在坐标 (100, 700) 处写文字注意 PDF 原点在左下角 */ HPDF_Page_BeginText(page); HPDF_Page_TextOut(page, 100, 700, Hello libharu); HPDF_Page_EndText(page); /* 画一条从 (100, 650) 到 (400, 650) 的线 */ HPDF_Page_SetLineWidth(page, 2); HPDF_Page_MoveTo(page, 100, 650); HPDF_Page_LineTo(page, 400, 650); HPDF_Page_Stroke(page); /* 保存文件 */ HPDF_SaveToFile(pdf, output.pdf); /* 释放所有资源 */ HPDF_Free(pdf); return 0; }编译命令要同时指定头文件路径和库路径gcc test_hpdf.c -I/opt/libharu/include -L/opt/libharu/lib -lhpdf -o test_hpdf跑起来之后当前目录会出现output.pdf打开应该能看到一行字和一条横线。这段代码里几个关键点HPDF_New的错误回调必须传否则出错时你只能看到程序崩溃却不知道原因HPDF_SetCompressionMode建议开启尤其内容多的时候文件能小一半坐标原点在左下角和很多 GUI 框架的左上角原点相反第一次用容易把内容画到页面外面去。3.2 坐标系与单位为什么你的文字跑到了页面外libharu 默认使用点point作为单位1 点等于 1/72 英寸。A4 纸是 595×842 点。如果你按像素思维写HPDF_Page_TextOut(page, 800, 100, ...)x 坐标 800 已经超出 A4 宽度文字就看不见了。我一般会在代码里定义两个宏#define A4_WIDTH 595 #define A4_HEIGHT 842然后所有坐标都基于这两个值来算。比如居中文字先用HPDF_Font_GetBBox拿到字体的包围盒再结合HPDF_Page_TextWidth算出文字宽度最后(A4_WIDTH - text_width) / 2得到起始 x 坐标。这套流程在 demo 目录里有个text_demo.c写得很清楚直接抄过来改就行。另一个容易翻车的地方是HPDF_Page_SetSize之后页面尺寸变了但坐标系原点不变还是左下角。如果你先画了内容再改尺寸内容位置不会自动调整得自己重算。3.3 嵌入中文字体内置字体不支持中文怎么办内置的 Helvetica、Times 这些字体只覆盖拉丁字符写中文会显示成空白或乱码。要输出中文必须嵌入一个支持中文的 TTF 字体文件。常见做法是用思源黑体或文泉驿的 TTF加载方式如下/* 加载外部 TTF 字体第二个参数是字体在 PDF 里的名字随便起 */ HPDF_Font cn_font HPDF_LoadTTFontFromFile(pdf, SourceHanSansCN-Regular.ttf, HPDF_TRUE); /* 设置编码为 UTF-8这样中文字符串能正确解析 */ HPDF_UseUTFEncodings(pdf); HPDF_Page_SetFontAndSize(page, cn_font, 16); HPDF_Page_BeginText(page); /* 注意字符串要转成 UTF-8 编码源文件也存成 UTF-8 */ HPDF_Page_TextOut(page, 100, 600, 这是一段中文测试); HPDF_Page_EndText(page);HPDF_LoadTTFontFromFile的第三个参数HPDF_TRUE表示把字体子集嵌入 PDF只嵌入用到的字符能显著减小文件。如果传HPDF_FALSEPDF 里只记录字体引用换台机器没装这个字体就会显示异常。HPDF_UseUTFEncodings必须在加载字体之前调用否则中文会被截断。我踩过的坑是源文件保存成了 GBK 编码编译时字符串字面量就已经错了PDF 里出来一堆问号后来统一把源文件改成 UTF-8 才解决。4. 图像与矢量图形把 PNG 和路径塞进 PDF 的正确姿势4.1 加载 PNG 和 JPEG格式差异与透明度处理libharu 支持从文件或内存加载 PNG 和 JPEG。JPEG 直接嵌入体积小PNG 需要解码支持透明通道。加载方式/* 从文件加载 JPEG返回图像对象 */ HPDF_Image jpg HPDF_LoadJpegImageFromFile(pdf, photo.jpg); /* 从文件加载 PNG带透明通道 */ HPDF_Image png HPDF_LoadPngImageFromFile(pdf, logo.png); /* 把图像画到页面上指定左下角坐标和宽高 */ HPDF_Page_DrawImage(page, jpg, 50, 400, 200, 150); HPDF_Page_DrawImage(page, png, 300, 400, 100, 100);HPDF_Page_DrawImage的宽高参数是显示尺寸不是原始像素尺寸。如果你传的宽高比和原图不一致图像会被拉伸变形。我一般先调HPDF_Image_GetWidth和HPDF_Image_GetHeight拿到原始尺寸再按比例算显示尺寸。PNG 的透明部分在 PDF 里会正确保留但前提是加载时没有报错如果 PNG 用了 libharu 不支持的色彩模式比如 16 位深加载会失败错误回调里能看到HPDF_INVALID_PNG_IMAGE。注意JPEG 加载速度比 PNG 快很多如果图像不需要透明优先用 JPEG。我做过一个报表项目每页嵌 20 张 PNG 缩略图生成一份 50 页的 PDF 要 8 秒换成 JPEG 后降到 2 秒出头。4.2 画路径与填充贝塞尔曲线和奇偶填充规则除了直线libharu 支持贝塞尔曲线和多种填充规则。画一个带曲线的封闭图形HPDF_Page_SetRGBFill(page, 0.9, 0.9, 0.9); /* 浅灰色填充 */ HPDF_Page_SetRGBStroke(page, 0, 0, 0); /* 黑色描边 */ HPDF_Page_SetLineWidth(page, 1); /* 移动到起点 */ HPDF_Page_MoveTo(page, 100, 200); /* 画贝塞尔曲线两个控制点 终点 */ HPDF_Page_CurveTo(page, 150, 300, 250, 300, 300, 200); /* 画直线回到起点附近 */ HPDF_Page_LineTo(page, 300, 100); HPDF_Page_LineTo(page, 100, 100); /* 闭合路径 */ HPDF_Page_ClosePath(page); /* 填充并描边 */ HPDF_Page_FillStroke(page);HPDF_Page_CurveTo的参数是六个浮点数第一个控制点 x、y第二个控制点 x、y终点 x、y。控制点决定曲线弯曲方向和程度调参时可以先在纸上画个草图估算坐标。HPDF_Page_FillStroke同时填充和描边如果只想填充用HPDF_Page_Fill只描边用HPDF_Page_Stroke。填充规则默认是非零环绕复杂自交路径可能填充出意外结果这时可以改用HPDF_Page_Eofill走奇偶规则。4.3 多页文档与页面树批量生成报表时的内存管理生成多页文档时每调一次HPDF_AddPage就多一页页面按添加顺序排列。但如果你要生成几百页全部留在内存里最后一次性保存内存占用会很高。libharu 提供了HPDF_SaveToStream和分步写入的机制但更简单的做法是控制单次生成的页数分批保存成多个 PDF 再合并。不过 libharu 本身不带合并功能合并需要另外的库。我一般的做法是如果页数在 100 页以内直接全部生成再保存内存完全扛得住超过 100 页就按每 50 页一个文件输出文件名带序号后续用脚本合并。另外每页用完的字体和图像对象不需要手动释放HPDF_Free会统一回收但如果你在循环里反复加载同一个字体应该把HPDF_GetFont提到循环外面避免重复创建对象。/* 字体只加载一次循环里复用 */ HPDF_Font font HPDF_GetFont(pdf, Helvetica, NULL); for (int i 0; i page_count; i) { HPDF_Page page HPDF_AddPage(pdf); HPDF_Page_SetFontAndSize(page, font, 12); /* ... 填充每页内容 ... */ }这段代码里HPDF_GetFont在循环外调用返回的HPDF_Font句柄在所有页面间共享。如果放在循环里每页都会创建一个新字体对象虽然功能上没问题但文件里会重复记录字体信息体积白白增大。5. 避坑与排查编译链接和运行时最容易翻车的五个点5.1 链接时报 undefined reference库顺序和 C 名称修饰现象编译通过链接时一堆undefined reference to HPDF_New。原因通常是链接命令里库的顺序不对或者你在 C 工程里直接包含了hpdf.h但没加extern C。解决把-lhpdf放在源文件之后GCC 的链接顺序是从左到右解析依赖库必须出现在引用它的目标文件后面。C 工程里用extern C { #include hpdf.h }包住头文件或者直接在包含前定义#ifdef __cplusplus extern C { #endif。5.2 生成的 PDF 打开是空白忘记调用 SaveToFile 或页面尺寸为零现象程序没报错PDF 文件也有大小但打开一片空白。原因有两种一是忘了调HPDF_SaveToFile文件里只有 PDF 头没有内容二是HPDF_AddPage之后没有设置页面尺寸某些阅读器按零尺寸渲染。解决确认HPDF_SaveToFile在HPDF_Free之前调用并且每页都调用了HPDF_Page_SetSize或至少用默认尺寸。我习惯在HPDF_AddPage之后立刻设尺寸形成固定套路。5.3 中文显示为方框或问号字体未嵌入或编码未设置现象PDF 里中文位置显示成方框、问号或空白。原因用了内置字体不支持中文或者加载了 TTF 但没调HPDF_UseUTFEncodings或者源文件编码不是 UTF-8。解决按 3.3 节的流程先HPDF_UseUTFEncodings再HPDF_LoadTTFontFromFile加载中文字体源文件存成 UTF-8 无 BOM。如果还不行用HPDF_GetFont检查字体是否加载成功返回 NULL 说明 TTF 文件路径不对或文件损坏。5.4 内存泄漏HPDF_Free 没调或错误回调里再次操作文档现象长时间运行的服务进程内存持续增长。原因每次生成 PDF 后没有调HPDF_Free或者错误回调里又去操作了同一个HPDF_Doc导致状态混乱。解决确保每个HPDF_New都有对应的HPDF_Free用 RAII 思路封装成函数出口统一释放。错误回调里只做日志记录不要再调任何HPDF_*函数。5.5 跨平台编译时找不到头文件include 路径大小写敏感现象Linux 下编译正常换到 Windows 或 macOS 报hpdf.h: No such file or directory。原因Linux 文件系统大小写敏感而 Windows 不敏感源码里#include HPDF.h在 Linux 下找不到实际名为hpdf.h的文件。解决统一用源码里实际的文件名别凭记忆写。我一般直接在工程里搜#include把所有头文件名核对一遍改成和文件系统一致的大小写。6. 进阶技巧用 libharu 做批量报表的模板化封装当你需要生成几十种不同格式的报表时每次都从头写HPDF_Page_TextOut会疯掉。我的做法是抽一层薄封装把「页面布局」和「数据填充」分开。先定义一个结构体描述每个字段的位置、字体、大小然后写一个通用渲染函数遍历这个结构体。这样新增一种报表只需要改配置不用动渲染逻辑。/* 字段描述结构体 */ typedef struct { float x, y; /* 左下角坐标 */ const char *font; /* 字体名 */ float size; /* 字号 */ const char *text; /* 要写入的文字 */ } FieldDef; /* 通用渲染函数遍历字段数组逐个写入页面 */ void render_fields(HPDF_Doc pdf, HPDF_Page page, FieldDef *fields, int count) { for (int i 0; i count; i) { HPDF_Font f HPDF_GetFont(pdf, fields[i].font, NULL); HPDF_Page_SetFontAndSize(page, f, fields[i].size); HPDF_Page_BeginText(page); HPDF_Page_TextOut(page, fields[i].x, fields[i].y, fields[i].text); HPDF_Page_EndText(page); } }这个封装的代价是每次HPDF_GetFont都会查一次字体缓存如果字段很多会有轻微性能损失。优化方法是在render_fields外面先把用到的字体都加载好传一个字体句柄数组进来。我实测过100 个字段的报表优化前后生成时间差不到 5 毫秒所以除非字段上千否则不用太在意。另一个技巧是验证生成的 PDF 是否符合规范。libharu 本身不带校验工具我一般用qpdf --check output.pdf快速检查文件结构是否完整或者用pdftotext提取文字确认内容没丢。如果qpdf报file is damaged多半是HPDF_SaveToFile写入过程中出错检查磁盘空间和文件权限。从那以后我每次集成 libharu 到新工程都强制先跑一遍最小示例确认编译、链接、输出、打开四个环节都通了再往里加业务代码。这个习惯帮我省掉了至少三次在几百行代码里大海捞针找链接错误的痛苦。希望帮到你。本文还有配套的精品资源点击获取