CMYK JPEG wychodzący z PDF-a z błotnistymi, przesuniętymi albo negatywowymi kolorami prawie zawsze znaczy, że dekoder pomylił jeden z dwóch faktów: czy próbki były zapisane jako YCCK i wymagają konwersji, czy były zapisane odwrócone według konwencji Adobe. Dekodery HotPDF respektują parametr DCTDecode /ColorTransform dla obrazów trzy- i czterokanałowych, odrzucają wartości niezdefiniowane przez specyfikację i pozwalają markerowi Adobe APP14 wewnątrz danych JPEG mieć pierwszeństwo przed wpisem ze słownika, jak wymaga ISO 32000-1 Table 13. Dwie sąsiednie reguły należą do tej samej rodziny pułapek: strumienie wielofiltrowe muszą nieść tablice /DecodeParms zgrane z etapami, a obrazy JPEG 2000 zachowują jawne mapowanie /Decode
Wszystkie trzy łączy jeden temat. PDF już mówi, co ma na myśli, a pułapką jest renderer odpowiadający na lekko inne pytanie
Dlaczego CMYK JPEG renderuje się w PDF odwrócony albo wyblakły?
CMYK JPEG psuje się w PDF-ie z jednej z dwóch przyczyn i obie wyglądają na ekranie inaczej. Obraz negatywowy, w którym biały papier robi się czarny, a jasne tony ciemne, wskazuje odwrócone zapisanie. Obraz wyblakły albo z przesuniętą barwą, w którym skóra robi się zielona, a czernie szare przy ostrym detalu K, wskazuje transformację barw zastosowaną, gdy nie powinna być, albo pominiętą, gdy była wymagana
Problem transformacji bierze się z tego, jak JPEG kompresuje barwę. Baseline JPEG kompresuje najlepiej, gdy luminancja jest oddzielona od chrominancji, więc enkodery zwykle konwertują RGB na YCbCr przed kompresją, a enkodery czterokanałowe konwertują CMY na YCC, przepuszczając K nietknięte — to format zwany YCCK. Dekoder musi cofnąć dokładnie tę konwersję, którą zastosował enkoder. Cofnij taką, która nigdy nie nastąpiła, albo pomiń taką, która nastąpiła, i każdy piksel zmienia miejsce. Czarny kanał przeżywa w obu wypadkach, dlatego zepsuty obraz CMYK często trzyma ostrą czarną typografię, podczas gdy cała reszta dryfuje
Problem odwrócenia jest historyczny. Photoshop i inne narzędzia Adobe piszą CMYK JPEG z każdą próbką zapisaną jako 255 - value i oznaczają plik markerem Adobe APP14. Nic w standardzie JPEG tego nie mówi; to konwencja i każdy konsument musi o niej wiedzieć. W PDF od producenta oczekuje się, że odwrócenie wyrazi w słowniku obrazu, zamiast zostawiać je zgadywaniu
Co /ColorTransform mówi dekoderowi?
/ColorTransform to deklaracja na poziomie PDF, której konwersji dekoder musi cofnąć. ISO 32000-1 §7.4.8, Table 13, definiuje dwie wartości. Zero znaczy brak transformacji: dekoder zwraca próbki tak, jak zostały skompresowane. Jeden znaczy, że enkoder zastosował standardową konwersję, więc obraz trójkanałowy wraca z YCbCr do RGB, a czterokanałowy z YCCK do CMYK. Parametr jest ignorowany dla obrazów jedno- i dwukanałowych. Table 13 ustawia też regułę pierwszeństwa, łatwą do przeoczenia: gdy zakodowane dane niosą marker Adobe z własnym kodem transformacji, ten kod rządzi, a wpis ze słownika jest ignorowany. Tylko JPEG bez tego markera spada na słownik, a gdy i wpisu brak, obraz trójkanałowy domyślnie dostaje transformację, a czterokanałowy jej brak
Łatwy błąd to ścieżka obrazu, która przyjmuje słownik parametrów, ale nigdy nie podaje go dekoderowi JPEG. Jednopikselowy RGB JPEG z kolorem (128, 63, 31) obnaża to od razu: taki dekoder zwraca [128, 63, 31] i dla /ColorTransform 0, i dla /ColorTransform 1, podczas gdy poprawna odpowiedź dla zera to surowa trójka YCbCr [79, 101, 163]
HotPDF stosuje parametr na poziomie bajtów. HPDFDecodeHeadlessTerminalImage najpierw przechodzi przez strukturę segmentów JPEG aż do markera start-of-scan i szuka segmentu Adobe APP14; bajty jedynie przypominające go wewnątrz segmentu komentarza się nie liczą. Jeśli dane niosą marker, jego kod transformacji decyduje, a wartość ze słownika jest ignorowana. Jeśli nie niosą, dekoder buduje prywatną kopię JPEG z segmentem APP14 wstawionym tuż przed markerem start-of-scan, z kodem transformacji 0, 1 albo, dla obrazu czterokanałowego z wartością 1, kodem YCCK równym 2. Kiedy efektywna transformacja obrazu trójkanałowego wynosi 0, kopia dodatkowo rozbraja nagłówek JFIF, bo nagłówek JFIF sam z siebie każe libjpeg-turbo zakładać YCbCr. Bufor wywołującego nie jest ruszany, a gdy segment jest wstawiany, jego 16 dodatkowych bajtów obciąża budżet bajtów roboczych, zanim dekoder cokolwiek zaalokuje
Wstawianie tylko przy braku markera ma znaczenie przez detal libjpeg: on używa ostatniego segmentu APP14, jaki widzi. Wstaw segment po istniejącym markerze, a jawna wartość ze słownika po cichu nadpisze konfliktowy marker w danych, czyli dokładnie odwrotnie niż każe Table 13. HotPDF stosuje porządek z Table 13 na pełnej ścieżce image XObject i na ścieżce obrazów inline
Ten sam terminalowy dekoder możesz wołać wprost, gdy pojedynczy obraz wygląda źle i chcesz zobaczyć jego próbki bez renderowania strony. Funkcja mieszka w unit HPDFHeadlessImageCodecs, bierze nazwę filtra, zakodowane bajty, opcjonalny słownik parametrów i geometrię ze słownika obrazu, która musi pasować do nagłówka ramki JPEG
uses
SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;
// ColorTransform < 0 znaczy „brak wpisu": niech zdecydują markery JPEG
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;
Dekoder jest surowy co do tego, co liczy się jako legalna wartość. Jawne -1, 2 albo niecałkowite jak 0.5 rzuca EHPDFHeadlessImageError zamiast spadać na domyślne, bo wartość niezdefiniowana przez specyfikację nie daje dekoderowi niczego godnego zaufania. Kontrola zostaje w miejscu nawet wtedy, gdy marker Adobe w danych czyni wpis bezprzedmiotowym, bo niezdefiniowana wartość to błąd strukturalny samego dokumentu. Budżet za mały na skonwertowaną kopię zawodzi tak samo, z komunikatem nazywającym budżet bajtów roboczych
Jawne null to inny przypadek, bo nie jest wartością w ogóle. ISO 32000-1 §7.3.9 mówi, że wpis słownika o wartości null równoważny jest wpisowi nieobecnemu, więc /ColorTransform null spada na marker albo na domyślną wartość z liczby komponentów dokładnie tak, jakby klucza nie było. HotPDF traktuje to tak na każdej ścieżce, headless i VCL, strumieniowej i inline
Jeszcze jeden domyślny wybór zasługuje na wzmiankę, bo to miejsce, gdzie HotPDF celowo wykracza poza literę Table 13. JPEG o trzech komponentach bez nagłówka JFIF i bez markera Adobe, którego nagłówek ramki nazywa komponenty R, G i B, mówi własnymi bajtami, że trzyma RGB. Table 13 nadal przydzieliłaby mu domyślną transformację, ale libjpeg i czytniki na nim zbudowane dekodują go bez transformacji, i HotPDF też. Dekodery headless i VCL traktują go tak samo, więc czerwień i błękit zostają tam, gdzie umieściły je dane. Jawne 0 albo 1 ze słownika nadal nadpisuje ten wniosek
uses
System.IOUtils;
var
Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
// surowe bajty DCTDecode obrazu 640 x 480 CMYK, zapisane ze strumienia PDF
Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
try
// Przy markerze Adobe APP14 w danych wszystkie trzy wywołania się zgadzają:
// marker decyduje, a wartość ze słownika jest ignorowana
Stored := DecodeDCT(Jpeg, 640, 480, 4, 0); // bez markera: próbki jak skompresowano
Converted := DecodeDCT(Jpeg, 640, 480, 4, 1); // bez markera: YCCK cofnięte do CMYK
ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1); // bez markera: domyślne czterokanałowe, nic
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;
Czterokanałowy piksel referencyjny pokazuje, na co patrzeć. Piksel CMYK (10, 20, 30, 40) zapisany jako YCCK wraca jako [245, 235, 225, 215], gdy konwersja YCCK nie została cofnięta, i jako [0, 116, 0, 215], gdy została. Bajt K, 215, jest identyczny w obu. Jeśli płaszczyzna K wygląda dobrze, a pozostałe trzy nie, niedopasowana transformacja to pierwszy podejrzany
Odwrócona konwencja CMYK należy do /Decode
Terminalowy dekoder HotPDF zwraca próbki CMYK dokładnie tak, jak produkuje je libjpeg-turbo, i nie odwraca ich na podstawie markera Adobe. Liczby powyżej czynią konwencję widoczną: 245, 235, 225, 215 to po prostu 255 minus 10, 20, 30, 40, odwrócona forma, którą zapisują enkodery w stylu Adobe. Odwrócenie tego z powrotem na ilości farby to robota słownika obrazu, gdzie producent osadzający taki JPEG powinien napisać /Decode [1 0 1 0 1 0 1 0]. Renderer aplikuje tablicę /Decode do każdego komponentu, więc poprawnie napisany plik drukuje się poprawnie
Drugą stronę warto powiedzieć wprost. PDF osadzający odwrócone po adobe'owsku CMYK JPEG bez pasującej tablicy /Decode wyrenderuje się jako negatyw w HotPDF, bo plik sam opisuje obraz w ten sposób. Niektóre przeglądarki wykrywają marker APP14 i po cichu kompensują. HotPDF nie dodaje tej heurystyki, więc naprawa takiego pliku należy do oprogramowania, które go napisało
Dlaczego jeden słownik DecodeParms dla dwóch filtrów to błąd?
Strumień, którego /Filter jest tablicą kilku etapów, musi podać /DecodeParms jako tablicę tej samej długości, po jednym słowniku parametrów albo null na etap. ISO 32000-1 §7.3.8.2, Table 5, stanowi tę regułę i HotPDF jej pilnuje: strumień wielofiltrowy podający jeden współdzielony słownik jest raportowany jako nieważny dokument zamiast być dekodowany
Pobłażliwa alternatywa jest gorsza, niż wygląda. Dekoder sprawdzający długość tylko wtedy, gdy /DecodeParms już jest tablicą, przy jednym słowniku i dwóch filtrach po cichu użyje tego słownika na każdym etapie. Gdy słownik jest pusty, to nieszkodliwe, a gdy nie jest — niszczące: /Predictor 12 przeznaczony dla wewnętrznego etapu Flate zostanie też zastosowany do zewnętrznego, a wyjście robi się wyglądającymi wiarygodnie śmieciami zamiast czystego błędu. Ponieważ intencji takiego pliku nie da się odzyskać, HotPDF go odmawia, nawet w przypadku pustym, jak strumień zawartości strony z /Filter [/FlateDecode /FlateDecode] i /DecodeParms << >>
Przyjmowane i odrzucane kształty da się wyliczyć:
- Akceptowane:
/DecodeParmspominięty albonull, dla dowolnej liczby filtrów - Akceptowane: tablica o długości równej liczbie filtrów, każdy wpis słownikiem albo
null - Akceptowane: pojedynczy słownik, gdy
/Filternazywa dokładnie jeden filtr - Odrzucane: pojedynczy słownik współdzielony przez dwa lub więcej filtrów, z diagnostyką "Stream filter chain requires array DecodeParms"
- Odrzucane: tablica o długości różnej od liczby filtrów, z komunikatem "DecodeParms array must match the filter chain"
Ta sama kontrola działa dla zwykłych strumieni zawartości i dla strumieni obrazów, których ostatni filtr to terminalowy kodek taki jak DCTDecode, więc zepsuty łańcuch pada w tym samym miejscu niezależnie od tego, co strumień niesie. Interakcję z limitami dekompresji opisuje artykuł o budżetach łańcuchów filtrów i bombach dekodowania PDF. Z kodu aplikacji błąd wychodzi na wierzch przy renderowaniu strony
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;
Obrazy JPEG 2000 zachowują jawny /Decode
Obraz JPEG 2000 ignoruje /Decode tylko wtedy, gdy jego słownik nie ma /ColorSpace i nie jest maską obrazu. ISO 32000-1 §8.9.5 robi z tego warunek i HotPDF go stosuje. Wyrzucanie /Decode dla każdego obrazu JPX niebędącego stencil to łatwy skrót i sprawia, że jawne odwrócenie po prostu znika: próbka JPX DeviceGray 64 pod /Decode [1 0] ma wyrenderować się jako 191, nie 64
Trafienie w ten jeden warunek to za mało. Gałęzie przestrzeni barw jak Lab i ICCBased, które ustawiają domyślne zakresy komponentów ilekroć obraz jest JPX, nadpisałyby legalnie zastosowaną tablicę drugi raz, więc renderer zapisuje, czy ważna tablica /Decode faktycznie została zastosowana, i uzupełnia domyślne zakresy tylko, gdy żadnej nie było. Jawne zakresy Lab i ICCBased więc przeżywają, a deklaracja barwy z codestream nadal rządzi, gdy słownik milczy. Stronę dekodera tego formatu opisuje artykuł o JPEG 2000 i JPXDecode
Które ścieżki dekodowania stosują te reguły?
Oba pipeline'y je stosują. Po stronie headless oznacza to ładowanie THPDFHeadlessDocument, współdzielony dekoder strumieni i oparty na Cairo renderer stron za HPDFRenderHeadlessPagePNGFile. Po stronie VCL oznacza to ekstrakcję wczytanych obrazów do TBitmap i renderowanie stron VCL, które aplikują ten sam porządek z Table 13 przy dekodowaniu pełnego obrazu i zredukowanego regionu, więc nagłówek JFIF nie zdoła narzucić YCbCr ponad adobe'owską transformację 0, a nielegalna wartość czyni obraz niedekodowalnym też tam. Obrazy DCT inline stosują te same reguły markera, null, domyślnych i odrzuceń co image XObject, ze skróconymi i pełnymi nazwami kluczy, łącznie ze słownikiem /DP << … >> i skróconymi tablicami filtrów jak /F [/AHx /DCT], a obraz inline z nielegalnymi parametrami jest pomijany, podczas gdy reszta strony się renderuje. Przewodnik po wyciąganiu obrazów z wczytanego PDF-a opisuje resztę obsługi filtrów tego API
Kilka praktycznych notatek, zanim na tym zachowaniu sobie oprzesz:
- Dekodowanie JPEG potrzebuje w runtime zgodnej biblioteki TurboJPEG;
HOTPDF_TURBOJPEG_LIBRARYnadpisuje domyślneturbojpeg.dllalbolibturbojpeg.so.0 - Surowe kontrole strukturalne odrzucają pliki, które pobłażliwe czytniki otwierają, w szczególności współdzielony pusty słownik
/DecodeParms, a naprawa takich plików należy po stronie producenta
Dla workflow druku zarządzanego kolorem praktyczny wniosek jest krótki. Pisz CMYK JPEG z jawnym /ColorTransform zgadzającym się z każdym markerem Adobe w danych plus tablicą /Decode pasującą do ich zapisania, dawaj każdemu strumieniowi wielofiltrowemu tablicę /DecodeParms zgraną z etapami i oczekuj, że HotPDF będzie trzymał pliki przy tym, co faktycznie mówią
Headless loader, dekodery strumieni i renderer stron Cairo pokazane tutaj płyną z komponentem HotPDF Delphi PDF dla Delphi, C++Builder i Lazarus, obok klasycznego silnika dokumentów VCL