La cosa che mi rubava i venerdì
Ogni pomeriggio di venerdì, per circa un anno, avevo lo stesso piccolo rituale. Un contratto arrivava come tre file — l’accordo principale in Word, un allegato di prezzi in Excel e il foglio termini del partner come PDF — e dovevo consegnarli come un unico PDF pulito. Nulla di difficile. Apri Word, esporta in PDF. Apri Excel, esporta in PDF. Apri qualche app gratuita di fusione PDF, trascina i tre file, controlla l’ordine, salva.
Ci metteva circa otto minuti. Moltiplicato per quindici contratti a settimana, hai perso due ore a muovere il mouse. Peggio, ogni poche settimane qualcuno spediva un raccoglitore con l’allegato a pagina uno perché i nomi dei file venivano ordinati alfabeticamente nell’app di fusione.
Se ti suona familiare, il resto di questo post è il pomeriggio in cui ho finalmente sostituito il rituale con del codice.
Il vero costo non è il tempo — è quel singolo contratto su cinquanta in cui le pagine finiscono nell’ordine sbagliato e nessuno se ne accorge finché il cliente non firma la versione errata.
Quello che volevo davvero
Non un “pipeline di documenti sofisticato”. Solo tre cose:
- Dare a un metodo una lista di file (qualsiasi combinazione di DOCX, XLSX, PDF) e ricevere indietro un PDF.
- Puntare la stessa logica a una cartella e farla determinare autonomamente la lista di file.
- Estrarre un intervallo di pagine dal raccoglitore finito senza rifare l’intera fusione.
Questo è tutto il lavoro. Se la libreria non può fare queste tre cose in modo pulito, non voglio saperlo.
Configurazione
- .NET 6.0 o successivo
- GroupDocs.Merger for .NET 24.10+ (prendi una licenza temporanea così non spedisci la filigrana di valutazione)
- Una cartella con qualsiasi combinazione di documenti che normalmente fonderesti manualmente
dotnet add package GroupDocs.Merger
Tutto qui per le dipendenze. Nessun convertitore esterno, nessuna installazione di Office headless, nessuna libreria di manipolazione PDF aggiuntiva.
Passo 1 — Lasciare che una cartella sia l’input
Inizio sempre da qui perché è il punto di ingresso realistico. In pratica, qualcos’altro (un gestore di upload, un job di ingestione email, un dump notturno dalla finanza) deposita un mucchio di file in una directory, e il mio codice deve gestire quello che trova.
// 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}'.");
Il trucco OrderBy è la parte interessante. GroupDocs.Merger sceglie il formato di output dal file che apri per primo — se gli passo un DOCX come documento principale, otterrò un DOCX in output. Poiché il mio pipeline vuole sempre un PDF, mi assicuro che qualsiasi PDF presente nella cartella ottenga la posizione 0.
Due cose da menzionare:
ToLowerInvariant()perché un partner un giorno ti invieràREPORT.PDFe il tuo filtro solo minuscole lo scarterà silenziosamente.- Il
ThenBy(f)è lì solo per rendere l’output deterministico. Senza di esso, due esecuzioni sulla stessa cartella possono differire a seconda dell’umore del filesystem.
Passo 2 — La fusione vera e propria
Una volta che ho una lista ordinata di percorsi, la fusione è più breve della descrizione della fusione.
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)}");
Alcune note dall’uso in condizioni reali:
- Il
usingè importante.Mergertiene aperti i handle dei file sorgente; dimenticarsi di smaltirlo farà fallire eventualmente il worker della cartella di drop nel cancellare i propri input. JoinOptionsè vuoto qui perché le impostazioni predefinite sono quelle che voglio il 95 % delle volte. Quando ti servono, è lì che vivono gli intervalli di pagine, la rotazione e le posizioni di inserimento.- Quando Excel entra nel raccoglitore, il layout foglio‑a‑pagina è deciso dall’area di stampa della cartella di lavoro sorgente. Se il tuo XLSX finisce su 38 pagine e ne volevi tre, la correzione sta nel foglio di calcolo, non in
JoinOptions.
Un controllo di sanità che aggiungo sempre subito dopo il salvataggio:
using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");
Due secondi di codice che hanno catturato più bug di “allegato silenziosamente scartato” di quanti test io abbia scritto.
Passo 3 — Estrarre una fetta in seguito
La richiesta di follow‑up che ricevo ogni volta: “Puoi inviarmi solo la pagina di copertina?” o “Il cliente vuole solo le firme.” Ricostruire l’intero raccoglitore per consegnare due pagine è ridicolo — l’estrazione lo fa direttamente.
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 è un int[] di numeri di pagina basati su 1 che vuoi conservare. Tutto il resto viene scartato. È veloce perché il risultato è già un PDF — nessun round‑trip di conversione.
Prima vs. dopo, onestamente
| Come lo facevo prima | Con Merger.Join |
|
|---|---|---|
| Tempo per contratto | 5–10 minuti di clic | meno di 30 secondi end‑to‑end |
| Fallimento tipico | Pagine nell’ordine sbagliato, nessuno se ne accorge | Qualunque ordine dica la lista dei file, in modo ripetibile |
| Scalare a 100/giorno | Non è possibile — assumi una persona | Un solo worker, annoiato per la maggior parte del tempo |
| Codice da mantenere | Una pagina Confluence intitolata “Binder Process v4” | Una classe, ~70 righe |
| Output | Tre PDF e una preghiera | Un unico raccoglitore, con conteggio pagine registrabile |
La riga che mi interessa di più è quella del “fallimento”. La fusione manuale fallisce silenziosamente; il codice che registra il conteggio delle pagine fallisce rumorosamente.
Una storia reale da un piccolo team legal‑tech
Una startup di due persone con cui ho lavorato aveva una paralegale il cui mattino iniziava con l’assemblaggio dei contratti. Accordo Word, prezzi Excel, addendum PDF, incollati in un’app, caricati su DocuSign. Circa otto minuti per pacchetto, che a 30 pacchetti al giorno era praticamente tutta la sua mattina.
Hanno inserito il metodo di scansione della cartella nel servizio backend che già monitorava la loro email di ingresso. Venti secondi per pacchetto, più una riga di log con il conteggio delle pagine. La paralegale è passata a rivedere i contratti invece di assemblarli. Nessuno ha più spedito un raccoglitore in ordine sbagliato — non perché la libreria sia magica, ma perché la lista dei file è esplicita nel codice e puoi confrontarla.
string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";
var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");
Questo è l’intero integrazione. Tutto l’upstream (il listener email, il percorso di storage) era già in atto.
Cose che non mi servivano oggi ma mi serviranno domani
La stessa libreria fa un sacco di cose che non ho trattato perché l’articolo sarebbe diventato troppo lungo. In ordine approssimativo di utilizzo:
- Watermarks on the output per timbri “DRAFT” su copie pre‑firma.
- Page rotation per scansioni che arrivano in orizzontale.
- Custom page ordering quando l’ordine sorgente non è quello di consegna.
- PDF encryption per tutto ciò che va a una controparte esterna.
Tutto questo è disponibile dietro la stessa API Merger. La docs contiene l’elenco completo — volevo solo sottolineare che “merge” è l’opzione di base economica e il resto è disponibile quando serve.
Cosa direi al mio io del passato
Se stai per scrivere il tuo passo DOCX‑to‑PDF perché “è solo un metodo”, fermati. La conversione è la parte che marcisce silenziosamente — nuove funzionalità di Office, gestione di immagini scansionate, font incorporati, ecc. Lascia che qualcos’altro gestisca quella superficie e dedica il tuo venerdì pomeriggio a qualcosa che non sia l’ordinamento dei nomi file.
Dove andare dopo:
- Temporary license — necessaria per output senza filigrana
- Advanced merging options — JoinOptions, opzioni di salvataggio, compressione
- Supported formats — ben oltre i tre mostrati qui
- Sample projects on GitHub — incluso questo