Articolo tecnico

Imposizione N-up e riordino pagine con PDFium

Unione e divisione sono le due operazioni di pagina a cui tutti pensano per prime, e coprono parecchio terreno. Non coprono tutto. Esiste una famiglia di lavoro a parte che riorganizza le pagine invece di spostare interi file: disporre quattro diapositive su un unico foglio per una dispensa, trascinare una pagina dal fondo di un documento all'inizio, oppure estrarre le pagine 3, 7 e 12 in un breve estratto senza toccare il resto. PDFium espone tre metodi esattamente per questo, e ciascuno si comporta diversamente dall'unione e dalla divisione che già conoscete. Questo articolo illustra cosa fanno, dove vivono i punti di output e un dettaglio di proprietà che sul campo ha già causato un crash

I tre sono ImportNPagesToOne per l'imposizione N-up, MovePages per il riordino sul posto e ImportPagesByIndex per l'estrazione di un sottoinsieme. L'unione impila i documenti uno dopo l'altro e lascia il numero di pagine pari alla somma degli ingressi. La divisione scrive più file di output da un solo ingresso. Le tre operazioni di cui parliamo qui stanno nel mezzo: una cambia quante pagine sorgente condividono un foglio, una cambia l'ordine dentro un unico documento e una copia una manciata scelta di pagine in un altro documento. Sapere quale sia quale vi risparmia di forzare un balletto di unione e cancellazione dove basterebbe una sola chiamata

Cosa fa davvero l'imposizione N-up

Imposizione è il termine prestampa per disporre più pagine sorgente su un foglio più grande, così che il risultato stampato e piegato si legga nell'ordine giusto. La versione quotidiana è la dispensa 2-up, la segnatura di libretto 4-up o il provino a contatto che fa stare una dozzina di miniature in una pagina. PDFium gestisce la geometria con una sola chiamata:

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

NumX e NumY descrivono la griglia. Un valore di 2, 1 mette due pagine sorgente affiancate; 2, 2 ne impacchetta quattro in una disposizione a quadranti; 4, 3 costruisce un provino a dodici posti. PDFium legge le pagine sorgente in ordine, riduce ciascuna in scala perché entri nella sua cella e riempie la griglia da sinistra a destra e dall'alto in basso, iniziando un nuovo foglio di output ogni volta che la griglia corrente è piena. Le pagine sorgente non vengono modificate. Ciò che ottenete indietro è un nuovo documento le cui pagine sono composizioni

Diagramma dell'imposizione N-up di PDFium che impacchetta sei pagine sorgente su fogli compositi US Letter in versione due-up e quattro-up
ImportNPagesToOne impacchetta NumX per NumY pagine sorgente su fogli dimensionati in punti, riempiendo da sinistra a destra e dall'alto in basso prima di iniziare un nuovo foglio

La dimensione di output è in punti, non in pixel

OutputWidth e OutputHeight sono unità utente PDF, e una unità utente PDF è un punto, cioè un settantaduesimo di pollice. L'unità dichiara la dimensione fisica del foglio di output e non ha nulla a che vedere con i pixel dello schermo o con i DPI di rendering. Questo è il punto in assoluto più comune in cui si sbaglia un'imposizione, perché uno sviluppatore abituato alle bitmap pensa a un conteggio di pixel e si ritrova con un foglio grande come un francobollo o come un cartellone pubblicitario

I numeri che vale la pena memorizzare sono i due formati di pagina che userete di più. US Letter è 612 per 792 punti, perché 8,5 pollici per 72 fa 612 e 11 pollici per 72 fa 792. A4 è circa 595 per 842 punti, a partire dalle sue dimensioni di 210 per 297 millimetri. L'intestazione stessa del binding enuncia la regola senza giri di parole, cioè che una unità è un settantaduesimo di pollice, e l'unit fornisce una costante PointsPerInch pari a 72 se preferite calcolare una dimensione a partire dai pollici nel codice anziché scrivere il valore letterale

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;

    // Quattro pagine sorgente per foglio Letter, griglia 2 per 2.
    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;   // vedete la sezione seguente: questo è obbligatorio
    Source.Free;
  end;
end;

L'handle restituito spetta a voi liberarlo

Rileggete la firma. ImportNPagesToOne restituisce un TPdf, non un Boolean. Quel valore di ritorno è un handle di documento nuovo di zecca, allocato separatamente dalla sorgente, e il chiamante ne è proprietario. Il TPdf sorgente su cui avete chiamato il metodo resta intatto e possiede ancora il proprio handle; il composito è un secondo oggetto indipendente. Se lasciate che il TPdf restituito esca dallo scope senza liberarlo, perdete un intero documento PDFium

L'errore più pericoloso va nella direzione opposta. Sotto il cofano, il metodo chiede a PDFium un nuovo FPDF_DOCUMENT tramite FPDF_ImportNPagesToOne, poi avvolge quell'handle grezzo dentro il TPdf restituito, così che la vita del wrapper governi quella dell'handle. Da quel momento in poi esiste esattamente un proprietario dell'handle, ed esattamente un punto in cui va chiuso: quando fate Free sull'oggetto restituito. Un percorso di errore distratto che libera il wrapper e chiama anche FPDF_CloseDocument sull'handle grezzo che aveva catturato chiude due volte lo stesso documento PDFium. Questa è una doppia liberazione, ed è proprio il bug che una volta ha morso un chiamante qui. La regola che lo previene è breve. Chiudete il documento su un solo percorso, liberando il TPdf che il metodo vi ha consegnato, e non allungate mai la mano oltre il wrapper per chiudere l'handle che ha già adottato

Da qui discendono due corollari. Primo, il metodo restituisce nil quando PDFium rifiuta gli argomenti, per esempio uno zero su uno dei due assi della griglia o un fallimento di allocazione, quindi un controllo su nil va messo prima di toccare il risultato. Secondo, inizializzate la vostra variabile di output a nil prima del try e liberatela nel finally, come fa l'esempio qui sopra, così che un guasto a metà strada non possa lasciarvi a liberare un riferimento indefinito o a saltare del tutto la liberazione

Diagramma del ciclo di vita dell'handle TPdf restituito da ImportNPagesToOne di PDFium, con il percorso sicuro a liberazione singola contro il percorso di crash per doppia liberazione
Il TPdf restituito è un secondo documento di proprietà del chiamante: inizializzatelo a nil, controllatelo e liberate il wrapper su un solo percorso

Riordinare le pagine senza riscriverle

L'imposizione costruisce un nuovo documento. Il riordino modifica un documento sul posto. MovePages solleva un insieme di pagine dalle loro posizioni correnti e le deposita in una destinazione, spostando tutto il resto attorno al blocco mosso in modo che il numero di pagine resti lo stesso:

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

Gli indici partono da zero. PageIndices elenca le pagine da spostare, nell'ordine in cui devono finire, e DestPageIndex è l'indice su cui atterra la prima pagina spostata quando lo spostamento si assesta. Poiché PDFium ricolloca le pagine anziché copiarne e ricomprimerne il contenuto, l'operazione è economica e senza perdite: gli oggetti pagina mantengono i propri stream, le proprie risorse e la propria fedeltà. È questa la chiamata dietro un pannello pagine con riordino per trascinamento, dove un utente porta una miniatura in una nuova posizione e voi confermate il nuovo ordine con un solo spostamento. Restituisce False quando un indice è fuori intervallo, quindi convalidate il risultato invece di dare per scontato che il riordino sia andato a buon fine

PDFium Component: mappa di riordino MovePages che sposta la pagina quattro di un report di cinque pagine all'indice zero mentre le pagine restanti scalano di una posizione
MovePages solleva la pagina 4 fino all'indice 0 mentre le pagine restanti scalano di una posizione e il numero di pagine resta cinque
var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Sposta l'ultima pagina (indice 4 in un file di 5 pagine) in testa.
    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 per indice

La terza operazione copia un insieme esplicito di pagine da un documento a un altro. ImportPagesByIndex prende il documento sorgente e un array di indici a base zero, e inserisce quelle pagine nella destinazione in una posizione scelta:

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

La chiamate sul documento di destinazione e passate la sorgente come primo argomento. PageIndices nomina le pagine sorgente da prelevare, nell'ordine che volete; InsertAt è la posizione a base zero nella destinazione in cui va la prima pagina importata, quindi 0 le mette prima della pagina iniziale esistente e il numero di pagine corrente della destinazione le accoda in fondo. Un array vuoto importa ogni pagina, il che rende la chiamata una copia completa quando ne avete bisogno. Restituisce False se un qualunque indice è fuori intervallo nella sorgente

È qui che conta il contrasto con la divisione. La divisione scrive file separati, una sola operazione che produce molti output su disco. ImportPagesByIndex fa il lavoro di forma opposta: raccoglie un insieme scelto di pagine in un unico documento di destinazione in memoria, che poi salvate una sola volta. Quando il compito è "dammi le pagine 3, 7 e 12 come un unico PDF breve", questa è la via diretta, e sotto avvolge FPDF_ImportPagesByIndex

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // avvia una destinazione vuota

    // Estrae le pagine 3, 7 e 12 (in base zero 2, 6, 11) nell'estratto.
    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;

Mettere tutto insieme in modo pulito

La forma da capo a fondo è la stessa per tutte e tre: aprite la sorgente impostando FileName e portando Active a True, eseguite l'operazione, salvate con SaveAs e liberate ciò che possedete. L'unico ramo che richiede attenzione è quali chiamate allocano un nuovo documento. MovePages muta il documento che già tenete, quindi c'è un solo oggetto da liberare. ImportPagesByIndex scrive in una destinazione che avete creato voi, quindi liberate la sorgente e la destinazione che avete aperto. ImportNPagesToOne è l'eccezione, perché il nuovo documento è il valore di ritorno del metodo anziché qualcosa che avete costruito, e dimenticare che si tratta di un handle separato e di proprietà del chiamante è il modo in cui accadono sia la perdita di memoria sia la doppia liberazione. Inizializzate il risultato a nil, controllatelo dopo la chiamata e liberatelo su un solo percorso

Se il lavoro che avete davvero è combinare interi file anziché riorganizzare pagine, vedete unire più file PDF in un solo documento. Se è il contrario, cioè spezzare un documento in più file, vedete dividere documenti PDF in più file. I metodi di imposizione e riordino descritti qui sono distribuiti come parte di PDFium Component per Delphi e C++Builder, insieme alle API di caricamento, rendering e modifica trattate altrove in questo blog