💡 Полный рабочий пример доступен на GitHub:
compare-encrypted-pdf-and-word-documents-dotnet
Старый способ был болезненным
Две редакции договора поставки попадают в ваш почтовый ящик. Обе защищены паролем, каждый — своим паролем, и кому‑то нужна помеченная копия, показывающая, что изменилось. Библиотека сравнения, которой вы пользуетесь, ожидает открытый (plaintext) ввод, поэтому в конвейер добавляется шаг: расшифровать оба файла во временную папку, сравнить открытые копии, затем не забыть их удалить. Эта временная папка теперь самое слабое звено в рабочем процессе, который существует именно потому, что документы конфиденциальны.
Существует вторая версия той же проблемы, которую легче пропустить. Некоторые команды обходятся без временной папки и расшифровывают файлы в память, что решает вопрос очистки, но не вопрос формата: 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объявляет собственный enum с тем же именем, но другими значениями, поэтому простое имя не скомпилируется — нужно указывать полное имя.
Шаг 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 без изменения enum‑значения ничего не меняет — именно значение PasswordSaveOption активирует пароль при сохранении. Результат этого вызова открывается паролем 5678 и отклоняет 1234.
Почему мой try/catch вокруг Comparer не ловит неверный пароль?
Потому что конструктор никогда не открывает документ. Он лишь сохраняет путь, и то же делает 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 |
| Поддержка форматов | Инструменты расшифровки для каждого формата | Один LoadOptions.Password для PDF, DOCX, XLSX, PPTX |
| Требуемый код | Хелпер расшифровки + сравнение | 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