Об’єднання DOCX, XLSX та PDF в один файл‑папку — демонстрація

Річ, яка поглинала мої п’ятниці

Кожного п’ятничного пополудня протягом приблизно року у мене був один і той самий маленький ритуал. Контракт надходив у вигляді трьох файлів — основна угода у Word, додаток з цінами в Excel і лист умов партнера у PDF — і мені треба було зібрати їх в один чистий PDF. Нічого складного. Відкрити Word, експортувати в PDF. Відкрити Excel, експортувати в PDF. Відкрити безкоштовний додаток для злиття PDF, перетягнути три файли, перевірити порядок, зберегти.

Займало це близько восьми хвилин. Помножте це на п’ятнадцять контрактів на тиждень — і ви втратили дві години на переміщення миші. Гірше, кожні кілька тижнів хтось надсилав папку з додатком на першій сторінці, бо імена файлів сортувалися за алфавітом у програмі злиття.

Якщо це звучить знайомо, решта цього допису — це пополудень, коли я нарешті замінив ритуал кодом.

Справжня вартість — не час, а той один контракт із п’ятдесяти, у якому сторінки йдуть у неправильному порядку, і ніхто не помічає, доки клієнт не підпише неправильну версію.

Що я насправді хотів

Не «вишуканий конвеєр документів». Просто три речі:

  1. Передати методу список файлів (будь‑яка комбінація DOCX, XLSX, PDF) і отримати один PDF у відповідь.
  2. Показати ту ж логіку на папці, щоб вона сама визначила список файлів.
  3. Витягнути діапазон сторінок із готового файлу‑папки без повторного злиття всього.

Ось і все завдання. Якщо бібліотека не може виконати ці три пункти чисто, я не хочу про це знати.

Налаштування

  • .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, обробка сканованих зображень, вбудовані шрифти тощо. Дайте цим займатися комусь іншому і проведіть п’ятничне пополудня над тим, що не є простим сортуванням імен файлів.

Куди далі:

Корисні посилання