PDFium Component espone l'unione dei PDF attraverso un singolo metodo: ImportPages. Il pattern è sempre lo stesso: creare un documento di destinazione vuoto, aprire ciascun file di origine, chiamare ImportPages per copiare le pagine, chiudere l'origine e ripetere. Al termine del ciclo, SaveAs scrive il risultato sul disco. Non esiste una modalità di unione speciale o una configurazione da attivare. La complessità risiede nei casi limite, e ce ne sono alcuni che possono creare problemi senza preavviso
Il ciclo principale
Due istanze di TPdf sono tutto ciò di cui hai bisogno. Una contiene il documento di destinazione, creato vuoto con CreateDocument. L'altra apre a turno ciascun file di origine. Di seguito è riportata una procedura che accetta un elenco di percorsi di file e scrive l'output unito in un singolo percorso:
procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
PdfDest, PdfSrc: TPdf;
InsertAt, I: Integer;
begin
PdfDest := TPdf.Create(nil);
PdfSrc := TPdf.Create(nil);
try
PdfDest.CreateDocument;
InsertAt := 1; // ImportPages uses 1-based destination position
for I := 0 to FileList.Count - 1 do
begin
PdfSrc.FileName := FileList[I];
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);
PdfDest.ImportPages(
PdfSrc,
'1-' + IntToStr(PdfSrc.PageCount), // full document range
InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
end;
PdfDest.SaveAs(OutputPath);
finally
PdfSrc.Free;
PdfDest.Free;
end;
end;
Due aspetti di questo codice sono facili da trascurare a una prima lettura. Il primo è il modo in cui PDFium segnala i fallimenti di caricamento. Active := True non solleva mai un'eccezione: se il file è mancante, danneggiato o protetto da password, PDFium gestisce l'errore internamente e lascia Active a False. Senza il controllo esplicito alla riga 10, un file non valido verrebbe escluso silenziosamente dall'unione senza alcuna indicazione nell'output. Il PDF finale avrebbe meno pagine del previsto e non sapresti quale file sia stato il colpevole
Il secondo è il contatore InsertAt. Il terzo argomento di ImportPages è la posizione a base 1 nella destinazione in cui viene inserita la prima pagina importata. Partendo da 1 si posiziona il primo documento di origine all'inizio di un file altrimenti vuoto. Dopo ogni origine, il contatore avanza di PdfSrc.PageCount, in modo che il gruppo successivo di pagine venga accodato dopo l'ultimo. Dimenticarsi di incrementarlo farà sì che ogni origine successiva sovrascriva le pagine alla posizione 1, lasciando solo l'ultimo documento dell'elenco
Intervalli di pagine selettivi
Non è necessario importare tutte le pagine da un'origine. La stringa di intervallo passata come secondo argomento segue un semplice formato con virgole e trattini: "1-3" seleziona le pagine da 1 a 3, "2,4,6" seleziona tre pagine specifiche e "1-" indica dalla pagina 1 alla fine del documento. Gli intervalli possono essere combinati in una singola stringa, ad esempio "1-3,5,7-" salta le pagine 4 e 6. Un dettaglio importante: i numeri si riferiscono sempre alle pagine nel documento di origine, a partire da 1, indipendentemente da dove queste pagine finiranno nella destinazione. Se si desiderano le pagine da 40 a 50 di un catalogo di 200 pagine, la stringa dell'intervallo sarà "40-50", non una posizione relativa a ciò che è già presente nella destinazione
// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active := True;
if PdfSrc.Active then
begin
// Page 1 is the cover; pages 3-5 are the summary
PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
Inc(InsertAt, 4); // 1 cover + 3 summary pages = 4 pages added
PdfSrc.Active := False;
end;
Quando si calcola l'incremento di InsertAt, contare le pagine effettivamente importate, non il numero totale di pagine del file di origine. Se si passa '1,3-5', sono state importate 4 pagine, quindi si avanza di 4. Avanzare di PdfSrc.PageCount lascerebbe uno spazio vuoto nelle posizioni di destinazione, posizionando il successivo documento di origine più avanti nel file rispetto a quanto previsto
Cosa viene preservato da ImportPages e cosa no
Le pagine copiate da ImportPages mantengono intatto il loro contenuto visibile. Testo, grafica vettoriale, immagini raster, font incorporati e form XObject vengono tutti trasferiti como parte degli stream di contenuto della pagina. Anche le annotazioni a livello di pagina, inclusi commenti, evidenziature e tratti di penna, vengono trasferite perché memorizzate all'interno del dizionario della pagina anziché a livello di documento
Document-level metadata
I metadati a livello di documento sono una storia diversa. Le stringhe relative a titolo, autore, oggetto e parole chiave nel dizionario Info dell'origine rimangono escluse. Il documento di destinazione inizia con metadati vuoti dopo CreateDocument, quindi se l'output unito richiede che questi campi siano popolati, è necessario assegnarli direttamente a PdfDest prima di chiamare SaveAs. Le proprietà Title, Author, Subject, Keywords e Creator su TPdf accettano stringhe semplici e vengono scritte nel dizionario Info al momento del salvataggio
I campi modulo interattivi sono più complessi. Le definizioni dei campi AcroForm risiedono in un dizionario a livello di documento anziché all'interno dei singoli stream di pagina. Quando ImportPages copia una pagina che contiene campi modulo, l'aspetto visivo di tali campi viene trasferito perché renderizzato nello stream di contenuto della pagina, ma i widget dei campi che li rendono interattivi fanno parte della struttura AcroForm e non vengono copiati. In un'unione tipica, un campo di testo di un documento di origine mostrerà il valore che aveva al momento dell'importazione, ma non sarà modificabile nel file unito. Se è necessario che i campi rimangano compilabili, è consigliabile eseguirne l'appiattimento (flattening) in ciascun documento di origine prima dell'importazione: questo processo incorpora i valori correnti nello stream di contenuto e rimuove l'overlay interattivo, garantendo un risultato visivo pulito senza widget non funzionanti nell'output
File di origine crittografati
I documenti di origine protetti da password si aprono nello stesso modo di quelli non crittografati, con una proprietà aggiuntiva da impostare prima. Assegna la password a PdfSrc.Password prima di impostare Active := True, e PDFium la utilizzerà durante l'apertura:
PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.Create('Wrong password or file cannot be opened');
PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
Una password errata causa lo stesso esito silenzioso Active = False di un file mancante, quindi il controllo esplicito è altrettanto necessario in questo caso. La crittografia non si trasferisce alla destinazione: le pagine importate da un'origine protetta arrivano nella destinazione come contenuto non protetto. Se anche l'output unito richiede la crittografia, configurarla su PdfDest prima di chiamare SaveAs
Salvataggio del risultato
Il metodo SaveAs su TPdf accetta un percorso di file o un TStream. Per la maggior parte delle unioni, l'overload del file è quello consigliato:
PdfDest.SaveAs('merged-output.pdf');
Il secondo argomento opzionale è un valore TSaveOption che controlla la modalità di salvataggio. Il valore predefinito, saNone, scrive un aggiornamento incrementale se il documento è stato caricato da un file, o una riscrittura completa se è stato creato da zero. Poiché una destinazione creata con CreateDocument è sempre nuova, l'output sarà un file a revisione singola compatto. Il terzo argomento, TPdfVersion, consente di fissare l'intestazione della versione del PDF quando si hanno consumatori a valle che richiedono una versione specifica; lasciarlo a pvUnknown consente a PDFium di scegliere in base al contenuto
I metodi ImportPages e SaveAs mostrati qui fanno parte del Componente PDFium Component per Delphi e C++Builder