Technical Article

Imposizione N-up e riordino delle pagine con PDFium

Unione e divisione sono le due operazioni sulle pagine a cui si pensa per prime, e coprono molte esigenze. Tuttavia, non coprono tutto. Esiste una famiglia separata di attività che riorganizza le pagine anziché spostare interi file: disporre quattro diapositive su un unico foglio, trascinare una pagina dal fondo all'inizio di un documento o estrarre le pagine 3, 7 e 12 in un breve estratto senza modificare il resto. PDFium offre tre metodi specifici per questo scopo, e ognuno si comporta diversamente dalle operazioni di unione e divisione tradizionali. Questo articolo illustra il loro funzionamento, la gestione dell'output e un dettaglio sulla proprietà che ha causato crash in produzione.

I tre metodi sono ImportNPagesToOne per l'imposizione N-up, MovePages per il riordino in loco e ImportPagesByIndex per l'estrazione di sottoinsiemi. L'unione accoda i documenti e definisce un conteggio di pagine pari alla somma degli input. La divisione scrive diversi file di output a partire da un solo input. Le tre operazioni qui descritte si collocano a metà strada: una modifica il numero di pagine di origine che condividono un foglio, una cambia l'ordine all'interno di un singolo documento e l'ultima copia una serie di pagine in un altro documento. Conoscere queste distinzioni evita di dover eseguire complesse operazioni di unione ed eliminazione quando basterebbe una singola chiamata.

Cosa fa effettivamente l'imposizione N-up

L'imposizione è il termine tecnico utilizzato in prestampa per indicare la disposizione di diverse pagine di origine su un foglio più grande, in modo che il risultato stampato e piegato sia nell'ordine corretto. La versione comune è la dispensa a 2 pagine, il libretto a 4 pagine o il foglio provini che racchiude una dozzina di miniature in una pagina. PDFium gestisce la geometria con una singola chiamata:

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

I parametri NumX e NumY definiscono la griglia. Un valore di 2, 1 affianca due pagine di origine; 2, 2 ne organizza quattro in un layout a quadranti; 4, 3 crea una griglia provini da dodici pagine. PDFium legge le pagine di origine in ordine, ridimensiona ciascuna per adattarla alla sua cella e popola la griglia da sinistra a destra e dall'alto in basso, avviando un nuovo foglio di output quando la griglia corrente è completa. Le pagine sorgente non vengono modificate. Il risultato restituito è un nuovo documento le cui pagine sono composte dalle precedenti.

La dimensione di output è espressa in punti, non in pixel

I valori OutputWidth e OutputHeight sono unità utente PDF, e un'unità utente PDF equivale a un punto, ovvero un settanuduesimo di pollice. L'unità definisce la dimensione fisica del foglio di output e non ha alcuna relazione con i pixel dello schermo o la risoluzione di rendering (DPI). Questo è il punto in cui è più comune commettere errori nell'imposizione, poiché gli sviluppatori abituati alle bitmap tendono a utilizzare i pixel, ottenendo un foglio delle dimensioni di un francobollo o di un cartellone pubblicitario.

Le dimensioni da ricordare sono le due più utilizzate. Il formato US Letter corrisponde a 612 per 792 punti, poiché 8,5 pollici per 72 equivale a 612 e 11 pollici per 72 a 792. Il formato A4 misura circa 595 per 842 punti, a partire dalle sue dimensioni di 210 per 297 millimetri. L'intestazione stessa del binding enuncia chiaramente la regola secondo cui un'unità equivale a un settantaduesimo di pollice, e include una costante PointsPerInch impostata a 72 qualora si preferisca calcolare le dimensioni a partire dai pollici invece di inserire valori letterali.

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

L'handle restituito deve essere liberato dall'utente

Esaminiamo nuovamente la firma. Il metodo ImportNPagesToOne restituisce un oggetto TPdf, non un valore booleano. Questo valore di ritorno rappresenta un handle di documento completamente nuovo, allocato separatamente dalla sorgente, e appartiene al chiamante. L'oggetto TPdf sorgente su cui è stato richiamato il metodo rimane inalterato e conserva il proprio handle; il documento composito è un secondo oggetto indipendente. Se si lascia che l'oggetto TPdf restituito esca dall'ambito di validità senza essere liberato, si verificherà una perdita (leak) dell'intero documento PDFium.

L'errore più rischioso avviene nel senso opposto. Internamente, il metodo richiede a PDFium un nuovo FPDF_DOCUMENT tramite FPDF_ImportNPagesToOne, quindi racchiude tale handle all'interno dell'oggetto TPdf restituito in modo che la durata del wrapper gestisca quella dell'handle. Da quel momento esiste un solo proprietario per l'handle, e un solo punto in cui deve essere chiuso: quando si chiama Free sull'oggetto restituito. Un percorso di errore non corretto che rilascia il wrapper e chiama anche FPDF_CloseDocument sull'handle originale provoca la doppia chiusura dello stesso documento. Si tratta di una doppia liberazione (double free), il bug specifico che ha interessato un chiamante in precedenza. La regola per evitarlo è semplice: chiudi il documento in un unico percorso, liberando il TPdf restituito dal metodo, senza tentare di accedere direttamente all'handle gestito dal wrapper.

Da questo derivano due corollari. In primo luogo, il metodo restituisce nil se PDFium rifiuta i parametri, ad esempio in presenza di un valore pari a zero su uno degli assi della griglia o di un errore di allocazione, quindi è opportuno inserire una verifica di tipo nil prima di utilizzare il risultato. In secondo luogo, inizializza la variabile di output a nil prima del blocco try e liberala nella sezione finally, come mostrato nell'esempio precedente, in modo che un errore intermedio non causi il rilascio di un riferimento non definito o l'omissione della liberazione.

Riordinare le pagine senza riscriverle

L'imposizione crea un nuovo documento. Il riordino modifica il documento in loco. Il metodo MovePages sposta un gruppo di pagine dalle posizioni correnti a una destinazione, riorganizzando gli altri elementi intorno al blocco spostato affinché il numero totale di pagine rimanga inalterato:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Gli indici sono a base zero. PageIndices indica le pagine da spostare, nell'ordine finale desiderato, e DestPageIndex è l'indice in cui si posiziona la prima pagina spostata. Poiché PDFium sposta le pagine senza copiarne o ricomprimerne il contenuto, l'operazione è rapida e senza perdita di dati: gli oggetti pagina conservano i propri stream, risorse e qualità. Questa è la funzione utilizzata per i pannelli di riordino tramite trascinamento, in cui l'utente sposta una miniatura in una nuova posizione e il programma applica la modifica con un unico spostamento. Restituisce False se un indice non è valido, quindi è necessario verificare il risultato senza dare per scontata l'operazione.

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Estrarre un sottoinsieme tramite indice

La terza operazione copia un gruppo definito di pagine da un documento all'altro. Il metodo ImportPagesByIndex accetta il documento sorgente e un array di indici a base zero, inserendo tali pagine nel documento di destinazione alla posizione indicata:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

La chiamata deve essere eseguita sul documento di destinazione, passando la sorgente come primo argomento. PageIndices indica le pagine sorgente da importare, nell'ordine desiderato; InsertAt rappresenta la posizione a base zero nel documento di destinazione in cui inserire la prima pagina importata (ad esempio, 0 la inserisce prima della pagina iniziale esistente, e la dimensione del documento cresce di conseguenza). Un array vuoto importa tutte le pagine, realizzando una copia completa. Restituisce False se uno degli indici sorgente non è corretto.

Qui risiede la differenza principale rispetto alla divisione. La divisione scrive file separati su disco in un'unica operazione. Il metodo ImportPagesByIndex esegue l'opposto: raccoglie le pagine indicate in un singolo documento di destinazione in memoria, che viene poi salvato una volta sola. Se la richiesta consiste nell'estrarre le pagine 3, 7 e 12 in un unico breve PDF, questa è la soluzione diretta, che richiama internamente FPDF_ImportPagesByIndex.

var
  Source, Excerpt: TPdf;
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Integrare il codice in modo pulito

Il flusso complessivo è simile per tutte e tre le operazioni: apri il documento sorgente impostando FileName e attivando Active su True, esegui l'operazione, salva con SaveAs e rilascia le risorse allocate. L'unico passaggio che richiede attenzione consiste nell'individuare quali chiamate allocano un nuovo documento. MovePages modifica il documento esistente, quindi vi è un solo oggetto da liberare. ImportPagesByIndex scrive in una destinazione creata esplicitamente, costringendo a liberare sia la sorgente sia il documento di destinazione aperto. ImportNPagesToOne si comporta diversamente, poiché il nuovo documento rappresenta il valore di ritorno del metodo anziché un oggetto creato dall'utente; dimenticare che si tratta di un handle separato appartenente al chiamante causa le perdite di memoria e le doppie chiusure. Inizializza il risultato a nil, verificalo dopo la chiamata e liberalo in un unico percorso.

Se il tuo obiettivo consiste nell'unire file interi piuttosto che riorganizzare le pagine, consulta la sezione unione di più file PDF in un singolo documento. Se si desidera compiere l'operazione contraria, dividendo un documento in più file, leggi la guida sulla divisione di documenti PDF in più file. I metodi di imposizione e riordino qui descritti sono inclusi all'interno di PDFium Component per Delphi e C++Builder, insieme alle API di caricamento, rendering e modifica trattate in altre sezioni di questo blog.