Eine /Indexed-Lookup-Tabelle speichert Komponenten ihres Basis-ColorSpaces, keine RGB-Tripel. Eine DeviceGray-Basis packt ein Byte pro Eintrag, DeviceCMYK vier, Lab drei, die eine Range-Skalierung brauchen, und ICCBased das, was das Profil-/N deklariert. HotPDF normalisiert alle beim Parsen in die öffentliche RGB-Palette, die THPDFColorSpace bereitstellt
Dieser Satz ist die ganze Bugklasse. Fast jeder Paletten-Renderer startet mit der Annahme eines Drei-Byte-Stride, weil die überwältigende Mehrheit realer /Indexed-Spaces auf /DeviceRGB sitzt und die Annahme jahrelang trägt. Dann kommt eine gescannte Rechnung mit [/Indexed /DeviceCMYK 15 <...>] an, der Reader läuft die Lookup-Zeichenkette drei Bytes auf einmal durch, und jeder Index über null landet mitten in der Farbe eines anderen. Die Ausgabe ist nicht leer und crasht nicht. Es ist plausibel aussehender Müll – und das ist weit schlimmer, denn gegen Farben, die nur leicht daneben aussehen, meldet niemand einen Bug
Warum kommt eine Indexed-Palette falsch heraus?
Der Stride ist falsch, weil der Stride keine Konstante ist. ISO 32000-1 §8.6.6.3 definiert die Lookup-Tabelle als m × (hival + 1) Bytes, wobei m die Anzahl der Komponenten im Basisspace ist – und über RGB als Basis sagt die Stelle nichts. Derselbe Abschnitt verbietet nur zwei Basen rundweg: /Indexed selbst und /Pattern. Alles andere ist erlaubt, einschließlich aller vier CIE-basierten Familien von §8.6.5.2 bis §8.6.5.5
Die eigentliche Arbeit ist also nicht das Lesen von Bytes, sondern zuerst die Basis aufzulösen und sie zu fragen, wie breit ein Eintrag ist. ResolveIndexedPalette in HotPDF macht genau das: Es löst die Basis in einen THPDFColorSpace auf, nimmt ComponentCount aus dem aufgelösten Record und lehnt das Array rundweg ab, wenn die Basis als csfIndexed, csfPattern oder csfUnsupported zurückkam. Erst dann bemisst es das Bytebudget als PaletteCount * Count, was beim legalen Maximum von hival 255 und einer Vier-Komponenten-Basis bei 1024 Bytes endet. Zwei Details dieser Bemessung sind die Stellen, an denen eine feindliche Datei Sie sondiert. HotPDF klemmt hival vor der Verwendung auf 0..255, gemäß der Grenze aus §8.6.6.3, sodass ein Lookup-Array, das hival 100000 behauptet, nicht zu einer Gigabyte-Allokation wird; und es stutzt das Budget auf das, was die Lookup-Daten tatsächlich enthalten, und hört dann beim ersten Eintrag auf, dessen Bytes über das Ende hinauslaufen, die Palette zu füllen. Eine kurze Tabelle ergibt eine kurze Palette, kein Read über den Puffer hinaus
Vier verschiedene Basisspaces in eine RGB-Tabelle normalisieren
HotPDF konvertiert Paletten beim Parsen zu RGB statt beim Zeichnen, und diese Entscheidung treibt den Rest des Designs. Das öffentliche Feld THPDFColorSpace.Palette ist ein TBytes aus RGB-Tripeln mit der Länge 3 * PaletteCount, und diese Form hat es lange, bevor CIE-basierte Basen unterstützt wurden. Das Record-Layout umzuschreiben, um native Basiskomponenten zu tragen, hätte jeden Konsumenten des Felds gebrochen. Die Konvertierung beim Parsen hält das Layout intakt und schickt nebenbei Gray-, CMYK-, Lab-, CalGray-, CalRGB- und ICCBased-Einträge durch denselben HPDFResolveColor-Pfad, den der Renderer schon für direkte Color-Operatoren nutzt
Beim Skalierungsschritt hören die Basisspaces auf, austauschbar zu sein. Ein Lookup-Byte ist immer 0..255, aber was dieses Byte bedeutet, hängt vom Komponentenbereich ab, den die Basis deklariert. Device- und kalibrierte Komponenten bilden auf 0..1 ab. Lab nicht: Die L*-Komponente läuft von 0..100, während a* und b* über das vom Space deklarierte /Range laufen. HotPDF holt diese Grenzen aus den Feldern AMin, AMax, BMin und BMax von THPDFCIEParams, die der Lab-Parser aus dem /Range-Eintrag des Dictionaries füllt und bei fehlendem Eintrag auf −108..101 setzt – also keinen symmetrischen Bereich aus dem Gedächtnis hartkodieren. ICCBased-Basen bringen eine dritte Wahrheitsquelle: Die Komponentenzahl kommt aus dem Profil-Stream-/N, das als ICCN an die Oberfläche kommt, und ein ICCBased-Stream kann selbst ein /Range-Array tragen, das HotPDF über HPDFReadNumericArrayUpTo in dieselbe komponentenweise Grenztabelle liest. Eine Drei-Komponenten-ICC-Palette und eine Drei-Komponenten-Lab-Palette begehen die Lookup-Zeichenkette daher mit identischen Strides und völlig anderer Arithmetik – genau das Ergebnis, das ein fester RGB-Stride nicht produzieren kann
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj ist der /ColorSpace-Eintrag aus einem Image-XObject oder
// einem Resource-Dictionary; er kann ein Name, ein Array oder eine indirekte Referenz sein
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// Palette ist bereits RGB, egal was der Basisspace war
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // kurze Lookup-Tabelle, sauber stoppen
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
Wenn die Lookup-Tabelle in einem Stream lebt
ISO 32000-1 §8.6.6.3 erlaubt für die Lookup-Tabelle einen String oder einen Stream, und die Stream-Form ist die, die beißt. Ein Stream kann /FlateDecode tragen, er kann ein indirektes Objekt sein, das mit unzusammenhängenden Teilen der Datei geteilt wird, und sein TStream kann in dem Moment irgendwo positioniert sein, in dem Ihr Parser ihn erreicht. Den komprimierten Fall erledigt HotPDF, indem es an den Decoded-Bytes-Callback delegiert, der über HPDFSetColorStreamReader registriert ist – denselben Callback, den die Type-0-Sampled-Function-Engine nutzt
Ist kein Reader registriert, ist der Fallback bewusst eng statt Best-Effort. HotPDF prüft das Stream-Dictionary auf /Filter und lehnt die Palette komplett ab, wenn einer vorhanden ist, statt komprimierte Bytes in das Lookup zu füttern und eine selbstsichere falsche Tabelle zu erzeugen. Ohne Filter liest es rohe Bytes, gedeckelt auf das aus hival und der Basiskomponentenzahl berechnete Bytebudget. Der ganze Stream-Zweig sitzt in einem try..finally, das auf jedem Ausgangspfad – auch bei den Ablehnungen – die ursprüngliche Stream.Position wiederherstellt, denn das Objekt wird geteilt, und der nächste Konsument wird Ihnen einen verschobenen Cursor nicht danken. LookupStreamUsesDecodedReaderAndRestoresPosition in der Regression-Suite nagelt beide Hälften dieses Verhaltens fest
// Beide Callbacks registrieren, bevor ein ColorSpace aufgelöst wird, sonst
// werden indirekte Basen und komprimierte Lookup-Streams schlicht abgelehnt
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;
Was hält einen Indexed-ColorSpace davon ab, endlos zu rekursieren?
/Indexed als direkte Basis abzulehnen ist notwendig und bei weitem nicht hinreichend. Der interessante Angriff geht quer. Nehmen Sie ein gültiges [/Indexed /Separation ...], und erinnern Sie sich: §8.6.6.4 erlaubt einer Separation, einen Alternate-Space zu benennen, und §8.6.6.5 erlaubt DeviceN dasselbe. Machen Sie diesen Alternate zu einer indirekten Referenz zurück auf das ursprüngliche Indexed-Array. Nichts in der Kette verletzt die Direkt-Basis-Regel an einem einzelnen Hop: Die Basis ist eine Separation, der Separation-Alternate ist ein Array, und das Array ist nur zufällig das, von dem Sie ausgegangen sind. Lösen Sie naiv auf, und der Parser läuft die Schleife, bis der Stack aufgibt. Die Tint-Transform-Mechanik, auf die diese Spaces angewiesen sind, ist ein eigenes Thema, behandelt in dem Artikel zum Rendern von Separation- und DeviceN-Sonderfarben; hier zählen sie nur als Glied im Zyklus
HotPDF schließt das am einzigen Einstiegspunkt, durch den ohnehin jeder Pfad läuft. HPDFResolveColorSpace verpackt den internen Resolver in einen Tiefenzähler, der durch HPDFColorSpaceMaxDepth begrenzt ist – 64. Wer diese Grenze überschreitet, setzt im Guard ein Flag, und auf das Flag kommt es an: Beim Unwind verwirft jede Verschachtelungsebene, die es gesetzt sah, ihren eigenen teilaufgelösten Record und gibt csfUnsupported zurück, sodass der ganze Graph abgelehnt statt zur Hälfte behalten wird, und beide Zähler setzen sich zurück, wenn der äußerste Aufruf zurückkehrt. Diese Zähler sind als threadvar deklariert, nicht als Unit-Globals, und das ist keine beiläufige Sauberkeit: GResolveObj und GReadStream leben aus demselben Grund in Thread-lokalem Speicher, denn Seiten rendern parallel. Ein geteiltes Global würde erlauben, dass ein Thread tief in einer verschachtelten DeviceN-Kette für einen anderen Thread die Grenze auslöst, der gerade ein völlig gewöhnliches [/Indexed /DeviceRGB 255 <...>] parst – ein nichtdeterministischer, lastabhängiger Fehler, die am wenigsten debuggbare Sorte überhaupt. 64 ist komfortabler Spielraum für legitimen Inhalt, der selten tiefer als vier oder fünf Ebenen verschachtelt, also behandeln Sie csfUnsupported aus einem strukturell gültig aussehenden Array als protokollierenswert: In der Praxis bedeutet es einen Zyklus oder eine Tiefe, die kein realer Produzent emittiert. IndirectAlternateCycleIsUnsupported, parametrisiert über Separation und DeviceN, und ExcessivePatternDepthIsUnsupported decken beide Fälle ab
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported deckt hier eine wirklich unbekannte Familie, einen Zyklus
// durch einen indirekten Alternate und Tiefe jenseits von 64 Ebenen ab
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray, der PDF-Initialspace
end;
Eine API-Grenze, die sich für Ihre Aufrufer zu dokumentieren lohnt
HotPDF wendet die zum Parse-Zeitpunkt geltende ICC-Konvertierungskonfiguration an, wenn es eine Indexed-Palette baut, und spätere Aufrufe von HPDFSetColorSpaceRenderingIntent regenerieren eine bereits aufgelöste Palette nicht. Diese Funktion begeht die ICCBased- und Alternate-Transforms des Records, den Sie ihr geben, und baut diese Pipelines neu; die flachgedrückten RGB-Bytes in Palette sind ein Parse-Zeit-Artefakt und bleiben, wie sie waren. Das ist eine Folge der frühen Normalisierung, kein Versehen, und es lohnt sich, es in die eigenen API-Dokumente zu schreiben, statt es einen Kunden entdecken zu lassen. Wenn Ihre Anwendung einem Benutzer erlaubt, den Rendering-Intent mitten in der Sitzung zu wechseln, lösen Sie die ColorSpaces nach dem Wechsel neu auf; wenn sie den Intent einmal beim Pipeline-Setup setzt – der überwältigend übliche Fall –, gibt es nichts zu tun. Dieselbe Reihenfolge gilt für die über HPDFSetICCColorPipeline installierten Proofing-Profile: Konfigurieren Sie sie, bevor Sie mit dem Auflösen beginnen, nicht danach
Der spiegelbildliche Bug auf der Schreibseite
Alles oben dreht sich ums Lesen. Der symmetrische Defekt auf der Schreibseite ist leichter auszuliefern und viel leichter zu übersehen. Wenn HotPDF aus einer Windows-8-Bit-Bitmap ein /Indexed-Array baut, ruft es GetPaletteEntries, und diese API füllt nur so viele Einträge, wie die Palette tatsächlich hält, und gibt diese Zahl zurück. Der ursprüngliche Code prüfte das Ergebnis auf = 0 und iterierte dann fest 256 Einträge über ein Stack-Array. Bei einer 16-Farben-Bitmap schrieb das 240 Einträge uninitialisierten Stack-Speichers direkt in die Lookup-Zeichenkette eines PDF, das Sie dann einem Kunden geben – mit hartkodiertem hival 255, sodass jeder Reader den Rest pflichtbewusst als echte Farbdaten behandelte. Es ist ein Korrektheitsbug und ein Informationsoffenlegungsbug in denselben Kleidern. Der Fix ist unglamourös und vollständig: das Array vor dem Aufruf nullen, dann die zurückgegebene Zahl sowohl für die Schleifengrenze als auch für hival ehren. Wenn Sie irgendwo in Ihrem eigenen Code /Indexed-Bilder erzeugen, gehen Sie jetzt hival nachsehen
Zwei benachbarte Bausteine runden das Bild ab: Indexed-Paletten und Type-0-Sampled-Funktionen teilen sich den Decoded-Stream-Reader und dieselbe Klemmen-Disziplin, besprochen in dem Beitrag zu Type-0-Funktions-Farblookup-Tabellen, und die Filterregeln für Lookup-Streams sind dieselben, die Bilddaten in Extrahieren geladener Bilder über Decode-Filter regeln. Der hier beschriebene ColorSpace-Resolver wird als Teil der HotPDF Delphi Component für Delphi und C++Builder ausgeliefert, mit der vollständigen THPDFColorSpace-Referenz auf der Produktseite