En /Indexed-uppslagstabell lagrar komponenter av sitt basfärgrymd, inte RGB-tripplar. En DeviceGray-bas packar en byte per post, DeviceCMYK fyra, Lab tre som behöver omfångsskalning, och ICCBased vad profilen /N än deklarerar. HotPDF normaliserar alla vid parse-tidpunkten till den publika RGB-paletten som THPDFColorSpace exponerar
Den meningen är hela buggklassen. Nästan varje palettrenderare börjar sitt liv med att anta en tre-byte-stride, för den överväldigande majoriteten av verkliga /Indexed-rymder sitter på /DeviceRGB och antagandet håller i åratal. Sedan anländer en skannad faktura med [/Indexed /DeviceCMYK 15 <...>], läsaren vandrar igenom uppslagssträngen tre byte i taget, och varje index över noll hamnar mitt i någon annans färg. Utdata är inte blank och den kraschar inte. Det är trovärdigt skräp, vilket är mycket värre, för ingen anmäler en bugg mot färger som bara ser lite aviga ut
Varför blir en Indexed-palett fel?
Striden är fel för att striden inte är en konstant. ISO 32000-1 §8.6.6.3 definierar uppslagstabellen som m × (hival + 1) byte, där m är antalet komponenter i basrymden, och den säger ingenting om att basen är RGB. Samma avsnitt förbjuder bara två baser rakt av: /Indexed själv och /Pattern. Allt annat är fritt fram, inklusive alla fyra CIE-baserade familjerna från §8.6.5.2 till §8.6.5.5
Så det verkliga arbetet är inte att läsa byte, det är att lösa upp basen först och fråga den hur bred en post är. ResolveIndexedPalette i HotPDF gör exakt det: den löser upp basen till en THPDFColorSpace, tar ComponentCount ur den upplösta posten och avvisar arrayen rakt av om basen kom tillbaka som csfIndexed, csfPattern eller csfUnsupported. Först därefter storlekar den bytebudgeten som PaletteCount * Count, vilket för det lagliga maxvärdet hival 255 och en fyra-komponentbas toppar vid 1024 byte. Två detaljer i den storleksättningen är där en fientlig fil kommer att sondera dig. HotPDF klämmer hival till 0..255 innan den används, enligt gränsen i §8.6.6.3, så en uppslagsarray som påstår hival 100000 förvandlas inte till en gigabyte-allokering; och den trunkerar budgeten till vad uppslagsdatan faktiskt innehåller, och slutar sedan fylla paletten vid den första post vars byte rinner förbi slutet. En kort tabell ger en kort palett, inte en läsning förbi bufferten
Att normalisera fyra olika basrymder till en RGB-tabell
HotPDF konverterar paletter till RGB under parsningen snarare än vid ritningstidpunkten, och det beslutet driver resten av designen. Det publika fältet THPDFColorSpace.Palette är en TBytes av RGB-tripplar med längden 3 * PaletteCount, och det har haft den formen sedan långt innan CIE-baserade baser stöddes. Att skriva om postlayouten för att bära native baskomponenter skulle ha brutit varje konsument av fältet. Att konvertera vid parse-tidpunkten håller layouten intakt och, som bonus, dirigerar Gray-, CMYK-, Lab-, CalGray-, CalRGB- och ICCBased-poster genom samma HPDFResolveColor-väg som renderaren redan använder för direkta färgoperatorer
Skalningssteget är där basrymderna slutar vara utbytbara. En uppslagsbyte är alltid 0..255, men vad den byten betyder beror på det komponentomfång basen deklarerar. Device- och kalibrerade komponenter mappar på 0..1. Lab gör inte det: L*-komponenten går 0..100, medan a* och b* går över det /Range rymden deklarerar. HotPDF drar de gränserna ur fälten AMin, AMax, BMin och BMax i THPDFCIEParams, som Lab-parsern fyller från dictionary-postens /Range och sätter som standard till −108..101 när posten saknas, så hardcoda inte ett symmetriskt omfång ur minnet. ICCBased-baser lägger till en tredje sanningens källa: komponentantalet kommer från profilströmmens /N, exponerat som ICCN, och en ICCBased-ström kan själv bära en /Range-array som HotPDF läser genom HPDFReadNumericArrayUpTo in i samma per-komponent-gränstabell. En trekomponents-ICC-palett och en trekomponents-Lab-palett vandrar därför uppslagssträngen med identiska strider och helt olika aritmetik, vilket är exakt det utfall en fast RGB-stride inte kan producera
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj är /ColorSpace-posten från en bild-XObject eller en
// resursdictionary; den kan vara ett namn, en array eller en indirekt ref
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// Paletten är redan RGB oavsett vad basrymden var
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // kort uppslagstabell, stanna rent
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
När uppslagstabellen bor i en stream
ISO 32000-1 §8.6.6.3 tillåter att uppslagstabellen är en string eller en stream, och stream-formen är den som biter. En stream kan bära /FlateDecode, den kan vara ett indirekt objekt delat med orelaterade delar av filen, och dess TStream kan vara positionerad var som helst i det ögonblick din parser når den. HotPDF hanterar det komprimerade fallet genom att delegera till decoded-bytes-callbacken som registrerats genom HPDFSetColorStreamReader, samma callback som Type 0-sampled-function-motorn använder
När ingen läsare är registrerad är fallbacken medvetet smal snarare än best-effort. HotPDF kontrollerar stream-dictionaryn efter /Filter och vägrar paletten helt om en finns, i stället för att mata komprimerade byte in i uppslagningen och producera en självsäkert fel tabell. Utan filter läser den råa byte, begränsade till den bytebudget som beräknats ur hival och basens komponentantal. Hela stream-grenen sitter inuti en try..finally som återställer det ursprungliga Stream.Position på varje utgångsväg, inklusive vägran, för objektet är delat och nästa konsument kommer inte att tacka dig för en flyttad markör. LookupStreamUsesDecodedReaderAndRestoresPosition i regressionssviten låser fast båda halvorna av det beteendet
// Registrera båda callbackarna innan du löser upp något färgrymd, annars
// vägras indirekta baser och komprimerade uppslagsströmmar helt enkelt
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;
Vad stoppar ett Indexed-färgrymd från att rekursera i all evighet?
Att avvisa /Indexed som direkt bas är nödvändigt och långt ifrån tillräckligt. Den intressanta attacken går i sidled. Ta en giltig [/Indexed /Separation ...], och kom ihåg att §8.6.6.4 låter en Separation namnge ett alternativt färgrymd och §8.6.6.5 låter DeviceN göra detsamma. Gör det alternativet till en indirekt referens som pekar tillbaka på den ursprungliga Indexed-arrayen. Ingenting i kedjan bryter regeln om direkt bas vid något enskilt hopp: basen är en Separation, Separationens alternativ är en array, och arrayen råkar bara vara den du startade från. Lös upp den naivt och parsern vandrar loopen tills stacken ger upp. Tint-transform-maskineriet de rymderna förlitar sig på är ett ämne för sig, täckt i artikeln om att rendera Separation- och DeviceN-spotfärger; här spelar de roll bara som en länk i cykeln
HotPDF stänger detta vid den enda ingångspunkt varje väg redan rinner genom. HPDFResolveColorSpace sveper den interna resolveren i en djupräknare som begränsas av HPDFColorSpaceMaxDepth, vilket är 64. Korsa den gränsen och vakten sätter en flagga, och flaggan är det som spelar roll: vid unwind kastar varje nästlingsnivå som såg den satt bort sin egen partiellt upplösta post och returnerar csfUnsupported, så hela grafen avvisas snarare än att hälften behålls, och båda räknarna återställs när det yttersta anropet returnerar. De räknarna är deklarerade som threadvar, inte unit-globaler, och det är inte tillfällig prydlighet: GResolveObj och GReadStream bor i thread-lokal lagring av samma anledning, för sidor renderas parallellt. En delad global skulle låta en tråd djupt i en nästlad DeviceN-kedja utlösa gränsen för en annan tråd som parsar en fullt vanlig [/Indexed /DeviceRGB 255 <...>], vilket producerar ett icke-deterministiskt, lastberoende misslyckande — den minst felsökningsbara sorten som finns. Sextiofyra är bekvämt headroom för legitimt innehåll, som sällan nästlar förbi fyra eller fem nivåer, så behandla csfUnsupported från en strukturellt giltig-utseende array som värd att logga: i praktiken betyder det en cykel eller ett djup ingen verklig producent sänder ut. IndirectAlternateCycleIsUnsupported, parameteriserad över Separation och DeviceN, och ExcessivePatternDepthIsUnsupported täcker båda fallen
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported här täcker en genuint okänd familj, en cykel
// genom ett indirekt alternativ, och djup bortom 64 nivåer
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray, PDF:ens initiala färgrymd
end;
En API-gräns värd att dokumentera för dina anropare
HotPDF tillämpar den ICC-konverteringskonfiguration som gäller vid parse-tidpunkten när den bygger en Indexed-palett, och senare anrop av HPDFSetColorSpaceRenderingIntent regenererar inte en palett som redan är upplöst. Den funktionen vandrar igenom ICCBased- och alternativtransformerna i posten du lämnar den och bygger om de pipeline:na; de plattade RGB-byten i Palette är en parse-tids-artefakt och förblir som de var. Det är en konsekvens av att normalisera tidigt, inte en förbiseelse, och det är värt att skriva in i din egen API-dokumentation i stället för att lämna till en kund att upptäcka. Om din applikation låter en användare byta rendering intent mitt i en session, lös upp färgrymderna igen efter bytet; om den sätter intent en gång under pipeline-uppsättningen, vilket är det överväldigande vanliga fallet, finns det inget att göra. Samma ordning gäller för proofing-profilerna som installeras genom HPDFSetICCColorPipeline: konfigurera dem innan du börjar lösa upp, inte efter
Spegelbildsbuggen på skrivsidan
Allt ovan handlar om läsning. Den symmetriska defekten på skrivsidan är lättare att släppa och mycket lättare att missa. När HotPDF bygger en /Indexed-array från en Windows 8-bitars bitmap anropar den GetPaletteEntries, och det API:et fyller bara så många poster som paletten faktiskt håller och returnerar det antalet. Den ursprungliga koden testade resultatet för = 0 och itererade sedan över fasta 256 poster i en stack-array. För en 16-färgers bitmap skrev det 240 poster av oinitierat stackminne rakt in i uppslagssträngen i en PDF du sedan lämnar till en kund, med hival hardcodad till 255 så att varje läsare plikttroget behandlade resten som riktig färgdata. Det är en korrekthetsbugg och en informationsläckagebugg klädda i samma kläder. Fixen är oglamorös och fullständig: nollställ arrayen före anropet, hedra sedan det returnerade antalet för både loopgränsen och hival. Genererar du /Indexed-bilder någonstans i din egen kod, gå och kolla hival nu
Två angränsande bitar fullbordar bilden: Indexed-paletter och Type 0-sampled-funktioner delar decoded-stream-läsaren och samma klämdisciplin, diskuterat i stycket om Type 0-funktioners färguppslagstabeller, och filterreglerna som styr uppslagsströmmar är desamma som styr bilddata i att extrahera inlästa bilder genom avkodningsfilter. Färgrymdsresolveren som beskrivs här levereras som en del av HotPDF Delphi Component för Delphi och C++Builder, med fullständig THPDFColorSpace-referens på produktsidan