☰
Unity 文档预览:Word/Excel/PPT/PDF 渲染方案与避坑指南
2026/9/29 15:45:40 网站建设 项目流程

简介:这是一份面向Unity开发者的文档显示功能实现资源,适用于需要在Android平台集成Word、Excel、PDF、PPT等文件预览能力的教育、办公及文档管理类应用。压缩包共2000个文件,容量172.43MB,包含C#脚本、DLL库、XML配置、Meta与Asset资源文件,以及少量docx、xlsx、pptx、pdf示例文档,并带有若干说明文档与文本文件,方便对照结构进行二次开发和调试。已有913人学习下载,适合有一定C#基础、正在处理移动端文档展示需求的Unity中级开发者。资源围绕平台兼容性、第三方库调用、AndroidWebView集成和性能优化等要点,整理了实现思路与代码结构,能帮助读者快速理解在Unity中解析并呈现常见办公文档的技术路线,减少自行摸索和踩坑成本。

1. Unity 显示 Word、Excel、PPT、PDF 等文件:别急着找插件,先回答三个问题

我在做工业一体机和展厅项目时,经常接到一句话:“把文档列表做进 Unity,点开就能显示 word 和 pdf。”听起来像是加个 unity 扩展就完事,但第一个原型通常会翻车——文档是打开了,弹窗却把 Unity 界面挡在身后,或者直接绿屏、黑屏。这个需求,本质不是“显示文件”,而是“在 Unity 的渲染界面里,嵌入其他格式文档的预览通道”。能解决它的路子有四条:转图片、转 PDF 再渲染、嵌套 WebView、后端转换前端展示,每条的适用边界和坑完全不同。适合谁看这篇文章:做展厅大屏、培训考核、数据可视化配套工具、生产管理系统的 Unity 开发者,尤其是 Windows 平台为主、又不想把整个项目改成 Electron 的那类团队。动手前先回答三个问题:要不要让用户编辑文档?文档要离线还是要走内网?目标平台是 Windows 还是 Android/iOS?这三个答案,直接决定选型方向。

2. 选型:四条技术路线,先按“要不要编辑”砍掉一半

注意:本章不是罗列插件,而是理顺“谁在真正渲染文档”。选型错误会在项目后期付出数倍返工代价。

2.1 转图片方案:用 Office COM 把 Word/Excel/PPT 导成 PNG

先解释一下为什么优先考虑转图片。Unity 是游戏引擎,它的 UI 系统(UGUI/UI Toolkit)只认 Sprite/Texture,不认 docx 或 pptx。你把一个 Word 文件直接拖给 RawImage,它不会自己变成像素。所以最早的落地思路是“在进入 Unity 之前,先把文档变成图片”,锁定格式,消除变量。具体做法:Windows 机器上安装 Office,用 Word/Excel/PowerPoint 的 COM 接口把文档导出为 PDF 或 PNG,再把这个中间产物丢给 Unity 加载。很多团队的第一反应是去找 unity 扩展,但文档渲染最终不在 Unity 的图形管线上,扩展只是壳,底层绕不开“谁来做解析”这个问题。优点非常明显:所见即所得,导出结果就是 Office 排版引擎的真实输出,字体、分页、表格都不需要你重新实现;缺点是强依赖本机 Office,且 COM 进程管理很烦,这点我会在第 5 章展开。它适合什么样的项目?单机版工具、展厅大屏、文档总数在几百份以内、没有频繁更新需求的场景。如果文档源文件随时在变,每次更新都要重新跑一遍转换,维护成本会很快超过收益。

这个方案还有一个变体,是“只转第一页”或“只转指定页”,常用于做列表缩略图。比如在一体机左侧放一个文档列表,每项需要一张缩略图预览,那就没必要把整份 200 页的 PPT 全转掉,只转封面页就够。用 COM 的 ExportAsFixedFormat 转 PDF 后,再调 PDF 渲染接口取第一页图片。列表和详情的预处理链路可以分开:列表用低分辨率缩略图,详情用完整 PDF。

2.2 统一转 PDF 再渲染:借 PDFium/MuPDF 把 PDF 页变成 Texture2D

转图片方案有个软肋:如果一份 Excel 有几十个工作表,或者一份 Word 有 100 页,全转成图片序列,目录、内存和加载速度都很难接受。更常见的选择是“先转 PDF,进 Unity 后再动态渲染 PDF 页”。PDF 在这里不是展示格式,而是中间交换格式。原因有三:第一,PDF 是固定版式,无论在哪台机器上渲染,页面拆解和坐标计算都比直接解析 docx 可靠;第二,PDFium、MuPDF 都有独立的 native 渲染库,Unity 通过 DllImport/Plugins 调用,不需要 Office 常驻;第三,后端转换工具可选的很多,Windows 上可以继续用 Office COM,Linux 上可以用 LibreOffice headless,反正最终产物都是 PDF,客户端不需要关心来源。这条路线适合中到大批量文档、交互要求高的项目,也适合想做 Android 端的团队——移动端没有 Office COM,但 PDF 渲染库可以编出移动端版本。

PDFium 是 Chromium 项目里的 PDF 渲染引擎,只负责“把 PDF 画到一块内存 bitmap”,不负责 Word/Excel/PPT 的解析,所以必须配合前一步的“转 PDF”。MuPDF 也是同级别选择,它额外带 xps/epub 支持,但对 Unity 的接入方式没有本质区别。我不会在“选 PDFium 还是 MuPDF”上花太多时间,因为项目里多半是哪个能编出稳定 DLL 用哪个;如果只做 Windows,PDFium 的预编译包更好拿。渲染性能上,单页 A4 在 2 倍缩放(约 1400x2000)下,PDFium 的耗时通常在 30ms-100ms 之间,完全够翻页交互。这里唯一要提醒的是,PDFium 本身不做“pdf 解析”之外的文档结构理解,你要想从 PDF 里提取正文做全文搜索,那是另一套 API,预览阶段别碰,留到第 6 章再说。

2.3 内嵌 WebView 方案:WebView2 在 Windows 平台直接打开 Office 与 PDF

第三种思路是“不做渲染,做浏览器宿主”。Windows 上装 WebView2 Runtime 后,Unity 可以通过官方插件把一块浏览器窗口嵌进 UI 层级。浏览器内核本身就能显示 PDF,以及部分 Office 文件(取决于本机是否有 Office 关联或是否走 Office Online 预览)。这个方案的直接价值是:不需要维护 COM 转换链路,不需要引入 PDFium,代码量最小;只要把文件路径或网络 URL 丢给 WebView2 的 Navigate 方法,它自己搞定缩放、翻页、打印,web 页面本来就为打印做了全套准备。代价也很明显:渲染结果是一块独立于 Unity 渲染管线的原生窗口,很多团队遇到的“黑屏、被遮挡、无法叠加 UI”都在这一层;此外,Canvas 上的 Transform/遮罩对系统窗口不一定生效,你想在文档上画一个半透明遮罩做“强制阅读模式”,用 WebView2 会很别扭。

所以 WebView2 适配的场景是:项目本身就对 UI 层级要求不苛刻,例如内部工具、后台管理页面;或者文档预览里有大量网页交互需求,比如 PDF 内嵌了超链接、动态表单,那 WebView2 是天然合适的。还要注意运行时依赖:目标机器必须装了 WebView2 Runtime,Unity 包里要带 Loader DLL,具体坑在第 5 章写。如果是内网部署且不允许外发文件,Office Online 预览那条线要谨慎用——它会把文档传到在线服务去渲染,这个隐私决策要在项目启动前跟客户确认,而不是等上线前再解释。

2.4 服务器/局域网共享预览:把解析压力挪到后端,Unity 只当显示器

最后一类思路适合“多台一体机共用一个文档库”的架构。前端 Unity 只负责请求和展示,后端服务器负责把 Word/Excel/PPT 转成 PDF 或图片。这样做的好处是文档更新不用每台机器挨个跑转换,后端升级转换引擎也不影响客户端;坏处是需要额外维护一台带转换服务的机器。后端技术上,Windows 服务器可以装 Office 用 COM(就是第 3 章那套逻辑搬到服务端),Linux 服务器则用 LibreOffice headless,一条命令就能完成转换:

soffice --headless --convert-to pdf --outdir /data/preview /data/uploads/guide.docx

LibreOffice 的转换质量与 Office COM 相比,在复杂排版上略有差距,比如某些字体回退、Word 文本框浮动位置,但对付“预览”这个场景是足够的。如果你已经有内网文件服务,这个方案的增量成本其实不大。它和“转图片方案”的本质区别是:转换动作从客户端前置到了服务端,客户端的资源占用趋近于零。适合展厅里同时跑 Unity 展示和其他程序的机器,也适合后端已经有文档管理系统的企业项目。

四条路线对比,直接用一个表收束:

路线是否依赖 Office客户端资源占用交互能力部署成本推荐场景
转图片是(转换机)低弱(翻页要另做)低单机、文档数量少
转 PDF + PDFium是(转换机,可切换)中强(翻页缩放标注)中中大批量、跨平台
WebView2否(依赖 Runtime)中高强(浏览器内核)低内部工具、网页交互多
后端转换否(客户端)极低取决于客户端高多终端共享文档库

选型标准我一般这样定:先问“要不要编辑”。要编辑,就别在 Unity 里硬做,直接用 Application.OpenURL 唤起外部程序更省事;不要编辑,再按“离线/在线”和“单机/多机”往下二分。离线单机选转图片或 PDFium;在线多机选后端转换;如果文档里全是 PDF 且无 Office 文件,直接走 PDFium,省掉 Office COM 这一层。

3. 用 Office COM 把 Word/Excel/PPT 批量转 PDF:脚本与参数说明

很多 Unity 开发者看到“Office COM”第一反应是装 Microsoft.Office.Interop 的 NuGet 包,然后在 Unity 工程里直接引用。这能跑,但会让 Unity 工程的编译链路变重,而且 Unity 的编辑器编译和运行时工程是两个环境,程序集版本不一致会在打包时突然报错。我更常用的做法是:单独建一个 .NET Framework 的控制台或类库项目,专门做“文档转换”,Unity 通过子进程调用它。这样 Unity 工程里不需要引用任何 Office 相关程序集,把 COM 造成的编译期污染隔离在 Unity 之外。对应到第 2.4 节说的“后端转换”也同理——那个控制台程序可以直接部署到服务器。

3.1 先做转换工具:Word/Excel/PPT 分别怎么调 COM 接口

先用 Word 开刀。下面的代码是控制台程序里最核心的函数,用反射调 COM,不依赖 Interop 程序集。这里用反射的意图是“少装一个包、少一类版本冲突”,代价是代码难看一点,但这类工具写好一次就长期用,值得:

// WordToPdf.cs —— .NET Framework 4.7.2 控制台程序 using System; using System.IO; using System.Reflection; using System.Runtime.InteropServices; class WordConverter { public static void Convert(string src, string pdf, int optimize = 1) { object word = null, docs = null, doc = null; try { Type type = Type.GetTypeFromProgID("Word.Application"); if (type == null) throw new Exception("未安装 Microsoft Word,或 ProgID 注册表损坏"); word = Activator.CreateInstance(type); // 隐藏窗口、关闭弹窗和宏警告,是转换不卡死的前提 word.GetType().InvokeMember("Visible", BindingFlags.SetProperty, null, word, new object[] { false }); word.GetType().InvokeMember("DisplayAlerts", BindingFlags.SetProperty, null, word, new object[] { 0 }); docs = word.GetType().InvokeMember("Documents", BindingFlags.GetProperty, null, word, null); doc = docs.GetType().InvokeMember("Open", BindingFlags.InvokeMethod, null, docs, new object[] { src, true }); // 第二个参数 true = ReadOnly // ExportAsFixedFormat 参数1: 输出路径; 参数2: 17 = wdExportFormatPDF; // 参数4: 0 按打印优化, 1 按屏幕优化(字体嵌得更全,屏幕预览更清晰) doc.GetType().InvokeMember("ExportAsFixedFormat", BindingFlags.InvokeMethod, null, doc, new object[] { pdf, 17, false, optimize }); Console.WriteLine("OK: " + pdf); } finally { if (doc != null) { doc.GetType().InvokeMember("Close", BindingFlags.InvokeMethod, null, doc, new object[] { false }); Marshal.ReleaseComObject(doc); } if (word != null) { word.GetType().InvokeMember("Quit", BindingFlags.InvokeMethod, null, word, null); Marshal.ReleaseComObject(word); } } } }

逻辑说明:这段代码的关键有 4 处。第一,Type.GetTypeFromProgID 是拿注册表里的 COM 标识,需要机器真实装了 Word,WPS 如果兼容注册了 ProgID 也能用,但验证程度不如 Word。第二,Visible=false 和 DisplayAlerts=0 两个设置缺一不可,否则 Word 可能在转换中弹出“是否保留格式”之类的模态对话框,子进程直接挂在那里等一个永远不来的点击。第三,Open 只传路径和 ReadOnly 两个参数,后面的可选参数不传,用的是 Word 默认值,具体业务文档如果有密码,需要在第 4 个参数传密码字符串。第四,finally 里 Close 和 Quit 是必须的,如果函数中间抛异常,word 对象没 Quit,就会在任务管理器里留下一个 WINWORD.EXE 进程,下次再调转换就会撞上“文件被占用”,这就是最常见的进程残留坑。

接着是 Excel。Excel 的坑跟 Word 不太一样,最大的区别是“导出作用域挂在 Workbook 而不是 Sheet 上”:

// ExcelToPdf.cs 核心片段 static void ConvertExcel(string src, string pdf) { object excel = null, books = null, book = null; try { Type type = Type.GetTypeFromProgID("Excel.Application"); excel = Activator.CreateInstance(type); excel.GetType().InvokeMember("Visible", BindingFlags.SetProperty, null, excel, new object[] { false }); excel.GetType().InvokeMember("DisplayAlerts", BindingFlags.SetProperty, null, excel, new object[] { false }); books = excel.GetType().InvokeMember("Workbooks", BindingFlags.GetProperty, null, excel, null); // Open 参数: FileName, UpdateLinks=false, ReadOnly=true book = books.GetType().InvokeMember("Open", BindingFlags.InvokeMethod, null, books, new object[] { src, false, true }); // 关键:导出范围选择整个工作簿,而不是默认的第一个工作表 object sheets = book.GetType().InvokeMember("Worksheets", BindingFlags.GetProperty, null, book, null); sheets.GetType().InvokeMember("Select", BindingFlags.InvokeMethod, null, sheets, null); // Workbook.ExportAsFixedFormat 参数1:0=PDF, 参数2: 输出路径, 参数3: Quality(0=标准,1=最小) book.GetType().InvokeMember("ExportAsFixedFormat", BindingFlags.InvokeMethod, null, book, new object[] { 0, pdf, 0 }); Console.WriteLine("OK: " + pdf); } finally { if (book != null) { book.GetType().InvokeMember("Close", BindingFlags.InvokeMethod, null, book, new object[] { false }); Marshal.ReleaseComObject(book); } if (excel != null) { excel.GetType().InvokeMember("Quit", BindingFlags.InvokeMethod, null, excel, null); Marshal.ReleaseComObject(excel); } } }

逻辑说明:这段代码里最容易漏的是 Worksheets.Select。如果不 Select,Excel 的 ExportAsFixedFormat 默认导出的是“当前活动工作表”,一个包含 12 个月报表的工作簿最后只导出 1 页,这是最常见的 Excel 转 PDF 翻车,第 5 章会再提。参数方面,ExportAsFixedFormat 的第一个参数 0 是 xlTypePDF,如果传 1 就是导出成 XPS 文件;第三个参数品质,默认 0 标准,导出来用于屏幕预览已经足够。如果 Excel 文件里有“打印区域”设置,导出时也会遵守它,这会导致某些内容被裁剪,解决办法是在导出前把每个工作表的 PageSetup 重置一遍,后面会讲。

然后是 PowerPoint。PPT 的 COM 有个脾气:它不允许完全隐藏窗口,Open 时 WithWindow 参数必须为 true,否则直接抛异常:

// PptToPdf.cs 核心片段 static void ConvertPpt(string src, string pdf) { object ppt = null, pres = null, presentation = null; try { Type type = Type.GetTypeFromProgID("PowerPoint.Application"); ppt = Activator.CreateInstance(type); // PPT 不允许无窗口打开,只能把窗口移到屏幕外或最小化 ppt.GetType().InvokeMember("Visible", BindingFlags.SetProperty, null, ppt, new object[] { 1 }); // 1 = msoTrue pres = ppt.GetType().InvokeMember("Presentations", BindingFlags.GetProperty, null, ppt, null); // 参数含义依次是: FileName, ReadOnly, Untitled, WithWindow presentation = pres.GetType().InvokeMember("Open", BindingFlags.InvokeMethod, null, pres, new object[] { src, true, false, false }); if (presentation == null) throw new Exception("PPT 打开失败,可能是文件格式兼容问题"); // PowerPoint 的 ExportAsFixedFormat: 参数2: 2 = ppFixedFormatTypePDF presentation.GetType().InvokeMember("ExportAsFixedFormat", BindingFlags.InvokeMethod, null, presentation, new object[] { pdf, 2 }); Console.WriteLine("OK: " + pdf); } finally { if (presentation != null) { presentation.GetType().InvokeMember("Close", BindingFlags.InvokeMethod, null, presentation, null); Marshal.ReleaseComObject(presentation); } if (ppt != null) { ppt.GetType().InvokeMember("Quit", BindingFlags.InvokeMethod, null, ppt, null); Marshal.ReleaseComObject(ppt); } } }

逻辑说明:PPT 的 Open 参数比 Word 多两个,第 4 个 WithWindow 传 false 看起来很合理(不想弹窗),但 PowerPoint 会直接报“远程过程调用失败”。这是因为 PPT 的 COM 对象模型强制要求窗口存在,即使你 Visible 设成 1,它也会真开一个窗口;所以转换服务器上最好有桌面会话,Windows 服务模式下跑 PPT 转换会失败,这是一个环境限制。如果项目被迫在 Windows Server 上跑,可以考虑把转换做成计划任务而不是服务。另外,PPT 转 PDF 默认不会带演讲者备注,这符合预览需求;但如果你要导“备注版讲义”,得通过 PrintOptions 设置,不走 ExportAsFixedFormat,那个我建议另开工具脚本,不要跟预览链路混在一起。

3.2 Unity 侧改造成异步加载:协程与子线程怎么配合

转换工具是独立进程,Unity 这边要做的事就简单了:用 Process.Start 启动它,等退出码,然后加载产物。这里有个原则:不要在 Unity 主线程同步等待,否则文档一多界面就冻住。常见做法是协程 + Task 组合:

// DocPreviewManager.cs —— Unity 场景里的预览调度器 using System; using System.Collections; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; public class DocPreviewManager : MonoBehaviour { public RawImage previewTarget; public Text statusText; private Queue<string> _pending = new Queue<string>(); public void RequestPreview(string filePath) { if (!File.Exists(filePath)) { statusText.text = "文件不存在"; return; } _pending.Enqueue(filePath); if (_pending.Count == 1) StartCoroutine(PumpQueue()); } private IEnumerator PumpQueue() { while (_pending.Count > 0) { string src = _pending.Dequeue(); string ext = Path.GetExtension(src).ToLower(); string pdfPath = Path.Combine(Application.temporaryCachePath, Path.GetFileNameWithoutExtension(src) + ".pdf"); // 转换过程放到 Task 里,不阻塞主线程;WaitUntil 每帧检查完成 Task<bool> task = Task.Run(() => { if (ext == ".pdf") { File.Copy(src, pdfPath, true); return true; } return RunConverter(src, pdfPath, ext); }); yield return new WaitUntil(() => task.IsCompleted); if (task.Result && File.Exists(pdfPath)) { statusText.text = "渲染中..."; yield return StartCoroutine(RenderPdfToUi(pdfPath, previewTarget)); } else { statusText.text = "转换失败,请检查源文件"; } } } private bool RunConverter(string src, string pdf, string ext) { string exePath = Path.Combine(Application.streamingAssetsPath, "Tools", "OfficeToPdf.exe"); ProcessStartInfo psi = new ProcessStartInfo(exePath, $"\"{src}\" \"{pdf}\" \"{ext}\"") { CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; using (Process proc = Process.Start(psi)) { proc.WaitForExit(120000); // 120 秒超时,防止 Office 弹出模态框卡死进程 return proc.ExitCode == 0; } } }

逻辑说明:这里把“转换”从 Unity 进程里彻底摘了出去,好处是就算 Office COM 崩了,挂掉的只是子进程,Unity 不会跟着崩溃。ProcessStartInfo 的 UseShellExecute=false 是必须的,否则没法重定向输出,也无法拿到退出码。超时时间建议按文档类型区分:Word 一般 30 秒够,PPT 带复杂动画的可以放大到 120 秒;如果机器配置低,把 WaitForExit 的超时继续调大,改成 300 秒也正常。缓存目录用 Application.temporaryCachePath,系统会自动清理,别往 StreamingAssets 写,那在打包后是只读的。RenderPdfToUi 是第 4 章的内容,这里先留一个协程接口,到渲染章节再接上。

3.3 批量场景的参数预设:清晰度、页边距、隐私设置

转换工具里最容易被忽视的是参数预设。很多人第一次跑通后就不再管参数,结果换一批真实文档就出问题。我把常用参数整理成一张表,照着初始化就不会差:

参数WordExcelPPT
导出类型枚举17(PDF)0(PDF)2(PDF)
屏幕优化optimize=1Quality=0默认
是否嵌入字体导出设置中“嵌入所有字体”无默认嵌入
页面方向按文档设置统一设为横向再导出按幻灯片大小
可见窗口falsefalse必须 true,另开窗口
弹窗抑制DisplayAlerts=0DisplayAlerts=false无专门开关
超时参考30s60s120s

一个值得说清楚的参数是 Word 导出时的 optimize 值。ExportAsFixedFormat 的第 4 个参数其实是个大枚举,它同时决定“按打印优化”还是“按屏幕优化”。按打印优化时,PDF 里嵌入的字体子集会按打印机驱动的要求裁剪,屏幕上看可能发虚;按屏幕优化时,字体会更完整地嵌入,但文件体积略大。预览类项目我一般选 1(屏幕优化),打印类项目选 0。如果你发现 PDF 在 Unity 里渲染出来字迹边缘发虚,第一件事不是调 PDFium 的缩放,而是回头把转换工具的 optimize 改成 1 再导一次。

Excel 页面设置还有一个坑:很多业务表并不是按 A4 排版的,默认导出会按“打印区域”切页,列被拆到两三页里。统一做法是在导出前遍历所有 Sheet,把 PageSetup.Orientation 设为 2(横向),PageSetup.Zoom 设为 false,PageSetup.FitToPagesWide 设为 1,FitToPagesTall 设为 false,这样一张超宽的表会被缩放到单页宽,预览体验最好。PPT 则没有这个问题,它按幻灯片尺寸导,宽高比跟你做的幻灯片一致,唯一要注意的是嵌入的音频视频不会进 PDF,预览无碍。

隐私设置提醒一句:如果文档带修订痕迹、隐藏行、批注,默认导出可能把“批注”带进 PDF。在 Word 导出前,推荐把 doc.Revisions 的 AcceptAll 或者把“显示标记”设置为 false,具体看业务要求。Excel 的隐藏工作表默认不导出,这可能反而是你要的行为;如果要把隐藏页也导出,就得先遍历 Worksheets 把 Visible 改为 true,这个需求不常见,但遇到过“审计要求所有记录在 PDF 里可见”的项目,提前知道总比现场改好。

4. 在 Unity 里渲染 PDF:用 PDFium 把页面变成 Texture2D 的最小实现

PDF 显示环节是整个方案里最贴近 Unity 的一步,也是“为什么不能用自己写解析器”的答案所在。PDF 的解析复杂度在于它要还原出矢量图形、字体轮廓、图层、色彩空间,Unity 原生没有这套状态机。直接用 PDFium 渲染 bitmap,是工作量最小的路径。第 2 章选型里我提到了 PDFium/MuPDF,下面按 PDFium 讲,因为它在 Windows 下最容易拿到预编译 DLL,且 API 稳定;你如果换成 MuPDF,只需要替换 DllImport 的函数映射,架构不用变。

4.1 引入 PDFium:Windows 下加载 native DLL 的步骤

第一步是把 native 库放进 Unity 工程。以 Windows x64 为例,在 Assets 下建 Plugins/x86_64 目录,把 pdfium.dll 放进去,Unity 打包时会自动把它放进最终包。这里有两个基本注意点:一是 DLL 的位数必须和 Unity 目标平台一致,Editor 是 64 位就用 x86_64,如果还要出 32 位版,单独放一份 x86;二是 pdfium.dll 内部依赖 zlib、libjpeg 这些静态链入的版本,不要自己从系统目录拷贝,DLL 文件应该完整来自官方打包产物,否则启动时会报找不到入口点。这步看起来像是普通的 unity 插件安装流程,但 DLL 的位数和依赖检查比普通 C# 插件严格得多,别等打包到客户机器上再验证。

然后是 C# 侧的初始化与清理。PDFium 要求进程开一个全局库实例,Unity 的播放模式每进一次场景都会重新加载程序集,所以初始化要跟场景生命周期对好:

// PdfiumInitializer.cs using System; using System.Runtime.InteropServices; using UnityEngine; public sealed class PdfiumInitializer : MonoBehaviour { [DllImport("pdfium")] private static extern void FPDF_InitLibrary(); [DllImport("pdfium")] private static extern void FPDF_DestroyLibrary(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Init() { FPDF_InitLibrary(); Application.quitting += () => FPDF_DestroyLibrary(); } }

逻辑说明:RuntimeInitializeOnLoadMethod 保证 Unity 场景加载前就先调用 FPDF_InitLibrary,避免第一次渲染时才初始化引发卡顿。Application.quitting 注册释放函数,是为了在编辑器里反复进入播放模式时,不把 native 资源泄漏给下一次播放。如果项目里有场景热重载(Domain Reload),建议在 OnDisable 里也做一次保护性判断;不过最省心的方式还是把这个初始化脚本放在一个不销毁的场景里,配合 DontDestroyOnLoad 使用。

参数说明:DllImport 的第一参数“pdfium”是 DLL 名,Unity 在 Windows 下会自动去 Plugins/x86_64 找同名文件;如果 DLL 版本名字带后缀,比如 pdfium_x64.dll,那第一参数也要改成一致的名字。

4.2 渲染单页到 Texture2D:关键 API 与内存释放

渲染一页 PDF 到 Unity 的 Texture2D,核心调用链是:LoadDocument -> LoadPage -> 创建 bitmap -> RenderPageBitmap -> 拷贝像素 -> 释放句柄。下面是我在项目里抽出来的最小实现,包含所有必须的释放步骤:

// PdfPageRenderer.cs using System; using System.Runtime.InteropServices; using UnityEngine; public static class PdfPageRenderer { [DllImport("pdfium")] static extern IntPtr FPDF_LoadDocument(string path, string password); [DllImport("pdfium")] static extern void FPDF_CloseDocument(IntPtr doc); [DllImport("pdfium")] static extern int FPDF_GetPageCount(IntPtr doc); [DllImport("pdfium")] static extern IntPtr FPDF_LoadPage(IntPtr doc, int index); [DllImport("pdfium")] static extern void FPDF_ClosePage(IntPtr page); [DllImport("pdfium")] static extern double FPDF_GetPageWidth(IntPtr page); [DllImport("pdfium")] static extern double FPDF_GetPageHeight(IntPtr page); [DllImport("pdfium")] static extern IntPtr FPDFBitmap_CreateEx(int width, int height, int format, IntPtr buffer, int stride); [DllImport("pdfium")] static extern void FPDFBitmap_FillRect(IntPtr bitmap, int left, int top, int width, int height, uint color); [DllImport("pdfium")] static extern void FPDFBitmap_Destroy(IntPtr bitmap); [DllImport("pdfium")] static extern IntPtr FPDFBitmap_GetBuffer(IntPtr bitmap); [DllImport("pdfium")] static extern void FPDF_RenderPageBitmap(IntPtr bitmap, IntPtr page, int startX, int startY, int width, int height, int rotate, int flags); public static Texture2D RenderPage(string pdfPath, int pageIndex, float scale = 2f) { IntPtr doc = FPDF_LoadDocument(pdfPath, null); if (doc == IntPtr.Zero) return null; try { IntPtr page = FPDF_LoadPage(doc, pageIndex); if (page == IntPtr.Zero) return null; int w = (int)(FPDF_GetPageWidth(page) * scale); int h = (int)(FPDF_GetPageHeight(page) * scale); // format=4 对应 FPDFBitmap_BGRA,32位,与 Unity 的 BGRA32 纹理直接对应 IntPtr bitmap = FPDFBitmap_CreateEx(w, h, 4, IntPtr.Zero, w * 4); FPDFBitmap_FillRect(bitmap, 0, 0, w, h, 0xFFFFFFFF); // flags=0 表示正常渲染,不带文字抗锯齿的额外选项,兼容性最好 FPDF_RenderPageBitmap(bitmap, page, 0, 0, w, h, 0, 0); byte[] buffer = new byte[w * h * 4]; Marshal.Copy(FPDFBitmap_GetBuffer(bitmap), buffer, 0, buffer.Length); Texture2D tex = new Texture2D(w, h, TextureFormat.BGRA32, false); tex.LoadRawTextureData(buffer); tex.Apply(); FPDFBitmap_Destroy(bitmap); FPDF_ClosePage(page); return tex; } finally { FPDF_CloseDocument(doc); } } }

逻辑说明:这段代码的释放顺序是刻意安排的:先释放 bitmap,再关 page,最后关 doc。如果把 FPDF_CloseDocument 放在 page 还活着的时候调用,轻则渲染结果丢失,重则 native 层直接崩溃。buffer 的拷贝用 Marshal.Copy,是因为 FPDFBitmap_GetBuffer 返回的是非托管内存指针,Unity 的 Texture2D.LoadRawTextureData 要求 byte[] 托管数组,中间必须多一次拷贝;对单页 A4 来说,2 倍缩放下 buffer 是 8MB 左右,这个拷贝耗时在几毫秒,能接受。

参数说明:scale 是渲染缩放比。PDF 页面的宽度单位是“点”(Point),1 点约等于 1/72 英寸,在屏幕上通常乘以 96/72 得到物理像素。如果直接把 scale 固定为 1,A4 宽度只有约 595 像素,在 1080p 屏幕上会明显模糊;我一般设 1.5-2.0 让文字边缘干净,但要注意 scale 越大,纹理内存按平方增长,A4 在 scale=3 时约 2550 x 3300 像素,单张纹理就是 30MB+,这是 unity 游戏优化里常被忽略的一块,列表缩放图和详情页缩放一定要分开。flags=0 是常规渲染;如果要显示 PDF 注解/批注,需要把 FPDF_ANNOT 常量加进去,但这里做最小实现先不加。

4.3 翻页、缩放、旋转:交互参数的三个常调点

渲染单页的代码跑通后,交互层还有三个高频调整点:翻页缓存、自适应缩放、旋转参数。

翻页缓存:不要每次翻页都重新 LoadDocument,而应该把 doc 句柄保存在预览会话里,翻页时只关上一页、再 Load 下一页;可以预取前一页和下一页,让翻页接近零等待。如果文档很大,可以只缓存当前页前后各一页,再加一个 LRU 队列把超过 5 页的旧页纹理释放掉。

自适应缩放:推荐按显示区域宽度反推 scale,而不是用固定值。以 RawImage 的 rectTransform 为准,先算出显示区域里可容纳的宽度像素,再除以 FPDF_GetPageWidth,得到 scale。这样同一套代码接不同的 UI 布局都不会糊。如果要同时支持超宽表格类 PDF,记得保留“按宽度适配”和“显示整页”两个模式,让用户手动切换。

旋转参数:FPDF_RenderPageBitmap 的 rotate 参数取值 0-3,对应 0 度、90 度、180 度、270 度。竖向 PDF 在横屏一体机上展示时,旋转参数设置为 1 或 3,同时要交换宽高值,否则画面会被拉伸。这里的常见错误是只转图片不换宽高,导致显示比例不对,代码里应在 rotate>=1 时把 w/h 对调后再创建 bitmap。

最后补一句关于“pdf 解析”的边界:预览阶段只需要渲染,不需要提取文本。PDFium 虽然提供 FPDFText_LoadPage 系列接口做文本解析,但坐标、段落、分页的映射关系比渲染复杂得多,做全文搜索或复制粘贴是独立功能,建议放到后端或专门工具里,Unity 只消费结果,别把预览代码和解析代码混在一个类里,否则后添加的解析逻辑很容易把渲染性能拖垮。

5. 避坑指南:文档预览在 Unity 里的 5 个典型翻车现场

文档预览这功能,大部分时间不是写不出来,而是死在边界情况上。下面 5 个坑是我实际跟项目时频繁遇到的,每一条都按“现象 → 原因 → 解决”写,你对照自己的现象找解决路径,比逐个读报错更快。

5.1 现象:COM 转换偶尔报“拒绝访问”,Office 弹窗卡死转换

背景是转换服务跑在 Windows 服务器或一体机上,连续转换十几份后,突然某次调用报“拒绝访问”或“RPC 服务器不可用”,任务管理器里有一堆 WINWORD.EXE / EXCEL.EXE 残留。

原因:上一轮转换异常退出,COM 对象没有正确 Quit;或者 Word 在首次启动时弹出了“隐私设置”“登录 Office”之类的向导弹窗,Visible=false 挡不住这种模态窗,子进程被挂起。

解决:转换程序在启动时先做一次实例清理,用 taskkill 清掉同名进程,前提是确认这台机器没有别人正开着 Office;再把 Office 组件的“自动化安全”注册表项设好,允许脚本访问。更现实的宽松做法是:给每个子进程加超时,超时直接 Kill,不等待。转换程序本身用独立进程跑的好处就在这里,杀错也不影响 Unity 主程序。进程名清理可以用一句命令:taskkill /F /IM WINWORD.EXE,但注意别在用户自己的开发机上跑这个,会把正在编辑的文档一起关掉,这条只推荐在专用转换机/服务器上启用。

5.2 现象:转出来的 PDF 白边大、字发虚

背景:Word/PPT 用 COM 转 PDF 后在 PDFium 里渲染,发现左右两圈大黑边,文字边缘发毛,尤其 100% 缩放时明显。

原因:白边大的原因是文档本身的纸张方向和内容方向不一致,比如页面是 A4 竖版,但内容其实很窄,导出时默认带上了完整的纸张边距;字发虚的原因多半是 optimize 参数用了按打印优化的 0,打印机驱动按 300 DPI 去制作字型,而 PDFium 渲染时按 96 DPI 缩放,字形边缘被重采样。

解决:转换时把 optimize 参数切到按屏幕优化(1),并且统一把目标页面纸张设为 A4 或与内容一致的宽高。Excel 还要注意把 PageSetup.FitToPagesWide=1,避免被拆页。改完参数重新转换,白边和发虚会同时改善。如果还有边距,可以在 Unity 侧做“白边裁剪”,把纹理边缘的纯白像素列裁掉再显示,但这是治标,能留到最后用。

5.3 现象:PDFium 渲染中文/日文缺字或乱码

背景:转换出的 PDF 在 Windows 的 Edge 里打开正常,但在 Unity 的 PDFium 渲染里,中文变成方块,或日文假名消失。

原因:PDF 没有把字体完整嵌入,只嵌了子集,而 PDFium 渲染时找不到对应的字体映射;或者转换机器上的字体和运行机器上的字体不一致,PDF 里虽然嵌了字体名,但字形数据缺失。

解决:在转换端把“嵌入所有字体”打开(Word 导出设置里勾选,或者用 COM 设置 EmbedTrueTypeFonts=true),让 PDF 自包含字形。如果文档已经生成,没法重转,可以在渲染端给 PDFium 设置系统字体目录,让它去匹配中文字体。但最稳的还是源头嵌入,这条建议写进转换工具的默认配置。另一个附带问题:扫描版 PDF 本来就是图片,PDFium 渲染没问题,但如果要做文本搜索或复制,会发现提取出来是乱码,因为那是 OCR 前的纯图片,这个属于 pdf 解析范畴,和渲染缺字是两码事,别混在一起排查。

5.4 现象:WebView2 在 Unity 里黑屏/白屏,或打包后功能消失

背景:选型时走了 WebView2 路线,在编辑器里预览正常,打包到客户机器上,导航栏按钮有了,窗口区域却一直白屏;或者加载后黑屏,什么内容都不显示。

原因:三选一。第一,目标机器没装 WebView2 Runtime;第二,webview2loader.dll 被 Unity 打包时漏掉了;第三,Unity 的线程模型和 WebView2 的 UI 线程要求冲突,初始化时机不对,最常见是把 CoreWebView2Environment 创建放在了非主线程。

解决:把 webview2loader.dll 放到 Assets/Plugins/ 目录,让 Unity 按插件处理;在运行时检查 Runtime 是否安装,可以调用 CoreWebView2Environment.GetAvailableBrowserVersionString(),抛异常就往浏览器下载页引导;初始化 Environment 必须在主线程等 UI 事件,Unity 的协程里用“started + completed”两个标记来做,不要直接同步等待。

5.5 现象:打包到 Android/iOS 后用不了,“功能隐形消失”

背景:开发期一直用 Windows Editor 跑,交付前打 Android 包才发现 Word/Excel/PPT 全部打不开,代码里没有报错,但界面没有内容。

原因:COM 这条路是 Windows 专属,Android/iOS 不存在 ProgID;WebView2 也没有移动版。如果代码里没有按平台条件分支,调用方拿到 null,UI 上就表现为空白。

解决:移动端不要走 COM 转换和 WebView2,而是把文档在服务端转成 PDF,客户端只做 PDFium 渲染;如果不想上服务器,Android 用系统 Intent 调起 WPS/Office 打开原文件,iOS 用 Quick Look 预览框架,让系统去处理格式,Unity 退到后台。最省心的移动端方案其实是“后端转换 + PDFium”——第 4 章的实现稍加改动就能编到 Android,PDFium 有对应的 native 库,不需要改渲染逻辑。

6. 进阶技巧:PDF 覆层标注、远程转换接口与“系统打开”保险丝

如果只是“能看”,上面 4 章已经够用。下面三个技巧是把这套预览能力做成产品的最后一层,适用于“还要在文档上标记位置、圈出重点”的协同类需求。

6.1 在 PDF 页上叠 UI:把屏幕坐标映射回 PDF 坐标

批注的本质是一组“矩形 + 文本 + 页码”的数据。在 Unity 里做标注,不要把批注直接画进 PDF,而是维护一个与页面同尺寸的 Canvas 层,把 PDF 纹理作为背景,批注用一个 RectTransform 精确定位。坐标换算只有一个关键点:UGUI 的 RectTransform 原点是中心,Y 轴向上,而 PDF 渲染的 bitmap 原点是左上角,Y 轴向下,所以显示和取点需要各翻一次:

// 屏幕上的点转 PDF 页内坐标 Vector2 ScreenToPdf(Vector2 localPos, RectTransform viewRect, Texture2D pdfTexture) { float u = (localPos.x + viewRect.rect.width * 0.5f) / viewRect.rect.width; float v = (localPos.y + viewRect.rect.height * 0.5f) / viewRect.rect.height; return new Vector2(u * pdfTexture.width, (1f - v) * pdfTexture.height); }

逻辑说明:先把 UGUI 的中心原点转成左下角原点的比例,再通过 1-v 把左下角原点翻成左上角原点,最后乘纹理像素尺寸得到 PDF 坐标。存储时把页码+坐标+矩形宽高+颜色+文本序列化成 JSON,下次预览按页码加载对应批注,就成了一个轻量标注层。这个方案比直接在 PDFium 里画 annotation 简单,且不会污染源 PDF 文件。

6.2 远程转换服务的接口约定

如果走后端转换路线,接口不要设计成“上传后等同步结果”,转换耗时会拖垮请求。建议拆成两个动作:提交转换任务、轮询结果。

动作接口参数返回
上传并提交转换POST /api/convertmultipart/form-data: file, target=pdf任务 ID
查询转换结果GET /api/convert/{id}无pending / success / failed,success 附带下载路径
下载产物GET /api/files/{name}无PDF 文件流

服务端用 LibreOffice headless 转 PDF 那套命令,配合一个简单工作队列,每份文档转换完成就写文件系统,Unity 端协程轮询 1 秒一次,拿到 URL 后用 UnityWebRequest 下载到缓存目录,再交给 PdfPageRenderer。这个交互模型比同步接口稳健,尤其当某份 PPT 转换超过 60 秒时,不会让客户端一直挂在下载上。

6.3 最后的保险丝:保留一个“用系统打开”的按钮

所有的预览方案都有概率碰上“格式兼容”问题,尤其是客户发来的加密文档、宏文档、老版本 .doc。我的习惯是在详情页右下角留一个低调的按钮——用系统关联程序打开原文件(Application.OpenURL("file://" + path))。这个动作看起来很原始,但它能在预览翻车时兜住场景;对内部员工工具来说,甚至可以直接当作高性能预览方式。

我做这套功能时养成习惯:先把“要不要编辑、能不能联网、跑在谁的机器上”三个答案写在需求单第一行,再决定走哪条路。COM 的进程残留、PDFium 的内存释放、WebView2 的 Runtime 缺失,这三个是最耗时间也最容易复发的坑,值得在项目第一天就做好预案。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询