PDFium Component offre un unico metodo per la divisione dei PDF: ImportPages. Tutto il resto, sia che si tratti di isolare una singola pagina, tagliare lungo confini arbitrari o seguire la struttura dei segnalibri del documento, rappresenta solo modi diversi per decidere quali numeri di pagina inserire in ciascun file di output. I meccanismi rimangono gli stessi. Capire questo fin da subito evita molti errori di percorso
Come funziona il ciclo di divisione
Il pattern è lo stesso indipendentemente da come si divide il documento sorgente. Si crea una nuova istanza di TPdf, si chiama CreateDocument su di essa per inizializzare un PDF vuoto in memoria, si importano le pagine desiderate con ImportPages, si salva il risultato e infine si reimposta Active su False prima dell'iterazione successiva. Quest'ultimo passaggio è quello che spesso viene dimenticato: senza questo reset, la chiamata successiva a CreateDocument aggiunge pagine al documento ancora presente in memoria invece di iniziarne uno nuovo. L'istanza esterna di TPdf viene riutilizzata in tutte le iterazioni, mantenendo bassa la pressione sull'allocazione della memoria per i lavori di grandi dimensioni
Ecco come si presenta la divisione pagina per pagina ridotta all'essenziale:
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
PdfOut.ImportPages(Source, IntToStr(I), 1);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
PdfOut.SaveAs(OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Il parametro Range di ImportPages utilizza lo stesso formato di stringa che PDFium usa internamente: un elenco separato da virgole di numeri di pagina o intervalli delimitati da trattini, tutti a partire da 1. '3' importa la pagina 3. '1-5' importa le pagine da 1 a 5 in ordine. '2,5,8' importa queste tre pagine. Il terzo parametro è la posizione di inserimento a partire da 1 nel documento di destinazione; passando 1 le pagine importate verranno sempre inserite all'inizio di un file altrimenti vuoto, che è esattamente ciò che si desidera in questo caso
Divisione per intervalli di pagine
Quando il chiamante fornisce un elenco come 1-12,13-24,25-36, lo si analizza in coppie inizio/fine e si esegue lo stesso ciclo, costruendo la stringa dell'intervallo da ciascuna coppia:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeList[I], 1);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
La convalida prima di raggiungere ImportPages è fondamentale in questo contesto. ImportPages restituisce False quando un numero di pagina nella stringa dell'intervallo supera Source.PageCount, ma non solleva un'eccezione e non produce un file di output parziale rilevabile solo dal nome. È importante verificare il valore di ritorno di SaveAs e registrare gli errori separatamente; un intervallo che produce un file di output vuoto non appare palesemente errato finché l'utente non prova ad aprirlo
Divisione in corrispondenza dei segnalibri
Il terzo approccio utilizza la struttura stessa del documento piuttosto che un elenco fornito esternamente. Ogni segnalibro di primo livello contiene un numero di pagina di destinazione; la sezione che definisce si estende da quella pagina fino a una pagina prima di quella del segnalibro successivo, oppure fino alla fine del documento per l'ultima voce
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeStr, 1);
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Un documento privo di segnalibri non rappresenta una condizione di errore da segnalare all'utente; significa semplicemente che questa modalità di divisione non ha dati su cui lavorare. Il controllo Length(Bm) = 0 gestisce questa situazione in modo silenzioso. Ciò che invece merita attenzione è quando il numero di pagina di un segnalibro si trova al di fuori dell'intervallo del documento, cosa che si verifica nei file malformati in cui l'indice dei segnalibri non è mai stato aggiornato dopo l'eliminazione di alcune pagine. Il controllo dei limiti su StartPage ed EndPage salta queste voci invece di passare un intervallo errato a ImportPages
Denominazione dei file di output e reset di Active
La sicurezza dei nomi dei file derivati dai segnalibri richiede un'attenzione esplicita. I titoli dei segnalibri possono contenere caratteri validi in una stringa PDF ma non in un percorso del file system. Come minimo, è necessario sostituire la barra (slash), la barra rovesciata (backslash) e i due punti prima di generare il percorso di output. Su Windows sono vietati anche i caratteri *, ?, ", <, > e |; un semplice ciclo su un set di caratteri fisso è sufficiente per gestirli senza ricorrere alle espressioni regolari
La riga Active := False alla fine di ogni iterazione merita particolare attenzione poiché è l'unico requisito non ovvio in questo pattern. CreateDocument non chiude implicitamente il documento aperto. Se Active è ancora True quando viene eseguito nuovamente CreateDocument, PDFium scarta il documento corrente e ne avvia uno nuovo senza segnalare errori, ma questo comportamento è definito dall'implementazione nei casi limite ed è preferibile specificare l'intento reimpostando esplicitamente lo stato. Può essere considerato come il corrispettivo del blocco try/finally: il blocco finally libera l'oggetto esterno, mentre Active := False reimposta lo stato interno del documento tra le iterazioni del ciclo
L'uso della memoria durante un processo di divisione di grandi dimensioni rimane stabile con questo approccio, poiché non si conserva in memoria più di un documento di output alla volta. Il documento sorgente rimane aperto e in sola lettura per tutta la durata del processo; ImportPages copia i dati delle pagine nel nuovo documento senza modificare l'originale. Se il sorgente è crittografato, aprendolo con la relativa password prima del ciclo, le pagine copiate in ciascun file di output risulteranno decrittografate, che è solitamente il comportamento corretto per i file divisi destinati a diversi destinatari
Un'ultima considerazione su SaveAs: restituisce un valore Boolean. Una directory di output inesistente, un percorso con caratteri non consentiti dal sistema operativo o una condizione di disco pieno causeranno la restituzione di False da parte di SaveAs senza sollevare alcuna eccezione. In un processo batch che divide un documento di 200 pagine in altrettanti singoli file, un errore silenzioso alla pagina 147 è facile da ignorare. È opportuno verificare il valore di ritorno ad ogni chiamata e contare i successi rispetto al totale atteso al termine del ciclo
I metodi ImportPages e CreateDocument mostrati qui fanno parte di PDFium Component per Delphi e C++Builder
Il ciclo aggiornato controlla il risultato di `SaveAs`, ripristina `Active` dopo ogni documento e supporta sia intervalli di pagine sia confini individuati dai segnalibri