Applicare una filigrana o un logo su ogni pagina di un documento sembra un lavoro da cinque minuti, finché non si esamina la dimensione del file risultante. L'approccio ovvio consiste nello scorrere le pagine e, su ciascuna di esse, ricreare gli stessi oggetti di testo o immagine. Visivamente il risultato è corretto, ma comporta uno spreco cumulativo. Una filigrana diagonale "BOZZA" disegnata direttamente su un report di cento pagine si traduce in cento copie degli stessi dati di tracciato e testo inseriti nei flussi dei contenuti, e il file salvato le conterrà tutte quante.
Un Form XObject è il costrutto fornito dal formato PDF proprio per evitare questo problema. Racchiude un elemento di contenuto riutilizzabile (un'intera pagina o un piccolo modello) in un singolo oggetto dotato di nome che può essere disegnato più volte in posizioni diverse. Il contenuto risiede nel file una sola volta. Ogni pagina che richiede il timbro contiene una breve istruzione del tipo "disegna l'XObject N qui, con questa trasformazione". Una filigrana su cento pagine aggiunge così un solo oggetto di contenuto al file invece di cento, facendo la differenza tra un documento che cresce linearmente con il numero di pagine e uno che mantiene dimensioni stabili. Filigrane, loghi, numerazioni di pagina e sigilli sono tutti esempi dello stesso problema, e il Form XObject è lo strumento corretto per ciascuno di essi.
Perché un unico oggetto memorizzato è preferibile a cento ridisegni
Il risparmio è strutturale, non estetico. Una pagina PDF viene renderizzata eseguendo il suo flusso di contenuti, ovvero una sequenza di operatori di disegno. Quando si ridisegna un timbro su ciascuna pagina, si aggiunge l'intera sequenza di operatori per quel timbro al flusso di ogni pagina, e i byte vengono duplicati per il numero di pagine. Un Form XObject sposta tali operatori in un unico flusso memorizzato una sola volta nel documento. Il riferimento conservato da ciascuna pagina è minimo: carica una matrice di trasformazione, richiama l'XObject e ripristina lo stato. Il numero di pagine cessa così di moltiplicare il costo degli elementi grafici.
Questo aspetto è fondamentale quando il timbro è pesante. Un sigillo vettoriale con centinaia di segmenti di tracciato, o la bitmap di un logo, hanno un costo di memorizzazione elevato. Memorizzando l'elemento una sola volta e referenziandolo, la parte più pesante viene pagata una sola volta e l'impatto per pagina si riduce a pochi byte di chiamata. Il risultato visivo sulla pagina è identico a un ridisegno diretto, ed è proprio questo l'obiettivo. Il lettore non noterà alcuna differenza; la dimensione del file decisamente sì.
Catturare una pagina in un XObject
PDFium crea l'oggetto riutilizzabile a partire da una pagina esistente. La sorgente può essere una pagina di un documento aperto, un piccolo PDF di una sola pagina contenente unicamente la grafica della filigrana o una pagina specifica di un file più ampio. CreateXObjectFromPage cattura il contenuto di tale pagina sorgente in un riferimento (handle) riutilizzabile associato al documento di destinazione su cui si applica il timbro.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
La firma del metodo è CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Il metodo restituisce nil in caso di errore invece di sollevare un'eccezione, perciò la verifica esplicita sopra indicata è obbligatoria. L'handle restituito è un oggetto TPdfXObject di cui si ha la proprietà, e i due vincoli sul ciclo di vita associati ad esso rappresentano l'aspetto che più spesso genera errori, perciò sono trattati nella sezione seguente.
Posizionare il timbro su una pagina
Un XObject catturato non produce alcun effetto da solo. Per visualizzarlo, se ne inserisce una copia sulla pagina corrente del documento tramite InsertFormObjectFromXObject. Tale chiamata restituisce l'oggetto di pagina sottostante, un tipo FPDF_PAGEOBJECT, e l'handle restituito consente di gesterne la posizione. Senza trasformazioni, il timbro viene posizionato all'origine nelle coordinate della pagina sorgente, punto in cui raramente si desidera collocarlo.
Poiché InsertFormObjectFromXObject inserisce una copia per ogni chiamata e restituisce ogni volta un nuovo oggetto pagina, è possibile tracciare lo stesso XObject più volte su una pagina con trasformazioni diverse, e il contenuto memorizzato continuerà a pesare una sola volta nel file. Un logo nell'angolo e una filigrana sfumata a tutta pagina possono derivare dallo stesso oggetto catturato.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Un dettaglio sulla proprietà rende sicura la rimozione. Una volta inserito, l'oggetto pagina appartiene alla pagina, non all'XObject. Liberare l'XObject in un secondo momento non rende non validi i posizionamenti già effettuati. Questo consente il corretto funzionamento della sequenza crea-posiziona-libera descritta di seguito.
La regola sul ciclo di vita dell'handle che genera errori
Due vincoli regolano l'handle dell'XObject, e ignorarne uno produce un errore all'apparenza slegato dalla causa reale. Primo, il documento sorgente deve essere attivo nel momento in cui si chiama CreateXObjectFromPage. La cattura legge il contenuto della pagina sorgente dal documento attivo, perciò quel documento e la relativa pagina devono essere aperti e validi alla creazione dell'handle. Secondo, aspetto che spesso sorprende, l'handle deve essere liberato prima della chiusura della pagina sorgente, e all'atto pratico prima di chiudere o liberare il documento sorgente da cui proviene.
Il motivo risiede nel fatto che l'XObject è un riferimento a una struttura di cui il documento sorgente mantiene la proprietà. Non si tratta di una copia autonoma e indipendente da poter conservare dopo la chiusura della sorgente. Chiudendo prima la sorgente, l'handle farebbe riferimento a un contenuto rimosso, perciò liberarlo successivamente, o qualsiasi altro utilizzo, opererebbe su memoria non più valida. Il sintomo è quello tipico di un handle sospeso: una violazione di accesso in fase di arresto, o corruzione intermittente che varia in base all'ordine di allocazione, con uno stack che punta al codice di pulizia invece che alla riga che ha effettivamente originato il problema. La soluzione è legata all'ordine di esecuzione, non a codice difensivo. Si crea l'XObject, lo si inserisce in ogni pagina in cui è richiesto, si libera l'XObject, e solo allora si chiude il documento sorgente. Il distruttore di TPdfXObject rilascia l'handle PDFium sottostante, perciò liberare il wrapper al momento corretto rappresenta l'unica responsabilità richiesta.
La matrice, e il significato dei suoi sei numeri
Il posizionamento corrisponde a una trasformazione affine 2D, la stessa utilizzata ovunque nel formato PDF per posizionare i contenuti (ISO 32000-1, sezione 8.3.4). È composta da sei numeri, indicati come a, b, c, d, e, f, ed esposti in PDFium sotto forma di record FS_MATRIX. Mappano un punto dallo spazio dell'oggetto allo spazio della pagina:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
È possibile compilare questi sei valori manualmente, ma farlo direttamente espone a errori di calcolo nella rotazione, poiché la rotazione combina insieme i parametri a, b, c, d. Il wrapper TPdfMatrix compone le operazioni comuni per conto tuo e applica le moltiplicazioni di volta in volta, perciò Translate, Scale e Rotate si collegano nell'ordine di chiamata. Una filigrana diagonale corrisponde a una rotazione seguita da una traslazione per ricentrarla; un logo nell'angolo è una scala seguita da una traslazione. Quando la matrice è pronta, si passa il suo valore a FPDFPageObj_SetMatrix(PageObj, M.Handle), dove M.Handle rappresenta la struttura FS_MATRIX sottostante. Il metodo a basso livello FPDFPageObj_Transform, che accetta direttamente i sei valori come double, è disponibile qualora si preferisca passare i numeri direttamente senza creare un wrapper.
Applicare il timbro su ogni pagina nell'ordine corretto
Il pattern completo unisce i vari passaggi rispettando l'ordine imposto dalla regola sul ciclo di vita. Si aprono entrambi i documenti, si cattura il timbro una sola volta, si scorrono le pagine di destinazione selezionandole in sequenza per inserire e posizionare una copia del timbro, si libera l'XObject, si salva e infine si chiude il documento sorgente.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
La struttura dei blocchi try svolge il compito fondamentale. Il blocco finally interno libera l'XObject prima che l'esecuzione possa raggiungere il finally esterno che libera Stamp, in questo modo l'handle viene sempre rilasciato mentre la sua sorgente è ancora attiva, anche in caso di eccezioni durante il ciclo. Impostando correttamente questa nidificazione, la gestione del ciclo di vita avviene in sicurezza. (Usa il selettore di pagina corrente esposto dalla tua build; la logica del ciclo rimane la medesima.)
L'applicazione di timbri è solo una parte di un toolkit più ampio per la creazione e la modifica del contenuto delle pagine. Se il timbro è un'immagine invece di una pagina catturata, la sezione convertire immagini in documenti PDF con PDFium ne illustra l'inserimento preventivo nel documento. E quando l'elemento da inserire insieme al timbro visibile è un file anziché inchiostro sulla pagina, la sezione lavorare con gli allegati PDF in Delphi ne illustra la gestione come file incorporati. Tutto questo è fornito con il PDFium Component per Delphi e C++Builder, insieme alle API di rendering, modifica e gestione documenti trattate altrove in questo blog.