Delphi 12.3集成pdfium预编译库实现PDF渲染与文本提取
简介本资源是面向Delphi开发者尤其是使用Delphi 12.3版本的PDFium Windows x64原生库集成包用于在Delphi项目中实现高性能PDF渲染、文本提取、表单填充、注释处理、签名验证等核心功能。资源以tgz压缩格式提供共30个文件涵盖24个C/C头文件如fpdf_view.h、fpdf_edit.h、fpdf_signature.h等、1个静态链接库pdfium.dll.lib、1个动态链接库pdfium.dll、1个CMake配置脚本PDFiumConfig.cmake、1个版本说明VERSION、1个构建参数文件args.gn及LICENSE等关键组件完整支撑Delphi调用PDFium原生API的开发闭环。包体精简高效仅2.88MB兼顾功能性与部署轻量性。目前已有30人学习下载适合中高级Delphi工程师快速接入PDFium底层能力无需自行编译PDFium源码可直接集成至VCL或FireMonkey项目显著降低PDF处理模块的开发门槛与兼容风险。 做Delphi桌面开发的人迟早会遇到一个绕不开的需求在程序里显示PDF、提取PDF里的文本或者把PDF页面转成图片。早些年我习惯用Adobe的ActiveX接口后来被用户机器上的版本兼容问题折腾得够呛再后来试过几款商业PDF控件功能确实全但授权费用和安装流程对很多中小项目来说都是不小的负担。直到我接触到Google开源的pdfium——这个藏在Chromium浏览器内核里的PDF解析引擎整个方案才算真正稳定下来。这篇文章想聊的正是我在Delphi 12.3里接入pdfium-win-x64.tgz这套预编译库的完整过程。从拿到tgz压缩包开始到解压、封装、渲染、提取文本再到实际场景里的各种坑都会一步步拆开讲。严格来说pdfium本身是一个C库并不是现成的Delphi控件但通过一层封装完全可以变成工程里一个“类控件”的能力单元。手头正在做Delphi PDF功能、又不想依赖付费组件的朋友这篇应该能帮你少走不少弯路。1. 为什么在Delphi 12.3里选pdfiumPDF处理方案的取舍1.1 先做一次PDF集成方案的横向对比Delphi生态里处理PDF大部分团队都绕不开下面四类路子。我直接按实际体验做个对比。方案典型实现优点主要痛点Adobe COM/ActiveX调用Acrobat的OLE接口无需自己写渲染逻辑代码量小用户机器必须安装Acrobat或Reader版本一换接口就变安装包体积巨大商业PDF控件QuickPDF、Gnostice等开箱即用、功能齐全技术支持和文档完善授权费用不低对工具型小项目不划算闭源遇到问题不好深入排查pdfium预编译库pdfium.dll 自写Delphi封装免费、稳定、渲染质量高社区活跃需要自己写封装层初次上手有门槛纯Pascal开源方案SynPDF、ReportPrinter等完全源码可控无外部依赖大多偏重生成而不是解析渲染处理复杂PDF文件时能力有限我早期用过第一类方案交付给客户时最怕听到“我这台电脑上显示不出来”拷过去一个Adobe Reader安装包就几百兆体验很糟糕。商业控件我也评估过功能确实全但我们项目真要用的只是“显示PDF 提取文字”这两块为这两块功能承担全套授权还是有点重。最终定下来用pdfium核心原因是它在维持零成本的前提下把最关键的“解析和渲染”做到了浏览器内核级别。1.2 pdfium到底强在哪里pdfium是Chromium浏览器底层的PDF引擎你在Chrome里打开一个PDF看到的效果就是它渲染出来的。这意味着它对PDF规范的兼容性经过了海量真实网页文件的验证各种乱排版、特殊字体、加密文档、表单注释处理起来都相对稳。具体到Delphi项目里它有几点特别有价值C接口友好。pdfium对外暴露的是纯C APIDelphi可以直接通过LoadLibrary GetProcAddress动态调用不需要额外引入复杂依赖。跨平台能力强。Windows、Linux、macOS、Android、iOS都有预编译产物同一套接口设计可以平移到FireMonkey项目里复用。文本提取能力扎实。通过FPDFText系列接口可以按页取出Unicode文本做搜索、索引、内容抽取都很方便。Apache 2.0协议。商用项目集成没有授权负担保留LICENSE声明即可法务上比较省心。还有一点很多人容易忽略pdfium作为浏览器组件的一部分在内存回收和异常处理上做得比较稳妥长时间批量处理PDF时不太容易出现野指针崩溃。这点对桌面应用来说太重要了。1.3 为什么直接用预编译库而不是自己编译源码pdfium-windows-x64.tgz这种文件通常来自开源社区维护的预编译发布渠道最有名的就是github上的pdfium-binaries项目。下载解压后拿到的就是编译好的dll和头文件。有同学会问“我自己拉源码编译不行吗更可控。”可以但性价比很低。pdfium的构建系统用的是GN ninja还要配合depot_tools同步大量第三方依赖第一次配置环境就得折腾半天编译一次又是几十分钟起步。而且pdfium有很多编译开关比如是否启用XFA表单、是否内置字体表、是否带V8 JavaScript引擎不同开关组合出来的dll行为差异很大没有经验的话很容易编译出一个能跑但显示效果不对的版本。与其自己在构建系统里挣扎不如直接用社区里维护好的稳定构建把精力花在Delphi封装层上。这相当于你只负责“怎么用好这个引擎”而“怎么造好这个引擎”让更专业的人去做。预编译包里一般都会有对应的头文件和LICENSE声明用作第三方库集成完全够用。2. 拿到pdfium-win-x64.tgz之后解压、目录规划与运行库检查2.1 Windows下解压tgz多数人第一步就卡住tgz是tar.gz的缩写在Linux下一条命令就解开了但Windows下的同学经常双击没反应。其实Win10 1803以后的系统自带了tar命令直接打开cmd或者PowerShell切到文件所在目录执行tar -xzf pdfium-win-x64.tgz几秒钟就会在当前目录下解出文件。如果你的系统比较老或者图省事也可以用7-Zip、WinRAR这类图形化工具打开效果是一样的。我一般推荐先用tar命令行因为它的解压行为和Linux完全一致文件权限、目录结构都不会变。解压完成后先把目录结构看清楚再动手。一个典型的预编译包通常长这样文件/目录作用pdfium.dll核心动态库所有API都在这个文件里pdfium.h、fpdfview.h等头文件C接口声明写Delphi封装时对照函数签名用LICENSE / NOTICEApache 2.0声明商用项目需要保留lib/ 目录如有里面可能是MSVC的静态导入库pdfium.libDelphi用不到但可以留着备查这里有个经验Delphi封装时解压出来的头文件是你最可靠的参照物一定要留着。网上很多封装代码因为pdfium版本升级导致函数签名对不上这个时候回头查fpdfview.h比自己瞎猜要靠谱得多。2.2 把dll放进工程目录三种策略与取舍解压只是开始pdfium.dll放在哪里直接决定了后续LoadLibrary能不能成功。我常用的有三种做法。第一种放exe同级目录最简单推荐优先用。FModule : LoadLibrary(PChar(ExtractFilePath(Application.ExeName) pdfium.dll));这种方式适合普通Win32/Win64桌面程序。发布时把pdfium.dll和exe放在一起部署直观排查问题也方便。缺点是当程序被当作插件嵌入到别的宿主里时当前目录可能不是exe目录这条路就走不通了。第二种放工程子目录运行时用绝对路径加载。比如在exe同级建一个lib目录把pdfium.dll放进去。加载时先拼出完整路径再调用LoadLibrary。好处是主程序目录清爽文件结构清晰坏处是路径拼接代码要谨慎尽量用相对exe路径算出来别写死绝对路径否则拷到别的机器就崩。第三种打包进资源释放到临时目录不推荐。这种做法是先把dll作为二进制资源编进exe运行时释放到临时目录再加载。看起来“单文件分发”很干净但杀毒软件对运行时可执行文件自释放的行为很容易误报而且每次启动都有释放和清理逻辑代码复杂度上去了收益却不大。除非你的产品对“只能有一个exe”有硬性要求否则别走这条路。2.3 初始排查VC运行库和位数匹配pdfium.dll是C编译出来的动态库依赖Visual C运行库主要是vcruntime140.dll、msvcp140.dll和通用C运行时ucrtbase.dll。目标机器如果缺少这些运行库LoadLibrary会直接失败。我在新环境里第一次跑通之前习惯先装一遍最新的Visual C Redistributablex64版本这样能排除一大半“dll加载失败”的干扰项。如果是纯净系统这一步基本是必须的。另外一个很容易踩的坑是位数匹配。我之前就遇到过工程默认编译成Win32结果加载的却是x64的pdfium.dllLoadLibrary直接返回0页面根本起不来。Delphi 12.3本身对Win64支持已经很成熟工程目标平台一定要和dll位数保持一致。pdfium-win-x64这个名字已经说得很清楚是Windows 64位版你的工程必须是x64或者另外找一份win-x86的构建。3. 手写pdfium的Delphi封装从DLL到可调用的类3.1 为什么必须用动态加载而不是静态导入很多Delphi开发者熟悉的是静态导入方式引用dll里的导出函数直接调用。但pdfium这套库不适合这么玩原因有三个。第一pdfium.dll文件体积不小带V8的版本可能有几十兆如果采用静态导入exe启动时系统会自动加载这个dll一旦找不到整个程序直接启动失败连错误提示都很含糊。动态加载则可以把“dll未找到”变成一个可捕获的业务异常弹窗提示比启动崩溃友好得多。第二pdfium的导出函数在Windows下走的是stdcall调用约定在Linux/macOS上才是cdecl和Delphi默认的外部过程声明需要显式匹配。动态加载时我们可以把调用约定写死在函数指针类型里避免头文件和编译器默认值出现偏差。第三不同构建版本的pdfium会新增或改名一些函数。动态加载时可以在运行时检查某个函数指针是否为空从而做版本兼容静态导入就没有这个弹性。所以封装思路很明确LoadLibrary加载dllGetProcAddress逐个取函数地址再通过函数指针调用。这样整个dll的管理完全掌握在自己手里。3.2 核心封装代码函数指针声明与加载逻辑下面这段代码是我在Delphi 12.3里实际使用的封装骨架我精简掉了一些非核心函数只保留渲染和文本提取最关键的部分。unit PdfiumCore; interface uses System.SysUtils, System.Classes, Winapi.Windows; type // 函数指针类型定义注意Windows下调用约定是stdcall TFPDF_InitLibrary procedure; stdcall; TFPDF_DestroyLibrary procedure; stdcall; TFPDF_LoadDocument function(file_path: PAnsiChar; password: PAnsiChar): Pointer; stdcall; TFPDF_GetPageCount function(document: Pointer): Integer; stdcall; TFPDF_LoadPage function(document: Pointer; page_index: Integer): Pointer; stdcall; TFPDF_ClosePage procedure(page: Pointer); stdcall; TFPDF_CloseDocument procedure(document: Pointer); stdcall; TFPDF_GetPageWidth function(page: Pointer): Double; stdcall; TFPDF_GetPageHeight function(page: Pointer): Double; stdcall; TFPDFBitmap_Create function(width: Integer; height: Integer; alpha: Integer): Pointer; stdcall; TFPDFBitmap_GetBuffer function(bitmap: Pointer): Pointer; stdcall; TFPDFBitmap_FillRect procedure(bitmap: Pointer; left, top, width, height: Integer; color: Cardinal); stdcall; TFPDF_RenderPageBitmap function(bitmap: Pointer; page: Pointer; start_x, start_y, size_x, size_y: Integer; rotate: Integer; flags: Integer): LongBool; stdcall; TFPDFBitmap_Destroy procedure(bitmap: Pointer); stdcall; TFPDFText_LoadPage function(page: Pointer): Pointer; stdcall; TFPDFText_CountChars function(text_page: Pointer): Integer; stdcall; TFPDFText_GetText function(text_page: Pointer; start_index, count: Integer; result_buf: PWord): Integer; stdcall; TFPDFText_Close procedure(text_page: Pointer); stdcall; var // 实际加载的dll模块句柄 FModule: THandle 0; FPDF_InitLibrary: TFPDF_InitLibrary nil; FPDF_DestroyLibrary: TFPDF_DestroyLibrary nil; FPDF_LoadDocument: TFPDF_LoadDocument nil; FPDF_GetPageCount: TFPDF_GetPageCount nil; FPDF_LoadPage: TFPDF_LoadPage nil; FPDF_ClosePage: TFPDF_ClosePage nil; FPDF_CloseDocument: TFPDF_CloseDocument nil; FPDF_GetPageWidth: TFPDF_GetPageWidth nil; FPDF_GetPageHeight: TFPDF_GetPageHeight nil; FPDFBitmap_Create: TFPDFBitmap_Create nil; FPDFBitmap_GetBuffer: TFPDFBitmap_GetBuffer nil; FPDFBitmap_FillRect: TFPDFBitmap_FillRect nil; FPDF_RenderPageBitmap: TFPDF_RenderPageBitmap nil; FPDFBitmap_Destroy: TFPDFBitmap_Destroy nil; FPDFText_LoadPage: TFPDFText_LoadPage nil; FPDFText_CountChars: TFPDFText_CountChars nil; FPDFText_GetText: TFPDFText_GetText nil; FPDFText_Close: TFPDFText_Close nil; function LoadPdfium(const ADllPath: string): Boolean; procedure UnloadPdfium; implementation function LoadPdfium(const ADllPath: string): Boolean; function BindFunc(procName: string; var procAddr): Boolean; var addr: Pointer; begin addr : GetProcAddress(FModule, PChar(procName)); if Assigned(addr) then Pointer(procAddr) : addr; Result : Assigned(addr); end; begin Result : False; if FModule 0 then Exit; FModule : LoadLibrary(PChar(ADllPath)); if FModule 0 then raise Exception.Create(加载pdfium.dll失败: SysErrorMessage(GetLastError)); // 逐个绑定导出函数这里挑关键的校验 if not BindFunc(FPDF_InitLibrary, FPDF_InitLibrary) then Exit; if not BindFunc(FPDF_LoadDocument, FPDF_LoadDocument) then Exit; if not BindFunc(FPDF_GetPageCount, FPDF_GetPageCount) then Exit; if not BindFunc(FPDF_LoadPage, FPDF_LoadPage) then Exit; if not BindFunc(FPDF_ClosePage, FPDF_ClosePage) then Exit; if not BindFunc(FPDF_CloseDocument, FPDF_CloseDocument) then Exit; if not BindFunc(FPDF_GetPageWidth, FPDF_GetPageWidth) then Exit; if not BindFunc(FPDF_GetPageHeight, FPDF_GetPageHeight) then Exit; if not BindFunc(FPDFBitmap_Create, FPDFBitmap_Create) then Exit; if not BindFunc(FPDFBitmap_GetBuffer, FPDFBitmap_GetBuffer) then Exit; if not BindFunc(FPDFBitmap_FillRect, FPDFBitmap_FillRect) then Exit; if not BindFunc(FPDF_RenderPageBitmap, FPDF_RenderPageBitmap) then Exit; if not BindFunc(FPDFBitmap_Destroy, FPDFBitmap_Destroy) then Exit; if not BindFunc(FPDFText_LoadPage, FPDFText_LoadPage) then Exit; if not BindFunc(FPDFText_CountChars, FPDFText_CountChars) then Exit; if not BindFunc(FPDFText_GetText, FPDFText_GetText) then Exit; if not BindFunc(FPDFText_Close, FPDFText_Close) then Exit; // 关键函数绑定成功后再初始化 FPDF_InitLibrary; Result : True; end; procedure UnloadPdfium; begin if FModule 0 then begin if Assigned(FPDF_DestroyLibrary) then FPDF_DestroyLibrary; FreeLibrary(FModule); FModule : 0; end; end; end.有几个细节提醒一下TFPDF_RenderPageBitmap返回类型我用的是LongBool因为C头文件里FPDF_BOOL其实是int4字节。如果用Delphi的Boolean1字节在64位下可能不出问题但在32位下返回值读取会错位。TFPDFText_GetText的缓冲区参数是PWord因为pdfium输出的是UTF-16编码的code unit。如果用PAnsiChar接收中文内容会整个丢光。BindFunc里的Pointer(procAddr) : addr是Delphi给函数指针变量赋值的标准写法注意前面要加Pointer强转编译期才能通过。3.3 用一个类管好生命周期TPdfDocument的封装思路底层接口绑定好之后直接用还是有点散。我会再包一层TPdfDocument把“打开文档—管理页面—渲染—析构”的流程集中起来这样业务代码里就不需要关心dll函数细节了。type TPdfDocument class private FDoc: Pointer; FFileName: string; function GetPageCount: Integer; public constructor Create(const AFileName: string); destructor Destroy; override; procedure RenderPage(AIndex: Integer; ABitmap: Vcl.Graphics.TBitmap; AScale: Double); function GetPageText(AIndex: Integer): string; property PageCount: Integer read GetPageCount; end;构造函数里调用FPDF_LoadDocument要注意文件路径参数类型是PAnsiChar。Delphi的string默认是UnicodeString直接传进去不会自动转要先做一次Utf8Encode或者TEncoding.UTF8.GetBytes否则路径里带中文就出问题。析构函数里调用FPDF_CloseDocument释放资源。这样页面对象随类创建随类释放生命周期一目了然。4. 渲染、文本提取与坐标换算核心功能的落地代码4.1 把PDF页面渲染成位图从画布到ScanLinePDF页面渲染是使用频率最高的功能我把核心代码拆开讲。procedure TPdfDocument.RenderPage(AIndex: Integer; ABitmap: Vcl.Graphics.TBitmap; AScale: Double); var page, pdfBitmap: Pointer; w, h: Integer; buf: PByte; srcLine, dstLine: PByte; rowSize: Integer; i: Integer; begin page : FPDF_LoadPage(FDoc, AIndex); if page nil then raise Exception.Create(加载页面失败); try w : Round(FPDF_GetPageWidth(page) * AScale); h : Round(FPDF_GetPageHeight(page) * AScale); // 参数3传1表示BGRA 32位格式 pdfBitmap : FPDFBitmap_Create(w, h, 1); if pdfBitmap nil then raise Exception.Create(创建位图失败); try // 填充白色背景避免渲染后四周留黑 FPDFBitmap_FillRect(pdfBitmap, 0, 0, w, h, $FFFFFFFF); // 把页面内容渲染到位图上 FPDF_RenderPageBitmap(pdfBitmap, page, 0, 0, w, h, 0, 0); buf : FPDFBitmap_GetBuffer(pdfBitmap); rowSize : w * 4; // BGRA每像素4字节 // 拷贝到VCL位图 ABitmap.PixelFormat : pf32bit; ABitmap.Width : w; ABitmap.Height : h; for i : 0 to h - 1 do begin srcLine : PByte(NativeUInt(buf) NativeUInt(i) * rowSize); dstLine : ABitmap.ScanLine[i]; Move(srcLine^, dstLine^, rowSize); end; finally FPDFBitmap_Destroy(pdfBitmap); end; finally FPDF_ClosePage(page); end; end;这里有两个容易出问题的细节。第一个是位图内存布局。FPDFBitmap_Create第三个参数传1生成的是BGRA顺序的32位位图每个像素4字节。VCL的pf32bit位图在Windows下同样是BGRA内存布局所以可以逐行直接Move。如果你用的是pf24bit3字节像素内存对齐带来的踩线问题会让你校对很久建议统一用pf32bit。第二个是ScanLine的方向。pdfium位图的第0行是页面的顶部而VCL的TBitmap.ScanLine[0]也是图片的顶部行所以这里可以按顺序复制。如果你使用了某些第三方图像库或者Direct2D纹理它们的原点可能定义在左下角复制时需要反转行的顺序否则整张图会上下颠倒。这个坑我在做打印预览时踩过一次印象特别深。4.2 提取页面文本UTF-16到Delphi string文本提取的接口和渲染是独立的一套核心步骤是“加载文本页 → 统计字符数 → 取出全部UTF-16 code unit → 组装成Delphi string”。function TPdfDocument.GetPageText(AIndex: Integer): string; var page, textPage: Pointer; count, i: Integer; chars: array of Word; sb: TStringBuilder; begin Result : ; page : FPDF_LoadPage(FDoc, AIndex); if page nil then Exit; try textPage : FPDFText_LoadPage(page); if textPage nil then Exit; try count : FPDFText_CountChars(textPage); if count 0 then Exit; SetLength(chars, count); // 取出全部字符参数依次是文本页、起始下标、字符个数、目标缓冲区 FPDFText_GetText(textPage, 0, count, PWord(chars)); sb : TStringBuilder.Create; try for i : 0 to count - 1 do sb.Append(Char(chars[i])); Result : sb.ToString; finally sb.Free; end; finally FPDFText_Close(textPage); end; finally FPDF_ClosePage(page); end; end;关于FPDFText_CountChars有一点要搞清楚它返回的是UTF-16 code unit的个数不是Unicode字符个数。对大部分中文和生僻字来说一个字符正好是一个code unit所以问题不大但遇到emoji或者某些扩展区的汉字时一个字符会占两个code unit。如果你要做“按字符个数截断”的操作这个差异就会显现出来。好在Delphi自身的string也是UTF-16索引逻辑和pdfium完全一致。文本提取最神秘的坑是中文提取为空。遇到这种情况先检查你是不是用了PAnsiChar接收结果。因为pdfium按UTF-16输出你一旦声明成AnsiChar数组GetText写入时每个中文的2字节都被截成1字节最后全变成乱码或空。正确做法就是上面的PWord数组接收后再逐个转成Char。4.3 坐标换算PDF的点、屏幕像素和打印DPIPDF内部统一使用“点”point作为长度单位1点 1/72英寸。为什么是72这是传统排版印刷的约定一个点就是1/72 inch。屏幕显示时需要通过DPI把点换算成像素。以最常用的96 DPI屏幕为例1 point 96 / 72 1.3333 像素所以上面RenderPage里的AScale在屏幕上取96 / 72A4页面595点宽842点高渲染出来约为793像素 × 1122像素。如果你做的是高清屏适配可以把屏幕的ActualDpi代入var scale: Double; begin scale : Screen.PixelsPerInch / 72.0; end;打印场景更直接。打印机的物理DPI通常比屏幕高很多常见激光打印机是600 DPI或1200 DPI。此时1 point对应600 / 72 8.3333个设备像素。如果你在打印时直接用屏幕DPI的值打出来字会偏小正确做法是先获取打印机Canvas的PixelsPerInch再按比例渲染最后把渲染结果作为位图发给打印机。还有一个旋转参数要注意。FPDF_RenderPageBitmap的rotate参数0表示不旋转1表示顺时针90度2表示180度3表示270度。旋转90度或270度时页面宽高会互换。如果你在做翻页阅读器实现“横屏阅读”时要记得在渲染前交换目标宽高否则内容会被压缩变形。5. 实测中一定会遇到的几个坑与排查方法5.1 LoadLibrary返回0位数、路径、依赖三连查这可能是最频繁出现的问题。LoadLibrary返回0说明dll压根没加载进来。我摸索出一套三连查的流程基本能覆盖90%的情况。现象可能原因处理方式LoadLibrary返回0exe位数和dll位数不一致打开任务管理器看进程架构和dll位数对齐LoadLibrary返回0路径拼接不对文件不存在用FileExists确认完整路径不要依赖CurrentDir弹出0xc000007b错误VC运行库缺失或位数混合安装对应位数的vc_redist重启程序LoadLibrary成功但某些函数指针为空dll是老版本缺少新导出函数核对头文件里的函数列表换新版dll我见过一个同事把pdfium.dll放到源码目录然后LoadLibrary用的相对路径pdfium.dll自己开发机上运行正常因为IDE把源码目录设成了当前目录但双击exe运行时当前目录变成了exe目录dll找不到程序直接崩。后来统一改成用ExtractFilePath(Application.ExeName)拼接绝对路径才彻底解决。5.2 页面渲染出来一片黑或空白渲染成功但页面是黑/白多半是位图初始化没做对。黑色问题通常是因为没有调用FPDFBitmap_FillRect填充背景。pdfium位图创建后内存里的数据是未定义的如果你直接渲染页面内容以外的区域可能是随机数据视觉上就是花屏或者黑块。所以创建位图后第一件事就是用白色$FFFFFFFF填充整个位图。空白问题则跟FPDFBitmap_Create的alpha参数有关。如果你把alpha传成0pdfium会按24位BGR分配内存每行没有padding。这时候你用w * 4去算行字节数读出来的数据全是错位的画面自然不对。要么统一用alpha1要么老老实实按w * 3处理并做对齐计算。我建议无脑用alpha1省心。还有一种情况是图片上下颠倒。前面说过ScanLine方向问题如果你用自绘控件直接Draw而控件的画布坐标系Y轴向下和你复制的方向相反就会倒置。验证方法很土渲染一页带页眉的PDF如果页眉显示在下方就是行的顺序反了。5.3 中文变成方框或者提取结果异常中文问题分两种一个是“显示为方框”一个是“提取为空”。虽然都和中文有关但原因完全不同。显示为方框是渲染字体缺失。pdfium在渲染文字时需要找到对应的字体文件它的默认行为是使用自带的字体表但某些精简构建为了压缩dll体积会把CJK字体数据裁掉导致中文渲染时找不到字体只能画方块。解决办法是换一个带完整字体表的构建或者在程序启动后调用pdfium的系统字体设置接口把Windows的Fonts目录本文还有配套的精品资源点击获取