ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#实现PDF差异对比:从文本提取到像素定位的完整方案

C#实现PDF差异对比:从文本提取到像素定位的完整方案 最近在给公司做合同管理工具时接到一个需求用C#自动对比两个PDF文档的差异把改动点找出来。我心想这事儿简单无非是抽文本、做字符串diff结果一上手才意识到PDF这个格式天生就不是给人做对比用的。它不像Word有段落树也不像HTML有DOM结构PDF的本质是一套“版面描述语言”每页存的是大量绘图指令告诉渲染器在哪个坐标画哪段路径、哪个字符。所以对比两个PDF的差异本质上是在对比两堆“绘图指令执行后的最终结果”。这篇文章把我在这个项目里踩过的坑、落地的方案、以及可以直接抄走的代码骨架完整分享出来希望能给同样接到这个需求的C#开发者省点时间。1. 为什么“对比两个PDF”比想象中麻烦PDF的内容模型与对比粒度1.1 PDF不是文档是版面坐标的集合你如果直接拿记事本打开一个PDF文件会看到一堆BT /F1 12 Tf 72 720 Td (Hello) Tj ET之类的指令这就是内容流。72 720 Td表示把文本基线移动到坐标 (72, 720)Tj表示绘制后面的字符串。每个字符在PDF里并不像Word那样属于一个段落对象它只是页面上的一个“涂鸦点”。这就带来了第一个麻烦文本的存储顺序不一定是阅读顺序。同一个PDF用Word导出、用LaTeX编译、用浏览器打印内容流的排列方式可能完全不同甚至一句话的字符会被拆成多个片段存储。直接按文件里的流顺序提取文本经常会出现“第一行文字跑到第二段上面”“同一句话被截成几节”这种乱象。1.2 三种对比粒度决定了技术路线的分叉我把做PDF差异对比的方案分成三个层级自己在实际项目里也是按这个思路往下拆的对比层级核心思路优点缺点适用场景文本层提取文字内容做逐行比对快能直接给出“哪些字变了”排版变化发现不了还容易遇到顺序错乱纯文本合同、报告、标书视觉层渲染成图片逐像素比对任何PDF都能比排版、字体、颜色变化都能看到慢且只能知道“哪里变了”需要辅助信息定位语义扫描件、设计稿、排版校验语义层解析段落、表格、标题树理解内容逻辑结果最精准能区分“格式变化”和“内容变化”实现成本极高PDF结构解析容易出错暂无特殊需求通常不选我最终的方案是“文本层为主视觉层兜底”。先用文本层筛掉完全相同的页面再用视觉层精确找差异最后用坐标信息把两种结果合并成一份可读的差异报告。2. 库选型PdfPig、iText、PdfiumViewer开源优先还是商业授权2.1 .NET生态里能选的主流PDF库说实话C#的PDF解析库远没有Java生态那么丰富挑来挑去其实就那几款。我先把调研结果里真正进入候选名单的列出来库名称授权方式文本提取渲染能力坐标信息备注PdfPigApache 2.0优秀无每个字符都有坐标纯托管代码社区维护iText7AGPL/商业优秀无有坐标功能全面但AGPL传染性强商用需买授权PdfiumViewerApache 2.0一般有无封装Google PDFium适合渲染成图DocnetMIT一般有一般也是PDFium封装API较新Aspose.PDF商业收费优秀有有功能最全但只适合预算充足的项目Spire.PDF商业收费优秀有有免费版有水印限制商用注意这里的核心矛盾是别人帮你做完所有事的商业库很贵开源库很便宜但都要自己拼装。如果你公司对开源许可证比较敏感iText7的AGPL基本直接劝退因为AGPL要求你在网络上提供服务的场景下开放源码很多做企业内部系统、对业务代码保密要求极高的项目根本没法接受。Aspose和Spire功能确实强大一个方法就能对比PDF但那是用白花花的银子换来的而且深入定制差异标注时反而没开源库灵活。2.2 我最终选定的组合与理由最终落地方案选了PdfPig PdfiumViewer DiffLib的组合PdfPig负责提取文本和字符坐标Apache 2.0协议在文本提取质量上比PdfiumViewer那套顺手很多关键是能拿到每个字符的GlyphRectangle方便做坐标分组和排序。PdfiumViewer负责把页面渲染成Bitmap做视觉层对比。它本质是对Google PDFium原生库的C#封装性能稳定支持页面注释、表单元素渲染。DiffLibNuGet上的一个diff算法库用于文本层的行级差异比较实现的是Myers算法。如果你不想引入额外包自己写一个最长公共子序列LCS也不复杂后文会给代码。选择这个组合的核心原因很简单对比功能只是项目里的一块我不需要为它引入重量级商业依赖同时必须保证License上没有后患。2.3 环境搭建时最容易忽略的细节NuGet安装就三句话dotnet add package UglyToad.PdfPig dotnet add package PdfiumViewer dotnet add package DiffLib但有个坑必须说明PdfiumViewer依赖原生DLL不是纯托管代码。安装完NuGet包后输出目录里会出现x86和x64两个文件夹里面是pdfium.dll。如果你在IIS或者Windows服务里部署记得要允许这两个子目录随发布一起输出。我第一次部署时没注意本地调试好好的一上测试服务器就报“找不到PDFium native library”查了半天才想起来发布目录里少了原生DLL。PdfPig这边有个版本问题也要提一下早期版本的Letter.GlyphRectangle在较新版本里有过调整如果你用的是0.1.x之后的新版API名称可能变了。我的建议是写代码前先看一眼当前NuGet包版本的智能提示坐标结构体里一定包含X/Y属性只是命名可能略有不同逻辑完全不变。3. 第一道工序文本层对比的实现与那些“提取乱序”的坑3.1 用PdfPig提取“带坐标”的文本行文本层对比的第一步不是直接拿page.Text去比因为整页文本相当于把所有字符流按内部顺序拼成一个字符串顺序大概率不对。我的做法是手动按坐标分行分列。PDF的坐标系统中Y轴从页面底部往上增长而我们在屏幕上看到的阅读顺序是从上到下。所以分行时要按Y坐标降序分组组内再按X坐标升序排列using UglyToad.PdfPig; public class PdfLine { public int PageNumber { get; set; } public int LineIndex { get; set; } public double Y { get; set; } public string Text { get; set; } } public static ListPdfLine ExtractLines(string pdfPath) { var result new ListPdfLine(); using (var pdf PdfDocument.Open(pdfPath)) { foreach (var page in pdf.GetPages()) { // 按Y坐标分组Y相同视为同一行 var groups page.Letters .GroupBy(letter Math.Round(letter.GlyphRectangle.Bottom, 1)) .OrderByDescending(group group.Key); var lineIndex 0; foreach (var group in groups) { var text string.Concat(group .OrderBy(letter letter.GlyphRectangle.Left) .Select(letter letter.Value)); result.Add(new PdfLine { PageNumber page.Number, LineIndex lineIndex, Y group.Key, Text text }); } } } return result; }这里用Math.Round(..., 1)对Y坐标做了一定程度的模糊目的是把相邻一两像素的字符归到同一行。PDF里下行文字和上行文字的Y坐标并不总是完全相等有时候会因为基线略微偏移导致同一行被拆成两截这就是“同一行的字跑到两行”的根源。坐标模糊化能解决大部分问题。3.2 坐标分组不是万能的双栏和表格怎么处理坐标分组看起来很爽但遇到双栏排版的PDF就露馅了。双栏文档的第一栏底部和第二栏顶部Y坐标是交错的单纯按Y分组会把左右两栏混在一起。网上有人用聚类算法按X坐标先分区域再在每个区域内按Y分组这种思路对固定版式有效但遇到更复杂的图文混排仍然会崩。我的经验是在差异对比场景下不要追求100%精确的分行只需要保证“两份文档用同一套规则处理”。既然是做对比旧文档和新文档即使分行顺序有点怪只要它们用相同的算法提取产生的错位是系统性的diff结果依然有参考价值。真正需要人工介入的是页面差异点多的页面那时候我会直接跳到视觉层看渲染图。3.3 文本归一化那些不该被算作“差异”的差异即使文本提取成功直接比对还会出现大量“伪差异”。最常见的三类空格数量不同PDF排版时为了保证两端对齐会在单词间插入可变宽度的空格两个单词之间可能是一个空格也可能是三个。处理方式是把连续空格压缩成一个。连字符断行英文和拼音里常见的前一行末尾带-的断词比如informa-和下一行tion实际上是一个单词information。这个需要按语言习惯做拼接处理。页码、页眉页脚、日期水印这些内容经常版本每次生成都会变比如PDF生成工具的当前时间戳但业务上不算文档差异。可以选择在对比前过滤掉。归一化代码我封装成一个方法public static string NormalizeText(string input) { if (string.IsNullOrEmpty(input)) return input; var result input; // 1. 把各种空白字符统一成空格 result System.Text.RegularExpressions.Regex.Replace(result, \s, ); // 2. 去掉行尾的-断行符例如 informa- tion - information result System.Text.RegularExpressions.Regex.Replace(result, -\s, ); // 3. 全角转半角按需 result result.Normalize(NormalizationForm.FormKC); return result.Trim(); }全角转半角这步要看业务需求如果合同里有中文标点新旧版本可能全角半角混用统一成半角后对比匹配度会明显提高。这个阶段的目标是尽量降低“格式噪音”让diff算法聚焦在真正的内容变更上。3.4 diff算法逐行比对并输出增删结果拿到两个文档的文本行列表后就可以做行级diff了。这里有个选择用成熟的DiffLib库还是自己写LCS。我个人建议项目里直接用DiffLib不仅支持行级diff还支持自定义相似度判断方便实现“模糊匹配”using DiffLib; var oldLines ExtractLines(old.pdf).Select(l l.Text).ToArray(); var newLines ExtractLines(new.pdf).Select(l l.Text).ToArray(); var sections Diff.Compare(oldLines, newLines, StringSimilarity); foreach (var section in sections) { if (section.Equal) { // 相同的部分跳过 continue; } Console.WriteLine(差异片段); for (int i section.LeftSequence.Start; i section.LeftSequence.End; i) { Console.WriteLine($ - {oldLines[i]}); } for (int i section.RightSequence.Start; i section.RightSequence.End; i) { Console.WriteLine($ {newLines[i]}); } }如果你不想引入外部库自己实现一个LCS也不难核心是二维动态规划public static Liststring DiffLines(string[] oldLines, string[] newLines) { int n oldLines.Length, m newLines.Length; var dp new int[n 1, m 1]; for (int i 1; i n; i) for (int j 1; j m; j) dp[i, j] (oldLines[i - 1] newLines[j - 1]) ? dp[i - 1, j - 1] 1 : Math.Max(dp[i - 1, j], dp[i, j - 1]); var result new Liststring(); int x n, y m; while (x 0 || y 0) { if (x 0 y 0 oldLines[x - 1] newLines[y - 1]) { result.Add( oldLines[x - 1]); x--; y--; } else if (y 0 (x 0 || dp[x, y - 1] dp[x - 1, y])) { result.Add( newLines[y - 1]); y--; } else { result.Add(- oldLines[x - 1]); x--; } } result.Reverse(); return result; }这段代码在几十页的合同文本上速度是毫秒级的内存占用也不大作为文本层工具非常够用。3.5 文本层失效的典型状况字体编码和乱码文本层方案有一个致命弱点PDF里的字体如果没有正确的ToUnicode映射提取出来就是乱码。尤其是一些老的PDF生成工具会把字符画成自定义字形但字符编码映射表是空的PdfPig拼出来的文本可能是一堆空白或者无意义符号。还有的字体会做子集化只嵌入文档里用到的那些字形映射表也经常不完整。遇到这种情况我的判断标准很粗暴如果提取出来的文本里可打印字符比例低于60%或者明显的词句数量为0就直接放弃文本层把整页交给视觉层处理。文本层是性能优化用的快车道不是必须通过的关卡这一点一定要在方案设计时就想明白。4. 第二道工序渲染成图像像素级找茬以及标注差异区域4.1 为什么文本层搞定后还要做视觉层文本层能告诉你“第3页第2行多了几个字”但如果在页面上有人把一段话的字号从10号改成了12号、把标题颜色从黑色改成了红色、或者把一张图片向右挪了5厘米文本层提取到的字符内容可能完全一样但视觉上文档已经变了。这种“排版级差异”是文本层永远发现不了的必须渲染成图像比对。还有个场景是扫描版PDF整页就是一张图片文本层提取为空不做视觉层就完全无法工作。4.2 用PdfiumViewer把PDF页面渲染成BitmapPdfiumViewer渲染页面的核心方法是Render它需要一个目标位图大小和DPI参数。这里先说清楚一个概念PDF的尺寸单位是“点”pt1点1/72英寸。A4纸宽约595pt在150DPI下渲染出来就是595 * 150 / 72 ≈ 1240像素宽。DPI越高渲染结果越精细但耗时和内存也线性增长。using PdfiumViewer; using System.Drawing; public static Bitmap RenderPage(string pdfPath, int pageIndex, int dpi 150) { using (var document PdfiumViewer.PdfDocument.Load(pdfPath)) { var size document.PageSizes[pageIndex]; int width (int)(size.Width * dpi / 72f); int height (int)(size.Height * dpi / 72f); var bitmap document.Render( pageIndex, width, height, dpi, dpi, PdfRenderFlags.Annotations | PdfRenderFlags.CorrectFromDpi); // 注意Render返回的Bitmap由调用方负责释放这里不写using return bitmap; } }DPI的建议快速筛查用96就够了肉眼能看出大面积移动精确定位用150-200如果你的业务需要捕捉很细微的表格线变化可以到300但一页A4的300DPI位图大约是2480 x 3508像素内存直接上几十兆多页并行时很容易把内存吃满。4.3 像素对比算法从“完全一致”到“相似度阈值”图像对比最朴素的实现是GetPixel但这个方法慢到让人怀疑人生——对一张200DPI的A4页面大约250万像素每像素调用一次GetPixel在普通机器上要跑好几秒。我用LockBits加指针操作来提速using System.Drawing; using System.Drawing.Imaging; public static unsafe double CompareBitmap(Bitmap bmpA, Bitmap bmpB, int tolerance 10) { int width Math.Min(bmpA.Width, bmpB.Width); int height Math.Min(bmpA.Height, bmpB.Height); var rect new Rectangle(0, 0, width, height); var dataA bmpA.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); var dataB bmpB.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); byte* ptrA (byte*)dataA.Scan0; byte* ptrB (byte*)dataB.Scan0; int strideA dataA.Stride; int strideB dataB.Stride; long diffCount 0; long totalCount 0; for (int y 0; y height; y) { byte* rowA ptrA y * strideA; byte* rowB ptrB y * strideB; for (int x 0; x width; x) { int b1 rowA[x * 3]; int g1 rowA[x * 3 1]; int r1 rowA[x * 3 2]; int b2 rowB[x * 3]; int g2 rowB[x * 3 1]; int r2 rowB[x * 3 2]; if (Math.Abs(b1 - b2) tolerance || Math.Abs(g1 - g2) tolerance || Math.Abs(r1 - r2) tolerance) { diffCount; } totalCount; } } bmpA.UnlockBits(dataA); bmpB.UnlockBits(dataB); return (double)diffCount / totalCount; }tolerance参数非常关键。PDF抗锯齿渲染会在文字边缘产生半透明的过渡像素两份看起来一模一样的文档在最开始渲染时哪怕字体解析有细微差异边缘像素也不可能完全相等。把容差设为10-15可以忽略这类边缘噪声同时保住大面积的颜色差异。至于“这页到底算有差异还是没差异”我用的判定公式是差异像素占比超过0.5%就认为页面有变化。这个阈值你可以根据自己业务调对比盖章合同这种敏感场景建议再低一些对比日常报表可以放宽。4.4 生成差异标注图把变化区域用矩形框出来知道页面有差异还不够用户需要的是“哪里不同”。我的做法是在对比循环里额外维护一个“差异像素包围盒”把所有差异像素的X/Y范围记下来int minX int.MaxValue, minY int.MaxValue, maxX 0, maxY 0; long diffCount 0; for (int y 0; y height; y) { byte* rowA ptrA y * strideA; byte* rowB ptrB y * strideB; for (int x 0; x width; x) { if (IsDiffPixel(...)) { if (x minX) minX x; if (x maxX) maxX x; if (y minY) minY y; if (y maxY) maxY y; diffCount; } } }得到包围盒后用GDI在旧文档的渲染图上画一个红色矩形再把新的文档渲染图并排拼在一起给出“左右对照红框标注”的输出using (var marked (Bitmap)bmpA.Clone()) using (var g Graphics.FromImage(marked)) { using (var pen new Pen(Color.Red, 3f)) { g.DrawRectangle(pen, minX, minY, maxX - minX, maxY - minY); } marked.Save(diff_page1.png, ImageFormat.Png); }这个方法只能标出“单个最大差异区域”如果一页有多个分散的差异点就需要做连通区域分析——把所有连在一起的差异像素打成一个组每个组单独求包围盒。实现起来不复杂用一个BFS或双循环扫描即可网上“连通域标记”的算法模板到处都有。4.5 视觉层的误报来源与对策视觉层虽然万能但误报率也不低我实际踩得最多的有三个抗锯齿边缘差异前面提到了用容差解决。动态内容变化有些PDF页面底部会带时间戳或者“由某系统生成”的水印同一份文档不同时间导出这一块像素永远不同。最好的办法是在对比前让用户选择“忽略区域”把右下角时间戳位置排除掉。渲染引擎版本不同PDFium版本不一致对同一份PDF的字体渲染结果会有细微差别甚至可能导致整页版面偏移。这要求部署环境必须固定PdfiumViewer的原生DLL版本不能随便升级。5. 差异报告怎么做坐标对齐、结构化输出与页面定位5.1 把文本层和视觉层的结果对齐到同一个坐标系文本层和视觉层各有各的坐标系。文本层用的是PDF原生坐标原点在页面左下角Y向上图像坐标原点在左上角Y向下。想用视觉层的红框去定位“这个区域是哪些文本”必须做一次坐标转换double pdfY pageHeightInPoints - imageY * (pageHeightInPoints / imageHeightInPixels); double pdfX imageX * (pageWidthInPoints / imageWidthInPixels);其中pageHeightInPoints来自PdfiumViewer的document.PageSizes[pageIndex]pageHeightInPixels就是渲染位图的Height。转换后就能把图像上的差异矩形映射回PDF坐标再去匹配PdfPig提取出来的文本行坐标从而知道“红框里到底圈住了哪几行字”。5.2 差异报告的数据结构我做了一份JSON格式的差异报告方便前端页面展示。核心结构如下{ fileName: contract_v2.pdf, comparedWith: contract_v1.pdf, pageCount: 12, diffs: [ { page: 3, diffRatio: 0.0182, rectangles: [ { x: 72.0, y: 642.0, width: 210.0, height: 18.0 } ], changes: [ { type: modified, oldText: 甲方应在收到通知后 3 日内支付, newText: 甲方应在收到通知后 5 日内支付 }, { type: deleted, oldText: 原始条款内容, newText: } ] } ] }这个结构的好处是前端拿到rectangles可以在PDF预览组件上直接画矩形拿到changes可以展示成类似GitDiff的增删视图。视觉层负责“哪一块变了”文本层负责“具体怎么变了”两者各司其职。5.3 生成高亮对比HTML如果你的项目暂时没有前端可以直接输出一个HTML对比页面把差异行用不同背景色标出来。用DiffLib的结果渲染即可var diffHtml new StringBuilder(table); foreach (var section in sections) { if (section.Equal) { // 相同行不输出或灰显 } else { for (int i section.LeftSequence.Start; i section.LeftSequence.End; i) diffHtml.Append(tr stylebackground:#fddtd-/tdtd).Append(HtmlEncode(oldLines[i])).AppendLine(/td/tr); for (int i section.RightSequence.Start; i section.RightSequence.End; i) diffHtml.Append(tr stylebackground:#dfdtd/tdtd).Append(HtmlEncode(newLines[i])).AppendLine(/td/tr); } } diffHtml.AppendLine(/table); File.WriteAllText(diff.html, diffHtml.ToString());5.4 在原PDF页面画矩形标注如果业务方希望直接拿PDF文件看差异可以在原PDF上把差异区域画成红框。这个操作PdfPig做不了它是纯解析库。我用的方案是PdfSharp它支持简单的绘图操作和新增内容流using PdfSharp.Pdf; using PdfSharp.Pdf.IO; using PdfSharp.Drawing; public static void MarkPdf(string inputPath, string outputPath, IReadOnlyListDiffRectangle rects) { using (var document PdfReader.Open(inputPath, PdfDocumentOpenMode.Modify)) { foreach (var group in rects.GroupBy(r r.Page)) { var page document.Pages[group.Key - 1]; using (var gfx XGraphics.FromPdfPage(page)) { var pen new XPen(XColors.Red, 2); foreach (var rect in group) { gfx.DrawRectangle(pen, new XRect(rect.X, rect.Y, rect.Width, rect.Height)); } } } document.Save(outputPath); } }这个功能非常实用尤其是给业务方做人工复核时他们不愿意看技术报告只想要一份“被红圈标好的PDF文件”拿到手就能审。6. 扫描件、性能优化与最终落地的完整调用链6.1 扫描版PDF怎么处理OCR这条路绕不开如果PDF本身就是扫描件页面全是图片文本层直接失效视觉层能告诉你“第5页有差异”但说不清“改了哪个字”。我在项目里对这类文档接入了Tesseract OCR引擎用TesseractNuGet 包using Tesseract; public static string OcrPage(Bitmap pageImage) { using (var engine new TesseractEngine(./tessdata, chi_simeng, EngineMode.Default)) { using (var page engine.Process(pageImage)) { return page.GetText(); } } }把渲染出来的页面图像直接交给Tesseract识别出文本后再走文本层对比流程。然后从视觉层拿到的差异矩形如果和OCR识别出的某行文字坐标重叠就可以把“像素发生变化”和“文字发生了什么变化”关联起来。OCR方案的代价是时间和准确率。Tesseract识别中文的效果远不及商用OCR扫描质量稍差就满屏错字做diff时会引入大量假差异。比较务实的做法是扫描版PDF只标记“页面上有差异的区域”并把OCR出来的文字作为参考信息展示不自动给出结论最终还是让人眼复核。6.2 性能优化不该逐页渲染的页面坚决不渲染如果一次要对比几十页甚至上百页的PDF视觉层逐页渲染是吃不消的。我在实际项目里做的优化链路是先算两个PDF文件的MD5相同就直接返回“完全一致”零耗时。逐页提取文本并归一化文本完全相同的页面直接标记为“无差异”不进入渲染环节。只有文本不一致的页面才做图像渲染和像素对比。视觉对比也分两级先用96DPI快速比对有差异再用200DPI精确计算包围盒。这套链路的收益非常大。普通合同场景大多数页面只有页眉页脚变了或者完全没变真正需要渲染、像素比对的通常只有2-3页。实测对比一份36页的PDF纯文本层耗时不到100毫秒加上两页视觉对比总耗时稳定在1.5秒左右。渲染过程本身也可以用Parallel.For并行加速但要注意PdfiumViewer的PdfDocument不是线程安全的每个线程要独立打开一次文档Parallel.For(0, pageIndexes.Length, new ParallelOptions { MaxDegreeOfParallelism 4 }, i { using (var doc PdfiumViewer.PdfDocument.Load(path)) { // 每个线程都有自己的doc实例再渲染页面 } });这里不能只看渲染性能大PDF文件本身就是几十MB同时开四五个文档实例对内存压力很大MaxDegreeOfParallelism控制在4左右比较合理。6.3 最容易被忽视的“伪差异”来源汇总一下我在这类项目里踩过的伪差异坑全部是真实发生过的伪差异来源现象对策PDF生成器元数据两个文件Metadata中的Producer/CreationDate不同对比前移除元数据或只对比Content Stream嵌入字体版本不同同一个字体被不同版本工具子集化后字形微变视觉层容差调高或固定生成工具密码保护与加密部分页面提取文本为空先解密处理或用视觉层兜底时间戳水印页面角落数字每秒都变人工指定忽略区域表格线渲染差异同行表格在不同渲染DPI下线条宽度不同按比例归一化后再比或者统一DPI透明度和混合模式文字背后有透明阴影渲染结果不一致这种很难完全自动化只能标记人工复核第2条特别有意思我曾经排查过一个“为什么新旧一模一样的PDF每次视觉对比都显示3%差异”的问题最后发现是两台机器上安装的字体版本不同PDFium渲染时调用了本地字体替换导致同一份PDF在两台机器上渲染出来的像素不一样。从此我把视觉对比的每一步都锁定在同一版本的原生DLL并且保证生成PDF的原始环境尽量统一。6.4 最终在项目中落地的完整调用流程把前面的所有模块串起来最终的服务层方法大致是这样的public PdfDiffResult ComparePdfs(string oldPath, string newPath) { var result new PdfDiffResult(); // 0. 快速MD5判断 if (FileMD5(oldPath) FileMD5(newPath)) { result.Identical true; return result; } // 1. 提取文本行 var oldLines ExtractLines(oldPath); var newLines ExtractLines(newPath); // 2. 按页面分组先文本行级diff var pageDiffs DiffLines(oldLines, newLines); // 3. 有差异的页面渲染图像做视觉比对 var visualResults new ListPageVisualDiff(); foreach (var pageNum in pageDiffs.DiffPageNumbers) { using (var oldBmp RenderPage(oldPath, pageNum - 1, 150)) using (var newBmp RenderPage(newPath, pageNum - 1, 150)) { var (ratio, rectangle) CompareAndGetRect(oldBmp, newBmp); visualResults.Add(new PageVisualDiff { Page pageNum, DiffRatio ratio, BoundingBox rectangle }); } } // 4. 组装报告 result.BuildReport(pageDiffs, visualResults); return result; }实际项目里还要加上异步化、任务进度上报、超时控制但这套主流程就是你需要的全部核心逻辑了。做完这个功能后我最大的体会是PDF差异对比不是一个“用某个库一行搞定”的需求而是解析、渲染、算法、坐标映射多模块的组合工程。先想清楚你要对比的是文字内容还是视觉排版再决定走哪条路线可以少走很多弯路。如果你也是刚接到类似需求建议第一版先做文本层视觉层兜底别一上来就上OCR和语义分析否则很可能项目还没上线你就先被PDF这个格式的脾气磨秃了。
返回列表