En /Indexed-lookup-tabel gemmer komponenter af sit base color space, ikke RGB-tripels. En DeviceGray-base pakker én byte per post, DeviceCMYK fire, Lab tre, der har brug for range-skalering, og ICCBased hvad end profilen /N erklærer. HotPDF normaliserer dem alle ved parse-tidspunktet til den offentlige RGB-palette, som THPDFColorSpace eksponerer
Den sætning er hele bug-klassen. Næsten enhver palette-renderer starter livet med at antage et tre-byte-stride, fordi det overvældende flertal af virkelige /Indexed-spaces sidder på /DeviceRGB, og antagelsen holder i årevis. Så ankommer en skannet faktura med [/Indexed /DeviceCMYK 15 <...>], readeren gennemgår lookup-strengen tre bytes ad gangen, og hvert indeks over nul lander midt i en andens farve. Outputtet er ikke blankt, og det crasher ikke. Det er plausibelt udseende skrald, hvilket er langt værre, fordi ingen indleverer en bug mod farver, der bare ser lidt forkerte ud
Hvorfor bliver en Indexed-palette forkert?
Stride'en er forkert, fordi stride'en ikke er en konstant. ISO 32000-1 §8.6.6.3 definerer lookup-tabellen som m × (hival + 1) bytes, hvor m er antallet af komponenter i base-spacet, og den siger intet om, at det base er RGB. Samme afsnit forbyder kun to baser direkte: /Indexed selv og /Pattern. Alt andet er frit spil, inklusive alle fire CIE-baserede familier fra §8.6.5.2 til §8.6.5.5
Så det egentlige arbejde er ikke at læse bytes, det er at opløse basen først og spørge den, hvor bred en post er. ResolveIndexedPalette i HotPDF gør præcis det: Den opløser basen til en THPDFColorSpace, tager ComponentCount fra den opløste record og afviser arrayet direkte, hvis basen kom tilbage som csfIndexed, csfPattern eller csfUnsupported. Først derefter sætter den byte-budgettet til PaletteCount * Count, som ved det lovlige maksimum på hival 255 og en fire-komponent-base topper ved 1024 bytes. To detaljer i den størrelsesberegning er der, hvor en fjendtlig fil vil sondere dig. HotPDF klamper hival ind i 0..255, før den bruger det, ifølge §8.6.6.3-grænsen, så en lookup-array, der påstår hival 100000, bliver ikke til en gigabyte-allokering; og den trunkerer budgettet til hvad end lookup-dataene faktisk indeholder, og stopper derefter med at udfylde paletten ved den første post, hvis bytes løber forbi enden. En kort tabel giver en kort palette, ikke en read forbi bufferen
At normalisere fire forskellige base-spaces til én RGB-tabel
HotPDF konverterer paletter til RGB under parsing i stedet for ved draw-tidspunktet, og den beslutning driver resten af designet. Det offentlige THPDFColorSpace.Palette-felt er en TBytes af RGB-tripels med længde 3 * PaletteCount, og det har haft den form siden længe før CIE-baserede baser blev understøttet. At omskrive record-layoutet til at bære native base-komponenter ville have brudt hver forbruger af feltet. At konvertere ved parse-tidspunktet holder layoutet intakt og ruter, som bonus, Gray-, CMYK-, Lab-, CalGray-, CalRGB- og ICCBased-poster gennem den samme HPDFResolveColor-vej, som rendereren allerede bruger til direkte farveoperatorer
Skaleringsskridtet er der, hvor base-spaces holder op med at være udskiftelige. En lookup-byte er altid 0..255, men hvad den byte betyder, afhænger af det komponent-range, basen erklærer. Device- og kalibrerede komponenter mappes på 0..1. Lab gør ikke: L*-komponenten løber 0..100, mens a* og b* løber over det /Range, spacet erklærer. HotPDF trækker de grænser fra felterne AMin, AMax, BMin og BMax i THPDFCIEParams, som Lab-parseren udfylder fra ordbogens /Range-post og som standard sætter til −108..101, når posten mangler, så hardcode ikke et symmetrisk range fra hukommelsen. ICCBased-baser tilføjer en tredje kilde til sandhed: Komponentantallet kommer fra profil-streamens /N, eksponeret som ICCN, og en ICCBased-stream kan selv bære et /Range-array, som HotPDF læser gennem HPDFReadNumericArrayUpTo ind i den samme per-komponent-grænsetabel. En tre-komponent-ICC-palette og en tre-komponent-Lab-palette gennemgår derfor lookup-strengen med identiske strides og fuldstændig forskellig aritmetik, hvilket er præcis det resultat, et fast RGB-stride ikke kan producere
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj er /ColorSpace-posten fra et image XObject eller en
// resource-ordbog; det kan være et navn, en array eller en indirekte ref
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// Palette er allerede RGB uanset hvad base-spacet var
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // kort lookup-tabel, stop 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 lookup-tabellen bor i en stream
ISO 32000-1 §8.6.6.3 tillader lookup-tabellen at være en string eller en stream, og stream-formen er den, der bider. En stream kan bære /FlateDecode, den kan være et indirekte objekt delt med urelaterede dele af filen, og dens TStream kan være positioneret hvor som helst i det øjeblik, din parser når til den. HotPDF håndterer det komprimerede tilfælde ved at delegere til decoded-bytes-callbacken registreret gennem HPDFSetColorStreamReader, den samme callback, som Type 0 sampled-function-motoren bruger
Når ingen reader er registreret, er faldbacken bevidst snæver frem for best-effort. HotPDF tjekker stream-ordbogen for /Filter og afviser paletten fuldstændigt, hvis en er til stede, i stedet for at fodre komprimerede bytes ind i lookup'en og producere en selvsikkert forkert tabel. Uden et filter læser den rå bytes, kappet ved det byte-budget, der er beregnet fra hival og base-komponentantallet. Hele stream-grenen sidder inde i en try..finally, der gendanner den oprindelige Stream.Position på hver udgangsvej, inklusive afvisningerne, fordi objektet er delt, og den næste forbruger vil ikke takke dig for en flyttet cursor. LookupStreamUsesDecodedReaderAndRestoresPosition i regressionssuiten fastnåler begge halvdele af den adfærd
// Registrér begge callbacks, før du opløser nogen color space, ellers
// vil indirekte baser og komprimerede lookup-streams simpelthen blive afvist
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;
Hvad stopper et Indexed color space fra at rekursere i det uendelige?
At afvise /Indexed som en direkte base er nødvendigt og langt fra tilstrækkeligt. Det interessante angreb går sidelæns. Tag en gyldig [/Indexed /Separation ...], og husk, at §8.6.6.4 lader en Separation navngive et alternate-space, og at §8.6.6.5 lader DeviceN gøre det samme. Gør det alternate til en indirekte reference, der peger tilbage på den oprindelige Indexed-array. Intet i kæden bryder den direkte-base-regel ved noget enkelt hop: Basen er en Separation, Separation-alternaten er en array, og arrayen er bare tilfældigvis den, du startede fra. Opløs den naivt, og parseren går løkken rundt, indtil stakken giver op. Tint-transform-maskineriet, som de spaces er afhængige af, er et emne for sig selv, dækket i artiklen om at rendre Separation- og DeviceN-spotfarver; her betyder de kun noget som et led i cyklussen
HotPDF lukker dette ved det enkeltstående indgangspunkt, som enhver vej allerede løber gennem. HPDFResolveColorSpace pakker den interne resolver ind i en dybdetæller afgrænset af HPDFColorSpaceMaxDepth, som er 64. Krydser du den grænse, sætter værgten et flag, og flaget er det, der betyder noget: Ved unwinding kasserer ethvert nesting-niveau, der så det sat, sin egen delvist opløste record og returnerer csfUnsupported, så hele grafen afvises i stedet for at halvdelen beholdes, og begge tællere nulstilles, når det yderste kald vender tilbage. De tællere er erklæret som threadvar, ikke unit-globaler, og det er ikke tilfældig pænhed: GResolveObj og GReadStream bor i thread-local storage af samme grund, fordi sider renderer parallelt. En delt global ville lade én tråd dybt inde i en næstet DeviceN-kæde udløse grænsen for en anden tråd, der parser en helt almindelig [/Indexed /DeviceRGB 255 <...>], hvilket producerer en ikke-deterministisk, load-afhængig fejl — den mindst debuggbare slags, der findes. Fireogtres er behageligt hovedrum til legitimt indhold, som sjældent næster dybere end fire eller fem niveauer, så behandl csfUnsupported fra en strukturelt gyldigt udseende array som værd at logge: I praksis betyder det en cyklus eller en dybde, ingen rigtig producent emitterer. IndirectAlternateCycleIsUnsupported, parameteriseret over Separation og DeviceN, og ExcessivePatternDepthIsUnsupported dækker begge tilfælde
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported her dækker en ægte ukendt familie, en cyklus gennem
// en indirekte alternate og dybde ud over 64 niveauer
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray, PDF'ens begyndelsesspace
end;
Én API-grænse værd at dokumentere for dine callers
HotPDF anvender den ICC-konverteringskonfiguration, der er gældende ved parse-tidspunktet, når den bygger en Indexed-palette, og senere kald til HPDFSetColorSpaceRenderingIntent regenererer ikke en palette, der allerede er opløst. Den funktion gennemgår ICCBased- og alternate-transformene på den record, du giver den, og genopbygger de pipelines; de fladtrykte RGB-bytes i Palette er et parse-time-artefakt og forbliver som de var. Det er en konsekvens af at normalisere tidligt, ikke en forglemmelse, og det er værd at skrive ind i dine egne API-docs i stedet for at lade en kunde opdage det. Hvis din applikation lader en bruger skifte rendering-intent midt i en session, så opløs color spaces igen efter skiftet; hvis den sætter intent én gang under pipeline-opsætningen, hvilket er det overvældende almindelige tilfælde, er der intet at gøre. Den samme rækkefølge gælder proofing-profilerne installeret gennem HPDFSetICCColorPipeline: Konfigurér dem, før du begynder at opløse, ikke bagefter
Spejl-billede-buggen på skrivesiden
Alt ovenstående handler om læsning. Den symmetriske defekt på skrivesiden er nemmere at shippe og meget nemmere at misse. Når HotPDF bygger en /Indexed-array fra et Windows 8-bit bitmap, kalder den GetPaletteEntries, og det API udfylder kun lige så mange poster, som paletten faktisk holder, og returnerer det antal. Den oprindelige kode testede resultatet for = 0 og itererede derefter over et fast 256-poster stak-array. For et 16-farve-bitmap skrev det 240 poster af uinitialiseret stak-hukommelse direkte ind i lookup-strengen i en PDF, du derefter gav til en kunde, med hival hardcoded til 255, så enhver reader pligtskyldigt behandlede resten som rigtige farvedata. Det er en korrekthedsbug og en information-disclosure-bug iført samme tøj. Fixet er uglamourøst og komplet: Nulstil arrayet før kaldet, og honorér derefter den returnerede tælling for både løkke-grænsen og hival. Hvis du genererer /Indexed-billeder nogen steder i din egen kode, så gå og tjek det hival nu
To tilstødende emner runder billedet af: Indexed-paletter og Type 0 sampled-funktioner deler decoded-stream-readeren og den samme klampe-disciplin, diskuteret i emnet om Type 0-funktioners color lookup-tabeller, og filterreglerne, der styrer lookup-streams, er de samme, der styrer billeddata i at ekstrahere indlæste billeder gennem decode-filtre. Color-space-resolveren beskrevet her shipper som en del af HotPDF Delphi Component til Delphi og C++Builder, med den fulde THPDFColorSpace-reference på produktsiden