💡 完整可运行示例已在 GitHub 上提供: compare-encrypted-pdf-and-word-documents-dotnet

过去的做法很痛苦

两份供应协议的修订版进入你的收件箱。两者都受密码保护,且密码各不相同,某人需要一份标记出更改的副本。你手头的比较库只能接受明文输入,因此流水线需要增加一步:将两个文件解密到临时文件夹,比较明文副本,然后记得删除它们。这个临时文件夹成为工作流中最薄弱的环节,而该工作流正是因为文档敏感才存在的。

还有一种更容易被忽视的同类问题。某些团队跳过临时文件夹,直接在内存中解密,这解决了清理问题,却没有解决格式问题:不同格式的解密 API 各不相同,所以在支持加密的 PDF 之后再支持加密的电子表格,就意味着需要进行第二次集成,而不是添加一行代码。

成本主要不在解密调用本身,而在其周边的一切。必须把明文副本写到某处,在每条退出路径(包括失败路径)都要清理,并且要防止它们进入备份和崩溃转储。这样生成的差异文件默认也是未受保护的,因此两个加密输入的输出成为链中唯一可以被任何人打开的文件。

解密绕行的真实成本:一个临时目录保存了因某种原因被加密的文档的明文副本,且必须在每个错误路径上正确清理。

有更好的办法

密码保护的比较是 GroupDocs.Comparison 在 .NET 中的功能,可直接打开加密的 PDF、DOCX、XLSX 和 PPTX 文件,并决定使用何种密码来保护比较结果。无需解密步骤,也不产生明文中间文件:密码随文档一起进入比较本身,作为 LoadOptions 的属性。

在开始之前,你需要:

  • .NET 8.0 SDK 或更高版本
  • GroupDocs.Comparison 26.9.0(临时许可证)
  • 两个相同格式的加密文档及其密码

使用一条命令安装:

dotnet add package GroupDocs.Comparison

新方法:直接将加密文档送入比较器

下面的示例比较两个加密的 PDF——源文件使用 1234 打开,目标文件使用 4321 打开——并将更改内联合并写入单个结果文件。特意使用不同的密码,因为错误往往隐藏在这里。

步骤 1 - 为每个文档提供各自的 LoadOptions

Comparer 持有一个源文档和任意数量的目标文档,每个文档都携带自己的保护信息。源文档的密码在构造函数中传入;每个目标文档的密码在各自的 Add 调用中传入。

// 每个文档一个 LoadOptions —— 构造函数的选项只解锁源文档,永不影响目标文档。
using var comparer = new Comparer("source.pdf",
    new LoadOptions { Password = "1234" });
comparer.Add("target.pdf", new LoadOptions { Password = "4321" });

这就是让人踩坑的细节。把单个 LoadOptions 传给构造函数并期望它覆盖所有目标,是最常见的错误做法;由于错误出现的时机,它不会在你查找的地方直接报错。

步骤 2 - 决定用什么来保护结果

CompareOptions.PasswordSaveOption 用来选择输出的保护方式:None、Source、Target 或 User。默认是 None,会悄悄把两个加密输入合并成一个未受保护的结果。

// 内联标记,结果复用源文档的密码。
var options = new PdfCompareOptions
{
    DisplayMode = PdfCompareOptions.ComparisonDisplayMode.Inline,
    PasswordSaveOption = PasswordSaveOption.Source
};

comparer.Compare("Result/1-pdf-inline.pdf", options);

关键点:

  • PasswordSaveOption:Source 在输出上复用源文档的密码。若想使用新密码,请使用 User 并在 SaveOptions.Password 中指定。
  • ComparisonDisplayMode:位于 PdfCompareOptions 中,还提供 SideBySide 和 Interleaved。WordCompareOptions 也有同名枚举但值不同,直接使用名称会编译错误——请使用全限定名。

步骤 3 - 为输出设置独立密码

当差异文件要交给不应持有任一原始密码的审阅者时,PasswordSaveOption.User 会从 SaveOptions.Password 中获取密码,而不是复用输入密码。

var compareOptions = new PdfCompareOptions
{
    DisplayMode = PdfCompareOptions.ComparisonDisplayMode.Inline,
    PasswordSaveOption = PasswordSaveOption.User
};
var saveOptions = new SaveOptions { Password = "5678" };

comparer.Compare("Result/4-own-password.pdf", saveOptions, compareOptions);

这两个对象一起传给三参数的 Compare 重载。单独设置 SaveOptions.Password 并不会产生任何效果——只有枚举值为 User 时,保存时才会使用该密码。此调用生成的文件使用 5678 打开,拒绝 1234。

为什么在 Comparer 周围的 try/catch 捕获不到错误密码?

因为构造函数根本不打开文档。它只记录路径,Add 也是如此。文档真正读取发生在 Compare 执行时,届时会抛出 PasswordProtectedFileException,消息为 Password is missing。错误密码的行为完全相同:构造时默默接受,随后在 Compare 时被拒绝。

因此应当捕获比较调用,而不是构造函数。我是通过慢慢调试发现的:先在 try 中包装构造,然后观察加密文件在三行代码后才抛异常。仓库会打印每个阶段,首次阅读时就能看出顺序:

using var comparer = new Comparer("source.pdf");   // 成功
comparer.Add("target.pdf");                        // 成功
comparer.Compare("Result/unreachable.pdf");        // 此处抛出

对比:改动前后

改动前(先解密) 改动后(使用 GroupDocs.Comparison)
流水线步骤 解密、比较、删除临时副本 直接比较
磁盘上的明文 两个副本,需要在每个错误路径清理 无
结果保护 需要额外的重新加密步骤 只需设置一个 PasswordSaveOption 值
格式覆盖 每种格式都需要单独的解密工具 PDF、DOCX、XLSX、PPTX 只需一个 LoadOptions.Password
所需代码量 解密助手 + 比较代码 4 行代码

加密输入并不会改变比较功能。显示模式、摘要页和样式检测的行为与明文文件完全相同,因为保护完全在加载层处理。

正是这种分层让格式覆盖成本极低。LoadOptions.Password 只是一个普通的 string 属性,同一属性即可解锁 PDF、DOCX、XLSX 和 PPTX——下面的 Word 示例代码几乎与 PDF 示例逐字符相同。唯一变化的是选项类,因为每种格式暴露的渲染选项不同。为已经支持加密 PDF 的代码添加加密电子表格支持,加载路径几乎不需要任何改动。

实际案例:律所之间的合同修订

法律团队收到的每个协议修订版都是加密的,且密码在每次交换后都会更换,以防泄漏的密码导致整个历史被曝光。审阅合伙人需要每轮一个标记了更改的文档,并且根据保留规定,标记后的副本不能以未受保护的形式存放在文件共享上。

两项设置即可满足需求。每个文档使用各自的 LoadOptions 解锁,密码轮换无需额外处理;PasswordSaveOption.User 为每个分发的差异文件分配独立密码——仅用于打开比较结果,其他内容保持受保护。

// Word 修订,以便审阅合伙人接受或拒绝每项编辑。
var options = new WordCompareOptions
{
    DisplayMode = WordCompareOptions.ComparisonDisplayMode.Revisions,
    PasswordSaveOption = PasswordSaveOption.Source
};

using var comparer = new Comparer("round3.docx",
    new LoadOptions { Password = "1234" });
comparer.Add("round4.docx", new LoadOptions { Password = "4321" });
comparer.Compare("Result/redline.docx", options);

GroupDocs.Comparison 还能做什么?

  • 比较多个受保护文档:向同一次比较中添加多个加密目标,适用于 Word 和演示文稿格式。
  • 生成原生 Word 修订:WordCompareOptions.ComparisonDisplayMode.Revisions 在 Word 中直接写入审阅者可接受或拒绝的更改。
  • 控制外部资源加载:阻止或白名单文档携带的远程引用,这是另一个 LoadOptions 的安全措施。
  • 生成摘要页:GenerateSummaryPage 为结果文档添加更改概览。

结论

解密绕行并非比较本身的问题,而是库无法读取你的文件导致的。为每个文档设置 LoadOptions.Password 即可消除临时文件夹、清理路径以及链末端的未受保护差异。剩下的只有三项决定:每个文档的密码、显式的 PasswordSaveOption(而非默认的 None),以及围绕 Compare 的错误处理——因为真正的失败就在这里出现。

准备好自动化你的文档工作流了吗?

其他资源