💡 完整可运行示例已在 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 的错误处理——因为真正的失败就在这里出现。
准备好自动化你的文档工作流了吗?
- 试用免费 API 试用版
- 探索 加载受密码保护的文档
- 阅读完整的 受保护文档比较指南
- 查看 GitHub 上的示例项目