PDFium Component crea gradienti PDF assiali e radiali in Delphi attraverso TPdf.SaveAsShadingCreated. Descrivete ogni gradiente in un record TPdfShadingCreate (tipo, spazio di colore, coordinate, colori o stop, pagina di destinazione), e il metodo scrive una copia verificata in cui ogni riempimento è un vero dizionario di shading ISO 32000-1, registrato nel dizionario /Resources /Shading della pagina e dipinto dall'operatore sh, così PDFium lo renderizza nativamente
Il bisogno si manifesta nei generatori di report e certificati. Un grafico consegna un mock-up con una fascia di intestazione morbida dal blu al bianco o un bagliore radiale dietro un sigillo, e l'API a page object che copre testo, tracciati e immagini non ha nulla al riguardo. Potete contraffare un gradiente con cento rettangoli sottili in colori leggermente diversi, ma il risultato produce bande visibili quando si ingrandisce, gonfia il content stream, e stampa male. Il PDF ha shading fluidi dalla 1.3; il lavoro sta nello scriverli così che un renderer li disegni davvero
Perché PDFium Component scrive i gradienti con un salvataggio separato?
TPdf.SaveAsShadingCreated è un'operazione di salvataggio invece che una chiamata a page object perché l'ABI pubblico di PDFium non ha alcuna funzione che crei un dizionario di shading o emetta un operatore sh. Il layer a page object usato quando create PDF da zero con PDFium Component costruisce tracciati, testo e immagini, e lì si ferma. Gli shading vivono un livello più in basso, nel grafo di oggetti e nella sintassi del content stream
Quindi il metodo lavora a stadi. PDFium salva prima il documento aperto nativamente, il che conserva ogni modifica fatta attraverso la normale API. Poi PDFium Component riscrive quell'output in scena: alloca un oggetto indiretto per shading, aggiunge la voce di risorsa a ogni pagina di destinazione, accoda un nuovo content stream al /Contents della pagina, e scrive una tabella di riferimenti incrociati fresca. Prima che qualcosa raggiunga il vostro stream o file, il risultato viene ricaricato con un controllo rigoroso dei riferimenti incrociati, e il metodo restituisce False con un messaggio nel report se quel ricaricamento fallisce. L'overload su file scrive prima su un file temporaneo, così un salvataggio fallito non distrugge una destinazione esistente
Costruire un gradiente assiale a due colori
Uno shading assiale (ISO 32000-1 §8.7.4.5.3, Type 2) fonde i colori lungo una retta da x0 y0 a x1 y1, quindi Coords prende quattro valori. Uno shading radiale (§8.7.4.5.4, Type 3) fonde tra due cerchi e ne prende sei: x0 y0 r0 x1 y1 r1. Con semplici StartColor e EndColor, PDFium Component emette una funzione esponenziale Type 2 (§7.10.3) con /N 1, che è un'interpolazione lineare dritta, e imposta /Extend [true true] così i colori finali continuano oltre entrambe le estremità dell'asse
uses
System.SysUtils, PDFium, FPdfCompress;
procedure AddHeaderBand(const InFile, OutFile: string);
var
Pdf: TPdf;
Fills: TPdfShadingCreateShadings;
Report: TPdfShadingCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
SetLength(Fills, 1);
Fills[0] := Default(TPdfShadingCreate);
Fills[0].ShadingType := pstAxial;
Fills[0].ColorSpace := pscDeviceRGB;
Fills[0].Coords := [0, 792, 0, 640]; // x0 y0 x1 y1, dall'alto in basso
Fills[0].StartColor := [0.12, 0.35, 0.75]; // blu scuro sul bordo superiore
Fills[0].EndColor := [1, 1, 1]; // bianco più in basso
Fills[0].PageNumber := 1; // in base 1
Fills[0].ResourceName := 'HeaderBand'; // facoltativo, predefinito è Sh1, Sh2...
if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Il numero di componenti di ogni colore deve combaciare con lo spazio di colore: una per pscDeviceGray e pscCalGray, tre per pscDeviceRGB, pscCalRGB e pscLab, quattro per pscDeviceCMYK. Per pscICCBased il numero viene dai byte del profilo che passate in IccProfile, che deve esporre 1, 3 o 4 componenti; il profilo viene incorporato come proprio stream ICC con il /N corrispondente. Gli spazi calibrati accettano CalWhitePoint facoltativo (tre valori, Y positivo), CalGamma (un valore per CalGray, tre per CalRGB) e, per Lab, un LabRange a sei valori. Ogni coordinata e componente viene controllata per NaN e infinito prima che un byte venga scritto
Notate che le coordinate sono nello spazio utente predefinito, con l'origine in basso a sinistra della pagina. Ricordate che l'asse definisce solo dove avviene la fusione: l'area dipinta è tutto ciò che il clip consente, il che conta più di quanto suoni, come mostrano le prossime sezioni
I gradienti multi-stop compilano in una funzione stitching Type 3
PDFium Component trasforma un array Stops nella funzione che un reader PDF si aspetta: due stop diventano una singola funzione Type 2, e da 3 a 256 stop diventano una funzione stitching Type 3 (ISO 32000-1 §7.10.4) fatta di un segmento lineare Type 2 per coppia adiacente. Ogni TPdfShadingCreateStop porta un Position e un Color. Quando Stops non è vuoto sostituisce del tutto StartColor ed EndColor
La funzione stitching ha bisogno di tre array che a mano è facile sbagliare di poco. /Functions contiene i segmenti, /Bounds contiene solo le posizioni interne degli stop (k segmenti richiedono k-1 bounds), e /Encode rimappa ogni sottodominio a [0 1] così ogni segmento vede il proprio intervallo di input completo. Il componente deriva tutti e tre dai vostri stop, ed è per questo che è rigoroso sull'input: le posizioni devono essere finite, strettamente crescenti, iniziare a 0 e finire a 1 (entro 1e-6). Un elenco di stop che parta da 0,1 lascerebbe parte del dominio senza funzione definita, e il metodo lo rifiuta invece di tirare a indovinare
function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
I: Integer;
begin
Result.Position := Position;
SetLength(Result.Color, Length(Rgb));
for I := 0 to High(Rgb) do
Result.Color[I] := Rgb[I];
end;
procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
Fill := Default(TPdfShadingCreate);
Fill.ShadingType := pstRadial;
Fill.ColorSpace := pscDeviceRGB;
// x0 y0 r0 x1 y1 r1: un punto al centro del sigillo che cresce fino a un cerchio da 90 pt
Fill.Coords := [450, 160, 0, 450, 160, 90];
Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
Stop(0.4, [0.95, 0.75, 0.30]),
Stop(1.0, [1.00, 1.00, 1.00])];
Fill.PageNumber := 1;
end;
Per un gradiente a tre stop ciò produce /Functions con due segmenti Type 2, /Bounds [0.4] e /Encode [0 1 0 1]. I confini dei segmenti sono giunzioni di colore nette, quindi un elenco di stop non smussa attraverso di loro; se una giunzione appare troppo tagliente, aggiungete stop invece di aspettarvi che il reader interpoli la curvatura
Perché PDFium non disegna silenziosamente nulla?
PDFium renderizza uno shading appena creato solo quando due dettagli sono giusti: /ShadingType deve essere un intero, e la chiamata sh deve stare dentro un tracciato di ritaglio. Sbagliate l'uno o l'altro e il file si apre, valida strutturalmente, e mostra una pagina intoccata. Nessun errore, nessun avviso, nessuna riga di log
Trappola uno: /ShadingType è un numero, non un nome
ISO 32000-1 Tabella 78 definisce /ShadingType come intero: 2 per assiale, 3 per radiale. La prima versione di questo writer emetteva /ShadingType /Axial perché si leggeva meglio in un dump esadecimale. PDFium legge la chiave con un getter intero, ottiene 0 per un nome, classifica lo shading come non valido, non riesce a caricarlo, e salta l'operatore sh senza lamentarsi. La pagina si renderizzava bianca e nulla in PDFium diceva perché. La causa è diventata visibile solo dopo aver costruito un PDF minimo scritto a mano e aperto in un secondo renderer indipendente, che riportava uno shading type sconosciuto pari a 0. La lezione vale per qualunque writer PDF: quando la specifica tipizza un'enumerazione come intero, scrivete l'intero, per quanto allettante appaia un nome auto-descrittivo
Trappola due: un content stream che contiene solo sh non ha bounding box
L'operatore sh (ISO 32000-1 §8.7.4.2) dipinge lo shading sull'attuale regione di ritaglio, quindi l'estensione propria della pagina lo delimita su carta. PDFium però costruisce un page object di shading il cui rettangolo viene dal tracciato di clip attivo o dal bounding box accumulato dagli operatori di disegno precedenti nello stesso stream, e il renderer scarta gli oggetti con rettangolo vuoto. Uno stream appena accodato che non contiene altro che /Sh1 sh non ha nessuna delle due cose, quindi il gradiente viene abbandonato prima della rasterizzazione. TPdf.SaveAsShadingCreated legge quindi il crop box di ogni pagina (ripiegando sul media box, poi sulla dimensione della pagina) e avvolge ogni invocazione in un clip esplicito re W n. Su una pagina US Letter con origine a zero, lo stream accodato appare così:
q
0 0 612 792 re W n
/HeaderBand sh
Q
La coppia q/Q isola il clip e qualunque transform dal contenuto esistente della pagina, così nulla di disegnato dopo sulla pagina li eredita. Se scrivete invocazioni di shading da voi in un altro strumento, copiate questa forma: un clip esplicito nello stesso stream costa poco e toglie la dipendenza da ciò che veniva prima
Come interagisce una matrice di gradiente personalizzata con il clip?
Una matrice di gradiente in PDFium Component si applica come operatore cm piazzato dopo il clip e prima di sh, così il clip resta a pagina intera mentre solo la geometria del gradiente viene trasformata. Impostate HasMatrix a True e riempite da MatrixA a MatrixF, gli stessi sei numeri di qualunque matrice di trasformazione PDF. I valori devono essere finiti e il determinante diverso da zero, perché una matrice singolare collasserebbe lo shading su una retta. Il componente registra anche i sei valori come /Matrix nel dizionario di shading, ma per una chiamata sh diretta è il cm nel content stream che imposta il sistema di coordinate in cui PDFium dipinge
L'ordine conta qui, ed è la terza trappola. Gli operandi di re si interpretano nella matrice di trasformazione corrente. Mettete prima il cm e il rettangolo di clip della dimensione della pagina viene scalato, ruotato o inclinato insieme al gradiente, così una matrice che schiaccia un'ellisse schiaccia anche l'area visibile e parti della pagina perdono il loro riempimento. Ritagliare prima nello spazio utente non trasformato, poi concatenare la matrice, dà il risultato atteso: un gradiente trasformato che copre l'intera pagina. La guida ai timbri Form XObject e alla loro matrice a sei numeri ripercorre cosa fa ogni componente della matrice se vi serve costruirne una
procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
BuildSealGlow(Fill); // riempimento radiale centrato a 450,160
Fill.Coords := [0, 0, 0, 0, 0, 90]; // cerchi all'origine
Fill.HasMatrix := True;
Fill.MatrixA := 1.6; // stira orizzontalmente
Fill.MatrixB := 0;
Fill.MatrixC := 0;
Fill.MatrixD := 0.8; // comprime verticalmente
Fill.MatrixE := 450; // poi sposta al centro del sigillo
Fill.MatrixF := 160;
end;
Leggere il report e conoscere i limiti
TPdfShadingCreateReport vi dice cosa è stato scritto: SourceByteCount e OutputByteCount, CreatedShadingObjectNumbers (un numero di oggetto per voce di input, in ordine), AttachedShadingCount, Succeeded ed ErrorMessage. Gli errori di validazione nominano la voce e il campo, per esempio un elenco di stop non strettamente crescente, quindi vale la pena scriverli nel log alla lettera
if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
Writeln(Format('%d shading(s) attached, %d -> %d bytes',
[Report.AttachedShadingCount, Report.SourceByteCount,
Report.OutputByteCount]))
else
Writeln('Rejected: ' + Report.ErrorMessage);
Il profilo è deliberatamente delimitato. Crea solo shading assiali e radiali; gli shading a funzione e a mesh (Type 1 e da 4 a 7) e gli spazi di colore pattern vengono rifiutati. Una chiamata accetta da 1 a 4096 riempimenti, i numeri di pagina devono esistere, e i nomi di risorsa sono limitati a 127 caratteri di lettere, cifre, underscore, meno e punto. L'input cifrato viene rifiutato allo stadio del trailer. Un documento firmato richiede AllowSignedDocument := True, e poiché l'output è una riscrittura completa invece che un aggiornamento incrementale, le firme esistenti non verificheranno più, quindi firmate dopo aver aggiunto i gradienti. Ogni riempimento viene dipinto alla fine del contenuto della pagina, sopra ciò che c'è già; per un effetto di sfondo, create prima il gradiente e aggiungete testo e immagini dopo, o usate il contenuto opzionale come descritto nell'articolo sul controllare i layer di contenuto opzionale PDF in Delphi quando il riempimento deve essere commutabile
FAQ
SaveAsShadingCreated supporta i gradienti multi-stop?
Sì. Passate da 2 a 256 voci in Stops con posizioni strettamente crescenti da 0 a 1. Due stop producono una funzione Type 2 e tre o più producono una funzione stitching Type 3 con array /Bounds e /Encode derivati
Perché il mio gradiente è invisibile in PDFium quando il file sembra valido?
Controllate che /ShadingType sia l'intero 2 o 3 invece che un nome, e che l'operatore sh giri dentro un clip re W n nello stesso content stream. PDFium salta silenziosamente i tipi di shading non validi e scarta gli oggetti shading con rettangolo vuoto
Quali spazi di colore può usare uno shading creato?
TPdfShadingCreate accetta DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab e ICCBased con un profilo da 1, 3 o 4 componenti. Gli spazi di colore Pattern e Indexed non fanno parte del profilo di creazione
La creazione di gradienti, il contenuto opzionale e il resto dei writer in scena arrivano con il PDFium Component per Delphi e C++Builder, che incapsula PDFium per progetti Delphi, C++Builder e Lazarus