Technický článek

CMYK JPEG v PDF: DCTDecode ColorTransform v Delphi

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

Mapa symptomů rozbitého CMYK JPEG v PDF v Delphi: negativ ukazuje na inverzní Adobe uložení, vybledlé odstíny na nesouhlasící ColorTransform
negativ znamená inverzní uložení žádající pole /Decode, posun odstínů znamená nesouhlasící transformaci a ostrá rovina K ukazuje nejdřív na transformaci

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

Rozhodovací matice DCTDecode ColorTransform pro Delphi: marker Adobe APP14 převáží nad slovníkem a nedefinované hodnoty HotPDF odmítá
Adobe marker uvnitř JPEG dat přebíjí položku slovníku a jen JPEG bez něj se vrací k /ColorTransform nebo k defaultu podle počtu komponent

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 << >>

Tvary DecodeParms, které HotPDF přijímá u multi-filter PDF streamů v Delphi: zarovnaná pole projdou, sdílený slovník nebo nesouhlasící délka pole se odmítá
multi-filter stream potřebuje jeden slovník nebo null na fázi a sdílený slovník se odmítá, i když je prázdný

Přijímané a odmítané tvary se dají snadno vyjmenovat:

  • Přijímané: /DecodeParms vynechaný, nebo null, 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ž /Filter jmenuje 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_LIBRARY přebije defaultní turbojpeg.dll nebo libturbojpeg.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