Technický článek

Indexované PDF palety v Delphi: základní prostory a cykly

Vyhledávací tabulka /Indexed ukládá komponenty svého základního barevného prostoru, ne RGB trojice. Základ DeviceGray balí jeden bajt na záznam, DeviceCMYK čtyři, Lab tři, které potřebují škálování rozsahu, a ICCBased tolik, kolik deklaruje profil /N. HotPDF je všechny při parsování normalizuje do veřejné RGB palety, kterou vystavuje THPDFColorSpace

Ta věta je celá třída bugů. Téměř každý renderer palet se narodí s předpokladem tříbajtového kroku, protože drtivá většina skutečných prostorů /Indexed sedí na /DeviceRGB a předpoklad léta drží. Pak dorazí naskenovaná faktura s [/Indexed /DeviceCMYK 15 <...>], čtečka prochází vyhledávací řetězec po třech bajtech a každý index za nulou přistane uprostřed cizí barvy. Výstup není prázdný a nespadne. Je to věrohodně vypadající odpad, což je mnohem horší, protože proti barvám, které jen vypadají špatně, nikdo nehlásí bug

Proč vyjde Indexed paleta špatně?

Krok je špatný, protože krok není konstanta. ISO 32000-1 §8.6.6.3 definuje vyhledávací tabulku jako m × (hival + 1) bajtů, kde m je počet komponent základního prostoru, a o tom, že by základ byl RGB, neříká nic. Stejná sekce rovnou zakazuje jen dvě báze: /Indexed samotné a /Pattern. Všechno ostatní je ve hře, včetně všech čtyř rodin založených na CIE z §8.6.5.2 až §8.6.5.5

Diagram Indexed PDF vyhledávací tabulky v HotPDF, kde krok záznamu pochází ze základního barevného prostoru: 8bajtový DeviceCMYK lookup rozsekaný špatným 3bajtovým RGB procházením dává odpadkové záznamy, správné 4bajtové seskupení dá dvě CMYK položky normalizované do RGB trojic, a tabulka uvádí komponenty na záznam pro DeviceGray, DeviceRGB, DeviceCMYK, Lab a ICCBased
HotPDF nejdřív rozřeší základní barevný prostor a zeptá se ho na počet komponent, takže CMYK lookup prochází čtyři bajty na záznam, než dojde k normalizaci na RGB

Takže skutečná práce není čtení bajtů, je to nejdřív rozřešit základ a zeptat se ho, jak široký záznam je. ResolveIndexedPalette v HotPDF dělá přesně to: rozřeší základ do THPDFColorSpace, vezme ComponentCount z rozřešeného záznamu a pole rovnou odmítne, pokud základ vyšel jako csfIndexed, csfPattern nebo csfUnsupported. Teprve pak velikost bajtového rozpočtu spočítá jako PaletteCount * Count, což pro legální maximum hival 255 a čtyřkomponentní základ končí na 1024 bajtech. Dva detaily v tomhle velikostním výpočtu jsou místa, kde vás bude zkoušet nepřátelský soubor. HotPDF svírá hival na 0..255, než ho použije, podle limitu z §8.6.6.3, takže vyhledávací pole hlásící hival 100000 se nepromění v alokaci gigabajtu; a rozpočet ořízne na to, co vyhledávací data skutečně obsahují, a paletu přestane plnit u prvního záznamu, jehož bajty přetékají konec. Krátká tabulka dá krátkou paletu, ne čtení za buffer

Normalizace čtyř různých základních prostorů do jedné RGB tabulky

HotPDF převádí palety na RGB při parsování, ne při vykreslování, a tohle rozhodnutí řídí zbytek návrhu. Veřejné pole THPDFColorSpace.Palette je TBytes RGB trojic s délkou 3 * PaletteCount a takový tvar má už dlouho předtím, než byly podporovány báze založené na CIE. Přepsat rozložení záznamu, aby neslo nativní komponenty báze, by rozbilo každého konzumenta pole. Převod při parsování drží rozložení nedotčené a jako bonus posílá záznamy Gray, CMYK, Lab, CalGray, CalRGB a ICCBased stejnou cestou HPDFResolveColor, kterou renderer už používá pro přímé barevné operátory

Krok škálování je místo, kde základní prostory přestávají být zaměnitelné. Bajt vyhledávání je vždy 0..255, ale co ten bajt znamená, závisí na rozsahu komponent, který základ deklaruje. Zařízení a kalibrované komponenty se mapují na 0..1. Lab ne: komponenta L* běží 0..100, zatímco a* a b* běží přes /Range, který prostor deklaruje. HotPDF bere tyto hranice z polí AMin, AMax, BMin a BMax v THPDFCIEParams, která Lab parser plní z položky /Range slovníku a při chybějící položce nastavuje na −108..101, takže si nezadrátovávejte symetrický rozsah zpaměti. Báze ICCBased přidává třetí zdroj pravdy: počet komponent pochází ze streamu profilu /N, vystavený jako ICCN, a stream ICCBased může sám nést pole /Range, které HotPDF čte přes HPDFReadNumericArrayUpTo do stejné tabulky hranic na komponentu. Tříkomponentní ICC paleta a tříkomponentní Lab paleta proto procházejí vyhledávací řetězec se stejnými kroky a úplně jinou aritmetikou, což je přesně výsledek, který pevný RGB krok nedokáže vyrobit

var
  CS: THPDFColorSpace;
  Entry: Integer;
  R, G, B: Byte;
begin
  // ColorSpaceObj je položka /ColorSpace z obrázkového XObjectu nebo
  // slovníku zdrojů; může to být jméno, pole nebo nepřímý odkaz
  CS := HPDFResolveColorSpace(ColorSpaceObj);
  if CS.Family <> csfIndexed then
    Exit;

  // Paleta je už RGB bez ohledu na to, jaký byl základní prostor
  for Entry := 0 to CS.PaletteCount - 1 do
  begin
    if (Entry * 3 + 2) >= Length(CS.Palette) then
      Break;                       // krátká vyhledávací tabulka, čistě zastavit
    R := CS.Palette[Entry * 3];
    G := CS.Palette[Entry * 3 + 1];
    B := CS.Palette[Entry * 3 + 2];
    UseColor(Entry, R, G, B);
  end;
end;

Když vyhledávací tabulka žije ve streamu

ISO 32000-1 §8.6.6.3 dovoluje vyhledávací tabulku jako řetězec nebo stream a streamová podoba je ta, která kouše. Stream může nést /FlateDecode, může být nepřímým objektem sdíleným s nesouvisejícími částmi souboru a jeho TStream může být v okamžiku, kdy ho váš parser dosáhne, kdekoli. HotPDF řeší komprimovaný případ delegováním na callback dekódovaných bajtů registrovaný přes HPDFSetColorStreamReader, na stejný callback, který používá engine vzorkovaných funkcí Type 0

Když není registrovaná žádná čtečka, je záložní chování záměrně úzké, ne best-effort. HotPDF zkontroluje slovník streamu na /Filter a paletu úplně odmítne, pokud tam je, místo aby krmil komprimované bajty do vyhledávání a vyrobil sebejistě špatnou tabulku. Bez filtru čte syrové bajty, omezené bajtovým rozpočtem spočítaným z hival a počtu komponent báze. Celá streamová větev sedí v try..finally, který na každé výstupní cestě, včetně odmítnutí, obnovuje původní Stream.Position, protože objekt je sdílený a další konzument vám za posunutý kurzor nepoděkuje. LookupStreamUsesDecodedReaderAndRestoresPosition v regresní sadě přibijí obě poloviny toho chování

Rozhodovací diagram HotPDF Indexed vyhledávací tabulky uložené jako PDF stream: registrovaný callback HPDFSetColorStreamReader dekóduje filtrované bajty, neregistrovaný stream s položkou /Filter je rovnou odmítnut a stream bez filtru se čte syrově až do bajtového rozpočtu, zatímco try..finally na každém výstupu obnovuje Stream.Position
Odmítnout filtrovaný stream je lepší než krmit komprimované bajty do vyhledávání a obnovení původní pozice streamu drží sdílený objekt bezpečný pro dalšího konzumenta
// Zaregistrujte oba callbacky, než rozřešíte jakýkoli barevný prostor,
// jinak budou nepřímé báze a komprimované vyhledávací streamy prostě odmítnuty
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;

Co zastaví Indexed barevný prostor v nekonečné rekurzi?

Odmítnout /Indexed jako přímou bázi je nutné a zdaleka ne dostatečné. Zajímavý útok jde stranou. Vezměte platné [/Indexed /Separation ...] a připomeňte si, že §8.6.6.4 nechá Separation pojmenovat alternativní prostor a §8.6.6.5 to samé dovolí DeviceN. Udělejte z toho alternativa nepřímý odkaz mířící zpět na původní Indexed pole. Nic v řetězu neporušuje pravidlo přímé báze v jediném skoku: báze je Separation, alternativa Separation je pole a to pole náhodou je to, ze kterého jste vyšli. Rozřešíte to naivně a parser prochází smyčku, dokud zásobník nevypoví. Stroj na tint transformace, na který se tyto prostory spoléhají, je vlastní téma, probrané v článku o vykreslování přímých barev Separation a DeviceN; tady hrají roli jen jako článek v cyklu

HotPDF to uzavírá v jediném vstupním bodu, kterým už prochází každá cesta. HPDFResolveColorSpace obaluje interní resolver čítačem hloubky omezeným HPDFColorSpaceMaxDepth, což je 64. Překročíte hranici a stráž nastaví příznak a ten příznak je to podstatné: při odvíjení zahodí každá úroveň vnoření, která ho viděla nastavený, svůj vlastní částečně rozřešený záznam a vrátí csfUnsupported, takže se odmítne celý graf, ne půlka si ponechá, a oba čítače se resetují, když se vrátí nejvzdálenější volání. Tyto čítače jsou deklarované jako threadvar, ne jako globální proměnné unitu, a to není náhodná upravenost: GResolveObj a GReadStream žijí v thread-local úložišti ze stejného důvodu, protože stránky se renderují paralelně. Sdílená globální proměnná by nechala jedno vlákno hluboko ve vnořeném řetězu DeviceN spustit limit pro jiné vlákno, které parsuje naprosto obyčejné [/Indexed /DeviceRGB 255 <...>], a vyrobila by nedeterministické selhání závislé na zátěži – ten nejhůře debugovatelný druh, jaký existuje. Šedesát čtyři je pohodlná rezerva pro legitimní obsah, který se zřídka vnoří přes čtyři nebo pět úrovní, takže csfUnsupported ze strukturálně platně vypadajícího pole berte jako hodné logování: v praxi to znamená cyklus nebo hloubku, kterou žádný skutečný producent neemituje. IndirectAlternateCycleIsUnsupported, parametrizovaný přes Separation a DeviceN, a ExcessivePatternDepthIsUnsupported pokrývají oba případy

Diagram cyklu a stráž pro HotPDF barevné prostory: Indexed pole se základní Separation, jejíž alternativa je nepřímým odkazem zpět na stejné pole, rozřešené HPDFResolveColorSpace s čítačem hloubky omezeným HPDFColorSpaceMaxDepth na 64, po kterém každá úroveň odvíjení zahodí svůj záznam a vrátí csfUnsupported
Nic v jediném skoku neporušuje pravidlo přímé báze, takže HotPDF omezuje hloubku rozřešení na 64 úrovní a odmítá celý graf, místo aby si ponechal půlku
function TryResolveImageSpace(Obj: THPDFObject;
  out CS: THPDFColorSpace): Boolean;
begin
  CS := HPDFResolveColorSpace(Obj);
  // csfUnsupported tady pokrývá skutečně neznámou rodinu, cyklus
  // přes nepřímou alternativu a hloubku přes 64 úrovní
  Result := CS.Family <> csfUnsupported;
  if not Result then
    CS := HPDFDefaultColorSpace;   // DeviceGray, počáteční prostor PDF
end;

Jedna hranice API, kterou stojí za to zdokumentovat pro své volající

HotPDF aplikuje konfiguraci ICC konverze platnou při parsování, když staví Indexed paletu, a pozdější volání HPDFSetColorSpaceRenderingIntent už rozřešenou paletu neregenerují. Tato funkce prochází transformace ICCBased a alternativ záznamu, který jí předáte, a přestavuje tyto pipeline; zploštěné RGB bajty v Palette jsou artefakt z doby parsování a zůstávají, jak byly. Je to důsledek časné normalizace, ne přehlédnutí, a stojí za to ho napsat do vlastní API dokumentace, než nechat zákazníka, aby ho objevil. Pokud vaše aplikace nechá uživatele přepnout rendering intent uprostřed relace, rozřešte barevné prostory po přepnutí znovu; pokud nastavuje intent jednou při sestavování pipeline, což je drtivě běžný případ, není co dělat. Stejné pořadí platí pro proofing profily instalované přes HPDFSetICCColorPipeline: nakonfigurujte je, než začnete rozřešovat, ne po

Zrcadlový bug na straně zápisu

Všechno výše je o čtení. Symetrická vada na straně zápisu se snáze dostane ven a mnohem snáz přehlédne. Když HotPDF staví pole /Indexed z Windows 8bitové bitmapy, volá GetPaletteEntries a toto API vyplní jen tolik záznamů, kolik paleta skutečně drží, a tento počet vrátí. Původní kód testoval výsledek na = 0 a pak iteroval pevných 256 záznamů přes pole na zásobníku. Pro 16barevnou bitmapu to zapsalo 240 záznamů neinicializované paměti zásobníku přímo do vyhledávacího řetězce PDF, které pak předáte zákazníkovi, s hival natvrdo na 255, takže každá čtečka svědomitě brala zbytek jako skutečná barevná data. Je to bug správnosti a bug úniku informací v jednom oblečení. Oprava je nenápadná a úplná: vynulujte pole před voláním a pak respektujte vrácený počet jak pro hranici smyčky, tak pro hival. Pokud generujete obrázky /Indexed kdekoli ve svém kódu, běžte teď zkontrolovat hival

Obraz dokreslují dvě sousední témata: Indexed palety a vzorkované funkce Type 0 sdílejí čtečku dekódovaného streamu a stejnou disciplínu svírání, probranou v článku o vyhledávacích tabulkách barev funkcí Type 0, a pravidla filtrů řídící vyhledávací streamy jsou stejná jako ta, která řídí obrazová data v extrakci načtených obrázků přes dekódovací filtry. Popisovaný resolver barevných prostorů se dodává jako součást HotPDF Delphi Component pro Delphi a C++Builder s plnou referencí THPDFColorSpace na produktové stránce