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:
- Przekazać metodzie listę plików (dowolną mieszankę DOCX, XLSX, PDF) i otrzymać jeden PDF z powrotem.
- Skierować tę samą logikę na folder i niech sama wyznaczy listę plików.
- 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.PDFi 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:
usingma znaczenie.Mergertrzyma 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ść.JoinOptionsjest 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:
- Temporary license — wymagana do wyjścia bez znaku wodnego
- Advanced merging options — JoinOptions, opcje zapisu, kompresja
- Supported formats — znacznie więcej niż trzy pokazane tutaj
- Sample projects on GitHub — w tym ten