En CMYK JPEG, der kommer ud af en PDF med mudrede, fortrukne eller negative farver, betyder næsten altid, at decoderen fik én af to fakta forkert: om samplesne var gemt som YCCK og skal konverteres, eller om de var gemt inverteret efter Adobe-konventionen. HotPDFs decodere efterlever DCTDecode-parametren /ColorTransform for tre- og firekanalbilleder, afviser værdier, som specifikationen ikke definerer, og lader en Adobe APP14-markør inde i JPEG-dataene have forrang for den dictionary-post, som ISO 32000-1 Table 13 kræver. To naboregler hører til samme fældefamilie: multi-filter streams skal bære trinalleterede /DecodeParms-arrays, og JPEG 2000-billeder beholder en eksplicit /Decode-mapping
Alle tre deler et tema. PDF'en siger allerede, hvad den mener, og fælden er en renderer, der svarer på et lidt andet spørgsmål
Hvorfor renderer en CMYK JPEG inverteret eller udvasket i en PDF?
En CMYK JPEG går galt i en PDF af én af to grunde, og de ser forskellige ud på skærmen. Et negativt billede, hvor hvidt papir bliver sort og lyse nuancer bliver mørke, peger på inverteret lagring. Et udvasket eller farvetoneforskudt billede, hvor hud bliver grøn og sort bliver grå, mens K-detaljen forbliver skarp, peger på en farvetransform, der blev anvendt, selvom den ikke skulle, eller sprunget over, selvom den var påkrævet
Transformproblemet stammer fra, hvordan JPEG komprimerer farve. Baseline JPEG komprimerer bedst, når luminans er adskilt fra krominans, så encodere konverterer normalt RGB til YCbCr før komprimering, og firekanalsencodere konverterer CMY til YCC, mens K sendes uberørt igennem — det er formatet YCCK. Decoderen skal omgøre præcis den konvertering, encoderen udførte. Omgør én, der aldrig skete, eller spring én over, der skete, og hver pixel flytter sig. Den sorte kanal overlever uanset hvad, og derfor beholder et ødelagt CMYK-billede ofte knivskarpt sort tekst, mens alt andet driver væk
Inverteringsproblemet er historisk. Photoshop og andre Adobe-værktøjer skriver CMYK JPEG'er med hver sample gemt som 255 - value, og de mærker filen med en Adobe APP14-markør. Intet i JPEG-standarden siger det; det er en konvention, som enhver forbruger skal kende. I en PDF forventes producenten at udtrykke inversionen i billedets dictionary i stedet for at lade det stå til gætteri
Hvad fortæller /ColorTransform decoderen?
/ColorTransform er PDF-niveauets erklæring om, hvilken konvertering decoderen skal omgøre. ISO 32000-1 §7.4.8, Table 13, definerer to værdier. Nul betyder ingen transformation: decoderen returnerer samplesne, som de var komprimeret. Én betyder, at encoderen udførte standardkonverteringen, så et trekanalsbillede går fra YCbCr tilbage til RGB, og et firekanalsbillede går fra YCCK tilbage til CMYK. Parametren ignoreres for én- og tokanalbilleder. Table 13 sætter også en forrangsregel, der er let at overse: når de kodede data bærer en Adobe-markør med egen transformkode, er det den kode, der styrer, og dictionary-posten ignoreres. Kun en JPEG uden den markør falder tilbage til dictionary'en, og når posten også mangler, defaulter et trekanalsbillede til transformen og et firekanalsbillede til ingen
Den lette fejl er en billedvej, der accepterer parameter-dictionary'en, men aldrig giver den videre til JPEG-decoderen. En én-pixel RGB JPEG med farven (128, 63, 31) afslører det med det samme: sådan en decoder returnerer [128, 63, 31] for både /ColorTransform 0 og /ColorTransform 1, mens det korrekte svar ved nul er det rå YCbCr-trippel [79, 101, 163]
HotPDF anvender parametren på byteniveau. HPDFDecodeHeadlessTerminalImage går først JPEG-segmentstrukturen igennem frem til start-of-scan-markøren og leder efter et Adobe APP14-segment; byte, der blot ligner ét inde i et kommentarsegment, tæller ikke. Bærer dataene markøren, er det dens transformkode, der afgør, og dictionary-værdien ignoreres. Gør de ikke, bygger decoderen en privat kopi af JPEG'en med et APP14-segment indsat lige før start-of-scan-markøren, med transformkode 0, 1 eller, for et firekanalsbillede med værdi 1, YCCK-koden 2. Når den effektive transform for et trekanalsbillede er 0, afbryder kopien også JFIF-headeren, for en JFIF-header på egen hånd får libjpeg-turbo til at antage YCbCr. Den kaldendes buffer røres aldrig, og når et segment indsættes, belastes dets 16 ekstra byte arbejdsbyte-budgettet, før decoderen allokerer noget
At indsætte kun når der ikke findes en markør betyder noget på grund af en libjpeg-detalje: den bruger det sidste APP14-segment, den ser. Indsæt et segment efter en eksisterende markør, og en eksplicit dictionary-værdi overskrider i stilhed en modstridende markør i dataene — præcis det modsatte af Table 13. HotPDF anvender Table 13-rækkefølgen på både den fulde image XObject-vej og inline image-vejen
Du kan kalde den samme terminaldecoder direkte, når et enkelt billede ser forkert ud, og du vil se dets samples uden at render siden. Funktionen ligger i HPDFHeadlessImageCodecs-unit'en, tager filternavnet, de kodede byte, en valgfri parameter-dictionary og geometrien fra billedets dictionary, som skal matche JPEG-frame-headeren
uses
SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;
// ColorTransform < 0 betyder "posten mangler": lad JPEG-markørerne afgøre det
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;
Decoderen er streng med, hvad der tæller som en lovlig værdi. En eksplicit -1, en 2 eller en ikke-heltal som 0.5 udløser EHPDFHeadlessImageError i stedet for at falde tilbage til en default, for en værdi, specifikationen ikke definerer, giver decoderen intet troværdigt at handle på. Tjekket forbliver på plads, selv når en Adobe-markør i dataene gør posten irrelevant, for en udefineret værdi er en strukturel fejl i selve dokumentet. Et budget for lille til den transformerede kopi fejler på samme måde, med en besked, der nævner arbejdsbyte-budgettet
En eksplicit null er et andet tilfælde, for den er slet ikke en værdi. ISO 32000-1 §7.3.9 siger, at en dictionary-post med værdien null svarer til en fraværende post, så /ColorTransform null falder igennem til markøren eller til komponentantal-defaulten, præcis som om nøglen manglede. HotPDF behandler den sådan på hver vej, headless og VCL, stream og inline ens
Endnu en default fortjener en omtale, for det er her, HotPDF bevidst går ud over bogstaven i Table 13. En trekommponent-JPEG uden hverken JFIF-header eller Adobe-markør, hvis frame header mærker dens komponenter R, G og B, siger i sine egne byte, at den indeholder RGB. Table 13 ville stadig defaulte den til transformen, men libjpeg og læserne bygget på den afkoder den utransformeret, og det gør HotPDF også. Både headless- og VCL-decoderen behandler den sådan, så rød og blå forbliver der, hvor dataene lagde dem. En eksplicit dictionary-0 eller 1 overskrider stadig denne slutning
uses
System.IOUtils;
var
Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
// rå DCTDecode-byte fra et 640 x 480 CMYK-billede, gemt fra PDF-streamen
Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
try
// Med en Adobe APP14-markør i dataene er alle tre kald enige:
// markøren afgør, og dictionary-værdien ignoreres
Stored := DecodeDCT(Jpeg, 640, 480, 4, 0); // ingen markør: samples som komprimeret
Converted := DecodeDCT(Jpeg, 640, 480, 4, 1); // ingen markør: YCCK omgjort til CMYK
ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1); // ingen markør: firekanalsdefault, ingen
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;
En firekanals referencepixel viser, hvad man skal kigge efter. En CMYK-pixel på (10, 20, 30, 40) gemt som YCCK kommer tilbage som [245, 235, 225, 215], når YCCK-konverteringen ikke er omgjort, og som [0, 116, 0, 215], når den er. K-byten, 215, er identisk i begge. Ser K-planet rigtigt ud og de andre tre ikke, er en mismatched transform den første mistænkte
Den inverterede CMYK-konvention hører til /Decode
HotPDFs terminaldecoder returnerer CMYK-samples præcis, som libjpeg-turbo producerer dem, og flipper dem ikke på grund af en Adobe-markør. Tallene ovenfor gør konventionen synlig: 245, 235, 225, 215 er simpelthen 255 minus 10, 20, 30, 40, den inverterede form, som encodere i Adobe-stil gemmer. At vende det tilbage til trykfarvemængder er billeddictionaryens job, hvor en producent, der indlejrer sådan en JPEG, bør skrive /Decode [1 0 1 0 1 0 1 0]. Rendereren anvender /Decode-arrayet på hver komponent, så en korrekt skrevet fil printes korrekt
Bagsiden af sagen er værd at sige klart. En PDF, der indlejrer en Adobe-inverteret CMYK JPEG uden det matchende /Decode-array, vil rendere som et negativ i HotPDF, for filen selv beskriver billedet sådan. Nogle viewers opdager APP14-markøren og kompenserer i stilhed. HotPDF tilføjer ikke den heuristik, så løsningen på en sådan fil hører hjemme i softwaren, der skrev den
Hvorfor er én DecodeParms-dictionary til to filtre en fejl?
En stream, hvis /Filter er et array af flere trin, skal levere /DecodeParms som et array af samme længde, én parameter-dictionary eller null pr. trin. ISO 32000-1 §7.3.8.2, Table 5, fastslår den regel, og HotPDF håndhæver den: en multi-filter stream, der leverer én delt dictionary, rapporteres som et ugyldigt dokument i stedet for at blive afkodet
Det eftergivende alternativ er værre, end det ser ud. En decoder, der kun tjekker længden, når /DecodeParms allerede er et array, vil givet én enkelt dictionary og to filtre lydløst genbruge den dictionary i hvert trin. Det er harmløst, når dictionary'en er tom, og ødelæggende, når den ikke er: en /Predictor 12 beregnet til det indre Flate-trin ville også blive anvendt på det ydre, og outputtet bliver plausibelt udseende skrald i stedet for en ren fejl. Fordi hensigten med sådan en fil ikke kan reddes, nægter HotPDF den, også i det tomme tilfælde, som en sideindholds-stream med /Filter [/FlateDecode /FlateDecode] og /DecodeParms << >>
De accepterede og afviste former er lette at opliste:
- Accepteret:
/DecodeParmsudeladt ellernull, for et vilkårligt antal filtre - Accepteret: et array, hvis længde svarer til filterantallet, hver post en dictionary eller
null - Accepteret: én enkelt dictionary, når
/Filternavngiver præcis ét filter - Afvist: én enkelt dictionary delt af to eller flere filtre, med diagnosen "Stream filter chain requires array DecodeParms"
- Afvist: et array, hvis længde afviger fra filterantallet, med "DecodeParms array must match the filter chain"
Samme tjek kører for almindelige indholdsstreams og for billedstreams, hvis sidste filter er en terminalcodec som DCTDecode, så en misdannet kæde fejler samme sted, uanset hvad streamen bærer. Samspillet med dekomprimeringsgrænser er dækket i artiklen om filterkæde-budgets og PDF-decode-bomber. Fra programkode ses fejlen, når siden renderes
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;
JPEG 2000-billeder beholder deres eksplicitte /Decode
Et JPEG 2000-billede ignorerer kun /Decode, når dets dictionary ikke har en /ColorSpace, og det ikke er et image mask. ISO 32000-1 §8.9.5 gør det betinget, og HotPDF følger det. At kassere /Decode for hvert ikke-stencil JPX-billede er den lette genvej, og den får en eksplicit inversion til simpelthen at forsvinde: en DeviceGray JPX-sample på 64 under /Decode [1 0] skal renderes som 191, ikke 64
At få den ene betingelse rigtigt er ikke nok i sig selv. Farverumsgrene som Lab og ICCBased, der sætter default-komponentområder, når billedet er JPX, ville overskrive en legitimt anvendt array en anden gang, så rendereren noterer, om et gyldigt /Decode-array faktisk blev anvendt, og fylder default-områder i kun, når intet blev. Eksplícitte Lab- og ICCBased-områder overlever derfor, og codestreamens farveerklæring styrer stadig, når dictionary'en er tavs. Formats decoder-side er beskrevet i JPEG 2000- og JPXDecode-artiklen
Hvilke decode-veje følger disse regler?
Begge pipelines følger dem. På headless-siden betyder det THPDFHeadlessDocument-indlæsning, den delte stream-decoder og den Cairo-baserede siderendering bag HPDFRenderHeadlessPagePNGFile. På VCL-siden betyder det loaded-image-ekstraktion til en TBitmap og VCL-siderendering, som anvender samme Table 13-rækkefølge ved både helbillede- og reduceret-region-afkodning, så en JFIF-header ikke kan tvinge YCbCr over en Adobe-transform på 0, og en ulovlig værdi gør billedet uafkodet der også. Inline DCT-billeder følger samme markør-, null-, default- og afvisningsregler som image XObjects, med forkortede og fulde nøglenavne ens, inklusiv en /DP << … >>-dictionary og forkortede filterarrays som /F [/AHx /DCT], og et inline-billede med ulovlige parametre springes over, mens resten af siden renderes. Guiden til at udtrække billeder fra en loadet PDF dækker resten af den APIs filterhåndtering
Et par praktiske noter, før du bygger på denne adfærd:
- JPEG-afkodning kræver et kompatibelt TurboJPEG-bibliotek på run time;
HOTPDF_TURBOJPEG_LIBRARYoverskrider defaultturbojpeg.dllellerlibturbojpeg.so.0 - Strikke strukturtjek afviser filer, som eftergivende læsere åbner, især den delte tomme
/DecodeParms-dictionary, og løsningen på de filer ligger på producersiden
Til et farvestyret print-workflow er den praktiske konklusion kort. Skriv CMYK JPEG'er med en eksplicit /ColorTransform, der stemmer med enhver Adobe-markør i dataene plus det /Decode-array, der matcher deres lagring, giv hver multi-filter stream et trinalleteret /DecodeParms-array, og forvent, at HotPDF holder filer op til, hvad de faktisk siger
Headless-loaderen, stream-decoderne og Cairo-siderenderen vist her følger med i HotPDF Delphi PDF component til Delphi, C++Builder og Lazarus, sammen med den klassiske VCL-dokumentmotor