Articolo tecnico

Divisione di documenti PDF con PDFium Component in Delphi

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