Articolo tecnico

Timbri di pagina riutilizzabili tramite Form XObject con PDFium

Apporre una filigrana o un logo su ogni pagina di un documento sembra un lavoro di cinque minuti finché non apri il risultato in un ispettore delle dimensioni del file. L'approccio ovvio è scorrere le pagine e, su ciascuna di esse, costruire di nuovo gli stessi oggetti di testo o immagine. Visivamente funziona, ma è dispendioso in un modo che si accumula. Una filigrana diagonale "BOZZA" disegnata direttamente su un rapporto di cento pagine equivale a cento copie dello stesso tracciato e degli stessi dati testuali che si trovano nei flussi di contenuto, e il file salvato le contiene tutte

Un Form XObject è il costrutto che il PDF fornisce per evitare esattamente questo. Avvolge una porzione di contenuto riutilizzabile, un'intera pagina o un piccolo modello, in un singolo oggetto con nome che può essere dipinto molte volte in molte posizioni. Il contenuto risiede nel file una sola volta. Ogni pagina che desidera il timbro contiene una breve istruzione che dice "dipingi l'XObject N qui, con questa trasformazione". Una filigrana di cento pagine aggiunge quindi un solo oggetto di contenuto al file anziché cento, ed è questa la differenza tra un documento che cresce linearmente con il numero di pagine e uno che non lo fa. Filigrane, timbri con logo, modelli per numeri di pagina e sigilli hanno tutti la stessa forma come problema, e il Form XObject è lo strumento giusto per ciascuno di essi

Perché un singolo oggetto memorizzato batte cento ridisegni

Il risparmio è strutturale, non cosmetico. Una pagina PDF viene renderizzata eseguendo il suo flusso di contenuti, una sequenza di operatori di disegno. Quando ridisegni un timbro per ogni pagina, stai aggiungendo l'intera sequenza di operatori per quel timbro al flusso di ogni pagina, e i byte vengono duplicati tante volte quante sono le pagine. Un Form XObject sposta quegli operatori in un unico flusso memorizzato una sola volta nel documento. Il riferimento che ogni singola pagina conserva è piccolo: applica una matrice di trasformazione, invoca l'XObject e ripristina lo stato. Il conteggio delle pagine non moltiplica più il costo della grafica

Questo è particolarmente importante quando il timbro è pesante. Un sigillo vettoriale con centinaia di segmenti di tracciato, o una bitmap di un logo, è costoso da memorizzare. Memorizzato una volta e poi referenziato, la parte pesante viene pagata una sola volta e il sovraccarico per pagina è di pochi byte per l'invocazione. Il risultato visivo sulla pagina è identico a un ridisegno diretto, che è proprio il punto. Il lettore non noterà la differenza; la dimensione del file decisamente sì

Catturare una pagina in un XObject

PDFium costruisce l'oggetto riutilizzabile da una pagina esistente. L'origine è una pagina in qualche documento che hai aperto, un piccolo PDF di una pagina che contiene solo la grafica della tua filigrana, oppure una pagina specifica di un file più grande. CreateXObjectFromPage cattura il contenuto di quella pagina sorgente in un handle riutilizzabile che appartiene al documento di destinazione, quello che stai timbrando

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // one page of artwork
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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 è CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Il metodo solleva un'eccezione se il documento sorgente non è Active, e restituisce nil invece di generare un'eccezione quando PDFium non può costruire l'oggetto, quindi il controllo esplicito mostrato sopra non è opzionale. L'handle restituito è un TPdfXObject di tua proprietà, e i due vincoli sulla sua durata (lifetime) ad esso associati sono la parte di tutto questo esercizio che solitamente inganna le persone, per cui meritano una sezione dedicata più avanti

Posizionare il timbro su una pagina

Un XObject catturato non fa nulla da solo. Per farlo apparire, ne inserisci una copia sulla pagina corrente del documento, quella selezionata dalla proprietà basata su 1 PageNumber, con InsertFormObjectFromXObject. Quella chiamata restituisce l'oggetto pagina sottostante, un FPDF_PAGEOBJECT, e l'handle restituito è il modo in cui posizioni il collocamento. Senza una trasformazione, il timbro atterra all'origine nelle coordinate stesse della pagina sorgente, che è raramente il punto in cui lo desideri

Poiché InsertFormObjectFromXObject inserisce una copia per chiamata e restituisce un nuovo oggetto pagina ogni volta, puoi dipingere lo stesso XObject più volte su una pagina con trasformazioni diverse, e il contenuto memorizzato viene ancora contato una sola volta nel file. Un logo nell'angolo e una tenue filigrana a tutta pagina possono derivare dallo stesso oggetto catturato

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
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);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // commit this page's edits to its content stream
  // if not Dest.SaveAs(...) then ... when every page is done.
end;

Due dettagli di manutenzione rendono questo sicuro. Primo, una volta inserito, l'oggetto pagina appartiene alla pagina, non all'XObject. Liberare l'XObject in un secondo momento non invalida i posizionamenti che hai già effettuato. Questo è ciò che permette all'ordine crea-posiziona-libera descritto di seguito di funzionare. Secondo, l'inserimento e il posizionamento modificano solo l'elenco degli oggetti della pagina in memoria; UpdatePage è ciò che serializza nuovamente quell'elenco nel flusso di contenuto della pagina, quindi una pagina che modifichi senza chiamarlo viene salvata come se il timbro non fosse mai stato inserito

La regola sulla durata dell'handle che inganna le persone

Due vincoli governano l'handle XObject, e ignorarne uno produce un fallimento che sembra non avere nulla a che fare con la sua causa. Primo, il documento sorgente deve essere attivo nel momento in cui chiami CreateXObjectFromPage. La cattura legge il contenuto della pagina sorgente dal documento sorgente in tempo reale, quindi quel documento e la sua pagina devono essere aperti e validi quando l'handle viene costruito. Secondo, e questo è ciò che sorprende le persone, l'handle deve essere liberato prima che la pagina sorgente venga chiusa, e in pratica prima di chiudere o liberare il documento sorgente da cui proviene

Il motivo è che l'XObject è un riferimento a una struttura che il documento sorgente ancora possiede. Non è una copia distaccata e autonoma che puoi portarti dietro dopo che l'origine è scomparsa. Chiudi prima la sorgente e l'handle rimarrà a puntare a un contenuto che è stato demolito, per cui liberarlo in seguito, o qualsiasi altro suo utilizzo, opererà su una memoria che non è più valida. Il sintomo è quello classico di un handle penzolante ("dangling handle"): una violazione di accesso in fase di chiusura, o una corruzione intermittente che si sposta a seconda dell'ordine di allocazione, con uno stack che punta al codice di pulizia piuttosto che alla riga che ha effettivamente causato il problema. La soluzione sta nell'ordinamento, non nel codice difensivo. Costruisci l'XObject, inseriscilo su ogni pagina che ne ha bisogno, libera l'XObject e solo allora chiudi il documento sorgente. Il distruttore di TPdfXObject rilascia l'handle PDFium sottostante per te, quindi liberare il wrapper al momento giusto è l'unica tua responsabilità

La matrice, e cosa significano i suoi sei numeri

Il posizionamento è una trasformazione affine 2D, la stessa che il PDF usa ovunque per il posizionamento dei contenuti (ISO 32000-1, sezione 8.3.4). Si tratta di sei numeri, scritti come a, b, c, d, e, f, e PDFium li espone come record FS_MATRIX. Essi 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)

Puoi compilare quei sei valori a mano, ma è proprio componendoli a mano che la rotazione va storta, perché la rotazione mescola tutti e quattro i valori a, b, c, d. Il wrapper TPdfMatrix, dall'unità FPdfMatrix, compone le operazioni comuni per te e post-moltiplica man mano che procede, quindi Translate, Scale e Rotate si incatenano nell'ordine in cui le chiami. Una filigrana diagonale è una rotazione seguita da una traslazione per ricentrarla; un logo nell'angolo è una scalatura seguita da una traslazione. Quando la matrice è pronta, copia il suo valore grezzo, la proprietà Handle di tipo FS_MATRIX, in una variabile locale e passala a FPDFPageObj_SetMatrix; l'importazione dichiara la matrice come parametro var, quindi una proprietà non può esserle passata direttamente, e il suo risultato è 0 in caso di fallimento. Il FPDFPageObj_Transform di livello inferiore, che prende i sei valori direttamente come double, è disponibile quando preferisci passare dei numeri piuttosto che costruire un wrapper

Timbrare ogni pagina, nel giusto ordine

Il pattern completo unisce i pezzi con l'ordinamento richiesto dalla regola della durata (lifetime). Apri entrambi i documenti, catturi il timbro una volta sola, scorri le pagine di destinazione impostando a turno PageNumber (che è basato su 1) e inserendo nonché posizionando una copia, salvando ogni pagina con UpdatePage, dopodiché liberi l'XObject, poi salvi con SaveAs e infine lasci chiudere il documento sorgente per ultimo

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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. PageNumber is 1-based.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := 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
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // commit this page's edits
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // source closes last
    Dest.Free;
  end;
end;

La struttura dei blocchi try sta svolgendo il vero lavoro. Il finally interno libera l'XObject prima che il controllo possa mai raggiungere il finally esterno che libera Stamp, così che l'handle viene sempre rilasciato mentre la sua sorgente è ancora in vita, anche se viene generata un'eccezione a metà del ciclo. Fai in modo che l'annidamento sia corretto e la regola sulla durata si prenderà cura di sé

L'apposizione di timbri è solo un aspetto di un set di strumenti più ampio per la costruzione e la modifica dei contenuti delle pagine. Se il tuo timbro è a sua volta un'immagine anziché una pagina catturata, convertire immagini in documenti PDF con PDFium spiega prima come inserire quella bitmap in un documento. E quando la cosa che vuoi portare insieme al timbro visibile è un file anziché inchiostro sulla pagina, lavorare con gli allegati PDF in Delphi mostra il lato dei file incorporati. Tutto questo viene fornito con il Componente PDFium per Delphi e C++Builder, insieme alle API per rendering, editing e gestione dei documenti trattate in altri articoli di questo blog