/Indexed lookup таблицата пази компонентите на базовото си color space, а не RGB тройки. DeviceGray базата пакова по един байт на запис, DeviceCMYK четири, Lab три, които се нуждаят от range scaling, а ICCBased — колкото декларира /N на профила. HotPDF нормализира всички тях по време на парсване в публичната RGB палитра, която THPDFColorSpace излага
Това изречение е целият клас бъгове. Почти всеки palette renderer започва живота си с предположение за трибайтов stride, защото огромното мнозинство от реалните /Indexed пространства стоят на /DeviceRGB и предположението издържа с години. После пристига сканирана фактура с [/Indexed /DeviceCMYK 15 <...>], reader-ът върви по lookup низа по три байта наведнъж, и всеки индекс след нулата каца в средата на нечий друг цвят. Изходът не е празен и не се срива. Той е правдоподобно изглеждащ боклук, което е далеч по-лошо, защото никой не отваря бъг срещу цветове, които просто изглеждат леко изкривени
Защо Indexed палитрата излиза грешна?
Stride-ът е грешен, защото stride-ът не е константа. ISO 32000-1 §8.6.6.3 дефинира lookup таблицата като m × (hival + 1) байта, където m е броят на компонентите в базовото пространство, и не казва нищо за това, че базата е RGB. Същият раздел забранява направо само две бази: самото /Indexed и /Pattern. Всичко останало е позволено, включително четирите CIE-базирани семейства от §8.6.5.2 до §8.6.5.5
Така че реалната работа не е четенето на байтове, а първо да разрешите базата и да я попитате колко широк е записът. ResolveIndexedPalette в HotPDF прави точно това: разрешава базата в THPDFColorSpace, взима ComponentCount от разрешения запис и отхвърля масива направо, ако базата се е върнала като csfIndexed, csfPattern или csfUnsupported. Едва тогава оразмерява byte бюджета като PaletteCount * Count, който при легалния максимум hival 255 и четирикомпонентна база стига до 1024 байта. Два детайла в това оразмеряване са където враждебен файл ще ви сондира. HotPDF затяга hival в 0..255, преди да го използва, според лимита в §8.6.6.3, така че lookup масив, твърдящ hival 100000, не се превръща в гигабайтова алокация; и орязва бюджета до каквото lookup данните действително съдържат, после спира да пълни палитрата на първия запис, чиито байтове минават отвъд края. Къса таблица дава къса палитра, не четене отвъд буфера
Нормализиране на четири различни базови пространства в една RGB таблица
HotPDF конвертира палитрите в RGB по време на парсване, а не по време на рисуване, и това решение движи останалата част от дизайна. Публичното поле THPDFColorSpace.Palette е TBytes от RGB тройки с дължина 3 * PaletteCount, и е с тази форма от дълго преди CIE-базираните бази да бъдат поддържани. Пренаписването на record оформлението, за да носи нативни базови компоненти, би счупило всеки consumer на полето. Конвертирането по време на парсване държи оформлението непокътнато и, като бонус, маршрутизира Gray, CMYK, Lab, CalGray, CalRGB и ICCBased записите през същия HPDFResolveColor път, който renderer-ът вече ползва за директни color оператори
Стъпката на scaling е където базовите пространства престават да са взаимозаменяеми. Lookup байтът винаги е 0..255, но какво означава този байт зависи от диапазона на компонента, който базата декларира. Device и калибрираните компоненти се мапират в 0..1. Lab — не: компонентът L* върви 0..100, докато a* и b* вървят по /Range, който пространството декларира. HotPDF вади тези граници от полетата AMin, AMax, BMin и BMax на THPDFCIEParams, които Lab parser-ът пълни от /Range записа на речника и подразбира на −108..101, когато записът отсъства, така че не hard-code-вайте симетричен диапазон по памет. ICCBased базите добавят трети източник на истина: броят на компонентите идва от /N на profile stream-а, изплувал като ICCN, а ICCBased stream може сам да носи масив /Range, който HotPDF чете през HPDFReadNumericArrayUpTo в същата таблица с граници по компонент. Затова трикомпонентна ICC палитра и трикомпонентна Lab палитра вървят по lookup низа с еднакви strides и напълно различна аритметика — точно изходът, който фиксиран RGB stride не може да произведе
var
CS: THPDFColorSpace;
Entry: Integer;
R, G, B: Byte;
begin
// ColorSpaceObj е записът /ColorSpace от image XObject или от
// resource dictionary; it may be a name, an array, or an indirect ref
CS := HPDFResolveColorSpace(ColorSpaceObj);
if CS.Family <> csfIndexed then
Exit;
// Palette-ът вече е RGB, независимо от това какво е било базовото пространство
for Entry := 0 to CS.PaletteCount - 1 do
begin
if (Entry * 3 + 2) >= Length(CS.Palette) then
Break; // къса lookup таблица, спри чисто
R := CS.Palette[Entry * 3];
G := CS.Palette[Entry * 3 + 1];
B := CS.Palette[Entry * 3 + 2];
UseColor(Entry, R, G, B);
end;
end;
Когато lookup таблицата живее в stream
ISO 32000-1 §8.6.6.3 позволява lookup таблицата да е string или stream, а stream формата е онази, която хапе. Stream може да носи /FlateDecode, може да е indirect обект, споделен с несвързани части на файла, и неговият TStream може да е позициониран къде да е в момента, в който parser-ът ви го достигне. HotPDF обработва компресирания случай, като делегира на decoded-bytes callback-а, регистриран през HPDFSetColorStreamReader — същия callback, който Type 0 sampled-function engine-ът ползва
Когато не е регистриран reader, fallback-ът е умишлено тесен, а не best-effort. HotPDF проверява stream речника за /Filter и отказва палитрата изцяло, ако такъв има, вместо да храни lookup-а с компресирани байтове и да произвежда уверено грешна таблица. Без filter чете сурови байтове, ограничени до byte бюджета, изчислен от hival и броя на компонентите на базата. Целият stream branch седи вътре в try..finally, което възстановява оригиналния Stream.Position на всеки изходен път, включително отказите, защото обектът е споделен и следващият consumer няма да ви благодари за преместен курсор. LookupStreamUsesDecodedReaderAndRestoresPosition в regression пакета заковава и двете половини на това поведение
// Регистрирай и двата callback-а, преди да разрешиш което и да е цветово пространство, иначе
// индиректните бази и компресираните lookup потоци просто ще бъдат отказани
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;
Какво спира Indexed color space да рекурсира вечно?
Отхвърлянето на /Indexed като директна база е необходимо и съвсем не достатъчно. Интересната атака отива встрани. Вземете валиден [/Indexed /Separation ...] и си припомнете, че §8.6.6.4 позволява на Separation да назове алтернативно пространство, а §8.6.6.5 позволява същото на DeviceN. Направете тази алтернатива indirect препратка, сочеща обратно към оригиналния Indexed масив. Нищо във веригата не нарушава правилото за директна база на нито един скок: базата е Separation, алтернативата на Separation е масив, а масивът просто се оказва онзи, от който сте тръгнали. Разрешете го наивно и parser-ът върви по цикъла, докато стекът свърши. Tint-transform машинерията, на която тези пространства разчитат, е отделна тема, покрита в статията за рендиране на Separation и DeviceN spot цветове; тук те имат значение само като звено в цикъла
HotPDF затваря това на единичната точка на влизане, през която всеки път вече минава. HPDFResolveColorSpace обвива вътрешния resolver в брояч на дълбочината, ограничен от HPDFColorSpaceMaxDepth, който е 64. Преминете тази граница и защитата вдига флаг, а флагът е това, което има значение: при unwind всяко ниво на влагане, видяло вдигнат флага, изхвърля собствения си частично разрешен запис и връща csfUnsupported, така че целият граф се отхвърля, вместо половината да се задържи, а двата брояча се нулират, когато най-външното извикване се върне. Тези броячи са декларирани като threadvar, а не unit глобални, и това не е случайна спретнатост: GResolveObj и GReadStream живеят в thread-local хранилище по същата причина, защото страниците се рендерират паралелно. Споделена глобална би позволила на една нишка, дълбоко във вложена DeviceN верига, да задейства лимита за друга нишка, парсваща съвсем обикновен [/Indexed /DeviceRGB 255 <...>], произвеждайки недетерминиран, зависещ от натоварването отказ — най-трудният за дебъгване вид. Шестдесет и четири е удобен хедрум за легитимно съдържание, което рядко се влага отвъд четири-пет нива, така че третирайте csfUnsupported от структурно валидно изглеждащ масив като нещо, което си струва да се логва: на практика означава цикъл или дълбочина, която никой реален producer не излъчва. IndirectAlternateCycleIsUnsupported, параметризиран по Separation и DeviceN, и ExcessivePatternDepthIsUnsupported покриват двата случая
function TryResolveImageSpace(Obj: THPDFObject;
out CS: THPDFColorSpace): Boolean;
begin
CS := HPDFResolveColorSpace(Obj);
// csfUnsupported тук покрива наистина неизвестно семейство, цикъл
// през индиректен alternate и дълбочина над 64 нива
Result := CS.Family <> csfUnsupported;
if not Result then
CS := HPDFDefaultColorSpace; // DeviceGray, първоначалното цветово пространство на PDF
end;
Една API граница, която си струва да документирате за caller-ите си
HotPDF прилага действащата ICC conversion конфигурация по време на парсване, когато строи Indexed палитра, а по-късните извиквания на HPDFSetColorSpaceRenderingIntent не регенерират вече разрешена палитра. Тази функция обхожда ICCBased и алтернативните трансформации на записа, който й подадете, и изгражда наново тези pipeline-и; изравнените RGB байтове в Palette са артефакт от парсването и си остават каквито са били. Това е следствие от ранната нормализация, не недоглеждане, и си струва да го впишете в собствената си API документация, вместо да го оставите клиент да го открие. Ако приложението ви позволява на потребител да сменя rendering intent по средата на сесията, разрешавайте color space-овете отново след смяната; ако задава intent веднъж по време на pipeline setup — което е преобладаващият случай — няма какво да правите. Същият ред важи за proofing профилите, инсталирани през HPDFSetICCColorPipeline: конфигурирайте ги, преди да започнете да разрешавате, не след това
Огледалният бъг на страната на писането
Всичко по-горе е за четенето. Симетричният дефект на страната на писането е по-лесен за изпращане и много по-лесен за пропускане. Когато HotPDF строи /Indexed масив от Windows 8-битов bitmap, вика GetPaletteEntries, а това API пълни само толкова записи, колкото палитрата действително държи, и връща този брой. Оригиналният код тестваше резултата за = 0 и после итерираше фиксирани 256 записа върху стеков масив. За 16-цветен bitmap това записваше 240 записа от неинициализирана стекова памет направо в lookup низа на PDF, който после подавате на клиент, с hival, hard-code-нат на 255, така че всеки reader добросъвестно третираше остатъка като реални цветови данни. Това е бъг за коректност и бъг за изтичане на информация, облечени в едни и същи дрехи. Поправката е небляскава и пълна: нулирайте масива преди извикването, после уважете върнатия брой и за границата на цикъла, и за hival. Ако генерирате /Indexed изображения къде да е в собствения си код, идете проверете hival сега
Две съседни части довършват картината: Indexed палитрите и Type 0 sampled функциите споделят decoded-stream reader-а и същата дисциплина за clamping, обсъдена в материала за Type 0 функция color lookup таблиците, а filter правилата, управляващи lookup stream-овете, са същите, които управляват image данните в извличането на заредени изображения през decode filter-и. Color-space resolver-ът, описан тук, се доставя като част от HotPDF Delphi Component за Delphi и C++Builder, с пълната THPDFColorSpace справка на продуктовата страница