Una lookup table /Indexed memorizza i componenti del suo spazio colore base, non triple RGB. Una base DeviceGray impacchetta un byte per voce, DeviceCMYK quattro, Lab tre che richiedono scaling dei range, e ICCBased qualunque cosa dichiari il /N del profilo. HotPDF le normalizza tutte al momento del parsing nella palette RGB pubblica che THPDFColorSpace espone
Quella frase è l'intera classe di bug. Quasi ogni renderer di palette nasce assumendo uno stride di tre byte, perché la stragrande maggioranza degli spazi /Indexed reali sta su /DeviceRGB e l'assunzione regge per anni. Poi arriva una fattura scansionata con [/Indexed /DeviceCMYK 15 <...>], il lettore percorre la stringa della lookup tre byte alla volta, e ogni indice oltre zero finisce in mezzo al colore di qualcun altro. L'output non è vuoto e non crasha. È spazzatura dall'aspetto plausibile, che è molto peggio, perché nessuno apre un bug contro colori che semplicemente sembrano storti
Perché una palette Indexed esce sbagliata?
Lo stride è sbagliato perché lo stride non è una costante. ISO 32000-1 §8.6.6.3 definisce la lookup table come m × (hival + 1) byte, dove m è il numero di componenti dello spazio base, e non dice nulla sul fatto che quella base sia RGB. La stessa sezione vieta esplicitamente solo due basi: /Indexed stesso e /Pattern. Tutto il resto è lecito, incluse tutte e quattro le famiglie basate su CIE dalla §8.6.5.2 alla §8.6.5.5
Quindi il lavoro vero non è leggere byte, è risolvere prima la base e chiederle quanto è larga una voce. ResolveIndexedPalette in HotPDF fa esattamente questo: risolve la base in un THPDFColorSpace, prende ComponentCount dal record risolto e rifiuta l'array secco se la base torna come csfIndexed, csfPattern o csfUnsupported. Solo allora dimensiona il budget di byte come PaletteCount * Count, che per il massimo legale di hival 255 e una base a quattro componenti arriva a 1024 byte. Due dettagli di quel dimensionamento sono dove un file ostile ti sonderà. HotPDF blocca hival in 0..255 prima di usarlo, secondo il limite della §8.6.6.3, così una lookup che dichiara hival 100000 non diventa un'allocazione da gigabyte; e tronca il budget a qualunque cosa contengano davvero i dati della lookup, poi smette di riempire la palette alla prima voce i cui byte superano la fine. Una tabella corta produce una palette corta, non una lettura oltre il buffer
Normalizzare quattro spazi base diversi in un'unica tabella RGB
HotPDF converte le palette in RGB durante il parsing invece che al momento del disegno, e quella decisione guida tutto il resto del design. Il campo pubblico THPDFColorSpace.Palette è un TBytes di triple RGB con lunghezza 3 * PaletteCount, ed ha quella forma da molto prima che le basi CIE-based fossero supportate. Riscrivere il layout del record per trasportare i componenti nativi della base avrebbe rotto ogni consumatore del campo. Convertire al parse time tiene intatto il layout e, come bonus, instrada le voci Gray, CMYK, Lab, CalGray, CalRGB e ICCBased attraverso lo stesso percorso HPDFResolveColor che il renderer usa già per gli operatori colore diretti
Il passo di scaling è dove gli spazi base smettono di essere intercambiabili. Un byte di lookup è sempre 0..255, ma ciò che quel byte significa dipende dal range di componenti che la base dichiara. I componenti Device e calibrati mappano su 0..1. Lab no: il componente L* va da 0..100, mentre a* e b* coprono il /Range che lo spazio dichiara. HotPDF prende quei limiti dai campi AMin, AMax, BMin e BMax di THPDFCIEParams, che il parser Lab riempie dalla voce /Range del dizionario e porta di default a −108..101 quando la voce manca, quindi non hardcodare a memoria un range simmetrico. Le basi ICCBased aggiungono una terza fonte di verità: il numero di componenti viene dallo stream del profilo /N, esposto come ICCN, e uno stream ICCBased può a sua volta portare un array /Range che HotPDF legge con HPDFReadNumericArrayUpTo nella stessa tabella di limiti per componente. Una palette ICC a tre componenti e una palette Lab a tre componenti percorrono quindi la stringa della lookup con stride identici e aritmetica completamente diversa, che è esattamente il risultato che uno stride RGB fisso non può produrre
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj è la voce /ColorSpace di un image XObject o di un
// dizionario risorse; può essere un nome, un array o un riferimento indiretto
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// Palette è già RGB qualunque fosse lo spazio base
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // lookup table corta, fermati con ordine
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
Quando la lookup table vive in uno stream
ISO 32000-1 §8.6.6.3 permette che la lookup table sia una stringa o uno stream, ed è la forma a stream quella che morde. Uno stream può portare /FlateDecode, può essere un oggetto indiretto condiviso con parti non correlate del file, e il suo TStream può trovarsi in qualunque posizione nel momento in cui il tuo parser lo raggiunge. HotPDF gestisce il caso compresso delegando alla callback di byte decodificati registrata tramite HPDFSetColorStreamReader, la stessa callback che usa il motore della funzione campionata Type 0
Quando nessun lettore è registrato, il fallback è volutamente stretto invece che best-effort. HotPDF controlla nel dizionario dello stream la presenza di /Filter e rifiuta del tutto la palette se ce n'è uno, invece di alimentare la lookup con byte compressi e produrre una tabella sicuramente sbagliata. Senza filtro legge byte grezzi, limitati al budget calcolato da hival e dal numero di componenti della base. L'intero ramo dello stream sta dentro un try..finally che ripristina la Stream.Position originale su ogni percorso di uscita, inclusi i rifiuti, perché l'oggetto è condiviso e il prossimo consumatore non ti ringrazierà per un cursore spostato. LookupStreamUsesDecodedReaderAndRestoresPosition nella suite di regressione fissa entrambe le metà di quel comportamento
// Registra entrambe le callback prima di risolvere qualsiasi spazio colore, altrimenti
// le basi indirette e le lookup stream compresse verranno semplicemente rifiutate
HPDFSetColorSpaceResolver(MyResolveIndirectObject);
HPDFSetColorStreamReader(MyDecodedStreamReader);
try
CS := HPDFResolveColorSpace(ColorSpaceObj);
if (CS.Family = csfIndexed) and (Length(CS.Palette) = 0) then
LogWarning('lookup table unreadable: filtered stream or no reader');
finally
HPDFSetColorStreamReader(nil);
HPDFSetColorSpaceResolver(nil);
end;
Cosa impedisce a uno spazio colore Indexed di ricorrere all'infinito?
Rifiutare /Indexed come base diretta è necessario e per niente sufficiente. L'attacco interessante va di lato. Prendi un valido [/Indexed /Separation ...] e ricorda che la §8.6.6.4 permette a una Separation di nominare uno spazio alternativo e la §8.6.6.5 permette a DeviceN di fare lo stesso. Fai sì che quell'alternativa sia un riferimento indiretto che punta di nuovo all'array Indexed originale. Nulla nella catena viola la regola della base diretta in un singolo salto: la base è una Separation, l'alternativa della Separation è un array, e l'array capita solo di essere quello da cui sei partito. Risolvilo in modo ingenuo e il parser percorre il ciclo finché lo stack cede. La macchina delle tint-transform su cui quegli spazi si appoggiano è un argomento a sé, coperto in l'articolo sul rendering dei colori spot Separation e DeviceN; qui contano solo come anello del ciclo
HotPDF chiude questo all'unico punto di ingresso in cui tutti i percorsi già confluiscono. HPDFResolveColorSpace avvolge il resolver interno in un contatore di profondità limitato da HPDFColorSpaceMaxDepth, che è 64. Supera quel limite e la guardia imposta un flag, ed è il flag che conta: durante l'unwind, qualsiasi livello di annidamento che lo ha visto impostato scarta il proprio record parzialmente risolto e restituisce csfUnsupported, quindi viene rifiutato l'intero grafo invece di tenerne metà, ed entrambi i contatori si azzerano quando la chiamata più esterna ritorna. Quei contatori sono dichiarati come threadvar, non globali di unit, e non è ordine casuale: GResolveObj e GReadStream vivono nella memoria locale dei thread per lo stesso motivo, perché le pagine vengono renderizzate in parallelo. Un globale condiviso lascerebbe che un thread in fondo a una catena DeviceN annidata faccia scattare il limite per un altro thread che analizza un perfettamente ordinario [/Indexed /DeviceRGB 255 <...>], producendo un fallimento non deterministico e dipendente dal carico, il tipo meno debuggabile che esista. Sessantaquattro è un margine comodo per i contenuti legittimi, che raramente si annidano oltre quattro o cinque livelli, quindi tratta un csfUnsupported da un array dall'aspetto strutturalmente valido come degno di log: in pratica significa un ciclo o una profondità che nessun produttore reale emette. IndirectAlternateCycleIsUnsupported, parametrizzata su Separation e DeviceN, e ExcessivePatternDepthIsUnsupported coprono entrambi i casi
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported qui copre una famiglia davvero sconosciuta, un ciclo
// attraverso un'alternativa indiretta e la profondità oltre 64 livelli
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray, lo spazio iniziale del PDF
end;
Un confine API che vale la pena documentare per i tuoi chiamanti
HotPDF applica la configurazione di conversione ICC in vigore al parse time quando costruisce una palette Indexed, e le chiamate successive a HPDFSetColorSpaceRenderingIntent non rigenerano una palette già risolta. Quella funzione percorre le trasformate ICCBased e alternative del record che le passi e ricostruisce quelle pipeline; i byte RGB appiattiti in Palette sono un artefatto del parse time e restano come erano. È una conseguenza della normalizzazione precoce, non una svista, e vale la pena scriverlo nella tua documentazione API invece di lasciare che lo scopra un cliente. Se la tua applicazione permette all'utente di cambiare rendering intent a metà sessione, risolvi di nuovo gli spazi colore dopo il cambio; se imposta l'intent una volta sola durante l'installazione della pipeline, che è il caso di gran lunga più comune, non c'è nulla da fare. Lo stesso ordine vale per i profili di proofing installati tramite HPDFSetICCColorPipeline: configurali prima di iniziare a risolvere, non dopo
Il bug speculare sul lato scrittura
Tutto quanto sopra riguarda la lettura. Il difetto simmetrico sul lato scrittura è più facile da rilasciare e molto più facile da non vedere. Quando HotPDF costruisce un array /Indexed da una bitmap Windows a 8 bit chiama GetPaletteEntries, e quella API riempie solo quante voci la palette contiene davvero e restituisce quel numero. Il codice originale testava il risultato per = 0 e poi iterava su un array di stack con 256 voci fisse. Per una bitmap a 16 colori questo scriveva 240 voci di memoria di stack non inizializzata dritte nella stringa della lookup di un PDF che poi dai a un cliente, con hival hardcodato a 255 così ogni lettore trattava fedelmente il residuo come veri dati colore. È un bug di correttezza e un bug di divulgazione di informazioni con gli stessi vestiti. La correzione è poco affascinante e completa: azzera l'array prima della chiamata, poi onora il conteggio restituito sia per il limite del ciclo sia per hival. Se generi immagini /Indexed da qualche parte nel tuo codice, vai a controllare quel hival adesso
Due pezzi adiacenti completano il quadro: le palette Indexed e le funzioni campionate Type 0 condividono il lettore di stream decodificati e la stessa disciplina di clamping, discussa in il pezzo sulle lookup table colore delle funzioni Type 0, e le regole sui filtri che governano le lookup stream sono le stesse che governano i dati immagine in estrarre le immagini caricate attraverso i filter di decodifica. Il resolver di spazi colore descritto qui fa parte della HotPDF Delphi Component per Delphi e C++Builder, con il riferimento completo di THPDFColorSpace nella pagina del prodotto