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
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(ofont): prima riusa un font già caricato nel documento con quel nome, poi mappa i nomi Standard 14 comeHelvetica-Bold,Times-RomanoCourier, e infine provaAddTrueTypeFontcon il nomefontsize(osize): la dimensione del testo passata all'adattatore, che la riduce se il testo non entrafillcolor(otextcolor,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-255alignment,alignepositionsi leggono insieme: left, right, center (o centre) e justify impostano l'allineamento orizzontale, top e bottom il piazzamento verticale, e il predefinito è centrato in verticalefitmethod:wordonofitsplitimpedisce che le parole lunghe vengano spezzate,scaleoexpandlascia 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
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
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