HotPDF renderizza le tinte piatte Separation e DeviceN sulle pagine di un PDF caricato risolvendo lo spazio colore tramite HPDFResolveColorSpace, valutando la funzione di tint transform con HPDFEvalTintTransform e convertendo il risultato attraverso lo spazio alternativo in RGB per lo schermo. Dalla v2.375.0 quella pipeline valuta tutti e quattro i tipi di funzione PDF, compresi i calcolatori PostScript Type 4, così un file pronto per la stampa con inchiostri Pantone mostra i suoi colori reali invece di un segnaposto. Questo articolo illustra il funzionamento della pipeline e, cosa altrettanto utile, i modi in cui ha fallito mentre la costruivamo
Il caso scatenante è sempre lo stesso. Un cliente riceve un PDF da una tipografia: il titolo è composto con un inchiostro speciale denominato, la grafica della confezione usa una miscela DeviceN a due inchiostri e il testo corrente è nero pieno. In Acrobat appare perfetto. Nella vostra applicazione Delphi il titolo viene reso in nero oppure, peggio, non viene reso affatto, e il cliente apre una segnalazione contro il vostro software anziché contro il file. Il file è corretto. È il renderer che non parla gli spazi colore usati dal file
Perché le tinte piatte vengono rese in nero in un visualizzatore PDF?
Le tinte piatte vengono rese in nero, o spariscono, quando il renderer implementa soltanto gli operatori di colore di dispositivo (rg, g, k) e ignora quelli generici. ISO 32000-1 §8.6 definisce tre gruppi di spazi colore: gli spazi di dispositivo (DeviceGray, DeviceRGB, DeviceCMYK), gli spazi basati su CIE (CalGray, CalRGB, Lab, ICCBased) e gli spazi speciali (Indexed, Separation, DeviceN, Pattern). Tutto ciò che sta fuori dal gruppo di dispositivo si seleziona con gli operatori generici: cs e CS scelgono uno spazio per nome dal dizionario delle risorse di pagina, poi sc, SC, scn e SCN forniscono i valori delle componenti. Un renderer che salta questi operatori conserva l'ultimo colore impostato che, per una pagina che si apre con un titolo in tinta piatta, è il nero DeviceGray iniziale
HotPDF ha aggiunto al proprio renderer di pagina il set di operatori generici nella v2.333.0, insieme a un percorso di risoluzione unificato: ogni voce di risorsa /ColorSpace, che sia un nome nudo, un array inline o un riferimento indiretto, viene analizzata in un unico record THPDFColorSpace, e ogni richiesta di colore di riempimento o di traccia converge in una sola chiamata a HPDFResolveColor. L'enumerazione delle famiglie mostra la copertura a colpo d'occhio
type
THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
csfIndexed, csfCalGray, csfCalRGB, csfLab,
csfICCBased, csfSeparation, csfDeviceN,
csfUnsupported);
function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;
function HPDFResolveColor(const CS: THPDFColorSpace;
const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;
Una scelta progettuale si ripaga di continuo: csfUnsupported è una famiglia di prima classe, non un errore. Uno spazio che il renderer non sa interpretare degrada verso un fallback definito invece di far abortire la pagina, il che corrisponde al comportamento dei visualizzatori più diffusi ed evita che un singolo riempimento esotico svuoti un documento per il resto renderizzabile
Come trasforma un tint transform un valore di inchiostro in un colore reale?
Uno spazio Separation porta con sé tre informazioni: il nome dell'inchiostro, uno spazio colore alternativo e una funzione di tint transform. L'array [/Separation /PANTONE485 /DeviceCMYK f] dice: quando il content stream scrive 0.8 scn, passa il valore di tinta 0.8 alla funzione f e dipingi la quadrupla CMYK risultante. DeviceN generalizza il meccanismo a N inchiostri con una funzione a N ingressi. Il nome dell'inchiostro di per sé è solo indicativo sullo schermo; il tint transform è l'intera semantica di rendering, quindi un renderer che analizza lo spazio ma salta la funzione non ha ancora fatto nulla di utile
HPDFEvalTintTransform è il motore di funzioni dietro quel passaggio. Introdotto nella v2.334.0, valuta le funzioni esponenziali Type 2 (C0 + x^N * (C1 - C0) con clamping su Domain e Range), le funzioni di cucitura Type 3 (ricorsione sulle sotto-funzioni selezionate da Bounds con rimappatura Encode) e le funzioni campionate Type 0 con campioni a 8, 16 e 32 bit. Type 0 è lo stesso meccanismo di lookup table che abbiamo trattato dal lato della creazione ne l'articolo sulla costruzione di LUT di colore Type 0; il lato di rendering percorre la struttura identica al contrario, dai byte dei campioni decodificati fino ai valori delle componenti
Le funzioni calcolatore PostScript Type 4 sono state l'ultima roccaforte. Fino alla v2.375.0 degradavano con garbo verso un segnaposto neutro; dalla v2.375.0 HPDFEvalPostScriptCalculator esegue l'intero set di operatori di ISO 32000-1 §7.10.5 su uno stack di operandi limitato: operatori aritmetici, di confronto, booleani e bit a bit, manipolazione dello stack compreso roll, e i condizionali if/ifelse. La semantica dei casi limite è più severa di quanto sembri. Il round di PostScript arrotonda il mezzo verso il valore maggiore, quindi non si può usare il Round di Delphi, che applica l'arrotondamento bancario; gli operatori trigonometrici lavorano in gradi, con atan che restituisce valori in [0, 360); e exp è una potenza a due operandi, non l'esponenziale naturale. Lo stesso valutatore alimenta anche i riempimenti sfumati basati su funzione, ed è per questo che il rendering delle shading assiali e radiali ha acquisito le rampe guidate dal calcolatore nella stessa release
// Valuta un tint transform Separation/DeviceN (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
const Inputs: THPDFColorComps; InputCount: Integer;
out AltComps: THPDFColorComps): Boolean;
// Calcolatore PostScript Type 4, set di operatori ISO 32000-1 7.10.5.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
const Inputs: THPDFColorComps; InputCount: Integer;
var Outputs: THPDFColorComps; OutCount: Integer): Boolean;
Onestà sulla precisione: un calcolatore software valutato in doppia precisione non corrisponderà bit per bit a un RIP, e il clamping al limite del Range può differire di un bit meno significativo rispetto a un'altra implementazione. Per la visualizzazione a schermo e per il rendering di regressione questo è irrilevante; se state costruendo un sistema di prova colore gestito, il tint transform è solo il primo stadio e a valle vi serve comunque un vero CMM
CalGray, CalRGB, Lab e ICCBased senza un motore ICC
Le famiglie basate su CIE prendono l'altro ramo dello stesso resolver. HotPDF converte i valori Lab attraverso la catena standard da Lab a XYZ a sRGB, compreso il cubo del punto di rottura 6/29 nella funzione di trasferimento inversa, e gestisce CalRGB con la sua gamma per canale più la matrice lineare 3x3 e CalGray con la sua gamma unica. Il dettaglio rilevante per le prestazioni è la gestione del punto di bianco: la matrice di conversione da XYZ a sRGB viene adattata con Bradford al punto di bianco dichiarato nello spazio colore e messa in cache nel record THPDFColorSpace risolto, così il lavoro per pixel resta una sola moltiplicazione 3x3 per quanto esotico sia l'illuminante dichiarato
Gli spazi ICCBased ricevono un trattamento deliberatamente pragmatico. La specifica PDF impone a ogni stream ICCBased di dichiarare uno spazio /Alternate, oppure uno implicito attraverso il conteggio delle componenti /N, proprio perché i visualizzatori privi di un motore di gestione colore possano comunque renderizzare in modo sensato. HotPDF risolve ICCBased attraverso quello spazio alternativo, oppure deduce DeviceGray, DeviceRGB o DeviceCMYK da /N pari a 1, 3 o 4 quando la voce manca, e non analizza mai i byte del profilo. Questo significa nessuna dipendenza da lcms e nessun costo di lookup del profilo, al prezzo dell'accuratezza colorimetrica: uno spazio ICCBased il cui profilo diverge fortemente dal proprio alternativo mostrerà il rendering dell'alternativo. Per la visione a schermo e per le miniature è lo stesso compromesso che accetta ogni visualizzatore leggero, ed è il confine da dichiarare senza giri di parole nella vostra documentazione
Il bug che faceva fallire ogni lookup di spazio colore per nome
L'impianto degli operatori della v2.333.0 è uscito con un difetto rimasto invisibile per quarantadue release: i gestori di cs e CS cercavano il proprio operando, con la barra iniziale intatta (/CS0), tra chiavi del dizionario delle risorse memorizzate senza barra (CS0). Ogni lookup di spazio colore per nome falliva, il cento per cento delle volte, e il codice ripiegava in silenzio sul DeviceGray predefinito. Il sintomo visibile era subdolo nel modo peggiore: un riempimento Separation di 1 scn diventava DeviceGray 1.0, che dipinge bianco, e inchiostro bianco su pagina bianca non è un errore di rendering di cui qualcuno faccia uno screenshot. La correzione, nella v2.375.0, è stata una funzione condivisa di normalizzazione dei nomi applicata a ogni lookup da operando a risorsa
Dalla stessa indagine sono emersi due difetti fratelli. Primo: i riferimenti indiretti a oggetti di tipo array venivano restituiti non risolti, perché l'accesso al documento del renderer disponeva di resolver tipizzati solo per stream e dizionari, quindi un /CS0 5 0 R che puntava a un array [/Separation ...] autonomo tornava come riferimento grezzo e lo spazio veniva analizzato come non supportato. Secondo: HPDFReadNumericArray applicava una semantica di lunghezza rigida, pretendendo che l'array PDF fosse lungo almeno quanto il buffer fornito. Leggere /C0 e /C1 di una funzione Type 2 in un buffer da quattro elementi falliva perciò per gli spazi alternativi a una e a tre componenti, lasciando entrambi gli array azzerati, e ogni tinta esponenziale non CMYK veniva resa in nero finché la v2.376.0 non ha introdotto il lettore permissivo HPDFReadNumericArrayUpTo. La lezione trasferibile: qualsiasi chiave PDF documentata come contenitore di un array numerico a lunghezza variabile va letta con un lettore che riempia ciò che esiste, perché un buffer di dimensione fissa con corrispondenza rigida trasforma file validi in zeri silenziosi
Come si collauda il rendering delle tinte piatte senza illudersi?
La domanda scomoda è perché la suite di test sia rimasta verde attraverso tutto questo. La regressione originale, SeparationRendersDistinguishable, verificava soltanto che il bitmap renderizzato non fosse interamente nero. Una pipeline che schiacciava ogni tinta piatta su DeviceGray produceva un output grigio e bianco, che non è nero, quindi l'asserzione passava mentre l'intera funzionalità era morta. Le asserzioni deboli nella forma "non è vuoto", "non è tutto nero" oppure "il digest non è zero" non sanno distinguere un renderer funzionante da uno rotto, perché quasi ogni modalità di guasto produce comunque dei pixel
Lo stile di asserzione che intercetta davvero questi guasti fissa il colore atteso: si renderizza un PDF minimo costruito a mano il cui inchiostro Separation si risolve in una tonalità nota, poi si contano i pixel dominanti in quella tonalità. Il rendering su bitmap per l'ispezione usa lo stesso punto di ingresso RenderLoadedPageToBitmap descritto nella guida al rendering da pagina a bitmap
var
Pdf: THotPDF;
Bmp: TBitmap;
X, Y, RedHits: Integer;
Px: TColor;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
begin
Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
try
RedHits := 0;
for Y := 0 to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do
begin
Px := Bmp.Canvas.Pixels[X, Y];
if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
(GetBValue(Px) < 100) then
Inc(RedHits);
end;
// Fissa l'inchiostro atteso: pretendi un'area reale di pixel a
// dominante rossa, non accontentarti mai di "non tutto nero".
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Il fixture conta quanto l'asserzione. Un PDF costruito a mano, lungo poche centinaia di byte, con un solo riempimento Separation e nient'altro, non lascia ambiguità su quale sia l'output atteso; un file del mondo reale esercita più codice ma non sa dirvi quale stadio abbia fallito. Oggi consideriamo il conteggio dei pixel del colore atteso la soglia minima per qualsiasi smoke test di rendering, perché è l'unico stile di asserzione che ha portato allo scoperto questi difetti
Il rendering delle tinte piatte e degli spazi colore CIE fa parte della pipeline per documenti caricati del componente HotPDF per Delphi per Delphi e C++Builder, insieme al motore di funzioni, al rendering delle shading e ai percorsi di esportazione bitmap mostrati sopra