Articolo tecnico

Template a blocchi PDF in Delphi: testo, immagini e PDF

PDFlibPas (losLab PDF Library) riempie i template a blocchi PDF da Delphi: un blocco è un rettangolo con nome su una pagina template, tipizzato come testo, immagine o PDF, e FillTextBlock, FillImageBlock e FillPDFBlock ci disegnano dentro dati variabili. I blocchi si trovano come annotazioni di pagina PDFlibBlock:Text, PDFlibBlock:Image e PDFlibBlock:PDF, o in un dizionario page-piece sotto /PieceInfo

Il caso d'uso è di quelli noiosi e ad alto volume. Un grafico impagina una volta sola un certificato, un biglietto per un evento o una lettera di sollecito, e il programma timbra nome, numero di posto, immagine della firma o pagina barcode pre-renderizzata negli slot fissi per diecimila record. I campi modulo fanno parte di quel lavoro, ma un campo AcroForm è un oggetto interattivo con il proprio appearance stream e le proprie regole, e di solito alla fine lo appiattite comunque. Un blocco è più semplice: un rettangolo più una manciata di suggerimenti di stile, consumato una volta e poi buttato via

Che cosa è esattamente un blocco PDF in PDFlibPas?

Un blocco PDFlibPas è un'annotazione di pagina il cui /Subtype è PDFlibBlock:Text, PDFlibBlock:Image o PDFlibBlock:PDF, il cui /Rect è l'area segnaposto, e la cui stringa /Contents contiene name;key=value;key=value. Il primo segmento è il nome del blocco, tutto ciò che segue è un elenco di proprietà. TPDFlib.AddBlock scrive l'annotazione con /F 4 (Print) e senza appearance stream, così un blocco non riempito non occupa pixel né a schermo né su carta

Anatomia del template a blocchi PDFlibPas per i dati variabili in Delphi: un'annotazione di pagina con /Subtype PDFlibBlock:Text, Image o PDF, un segnaposto /Rect, una stringa /Contents di nome e proprietà key=value, /F 4 Print e nessun appearance stream, con il dizionario page-piece /PieceInfo /PDFlib /Blocks come seconda fonte
I blocchi si enumerano in base 1 per pagina con prima le annotazioni e dopo i blocchi page-piece; le voci di array diventano Block1 e Block2, le chiavi /Custom si appiattiscono in proprietà, e tutto ciò che non è Image, PDF o Page viene trattato come testo

Il lato lettura ha una seconda fonte. Dopo aver enumerato le annotazioni di blocco, PDFlibPas ripiega sul percorso di dizionario di pagina /PieceInfo << /PDFlib << /Blocks ... >> >> e sulla sua variante /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. I dizionari page-piece sono PDF standard, definiti in ISO 32000-1 §14.5 come il posto in cui un'applicazione memorizza dati privati allegati a una pagina, e lì di solito vivono le definizioni di blocco degli strumenti esterni di authoring dei template. Il contenitore /Blocks può essere un dizionario con chiave il nome del blocco o un semplice array; nella forma array PDFlibPas chiama le voci Block1, Block2 e così via a meno che un blocco porti il proprio /Name

Dentro un blocco page-piece, il valore /Subtype (o /Type) Image corrisponde a un blocco immagine, PDF o Page a un blocco PDF, e tutto il resto viene trattato come testo. Ogni altra chiave tranne /Type, /Subtype, /Name, /Rect e /Custom diventa una proprietà, e le chiavi di un dizionario /Custom annidato vengono appiattite dentro a loro volta, così una voce /Custom << /department (Finance) >> si legge con GetBlockProperty(Page, Index, 'department'). Gli indici dei blocchi sono in base 1 per pagina, prima i blocchi annotazione, dopo quelli page-piece

Scrivere un template a blocchi con AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) aggiunge un blocco alla pagina attualmente selezionata e restituisce 1 in caso di successo. Restituisce 0 per un nome vuoto, una larghezza o altezza nulla o negativa, o un BlockType diverso da 'text', 'image' o 'pdf' (maiuscole e minuscole indifferenti). Properties viene accodato al nome con un punto e virgola, quindi usa la stessa sintassi key=value;key=value della stringa memorizzata

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // grafica statica: bordo, logo, testo fisso ...
    Lib.DrawText(72, 200, 'This certifies that');

    Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
      'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
    Lib.AddBlock('text', 'course', 72, 290, 450, 24,
      'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
    Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
    Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');

    if Lib.GetBlockCount(1) <> 4 then
      raise Exception.Create('Template blocks missing');
    Lib.SaveToFile('certificate-template.pdf');
  finally
    Lib.Free;
  end;
end;

Il parser delle proprietà è volutamente permissivo. Le chiavi si confrontano ignorando le maiuscole, i valori vengono trimmati ma per il resto restituiti alla lettera (un valore scritto come {top left} torna con le proprie graffe), e una chiave mancante restituisce una stringa vuota. L'unico limite rigido è il separatore: un valore non può contenere un punto e virgola, perché il punto e virgola chiude il segmento

Come applica FillTextBlock font, colore e allineamento?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) legge le proprietà del blocco, le applica allo stato di disegno, adatta il testo al rettangolo del blocco con FitTextBox e poi ripristina in un blocco finally font, corpo del testo, colore del testo e allineamento del chiamante. Quel ripristino conta in un ciclo di merge: un blocco intestazione rosso da 30 pt non può lasciarsi trapelare lo stile nella successiva chiamata DrawText che fate sulla stessa pagina

  • fontname (o font): prima riusa un font già caricato nel documento con quel nome, poi mappa i nomi Standard 14 come Helvetica-Bold, Times-Roman o Courier, e infine prova AddTrueTypeFont con il nome
  • fontsize (o size): la dimensione del testo passata all'adattatore, che la riduce se il testo non entra
  • fillcolor (o textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, elenchi nudi di 3 o 4 numeri, o i nomi black, white, red, green, blue e gray; le componenti RGB sopra 1 si leggono come 0-255
  • alignment, align e position si leggono insieme: left, right, center (o centre) e justify impostano l'allineamento orizzontale, top e bottom il piazzamento verticale, e il predefinito è centrato in verticale
  • fitmethod: word o nofitsplit impedisce che le parole lunghe vengano spezzate, scale o expand lascia crescere la dimensione per riempire il box

Il percorso CMYK è il motivo per cui le tipografie ci tengono. {cmyk 0 0.55 1 0.1} chiama SetTextColorCMYK, così il testo variabile viene emesso in DeviceCMYK e raggiunge il colore del brand in stampa senza un giro di andata e ritorno RGB. FillTextBlock restituisce 1 quando il testo è stato disegnato; restituisce 0 per un Text vuoto, un blocco mancante o un blocco che non è di testo, e in tutti questi casi il blocco resta al suo posto

Ordine di elaborazione di FillTextBlock in PDFlibPas per Delphi: le proprietà del blocco fontname, fontsize, fillcolor e alignment si applicano allo stato di disegno, il valore CMYK passa per SetTextColorCMYK, il testo viene adattato con FitTextBox, e font, corpo, colore e allineamento del chiamante tornano in un blocco finally
Lo stile del blocco si applica una volta e viene riportato indietro così un'intestazione rossa da 30 pt non può trapelare nella successiva DrawText; la chiamata restituisce 1 quando il testo è stato disegnato e 0 con il blocco al suo posto per testo vuoto o un blocco non di testo

Riempire blocchi immagine e PDF, e i bit di Options

FillImageBlock(Page, BlockIndex, ImageID, Options) disegna un'immagine aggiunta in precedenza, e FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importa una pagina di un altro documento caricato come Form XObject e la disegna, il che mantiene pienamente vettoriali contenuti vettoriali come un sigillo, una pagina QR code o una carta intestata. Entrambe preservano il rapporto d'aspetto della sorgente e centrano il risultato dentro il rettangolo del blocco. Tutte e tre le funzioni di riempimento condividono gli stessi bit di Options: il bit 1 (valore 1) rimuove il blocco dopo un riempimento riuscito, sia cancellando l'annotazione sia togliendo la voce dal contenitore /Blocks, e il bit 2 (valore 2) stira il contenuto di immagini e PDF fino al rettangolo esatto invece di adattarlo

function FillCertificate(const TemplateFile, SealFile, SignatureFile,
  OutFile: WideString; Values: TStrings): Boolean;
var
  Lib: TPDFlib;
  SealID, SignatureID, I: Integer;
  BlockName, BlockType: WideString;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
    SealID := Lib.SelectedDocument;          // il vero ID del documento
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // il template è ora il documento selezionato
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // si procede all'indietro: il bit 1 rimuove ogni blocco, rinumerando i successivi
    for I := Lib.GetBlockCount(1) downto 1 do
    begin
      BlockName := Lib.GetBlockName(1, I);
      BlockType := Lib.GetBlockType(1, I);
      if BlockType = 'text' then
        Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
      else if (BlockType = 'image') and (SignatureID <> 0) then
        Lib.FillImageBlock(1, I, SignatureID, 1)
      else if BlockType = 'pdf' then
        Lib.FillPDFBlock(1, I, SealID, 1, 1);
    end;
    Result := Lib.SaveToFile(OutFile) = 1;
  finally
    Lib.Free;
  end;
end;

Due dettagli in quel ciclo sono facili da sbagliare. Il ciclo va dall'indice più alto verso il basso perché rimuovere il blocco 2 trasforma il vecchio blocco 3 nel blocco 2; contando in su con il bit 1 impostato salta un blocco sì e uno no. E il documento sorgente per FillPDFBlock viene caricato prima e il suo ID catturato prima di caricare il template, perché gli ID delle immagini appartengono al documento selezionato, quindi AddImageFromFile deve girare mentre il template è selezionato. Per esecuzioni batch in cui il sigillo o la carta intestata cambia per record, lo stitching di pagine Form XObject e i template di watermark componibili in PDFlibPas usano lo stesso meccanismo di cattura e disegno su pagine intere

Perché le coordinate dei blocchi risultano capovolte?

I rettangoli dei blocchi risultano capovolti quando il codice mescola i due sistemi di coordinate con cui PDFlibPas ha a che fare. Un /Rect di annotazione è memorizzato nello spazio utente PDF, misurato dal fondo della pagina (ISO 32000-1 §12.5.2), mentre l'API di disegno di TPDFlib misura dall'alto. AddBlock prende input basato sull'alto e memorizza il bordo inferiore come PageHeight - Top - Height; GetBlockRect e le tre funzioni di riempimento rileggono il rettangolo e lo convertono con PageHeight - URy

In pratica significa: usate GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) quando vi serve la geometria dello slot, per esempio per disegnare un frame di debug o posizionare una didascalia accanto a un blocco, e non leggete /Rect tramite una chiamata generica di annotazione per poi darlo in pasto direttamente a DrawText. Un blocco page-piece con /Rect [40 700 240 730] su una pagina letter da 792 pt riporta Top = 62, non 700. Il bug che questo previene è il classico in cui ogni valore riempito atterra specchiato vicino al margine inferiore e nessun errore compare

Capovolgimento delle coordinate del rettangolo di blocco in PDFlibPas per Delphi: i valori /Rect vivono nello spazio utente PDF basato sul fondo mentre AddBlock prende input basato sull'alto e memorizza PageHeight meno Top meno Height, così /Rect 40 700 240 730 su una pagina letter da 792 pt si rilegge come Top 62 tramite GetBlockRect e la sua conversione PageHeight meno URy
Mescolare i due sistemi di coordinate specchia ogni valore riempito verso il margine inferiore senza un solo errore; leggete la geometria dello slot tramite GetBlockRect invece di dare /Rect in pasto direttamente a DrawText

Trappole da cui vale la pena imparare

Il bug più istruttivo nel codice dei blocchi fu una svista di un carattere. Le annotazioni di blocco si riconoscono con un test di prefisso sul subtype, e 'PDFlibBlock:' è lungo 12 caratteri. Una versione precoce confrontava Copy(Subtype, 1, 13) con quel letterale di 12 caratteri, che non può mai essere uguale, così GetBlockCount restituiva 0 per ogni template e ogni riempimento riportava "no such block". Niente crash e niente log. La lezione si generalizza a qualunque motore di template: asserite il numero di blocchi di un template noto in un test, perché un'enumerazione silenziosamente a zero match sembra esattamente una pagina vuota

La seconda trappola sono gli ID dei documenti. TPDFlib.LoadFromFile restituisce un codice di stato, 1 per successo e 0 per fallimento, non l'ID del documento appena caricato. Passare quel 1 come SourceDocumentID a FillPDFBlock fallisce perché 1 non è un ID documento valido. Chiamate SelectedDocument subito dopo un caricamento riuscito per ottenere l'ID vero, e SelectDocument per tornare al template se avete caricato la sorgente dopo

Una più piccola: FillTextBlock con una stringa vuota non fa nulla e conserva il blocco. Per un mail merge con campi facoltativi, come una seconda riga di indirizzo mancante, di solito è ciò che volete, ma l'annotazione non riempita resta allora nel file di output. Toglietela esplicitamente con DeleteAnnotation o riempite un singolo spazio con il bit 1 se l'output deve essere pulito. Tutte le chiamate sui blocchi salvano e ripristinano la pagina selezionata, quindi un riempimento a pagina 3 non cambia dove va la vostra successiva chiamata di disegno

Quali sono i limiti di interoperabilità delle annotazioni di blocco?

Le annotazioni di blocco sono una convenzione privata, non una funzionalità standard del PDF. PDFlibBlock:Text e i suoi fratelli non sono tra i tipi di annotazione elencati in ISO 32000-1 §12.5.6.1, quindi un viewer li tratta come annotazioni sconosciute. Poiché il flag Invisible è a zero e non c'è appearance stream, i viewer conformi non disegnano nulla per essi, ma alcuni viewer possono comunque elencarli in un pannello dei commenti, e gli altri strumenti PDF non capiranno né preserveranno la loro semantica

  • Riempite con il bit 1 impostato per tutto ciò che esce dal vostro sistema, così il file finale contiene solo normale contenuto di pagina
  • PDF/A non ammette tipi di annotazione fuori dalla specifica PDF (ISO 19005-1 §6.5.2), quindi un output da archivio non deve portare annotazioni di blocco non riempite
  • I blocchi page-piece sono più sicuri da tenere in un template, visto che /PieceInfo è standard e i consumatori ignorano i dati che non possiedono
  • I blocchi non sono campi modulo: niente interattività, niente valore di campo nel file e niente esportazione verso FDF o XFDF; per dati che devono restare modificabili vedete lo scambio di dati modulo FDF, XFDF e XFA in PDFlibPas

Se il template deve dichiarare i suoi campi variabili in modo standardizzato e verificabile da un validatore per un flusso di stampa, i blocchi sono lo strumento sbagliato. A questo serve PDF/VCR-1, e il manifest dei segnaposto dei template PDF/VCR-1 in PDFlibPas spiega come quei segnaposto vivono nell'albero della struttura invece che nelle annotazioni

FAQ

PDFlibPas può riempire template a blocchi creati da altri strumenti?

Sì, quando lo strumento memorizza i blocchi sotto /PieceInfo /PDFlib /Blocks o /PieceInfo /PDFlib /Private /Blocks, in forma dizionario o array, con un /Rect di quattro numeri. GetBlockCount li conta dopo le annotazioni PDFlibBlock:*, e le chiavi personalizzate si espongono tramite GetBlockProperty

FillTextBlock cambia il font selezionato prima della chiamata?

No. FillTextBlock salva il font selezionato, la dimensione del testo, il colore del testo e l'allineamento, applica le proprietà del blocco fontname, fontsize, fillcolor e alignment, e ripristina lo stato originale dopo, anche se l'adattamento fallisce

Come si mette un logo vettoriale o una pagina barcode in un blocco?

Caricate il PDF sorgente, memorizzate SelectedDocument come suo ID, riselezionate il documento template se serve, e chiamate FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). La pagina viene importata come Form XObject, quindi resta vettoriale e viene adattata al blocco con il rapporto d'aspetto preservato a meno che aggiungiate il bit 2

Template a blocchi, importazione di pagine Form XObject e il resto del kit per dati variabili sono inclusi in PDFlibPas, la losLab PDF Library per Delphi, che copre creazione, modifica e produzione di stampa del PDF per progetti Delphi e C++Builder