Articolo tecnico

Unione delle pagine e modelli di filigrana nei PDF Delphi

L'unione delle pagine (page stitching) compone il contenuto di pagine esistenti su una tela condivisa, operazione diversa dall'unione di file. La libreria PDF di losLab implementa l'unione di pagine tramite Form XObject e modelli di filigrana componibili per Delphi: StitchPageOverlay, StitchPagesSideBySide e StitchPagesVertically posizionano le pagine acquisite a qualsiasi scala con una fedeltà vettoriale totale, e TPDFlibCompositeWatermark sovrappone filigrane di testo e immagini su un intervallo di pagine in un'unica chiamata. Se ciò di cui hai effettivamente bisogno è aggiungere intere pagine da più documenti in un unico file, questo rientra nella famiglia dei merge, trattata nell'articolo sull'unione rapida di PDF a livello di byte; questo articolo riguarda la combinazione del contenuto della pagina all'interno di un unico spazio di coordinate

Le richieste che portano qui sono comuni e ricorrenti. Una scuola vuole stampare due pagine d'esame su un unico foglio orizzontale per dimezzare il consumo di carta. Un revisore della localizzazione vuole la pagina inglese e la sua traduzione affiancate su una singola pagina, scorrendo insieme. Un team legale desidera che ogni bozza sia stampata con tre livelli contemporaneamente: un logo nell'angolo, la scritta diagonale CONFIDENTIAL al centro e una piccola riga a piè di pagina, solo sulle pagine da 1 a 3. Tutti e tre sono lo stesso problema di fondo, ovvero posizionare il contenuto della pagina già impaginato da un'altra parte a una dimensione diversa senza ridurlo a una bitmap

Perché la concatenazione dei flussi di contenuto PDF non funziona?

L'unione ingenua, ovvero la copia del flusso /Contents della pagina di origine nella pagina di destinazione, fallisce per tre motivi strutturali. Primo, le coordinate collidono: entrambi i flussi di contenuto presuppongono la propria origine in basso a sinistra del proprio MediaBox, quindi il contenuto incollato finisce sopra il contenuto esistente nel posto sbagliato. Secondo, i nomi delle risorse collidono: ogni pagina risolve operatori come /F1 Tf e /Im0 Do rispetto al proprio dizionario /Resources, e due pagine associano abitualmente lo stesso nome a caratteri o immagini diversi, per cui una di esse viene renderizzata silenziosamente con la risorsa sbagliata. Terzo, una copia grezza del flusso non fornisce alcun ridimensionamento; il massimo che si può fare è traslare

I Form XObject risolvono tutti e tre i problemi in una volta sola, ed è per questo che la libreria PDF losLab vi basa la sua unione. Lo standard ISO 32000-1 §8.10 definisce un form XObject come un flusso di contenuto autonomo con il proprio dizionario /Resources e il proprio spazio di coordinate, posizionato all'interno di una pagina invocando il suo nome sotto qualsiasi matrice di trasformazione corrente attiva. CapturePage racchiude un'intera pagina esistente, contenuto più risorse, in un oggetto /Subtype /Form XObject, e le funzioni di unione lo posizionano poi con una matrice cm: scala, trasla, ruota, a piacimento. Poiché la sorgente rimane vettoriale, il testo rimane testo e i tracciati rimangono tracciati a qualsiasi livello di zoom, la stessa proprietà che rende indipendenti dalla risoluzione la grafica vettoriale e i pattern di sfumatura. Nulla viene rasterizzato e il dizionario delle risorse isolato fa sì che la pagina acquisita non possa entrare in conflitto con la pagina di destinazione per il nome /F1

Come si combinano due pagine PDF in una sola pagina?

StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) è la risposta diretta: acquisisce entrambe le pagine di origine, crea una nuova pagina più larga e posiziona le due acquisizioni a sinistra e a destra con uno spazio (gutter) in mezzo, ciascuna ridimensionata per adattarsi a TargetHeight. La funzione restituisce il nuovo numero di pagina (a base 1) o 0 in caso di fallimento, e entrambe le pagine di origine vengono acquisite e nascoste come parte dell'operazione. Il caso dell'esame con due pagine per foglio si risolve in tre righe di codice

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

Per la composizione verticale, StitchPagesVertically(PageRanges, Gap, TargetWidth) impila un intero intervallo di pagine in un'unica pagina alta, in stile poster. PageRanges accetta la sintassi standard degli intervalli come ad esempio '1-3,5'. Ogni pagina di origine viene ridimensionata in modo che la sua larghezza corrisponda a TargetWidth, quindi il contributo in altezza di ciascuna pagina è la sua stessa altezza moltiplicata per il suo fattore di scala, e l'altezza della pagina di output è la somma di questi contributi più un valore Gap tra ogni coppia. Questa è la struttura ideale per documenti di revisione a scorrimento continuo o lunghe ricevute assemblate da sorgenti di dimensioni letter

Come si sovrappone una pagina a un'altra?

StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) disegna una pagina esistente su un'altra pagina esistente, anziché crearne una nuova. La pagina sorgente viene acquisita come un form XObject e disegnata sulla pagina di destinazione nel rettangolo specificato, con un valore di Opacity da 0 a 1 (1 è opaco). Larghezza e altezza sono parametri liberi, quindi il posizionamento si allunga se il rapporto differisce dalle proporzioni della pagina originale; passa una coppia proporzionale per ottenere una miniatura non distorta della sorgente. Un uso tipico è per un documento il cui blocco di approvazione è gestito come una pagina a sé stante e stampato sulla copertina

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

Cosa succede ai numeri di pagina dopo un'acquisizione?

CapturePage consuma la pagina sorgente: il suo contenuto viene spostato nel Form XObject e la pagina ora vuota viene nascosta, quindi ogni pagina con un numero superiore a quella sorgente si sposta immediatamente in basso di uno. Questa è la semantica più importante dell'intera API di unione. La libreria PDF losLab gestisce i calcoli all'interno di ciascuna chiamata di unione, StitchPageOverlay regola l'indice di destinazione quando questa si trova sopra la sorgente, e StitchPagesSideBySide acquisisce prima la pagina con il numero più alto in modo che l'indice della seconda acquisizione sia ancora valido, ma il tuo codice di contorno deve tenere conto dello spostamento tra le chiamate. Due regole mantengono corretto il codice con più unioni. Registra le dimensioni o i numeri di pagina necessari prima della prima acquisizione, perché cambieranno in seguito. E quando acquisisci più pagine in un ciclo, esegui l'iterazione in ordine di pagina decrescente, in modo che nascondere una pagina successiva non modifichi mai l'indice di una pagina precedente non ancora acquisita

Come si applica una filigrana multistrato a un intervallo di pagine?

Il livello dei modelli di filigrana in PDFlibWatermark.pas trasforma il concetto di "definire una volta, applicare a molte pagine" in oggetti. TPDFlibWatermarkBase contiene ciò che ogni filigrana condivide: una Position a nove punti (da wpTopLeft a wpBottomRight, più wpCustom che utilizza coordinate X/Y grezze), un valore Angle in gradi, un'Opacity da 0 a 1 e una stringa PageRange in cui il valore vuoto indica tutte le pagine. TPDFlibTextWatermark aggiunge testo, un ID font (il valore predefinito è Helvetica), la dimensione e un colore RGB; TPDFlibImageWatermark aggiunge l'ID di un'immagine precedentemente inserita più una larghezza e un'altezza di posizionamento. TPDFlibCompositeWatermark contiene un elenco di sotto-filigrane e le applica in modo ricorsivo, e TPDFlib.ApplyWatermark rappresenta il punto di accesso a chiamata singola che rispetta l'intervallo di pagine del modello e restituisce il numero di pagine timbrate. Il timbro legale a tre livelli si presenta così

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

Vale la pena specificare due dettagli di composizione. L'ereditarietà dell'intervallo è unidirezionale e non distruttiva: un elemento figlio con un PageRange vuoto eredita temporaneamente l'intervallo del composite durante l'operazione Apply, mentre un figlio che imposta il proprio intervallo lo mantiene, consentendo ad esempio di applicare il piè di pagina a ogni pagina mentre il timbro diagonale copre solo la sezione bozza. E la proprietà segue il composite: il metodo TPDFlibCompositeWatermark.Destroy rilascia i suoi figli, quindi liberando una volta sola il composite si libera anche tutto il resto, come nell'esempio precedente

Limiti da conoscere

Si applicano due limiti oggettivi, ed entrambi derivano dal funzionamento del meccanismo. In primo luogo, un'acquisizione racchiude solo il flusso di contenuto e le risorse della pagina. Le annotazioni, i collegamenti, i campi modulo e i commenti vivono come oggetti separati nel dizionario della pagina, non nel flusso di contenuto, quindi non vengono trasferiti nel Form XObject; un layout unito trasporta il contenuto visibile della pagina ma non il livello interattivo delle sue sorgenti. In secondo luogo, il posizionamento del testo a nove punti è un'approssimazione: senza un motore di layout completo la libreria non può conoscere l'esatta larghezza di rendering di una stringa, quindi stima la casella di testo al 40% della larghezza della pagina quando risolve posizioni come wpCenter. Questo è sufficientemente accurato per centrare una breve etichetta, ma per un posizionamento preciso al pixel di stringhe lunghe, passa a wpCustom e calcola tu stesso le coordinate

La scala non è un limite pratico in questo contesto, poiché l'acquisizione e l'unione manipolano i riferimenti agli oggetti piuttosto che ricodificare il contenuto della pagina; se i tuoi documenti sorgente sono di centinaia di megabyte, prima di procedere con l'unione si applica la stessa logica descritta nella guida all'unione e alla divisione di file grandi con accesso diretto. Le funzioni di unione e le classi di modelli di filigrana mostrate qui sono fornite nell'attuale losLab PDF Library per Delphi, C# e VB.NET, insieme alle primitive di livello inferiore CapturePage, DrawCapturedPage e DrawCapturedPageMatrix per i casi in cui è necessario un layout non coperto dalle tre chiamate di unione predefinite