简介:这份资源是已编译完成的PDFium静态库,面向需要在C/C++项目中集成PDF解析、渲染与打印能力的开发者,尤其适合不愿从源码编译、希望快速接入PDF处理功能的工程团队。压缩包共45个文件,约2.92MB,以24个h头文件为核心,配套lib静态库、dll动态库及cmake配置文件,另含version、license、gn构建脚本与多个txt第三方许可说明,覆盖接口声明、链接配置与合规信息。资源已吸引295人学习下载,可作为浏览器、电子书阅读器或文档管理工具中PDF模块的底层依赖。开发者可直接将头文件与库文件引入工程,调用加载解析、页面渲染、文本提取、搜索、字体管理等API,省去繁琐的编译流程,提升构建速度与程序可移植性,同时便于排查版本兼容与跨平台适配问题。
1. 已经编译好的 pdfium 库:为什么我劝你先别急着从源码编译
如果你最近在找一个能直接塞进项目里用的 PDF 渲染库,大概率已经刷到过 pdfium 这个名字。它是 Chromium 里负责 PDF 显示的核心组件,渲染质量、字体兼容性、注释支持都相当能打,很多桌面阅读器、在线预览服务、电子签章系统底层用的就是它。但真正动手的人都知道,pdfium 的源码编译是个体力活:要拉 Chromium 的 depot_tools,要同步几个 G 的依赖,gn 参数一改就报错,Windows 上还得配 VS 工具链,macOS 上 Xcode 版本不对直接卡死。所以「已经编译好的 pdfium 库」这个资源的价值就很直接了——它把最耗时的构建环节替你走完了,你拿到的是可以直接链接的静态库文件,省掉从零搭建编译环境的那几天。这篇不是科普 pdfium 是什么,而是把这份预编译库怎么接进工程、参数怎么配、哪些坑我踩过,一次讲清楚。
2. 拿到静态库之后:先搞清楚你手里是什么,再决定怎么接
2.1 预编译 pdfium 静态库的组成与选型判断
一份完整的 pdfium 预编译产物,通常不是一个孤零零的.a或.lib文件,而是一组东西。以常见的目录结构来看,你会看到类似这样的布局:
pdfium_prebuilt/ ├── include/ │ ├── fpdfview.h │ ├── fpdf_text.h │ ├── fpdf_annot.h │ ├── fpdf_edit.h │ └── ... ├── lib/ │ ├── libpdfium.a (Linux / macOS) │ ├── pdfium.lib (Windows 静态库) │ └── pdfium.dll.lib (Windows 导入库,如果带动态版本) └── bin/ └── pdfium.dll (Windows 动态库,可选)头文件目录里最关键的是fpdfview.h,它是所有渲染操作的入口,FPDF_InitLibrary、FPDF_LoadDocument、FPDF_RenderPageBitmap这些核心函数都声明在这里。fpdf_text.h负责文本提取,fpdf_annot.h管注释,fpdf_edit.h管页面操作。你接项目之前先确认一件事:这份库是静态链接版还是动态链接版。静态库的好处是部署简单,一个可执行文件带走所有依赖;坏处是体积大,而且如果你的项目本身还有其他静态库,符号冲突的概率会上升。动态库则相反,部署时要多带一个 dll/so,但链接阶段干净。
判断方法很简单,看lib/目录下有没有.dll或.so伴随文件。如果只有.a/.lib,那就是纯静态。另外注意一点,pdfium 默认依赖一些第三方库(libjpeg、libpng、freetype、lcms2 等),预编译版本通常已经把这些静态编进去了,但有些精简版会裁掉部分功能。你可以通过头文件里是否包含fpdf_save.h、fpdf_flatten.h来判断功能完整度。
选型上我的建议是:桌面端优先静态库,减少运行时依赖问题;移动端和 WebAssembly 场景优先动态库或专门的 wasm 构建,因为静态库在移动端的链接脚本配置更麻烦。如果你只是做个内部工具,静态库一把梭最省心。
2.2 把静态库接进 CMake 工程:完整步骤与参数说明
假设你用的是 CMake 工程,下面是我一般会写的接入脚本。先建一个third_party/pdfium目录,把头文件和库文件按上面的结构放好,然后在顶层CMakeLists.txt里加:
# 声明 pdfium 的导入目标 add_library(pdfium STATIC IMPORTED) # 根据平台设置库文件路径 if(WIN32) set_target_properties(pdfium PROPERTIES IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/third_party/pdfium/lib/pdfium.lib" ) elseif(APPLE) set_target_properties(pdfium PROPERTIES IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/third_party/pdfium/lib/libpdfium.a" ) else() set_target_properties(pdfium PROPERTIES IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/third_party/pdfium/lib/libpdfium.a" ) endif() # 指定头文件目录 target_include_directories(pdfium INTERFACE "${CMAKE_SOURCE_DIR}/third_party/pdfium/include" ) # 链接系统依赖(Linux 下通常需要这些) if(UNIX AND NOT APPLE) target_link_libraries(pdfium INTERFACE pthread dl m ) endif()这段脚本的逻辑是:用IMPORTED目标把预编译库包装成一个 CMake 可识别的 target,这样你的主工程只需要target_link_libraries(your_app PRIVATE pdfium)就能链接。IMPORTED_LOCATION指向实际的库文件路径,INTERFACE表示头文件目录会传递给链接方。Linux 下额外链接pthread dl m是因为 pdfium 内部用到了线程、动态加载和数学库,不显式链接的话在某些发行版上会报未定义符号。
接好之后,写一个最小验证程序:
#include <fpdfview.h> #include <iostream> int main() { // 初始化 pdfium 库,必须在任何其他 API 之前调用 FPDF_InitLibrary(); // 加载一个 PDF 文件,注意路径要用宽字符或 UTF-8 转换 FPDF_DOCUMENT doc = FPDF_LoadDocument("test.pdf", nullptr); if (!doc) { unsigned long err = FPDF_GetLastError(); std::cerr << "load failed, error code: " << err << std::endl; FPDF_DestroyLibrary(); return -1; } int page_count = FPDF_GetPageCount(doc); std::cout << "page count: " << page_count << std::endl; // 释放文档和库 FPDF_CloseDocument(doc); FPDF_DestroyLibrary(); return 0; }FPDF_InitLibrary和FPDF_DestroyLibrary必须成对出现,前者初始化全局状态,后者释放。FPDF_LoadDocument的第二个参数是密码,没有就传nullptr。加载失败时用FPDF_GetLastError拿错误码,常见的有FPDF_ERR_FILE(文件打不开)、FPDF_ERR_FORMAT(格式不对)、FPDF_ERR_PASSWORD(需要密码)。这个最小程序能跑通,说明库接对了,再往下做渲染和文本提取才有意义。
2.3 渲染第一页:从 bitmap 创建到内存释放的完整链路
验证链接只是第一步,真正要用起来得能渲染。下面这段代码把第一页渲染成 BGRA 位图:
#include <fpdfview.h> #include <vector> #include <iostream> int main() { FPDF_InitLibrary(); FPDF_DOCUMENT doc = FPDF_LoadDocument("test.pdf", nullptr); if (!doc) { FPDF_DestroyLibrary(); return -1; } // 加载第一页 FPDF_PAGE page = FPDF_LoadPage(doc, 0); if (!page) { FPDF_CloseDocument(doc); FPDF_DestroyLibrary(); return -1; } // 获取页面尺寸(单位是点,1点 = 1/72 英寸) double width = FPDF_GetPageWidth(page); double height = FPDF_GetPageHeight(page); // 设定渲染缩放比例,2.0 表示 144 DPI float scale = 2.0f; int bmp_width = static_cast<int>(width * scale); int bmp_height = static_cast<int>(height * scale); // 创建位图,格式为 BGRA FPDF_BITMAP bitmap = FPDFBitmap_Create(bmp_width, bmp_height, 1); if (!bitmap) { FPDF_ClosePage(page); FPDF_CloseDocument(doc); FPDF_DestroyLibrary(); return -1; } // 填充白色背景 FPDFBitmap_FillRect(bitmap, 0, 0, bmp_width, bmp_height, 0xFFFFFFFF); // 渲染页面到位图 FPDF_RenderPageBitmap(bitmap, page, 0, 0, bmp_width, bmp_height, 0, 0); // 获取位图缓冲区指针,可以拿去存 PNG 或显示 void* buffer = FPDFBitmap_GetBuffer(bitmap); int stride = FPDFBitmap_GetStride(bitmap); std::cout << "buffer: " << buffer << ", stride: " << stride << std::endl; // 释放顺序:位图 -> 页面 -> 文档 -> 库 FPDFBitmap_Destroy(bitmap); FPDF_ClosePage(page); FPDF_CloseDocument(doc); FPDF_DestroyLibrary(); return 0; }这里有几个参数值得说清楚。FPDFBitmap_Create的第三个参数传 1 表示 alpha 通道有效,传 0 则忽略 alpha。FPDF_RenderPageBitmap的倒数第二个参数是旋转角度,0 表示不旋转,可选 1/2/3 对应 90/180/270 度。最后一个参数是渲染标志,常用的是FPDF_RENDER_LCD_TEXT(LCD 文本优化)和FPDF_ANNOT(渲染注释),多个标志用按位或组合。释放顺序不能乱,位图先于页面释放,页面先于文档,文档先于库,否则会出现野指针或内存泄漏。这套链路跑通,你就能把 PDF 转成图片做预览了。
3. 文本提取与注释处理:静态库真正拉开差距的地方
3.1 用 FPDF_Text 提取文本:字符编码与换行处理
渲染只是面子,文本提取才是很多业务的核心。pdfium 的文本 API 在fpdf_text.h里,基本流程是加载页面、加载文本页、逐字符读取:
#include <fpdf_text.h> #include <vector> #include <string> std::string ExtractPageText(FPDF_DOCUMENT doc, int page_index) { FPDF_PAGE page = FPDF_LoadPage(doc, page_index); if (!page) return ""; // 加载文本页,失败通常是因为页面没有文本层(纯扫描件) FPDF_TEXTPAGE text_page = FPDFText_LoadPage(page); if (!text_page) { FPDF_ClosePage(page); return ""; } int char_count = FPDFText_CountChars(text_page); std::vector<unsigned short> buf(char_count + 1); // 提取所有字符,返回实际写入的字符数 int written = FPDFText_GetText(text_page, 0, char_count, buf.data()); // pdfium 返回的是 UTF-16LE,需要转成 UTF-8 std::string utf8; for (int i = 0; i < written; i++) { unsigned short c = buf[i]; if (c == 0) break; if (c < 0x80) { utf8 += static_cast<char>(c); } else if (c < 0x800) { utf8 += static_cast<char>(0xC0 | (c >> 6)); utf8 += static_cast<char>(0x80 | (c & 0x3F)); } else { utf8 += static_cast<char>(0xE0 | (c >> 12)); utf8 += static_cast<char>(0x80 | ((c >> 6) & 0x3F)); utf8 += static_cast<char>(0x80 | (c & 0x3F)); } } FPDFText_ClosePage(text_page); FPDF_ClosePage(page); return utf8; }FPDFText_CountChars返回的是字符数,不是字节数。FPDFText_GetText把字符写进你提供的缓冲区,返回实际写入数量。注意 pdfium 返回的是 UTF-16LE 编码,中文、日文、韩文都在这个范围内,所以必须做编码转换,直接当 ASCII 用会乱码。另外换行符的处理:pdfium 会在文本流里插入\r\n,但具体位置取决于 PDF 内部的文本对象划分,有时候一个段落会被拆成多行,需要你自己根据坐标做合并。如果FPDFText_LoadPage返回空,说明这页是扫描件没有文本层,得走 OCR 路线,这不是 pdfium 能解决的。
3.2 注释读取与表单字段:FPDF_Annot 的实操边界
注释和表单是 PDF 交互性的体现,pdfium 提供了fpdf_annot.h和fpdf_formfill.h两套 API。读注释相对简单:
#include <fpdf_annot.h> void ListAnnotations(FPDF_DOCUMENT doc, int page_index) { FPDF_PAGE page = FPDF_LoadPage(doc, page_index); if (!page) return; // 获取第一个注释,然后遍历链表 FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, 0); int index = 0; while (annot) { // 获取注释子类型,比如 FPDF_ANNOT_TEXT、FPDF_ANNOT_HIGHLIGHT int subtype = FPDFAnnot_GetSubtype(annot); std::cout << "annot " << index << " subtype: " << subtype << std::endl; // 获取注释矩形区域 FS_RECTF rect; if (FPDFAnnot_GetRect(annot, &rect)) { std::cout << " rect: " << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << std::endl; } FPDFPage_CloseAnnot(annot); index++; annot = FPDFPage_GetAnnot(page, index); } FPDF_ClosePage(page); }FPDFPage_GetAnnot按索引取注释,越界返回空。FPDFAnnot_GetSubtype返回的是整数枚举,具体值在头文件里有定义。FPDFAnnot_GetRect拿到的是注释在页面坐标系里的矩形,注意 pdfium 的坐标系原点在左下角,和很多图像库的左上角原点不同,做 UI 叠加时要转换。
表单字段稍微复杂,需要先加载表单:
#include <fpdf_formfill.h> void ReadFormFields(FPDF_DOCUMENT doc) { // 创建表单填充环境 FPDF_FORMHANDLE form = FPDFDOC_InitFormFillEnvironment(doc, nullptr); if (!form) return; // 加载第一页并初始化表单 FPDF_PAGE page = FPDF_LoadPage(doc, 0); FORM_OnAfterLoadPage(page, form); // 遍历表单字段 int field_count = FPDFPage_GetAnnotCount(page); for (int i = 0; i < field_count; i++) { FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, i); if (FPDFAnnot_GetSubtype(annot) == FPDF_ANNOT_WIDGET) { // 这是一个表单控件 unsigned long name_len = FPDFAnnot_GetFormFieldName(form, annot, nullptr, 0); std::vector<unsigned short> name_buf(name_len + 1); FPDFAnnot_GetFormFieldName(form, annot, name_buf.data(), name_len + 1); std::wcout << L"field: " << name_buf.data() << std::endl; } FPDFPage_CloseAnnot(annot); } FORM_OnBeforeClosePage(page, form); FPDF_ClosePage(page); FPDFDOC_ExitFormFillEnvironment(form); }表单这块的边界在于:pdfium 能读能填,但表单的 JavaScript 动作(比如计算字段、提交按钮)它不执行,需要你自己在应用层处理。另外FPDFDOC_InitFormFillEnvironment的第二个参数是回调结构体,传nullptr会用默认行为,但如果你要自定义渲染表单控件,就得填这个结构体。表单字段名是 UTF-16,输出时注意编码。
4. 避坑与排查:预编译 pdfium 静态库最容易翻车的五个地方
4.1 链接报错「undefined reference to FPDF_InitLibrary」
现象:CMake 配置通过,编译也过了,但链接阶段报一堆 pdfium 函数的未定义引用。
原因:最常见的是库文件架构不匹配。比如你下载的是 x86_64 的.a,但工程在 ARM64 上编译;或者 Windows 下拿了 MinGW 编译的库去配 MSVC。其次是链接顺序问题,静态库在被依赖项之后链接会找不到符号。
解决:先用file libpdfium.a或dumpbin /headers pdfium.lib确认架构。CMake 里确保target_link_libraries(your_app PRIVATE pdfium)放在其他依赖之后。如果是交叉编译,确认工具链文件里的CMAKE_SYSTEM_PROCESSOR和库架构一致。
4.2 渲染出来是黑屏或全白
现象:程序不崩溃,位图也创建了,但保存成 PNG 后要么全黑要么全白。
原因:全黑通常是没调FPDFBitmap_FillRect填背景,alpha 通道默认是 0,保存时透明区域被当成黑色。全白则可能是FPDF_RenderPageBitmap的渲染标志没设对,或者页面本身是空的。
解决:渲染前一定先FPDFBitmap_FillRect(bitmap, 0, 0, w, h, 0xFFFFFFFF)填白底。如果页面有内容还是白,检查FPDF_LoadPage的返回值,确认页面索引从 0 开始。另外注意FPDFBitmap_Create的 alpha 参数,存 JPEG 时传 0,存 PNG 时传 1。
4.3 中文文本提取出来是乱码
现象:英文 PDF 提取正常,中文 PDF 提取出来是一堆问号或方块。
原因:pdfium 返回 UTF-16LE,直接按char*处理会截断。另外有些 PDF 内嵌字体没有 ToUnicode 映射表,pdfium 拿不到正确的 Unicode 码点。
解决:按 3.1 节的代码做 UTF-16 到 UTF-8 的转换。如果是字体缺映射表,FPDFText_GetText返回的可能是字形索引而不是 Unicode,这种情况没有通用解法,只能换 PDF 或走 OCR。判断方法是看FPDFText_GetFontInfo返回的字体信息里有没有 ToUnicode 标志。
4.4 多线程环境下崩溃
现象:单线程跑得好好的,放到线程池里并发渲染就随机崩溃。
原因:pdfium 的全局状态(字体缓存、内存分配器)不是线程安全的,FPDF_InitLibrary和FPDF_DestroyLibrary更不能在多线程里反复调。
解决:在主线程调一次FPDF_InitLibrary,所有渲染任务共享这个库实例。如果确实需要多线程渲染,每个线程用独立的FPDF_DOCUMENT,但库的初始化和销毁只在主线程做。另外FPDFBitmap_Create创建的位图不要跨线程传递,每个线程自己创建自己释放。
4.5 内存泄漏:文档关了但内存没降
现象:循环加载关闭 PDF,进程内存持续上涨,最后 OOM。
原因:pdfium 内部有缓存机制,FPDF_CloseDocument不会立即释放所有内存。另外如果FPDF_LoadPage之后没有FPDF_ClosePage,页面对象会泄漏。
解决:确保每个FPDF_LoadPage都有对应的FPDF_ClosePage,每个FPDFText_LoadPage都有FPDFText_ClosePage。如果内存敏感,可以定期调FPDF_InitLibrary和FPDF_DestroyLibrary来强制清缓存,但注意这会导致所有已加载文档失效,只能在所有文档关闭后做。用 Valgrind 或 ASan 跑一遍,能定位到具体的泄漏点。
5. 进阶技巧:用 pdfium 做增量保存与页面合并
预编译库能不能做 PDF 编辑,取决于它有没有包含fpdf_save.h和fpdf_edit.h。如果这两个头文件在,你就能做增量保存和页面合并,这是很多业务场景的刚需。
增量保存的核心是FPDF_SaveAsCopy,它把当前文档状态写到一个新的文件流里。下面这段代码演示了加载两个 PDF、把第二个的第一页插入到第一个文档末尾、然后保存:
#include <fpdf_edit.h> #include <fpdf_save.h> #include <fpdfview.h> // 自定义写入回调,pdfium 通过它把数据写到你指定的地方 struct FileWriter : public FPDF_FILEWRITE { FILE* fp; }; int WriteBlock(FPDF_FILEWRITE* pThis, const void* data, unsigned long size) { FileWriter* writer = static_cast<FileWriter*>(pThis); return fwrite(data, 1, size, writer->fp) == size ? 1 : 0; } bool MergePdf(const char* src1, const char* src2, const char* dst) { FPDF_InitLibrary(); FPDF_DOCUMENT doc1 = FPDF_LoadDocument(src1, nullptr); FPDF_DOCUMENT doc2 = FPDF_LoadDocument(src2, nullptr); if (!doc1 || !doc2) return false; // 从 doc2 导入第一页到 doc1 FPDF_PAGE page2 = FPDF_LoadPage(doc2, 0); FPDF_PAGE new_page = FPDFPage_New(doc1, FPDF_GetPageCount(doc1), FPDF_GetPageWidth(page2), FPDF_GetPageHeight(page2)); // 用 FPDF_ImportPages 更简单,这里演示底层方式 // 实际项目中推荐 FPDF_ImportPages(doc1, doc2, "1", page_count) FPDF_ClosePage(new_page); FPDF_ClosePage(page2); // 保存到新文件 FILE* fp = fopen(dst, "wb"); FileWriter writer; writer.fp = fp; writer.WriteBlock = WriteBlock; FPDF_SaveAsCopy(doc1, &writer, FPDF_NO_INCREMENTAL); fclose(fp); FPDF_CloseDocument(doc1); FPDF_CloseDocument(doc2); FPDF_DestroyLibrary(); return true; }FPDF_SaveAsCopy的第三个参数控制保存模式,FPDF_NO_INCREMENTAL表示全量保存,FPDF_INCREMENTAL表示增量更新(只写改动部分,但要求原文件可写且没被破坏)。FPDF_ImportPages是更省事的页面导入接口,签名是FPDF_ImportPages(dest_doc, src_doc, page_range, insert_index),page_range是字符串,比如"1,3,5"或"1-5"。用这个接口比自己手动创建页面再拷贝内容要可靠得多,因为页面内的资源引用、字体映射它都帮你处理了。
一个实际经验:增量保存对已经加密或签名的 PDF 可能失败,FPDF_SaveAsCopy返回 0 就说明保存没成功。这时候先检查文档权限,FPDF_GetDocPermissions返回的位掩码里如果FPDF_PERM_MODIFY没置位,就不能改。另外保存后的文件用其他阅读器打开验证一下,pdfium 自己读自己写的文件通常没问题,但跨阅读器兼容性偶尔会翻车,尤其是表单和注释的保留。
从那以后我每次接一个新的预编译库,都强制走一遍「最小加载 → 渲染一页 → 提取一段文本 → 保存一次」的验证链路,四个环节都过了才往业务代码里接。这套习惯帮我省掉了至少三次在项目后期才发现库功能残缺的返工。希望帮到你。
本文还有配套的精品资源,点击获取