CMYK JPEG, který vyleze z PDF s kalnými, posunutými nebo negativními barvami, téměř vždy znamená, že dekodér si spletl jedno ze dvou faktů: jestli vzorky byly uložené jako YCCK a potřebují konverzi, nebo jestli byly uložené inverzně podle Adobe konvence. Dekodéry HotPDF respektují parametr DCTDecode /ColorTransform u trojsložkových i čtyřsložkových obrázků, odmítají hodnoty, které specifikace nedefinuje, a nechají marker Adobe APP14 uvnitř JPEG dat převážit nad tou položkou slovníku, jak vyžaduje ISO 32000-1 Table 13. Do stejné rodiny pastí patří dvě sousední pravidla: multi-filter streamy musí nést pole /DecodeParms zarovnaná na jednotlivé fáze a obrázky JPEG 2000 si nechávají explicitní mapování /Decode
Všechny tři sdílejí jedno téma. PDF už říká, co myslí, a pastí je renderer, který odpoví na trochu jinou otázku
Proč se CMYK JPEG v PDF renderuje inverzně nebo vybledle?
CMYK JPEG se v PDF pokazí z jednoho ze dvou důvodů a na obrazovce vypadají různě. Negativní obrázek, ve kterém bílý papír zčerná a světlé tóny ztmavnou, ukazuje na inverzní uložení. Vybledlý obrázek s posunutým odstínem, ve kterém kůže zezelená a černá zešedne, zatímco detail K kanálu zůstane ostrý, ukazuje na barevnou transformaci aplikovanou, když se aplikovat neměla, nebo vynechanou, když byla potřeba
Problém s transformací pramení z toho, jak JPEG barvu komprimuje. Baseline JPEG komprimuje nejlépe, když je jas oddělený od chrominance, takže kodéry obvykle před kompresí převedou RGB na YCbCr a čtyřsložkové kodéry převedou CMY na YCC, přičemž K projde nedotčený — to je formát zvaný YCCK. Dekodér musí přesně zvrátit konverzi, kterou kodér udělal. Zvrátíte-li jednu, která se nikdy nestala, nebo přeskočíte-li jednu, která se stala, pohnou se všechny pixely. Černý kanál přežije v obou případech, a proto rozbitý CMYK obrázek často drží ostrý černý text, zatímco všechno ostatní plave
Problém s inverzí je historický. Photoshop a další Adobe nástroje zapisují CMYK JPEGy s každým vzorkem uloženým jako 255 - value a označí soubor markerem Adobe APP14. Nic v JPEG standardu to nepředepisuje; je to konvence a každý konzument o ní musí vědět. V PDF se od producenta očekává, že inverzi vyjádří ve slovníku obrázku, místo aby ji nechal hádání
Co říká /ColorTransform dekodéru?
/ColorTransform je vyjádření na úrovni PDF, jakou konverzi musí dekodér zvrátit. ISO 32000-1 §7.4.8, Table 13, definuje dvě hodnoty. Nula znamená žádnou transformaci: dekodér vrátí vzorky tak, jak byly komprimované. Jednička znamená, že kodér aplikoval standardní konverzi, takže trojsložkový obrázek jde z YCbCr zpět do RGB a čtyřsložkový z YCCK zpět do CMYK. U jedno- a dvousložkových obrázků se parametr ignoruje. Table 13 navíc stanoví pravidlo priority, které jde snadno přehlédnout: když komprimovaná data nesou Adobe marker s vlastním transformačním kódem, ten kód rozhoduje a položka slovníku se ignoruje. Jen JPEG bez toho markeru se vrací ke slovníku a když chybí i položka, trojsložkový obrázek defaultuje na transformaci a čtyřsložkový na žádnou
Snadná chyba je cesta obrázku, která slovník parametrů přijme, ale nikdy ho nepředá JPEG dekodéru. RGB JPEG o jednom pixelu s barvou (128, 63, 31) ji odhalí okamžitě: takový dekodér vrátí [128, 63, 31] pro /ColorTransform 0 i /ColorTransform 1, zatímco správná odpověď pro nulu je surová trojice YCbCr [79, 101, 163]
HotPDF aplikuje parametr na úrovni bajtů. HPDFDecodeHeadlessTerminalImage nejdřív projde segmentovou strukturu JPEG až k markeru start-of-scan a hledá segment Adobe APP14; bajty, které ho jen připomínají uvnitř komentářového segmentu, se nepočítají. Pokud data marker nesou, rozhoduje jeho transformační kód a hodnota slovníku se ignoruje. Pokud je nenese, dekodér si postaví soukromou kopii JPEG se segmentem APP14 vloženým těsně před marker start-of-scan, nesoucím transformační kód 0, 1 nebo, u čtyřsložkového obrázku s hodnotou 1, YCCK kód 2. Kdykoli je efektivní transformace trojsložkového obrázku 0, kopie navíc odzbrojí hlavičku JFIF, protože hlavička JFIF sama říká libjpeg-turbo, aby předpokládal YCbCr. Buffer volajícího se nikdy nezmění a když se segment vkládá, jeho 16 navíc bajtů se účtuje do pracovního bajtového rozpočtu, ještě než dekodér cokoliv alokuje
Vkládání jen tehdy, když marker neexistuje, má význam kvůli detailu libjpeg: použije poslední segment APP14, který uvidí. Vložíte-li segment za existujícím markerem, explicitní hodnota slovníku tiše přebije konfliktní marker v datech, což je přesně opačně, než říká Table 13. HotPDF aplikuje pořadí z Table 13 na celé cestě image XObject i na cestě inline obrázků
Tentýž terminálový dekodér můžete volat přímo, když jeden obrázek vypadá špatně a chcete vidět jeho vzorky bez renderování stránky. Funkce bydlí v unitě HPDFHeadlessImageCodecs, bere název filtru, komprimované bajty, volitelný slovník parametrů a geometrii ze slovníku obrázku, která se musí shodovat s JPEG frame hlavičkou
uses
SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;
// ColorTransform < 0 znamená „položka chybí": nech rozhodnout JPEG markery
function DecodeDCT(const Encoded: TBytes; Width, Height, Channels,
ColorTransform: Integer): TBytes;
var
Parms: THPDFDictionaryObject;
begin
Parms := nil;
if ColorTransform >= 0 then
begin
Parms := THPDFDictionaryObject.Create(nil);
Parms.AddNumericValue('ColorTransform', ColorTransform);
end;
try
Result := HPDFDecodeHeadlessTerminalImage('DCTDecode', Encoded, nil, Parms,
Width, Height, Channels, 8, 64 * 1024 * 1024, nil);
finally
Parms.Free;
end;
end;
Dekodér je přísný v tom, co počítá jako legální hodnotu. Explicitní -1, 2 nebo neceločíselná hodnota jako 0.5 vyhodí EHPDFHeadlessImageError místo návratu k defaultu, protože na hodnotě, kterou specifikace nedefinuje, nemá dekodér na čem spolehlivě stavět. Kontrola zůstává na místě i tehdy, když Adobe marker v datech učiní položku bezpředmětnou, protože nedefinovaná hodnota je strukturální chyba v dokumentu samotném. Rozpočet příliš malý na transformovanou kopii selže stejným způsobem, se zprávou, která pojmenovává pracovní bajtový rozpočet
Explicitní null je jiný případ, protože to hodnota vůbec není. ISO 32000-1 §7.3.9 říká, že položka slovníku s hodnotou null je rovnocenná chybějící položce, takže /ColorTransform null propadne k markeru nebo k defaultu podle počtu komponent přesně tak, jako by klíč chyběl. HotPDF to tak bere na každé cestě, headless i VCL, stream i inline
Jeden default si zaslouží zmínku, protože tady HotPDF záměrně jde za písmeno Table 13. Trojsložkový JPEG bez hlavičky JFIF i bez Adobe markeru, jehož frame hlavička pojmenovává komponenty R, G a B, říká ve vlastních bajtech, že drží RGB. Table 13 by ho pořád defaultovala na transformaci, ale libjpeg a čtečky na něm postavené ho dekódují bez transformace a HotPDF taky. Headless i VCL dekodéry to tak berou, takže červená a modrá zůstanou tam, kde je dala data. Explicitní 0 nebo 1 ve slovníku i tuto inference přebije
uses
System.IOUtils;
var
Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
// surové bajty DCTDecode obrázku 640 x 480 CMYK, uložené ze streamu PDF
Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
try
// S markerem Adobe APP14 v datech se všechna tři volání shodnou:
// rozhoduje marker a hodnota slovníku se ignoruje
Stored := DecodeDCT(Jpeg, 640, 480, 4, 0); // bez markeru: vzorky tak, jak byly komprimované
Converted := DecodeDCT(Jpeg, 640, 480, 4, 1); // bez markeru: YCCK zvrácené do CMYK
ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1); // bez markeru: čtyřsložkový default, žádná
Writeln(Format('pixel 0: stored %d,%d,%d,%d converted %d,%d,%d,%d',
[Stored[0], Stored[1], Stored[2], Stored[3],
Converted[0], Converted[1], Converted[2], Converted[3]]));
except
on E: EHPDFHeadlessImageError do
Writeln('Decoder refused the image: ', E.Message);
end;
end;
Referenční pixel čtyřsložkového obrázku ukazuje, na co se dívat. CMYK pixel (10, 20, 30, 40) uložený jako YCCK se vrátí jako [245, 235, 225, 215], když se konverze YCCK nezvrátí, a jako [0, 116, 0, 215], když se zvrátí. Bajt K, 215, je v obou stejný. Pokud rovina K vypadá správně a ty ostatní tři ne, nesouhlasící transformace je první podezřelý
Inverzní konvence CMYK patří /Decode
Terminálový dekodér HotPDF vrací CMYK vzorky přesně tak, jak je produkuje libjpeg-turbo, a neobrací je na základě Adobe markeru. Čísla výše konvenci zobrazují: 245, 235, 225, 215 jsou prostě 255 minus 10, 20, 30, 40, inverzní podoba, kterou ukládají kodéry ve stylu Adobe. Převést to zpátky na množství inkoustu je práce slovníku obrázku, kde by producent vkládající takový JPEG měl zapsat /Decode [1 0 1 0 1 0 1 0]. Renderer aplikuje pole /Decode na každou komponentu, takže správně napsaný soubor se tiskne správně
Druhou stranu stojí za to říct napřímo. PDF, které vloží Adobe inverzní CMYK JPEG bez odpovídajícího pole /Decode, se v HotPDF renderuje jako negativ, protože soubor sám obrázek takhle popisuje. Některé prohlížeče marker APP14 detekují a tiše kompenzují. HotPDF tuhle heuristiku nepřidává, takže oprava takového souboru patří do softwaru, který ho napsal
Proč je jeden slovník DecodeParms pro dva filtry chyba?
Stream, jehož /Filter je pole několika fází, musí dodat /DecodeParms jako pole stejné délky, jeden slovník parametrů nebo null na fázi. To pravidlo stanovuje ISO 32000-1 §7.3.8.2, Table 5, a HotPDF je vynucuje: multi-filter stream, který dodá jeden sdílený slovník, se nahlásí jako neplatný dokument místo dekódování
Shovívavá alternativa je horší, než vypadá. Dekodér, který kontroluje délku jen tehdy, když už /DecodeParms je pole, si při jednom slovníku a dvou filtrech tiše znovu použije ten slovník v každé fázi. To je neškodné, když je slovník prázdný, a ničivé, když není: /Predictor 12 určený pro vnitřní fázi Flate by se aplikoval i na vnější a výstupem by byl věrohodně vypadající odpad místo čisté chyby. Protože záměr takového souboru nejde obnovit, HotPDF ho odmítá, i v prázdném případě, třeba u content streamu stránky s /Filter [/FlateDecode /FlateDecode] a /DecodeParms << >>
Přijímané a odmítané tvary se dají snadno vyjmenovat:
- Přijímané:
/DecodeParmsvynechaný, nebonull, pro libovolný počet filtrů - Přijímané: pole, jehož délka rovná počtu filtrů, každá položka slovník nebo
null - Přijímané: jeden slovník, když
/Filterjmenuje přesně jeden filtr - Odmítané: jeden slovník sdílený dvěma a více filtry, s diagnostikou „Stream filter chain requires array DecodeParms"
- Odmítané: pole, jehož délka se liší od počtu filtrů, s „DecodeParms array must match the filter chain"
Tentýž kontrol běží u obyčejných content streamů i u obrázkových streamů, jejichž poslední filtr je terminálový kodek jako DCTDecode, takže deformovaný řetěz selže na stejném místě, ať stream nese cokoliv. Interakci s dekompresními limity pokrývá článek o rozpočtech filtrových řetězů a PDF decode bombách. Z aplikačního kódu se chyba projeví při renderování stránky
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
var
Document: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Document := THPDFHeadlessDocument.Create;
try
try
Document.LoadFromFile('catalog-cmyk.pdf');
Options := THPDFHeadlessRenderOptions.Default;
Info := HPDFRenderHeadlessPagePNGFile(Document.GetRenderAccess(0),
'page1.png', Options);
Writeln('images=', Info.Images, ' decoded bytes=', Info.DecodedBytes);
except
on E: Exception do
Writeln('Rejected: ', E.ClassName, ': ', E.Message);
end;
finally
Document.Free;
end;
end;
Obrázky JPEG 2000 si drží explicitní /Decode
Obrázek JPEG 2000 ignoruje /Decode jen tehdy, když jeho slovník nemá /ColorSpace a není image maska. ISO 32000-1 §8.9.5 to dělá podmíněným a HotPDF to následuje. Zahazovat /Decode u každého nestencilového JPX obrázku je snadná zkratka a explicitní inverze z ní jednoduše zmizí: vzorek DeviceGray JPX 64 pod /Decode [1 0] se musí renderovat jako 191, ne jako 64
Samotné správné nastavení té jedné podmínky nestačí. Větve barevných prostorů jako Lab a ICCBased, které nastavují defaultní rozsahy komponent, kdykoli je obrázek JPX, by legitimně aplikované pole přepsaly znovu, takže renderer si zaznamenává, zda se platné pole /Decode skutečně aplikovalo, a defaultní rozsahy doplní jen tehdy, když žádné nebylo. Explicitní rozsahy Lab a ICCBased proto přežijí a deklarace barev v codestreamu vládne i dál, když je slovník mlčí. Stranu dekodéru formátu popisuje článek o JPEG 2000 a JPXDecode
Které dekódovací cesty se těmito pravidly řídí?
Obě pipeline se jimi řídí. Na headless straně to znamená načítání THPDFHeadlessDocument, sdílený stream dekodér a renderer stránek na bázi Cairo za HPDFRenderHeadlessPagePNGFile. Na straně VCL to znamená extrakci načtených obrázků do TBitmap a VCL renderování stránek, které aplikují totéž pořadí z Table 13 při dekódování celého obrázku i zmenšeného regionu, takže hlavička JFIF nemůže nad Adobe transformou 0 vnutit YCbCr a nelegální hodnota učiní obrázek tamtéž nedekódovatelným. Inline DCT obrázky následují stejná pravidla markeru, null, defaultu a odmítnutí jako image XObjects, se zkrácenými i plnými názvy klíčů, včetně slovníku /DP << … >> a zkrácených polí filtrů jako /F [/AHx /DCT], a inline obrázek s nelegálními parametry se přeskočí, zatímco zbytek stránky se renderuje. Průvodce extrakcí obrázků z načteného PDF pokrývá zbytek obsluhy filtrů toho API
Pár praktických poznámek, než se na tohle chování spolehnete:
- Dekódování JPEG potřebuje za běhu kompatibilní knihovnu TurboJPEG;
HOTPDF_TURBOJPEG_LIBRARYpřebije defaultníturbojpeg.dllnebolibturbojpeg.so.0 - Přísné strukturální kontroly odmítají soubory, které shovívavé čtečky otevřou, zejména sdílený prázdný slovník
/DecodeParms, a oprava těch souborů leží na straně producenta
Pro tiskový workflow se správou barev je praktické poučení stručné. Zapisujte CMYK JPEGy s explicitním /ColorTransform, které souhlasí s každým Adobe markerem v datech, plus polem /Decode odpovídajícím jejich uložení, dodávejte každému multi-filter streamu pole /DecodeParms zarovnané na fáze a počítejte s tím, že HotPDF drží soubory u toho, co doopravdy říkají
Headless loader, stream dekodéry a Cairo renderer stránek zobrazené tady dodává HotPDF Delphi PDF component pro Delphi, C++Builder a Lazarus, spolu s klasickým VCL document engine