Scalanie DOCX, XLSX i PDF w jedną teczkę — demo

Rzecz, która pożerała moje piątki

Każdego piątkowego popołudnia, przez około rok, miałem ten sam mały rytuał. Umowa przychodziła w trzech plikach — główna umowa w Wordzie, aneks cenowy w Excelu i arkusz warunków partnera jako PDF — i musiałem je przekazać jako jeden czysty PDF. Nic trudnego. Otwórz Word, wyeksportuj do PDF. Otwórz Excel, wyeksportuj do PDF. Otwórz darmową aplikację do scalania PDF‑ów, przeciągnij trzy pliki, sprawdź kolejność, zapisz.

Zajmowało to może osiem minut. Pomnóż to przez piętnaście umów tygodniowo i straciłeś dwie godziny na przesuwanie myszy. Co gorsza, co kilka tygodni ktoś wysyłał teczkę z aneksem na pierwszej stronie, bo nazwy plików sortowały się alfabetycznie w aplikacji do scalania.

Jeśli brzmi to znajomo, reszta tego wpisu opisuje popołudnie, w którym w końcu zastąpiłem rytuał kodem.

Prawdziwy koszt to nie czas — to jedna umowa na pięćdziesiąt, w której strony są w niewłaściwej kolejności i nikt tego nie zauważa, dopóki klient nie podpisze niewłaściwej wersji.

Czego tak naprawdę chciałem

Nie „wyszukanego potoku dokumentów”. Po prostu trzech rzeczy:

  1. Przekazać metodzie listę plików (dowolną mieszankę DOCX, XLSX, PDF) i otrzymać jeden PDF z powrotem.
  2. Skierować tę samą logikę na folder i niech sama wyznaczy listę plików.
  3. Wyciągnąć zakres stron z gotowej teczki bez ponownego scalania wszystkiego.

To cała praca. Jeśli biblioteka nie potrafi zrobić tych trzech rzeczy czysto, nie chcę o tym wiedzieć.

Konfiguracja

  • .NET 6.0 lub nowszy
  • GroupDocs.Merger for .NET 24.10+ (pobierz tymczasową licencję, aby nie wysyłać znaku wodnego wersji ewaluacyjnej)
  • Folder z dowolną mieszanką dokumentów, które normalnie scalałbyś ręcznie
dotnet add package GroupDocs.Merger

To wszystko, jeśli chodzi o zależności. Bez zewnętrznego konwertera, bez instalacji Office w trybie headless, bez dodatkowej biblioteki do manipulacji PDF.

Krok 1 — Niech folder będzie wejściem

Zawsze zaczynam tutaj, bo to realistyczny punkt wejścia. W praktyce coś innego (obsługa uploadu, zadanie przetwarzające e‑maile, nocny zrzut z finansów) wrzuca mnóstwo plików do katalogu, a mój kod musi sobie poradzić z tym, co znajdzie.

// 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}'.");

Sztuczka z OrderBy to ciekawy element. GroupDocs.Merger wybiera format wyjściowy na podstawie pierwszego otwartego pliku — jeśli podam mu DOCX jako dokument główny, otrzymam DOCX na wyjściu. Ponieważ mój potok zawsze chce PDF, zapewniam, że istniejący PDF w folderze dostaje pozycję 0.

Dwie rzeczy warte wspomnienia:

  • ToLowerInvariant() — partner kiedyś może wysłać REPORT.PDF i Twój filtr działający tylko na małe litery po cichu go odrzuci.
  • ThenBy(f) jest tam wyłącznie po to, by wynik był deterministyczny. Bez tego dwa uruchomienia na tym samym folderze mogą różnić się w zależności od nastroju systemu plików.

Krok 2 — Samo scalanie

Gdy mam już uporządkowaną listę ścieżek, scalanie jest krótsze niż opis scalania.

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)}");

Kilka uwag z praktyki:

  • using ma znaczenie. Merger trzyma uchwyty plików źródłowych; zapomnij go zwolnić, a pracownik monitorujący folder w końcu nie będzie mógł usunąć własnych wejść.
  • JoinOptions jest tutaj pusty, bo domyślne ustawienia spełniają moje potrzeby w 95 % przypadków. Gdy potrzebujesz czegoś innego, właśnie tam znajdują się zakresy stron, rotacje i pozycje wstawiania.
  • Gdy Excel trafia do teczki, układ arkusz‑na‑stronę jest określany przez obszar wydruku w skoroszycie źródłowym. Jeśli Twój XLSX rozciąga się na 38 stron, a chciałeś trzy, poprawka musi być w arkuszu, nie w JoinOptions.

Jedno sprawdzenie, które zawsze dodaję zaraz po zapisie:

using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");

Dwie sekundy kodu, które wyłapały więcej „cicho odrzuconych aneksów” niż jakikolwiek test, który napisałem.

Krok 3 — Wyciągnięcie fragmentu później

Często dostaję prośbę: „Czy możesz po prostu przesłać stronę tytułową?” albo „Klient chce tylko podpisy.” Przebudowywanie całej teczki, by oddać dwie strony, jest głupie — wyciąganie robi to od razu.

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 to int[] z numerami stron (liczonymi od 1), które chcesz zachować. Wszystko inne zostaje odrzucone. To szybkie, bo wynik jest już PDF‑em — nie ma potrzeby konwersji w dwie strony.

Przed vs. po, szczerze

Jak to robiłem wcześniej Z Merger.Join
Czas na jedną umowę 5–10 minut klikania poniżej 30 sekund od początku do końca
Typowa awaria Strony w niewłaściwej kolejności, nikt nie zauważa Kolejność dokładnie taka, jak w liście plików, powtarzalna
Skalowanie do 100/dzień Nie działa — zatrudniasz osobę Jeden pracownik, najczęściej znudzony
Kod do utrzymania Strona w Confluence zatytułowana „Binder Process v4” Jedna klasa, ~70 linii
Wynik Trzy PDF‑y i modlitwa Jedna teczka, z liczbą stron, którą możesz logować

Wiersz, który mnie najbardziej interesuje, to „awaria”. Ręczne scalanie milczy, kod, który loguje liczbę stron, krzyczy.

Prawdziwa historia z małego zespołu legal‑tech

Dwóch‑osobowy startup, w którym pracowałem, miał paralegal, której poranek zaczynał się od składania umów. Umowa w Wordzie, wycena w Excelu, aneks w PDF, połączone w aplikacji, wysłane do DocuSign. Około ośmiu minut na paczkę, co przy 30 paczkach dziennie było praktycznie jej całym porankiem.

Wdrożyli metodę skanowania folderu w usługę backendową, która już nasłuchiwała ich skrzynki mailowej. Dwadzieścia sekund na paczkę, plus linijka logu z liczbą stron. Paralegal przeszła od składania umów do ich przeglądania. Nikt już nie wysłał nieprawidłowo uporządkowanej teczki — nie dlatego, że biblioteka jest magiczna, ale dlatego, że lista plików jest jawna w kodzie i można ją porównać.

string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";

var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");

To cała integracja. Wszystko po stronie źródła (nasłuchiwacz e‑mail, ścieżka przechowywania) już było gotowe.

Rzeczy, których nie potrzebowałem dziś, ale przydadzą się jutro

Ta sama biblioteka oferuje mnóstwo funkcji, których nie omówiłem, bo artykuł by się rozciągnął. W przybliżonej kolejności, w jakiej sięgałem po nie:

  • Watermarks on the output dla znaczków „DRAFT” na kopiach przed podpisem.
  • Page rotation dla skanów, które przychodzą w bok.
  • Custom page ordering gdy kolejność źródłowa nie jest kolejnością dostawy.
  • PDF encryption dla wszystkiego, co trafia do zewnętrznego kontrahenta.

Wszystko to dostępne jest przez ten sam interfejs Merger. Pełna lista w docs — chciałem tylko podkreślić, że „merge” to tani start, a reszta jest dostępna, gdy jej potrzebujesz.

Co powiedziałbym sobie z przeszłości

Jeśli zamierzasz napisać własny krok DOCX‑to‑PDF, bo „to tylko jedna metoda”, zatrzymaj się. Konwersja to część, która cicho się psuje — nowe funkcje Office, obsługa skanowanych obrazów, wbudowane czcionki i tak dalej. Niech coś innego zajmuje się tą powierzchnią, a piątkowe popołudnie poświęć na coś, co nie jest sortowaniem nazw plików.

Gdzie dalej:

Przydatne linki