Річ, яка поглинала мої п’ятниці
Кожного п’ятничного пополудня протягом приблизно року у мене був один і той самий маленький ритуал. Контракт надходив у вигляді трьох файлів — основна угода у Word, додаток з цінами в Excel і лист умов партнера у PDF — і мені треба було зібрати їх в один чистий PDF. Нічого складного. Відкрити Word, експортувати в PDF. Відкрити Excel, експортувати в PDF. Відкрити безкоштовний додаток для злиття PDF, перетягнути три файли, перевірити порядок, зберегти.
Займало це близько восьми хвилин. Помножте це на п’ятнадцять контрактів на тиждень — і ви втратили дві години на переміщення миші. Гірше, кожні кілька тижнів хтось надсилав папку з додатком на першій сторінці, бо імена файлів сортувалися за алфавітом у програмі злиття.
Якщо це звучить знайомо, решта цього допису — це пополудень, коли я нарешті замінив ритуал кодом.
Справжня вартість — не час, а той один контракт із п’ятдесяти, у якому сторінки йдуть у неправильному порядку, і ніхто не помічає, доки клієнт не підпише неправильну версію.
Що я насправді хотів
Не «вишуканий конвеєр документів». Просто три речі:
- Передати методу список файлів (будь‑яка комбінація DOCX, XLSX, PDF) і отримати один PDF у відповідь.
- Показати ту ж логіку на папці, щоб вона сама визначила список файлів.
- Витягнути діапазон сторінок із готового файлу‑папки без повторного злиття всього.
Ось і все завдання. Якщо бібліотека не може виконати ці три пункти чисто, я не хочу про це знати.
Налаштування
- .NET 6.0 або новіший
- GroupDocs.Merger for .NET 24.10+ (отримайте тимчасову ліцензію, щоб не додавати водяний знак оцінки)
- Папка з будь‑якою комбінацією документів, які ви зазвичай з’єднуєте вручну
dotnet add package GroupDocs.Merger
Ось і все щодо залежностей. Ніякого зовнішнього конвертера, безголового встановлення Office, бібліотеки для роботи з PDF зверху.
Крок 1 — Нехай папка буде вхідними даними
Я завжди починаю саме тут, бо це реальна точка входу. На практиці щось інше (обробник завантажень, завдання з обробки електронної пошти, нічний дамп з фінансів) кладуть купу файлів у каталог, і мій код має розбиратися з тим, що знайде.
// Pick up every supported file in the drop folder; the PDF wins
// the tie-break for position 0 so the merger keeps the output
// as a PDF regardless of how files are named.
string[] extensions = { ".pdf", ".docx", ".xlsx" };
var files = Directory.EnumerateFiles(folderPath)
.Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.OrderBy(f => Path.GetExtension(f).ToLowerInvariant() == ".pdf" ? 0 : 1)
.ThenBy(f => f)
.ToArray();
if (files.Length == 0)
throw new InvalidOperationException(
$"No supported documents found in '{folderPath}'.");
Трюк з OrderBy — це цікава частина. GroupDocs.Merger вибирає формат вихідного файлу за тим, який файл ви відкрили першим — якщо я передаю йому DOCX як первинний документ, я отримаю DOCX на виході. Оскільки мій конвеєр завжди хоче PDF, я переконуюсь, що будь‑який існуючий PDF у папці отримує позицію 0.
Два моменти, які варто згадати:
ToLowerInvariant()тому, що колись партнер може надіслати вамREPORT.PDF, і ваш фільтр лише для нижнього регістру тихо його відкине.ThenBy(f)потрібен лише для того, щоб вихід був детермінованим. Без нього два запуски в одній і тій же папці можуть різнитися в залежності від стану файлової системи.
Крок 2 — Сам процес злиття
Коли у мене є впорядкований список шляхів, злиття коротше, ніж його опис.
Console.WriteLine($"Primary source: {sourcePaths[0]}");
using var merger = new Merger(sourcePaths[0]);
var joinOptions = new JoinOptions();
for (int i = 1; i < sourcePaths.Length; i++)
{
Console.WriteLine($"Joining: {sourcePaths[i]}");
merger.Join(sourcePaths[i], joinOptions);
}
merger.Save(outputPath);
Console.WriteLine($"Unified PDF binder saved to: {Path.GetFullPath(outputPath)}");
Кілька нотаток з практичного використання:
usingмає значення.Mergerтримає файлові дескриптори на вихідних файлах; якщо не звільнити його, ваш процес, що працює з папкою, зрештою не зможе видалити власні вхідні файли.JoinOptionsтут порожній, бо типові налаштування підходять у 95 % випадків. Коли потрібні інші параметри, саме тут задаються діапазони сторінок, обертання та позиції вставки.- Коли Excel потрапляє у папку, розташування листа на сторінці визначається областю друку у вихідній книзі. Якщо ваш XLSX розтягується на 38 сторінок, а треба три, виправляти треба у самій електронній таблиці, а не в
JoinOptions.
Один контрольний пункт, який я завжди додаю одразу після збереження:
using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");
Два рядки коду, які виявили більше «тихо відкинутих додатків», ніж будь‑які тести, які я писав.
Крок 3 — Пізніше витягнути частину
Запит, який я отримую щоразу: «Можете надіслати лише титульну сторінку?» або «Клієнт хоче лише підписи». Перебудовувати всю папку, щоб передати дві сторінки, — дурниця; витяг робить це напряму.
using var merger = new Merger(binderPath);
merger.ExtractPages(new ExtractOptions(pages));
merger.Save(outputPath);
Console.WriteLine($"Extracted pages [{string.Join(",", pages)}] to " +
Path.GetFullPath(outputPath));
pages — це int[] номерів сторінок (нумерація з 1), які треба залишити. Все інше відкидається. Це швидко, бо результат уже PDF — без зайвих конвертацій.
До і після, чесно
| Як я раніше працював | З Merger.Join |
|
|---|---|---|
| Час на один контракт | 5–10 хвилин кліків | менше 30 секунд від початку до кінця |
| Типова помилка | Сторінки в неправильному порядку, ніхто не помічає | Той порядок, який задає список файлів, повторювано |
| Масштабування 100/день | Не виходить — наймаєте людину | Один працівник, який більшість часу нудиться |
| Підтримуваний код | Сторінка Confluence «Binder Process v4» | Один клас, ~70 рядків |
| Результат | Три PDF і молитва | Один файл‑папка, кількість сторінок можна логувати |
Найважливіший рядок — це «помилка». Ручне злиття мовчки провалюється; код, який логуватиме кількість сторінок, провалюється голосно.
Реальна історія з маленької legal‑tech команди
Двоособовий стартап, у якого був паралегал, що щодня починав з складання контракту. Угода у Word, ціни в Excel, додаток у PDF, зшивали в додатку, завантажували в DocuSign. Приблизно вісім хвилин на пакет, а при 30 пакетах на день це був її увесь ранок.
Вони впровадили метод сканування папки у бекенд‑службу, яка вже стежила за вхідною поштою. Двадцять секунд на пакет, плюс рядок логу з кількістю сторінок. Паралегал перейшов до перегляду контрактів, а не їх складання. Ніхто більше не надсилав неправильний порядок — не тому, що бібліотека чарівна, а тому, що список файлів явно прописаний у коді і його можна порівнювати.
string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";
var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");
Ось і вся інтеграція. Все, що стоїть вище (слухач електронної пошти, шлях зберігання), вже було налаштовано.
Те, що сьогодні не знадобилося, а завтра може знадобитися
Той самий пакет робить безліч речей, які я не розкрив, бо стаття стала б надто довгою. Приблизно у такому порядку я їх використовував:
- Watermarks on the output для штампів «DRAFT» на копіях до підпису.
- Page rotation для сканів, що приходять у бічному положенні.
- Custom page ordering коли порядок джерел не збігається з порядком доставки.
- PDF encryption для будь‑якого матеріалу, що надсилається зовнішньому контрагенту.
Все це доступно через той самий API Merger. У документації повний список — я просто хотів підкреслити, що «merge» це дешевий старт, а решта доступна, коли потрібна.
Що я сказав би собі в минулому
Якщо ви збираєтеся писати власний крок DOCX‑to‑PDF, бо «це лише один метод», зупиніться. Конвертація — це частина, яка тихо гниє: нові функції Office, обробка сканованих зображень, вбудовані шрифти тощо. Дайте цим займатися комусь іншому і проведіть п’ятничне пополудня над тим, що не є простим сортуванням імен файлів.
Куди далі:
- Temporary license — потрібна для виводу без водяного знака
- Advanced merging options — JoinOptions, параметри збереження, стиснення
- Supported formats — значно більше трьох, що я показав
- Sample projects on GitHub — включаючи цей