Teknisk artikel

CMYK JPEG i PDF: DCTDecode ColorTransform i Delphi

En CMYK JPEG som kommer ut ur en PDF med grumliga, förskjutna eller negativa färger betyder nästan alltid att avkodaren fått fel på en av två saker: om samplesna lagrats som YCCK och behöver konverteras, eller om de lagrats inverterade enligt Adobe-konvention. HotPDF:s avkodare följer DCTDecode-parametern /ColorTransform för tre- och fyrkanalsbilder, avvisar värden specifikationen inte definierar, och låter en Adobe APP14-markör inuti JPEG-datan före dictionary-posten, som ISO 32000-1 Table 13 kräver. Två grannregler hör till samma fällfamilj: flerfilterströmmar måste bära stage-anpassade /DecodeParms-arrayer, och JPEG 2000-bilder behåller en explicit /Decode-mappning

Alla tre delar ett tema. PDF:en säger redan vad den menar, och fällan är en renderare som svarar på en lite annan fråga

Varför renderas en CMYK JPEG inverterad eller urtvättad i en PDF?

En CMYK JPEG går snett i en PDF av en av två orsaker, och de ser olika ut på skärmen. En negativ bild, där vitt papper blir svart och ljusa toner blir mörka, pekar på inverterad lagring. En urtvättad eller kulörförskjuten bild, där hud blir grön och svartan blir grå medan K-detaljen förblir skarp, pekar på en färgtransform tillämpad när den inte skulle ha varit, eller hoppad över när den krävdes

Symptomkarta för en trasig CMYK JPEG i en Delphi-PDF: negativ pekar på inverterad Adobe-lagring, urtvättade kulörer på en felmatchad ColorTransform
ett negativ betyder inverterad lagring som ber om en /Decode-array, kulördrift betyder en felmatchad transform, och ett skarpt K-plan pekar på transformen först

Transformproblemet kommer från hur JPEG komprimerar färg. Baseline JPEG komprimerar bäst när luminansen är skild från krominansen, så kodare konverterar oftast RGB till YCbCr före komprimeringen, och fyrkanalskodare konverterar CMY till YCC medan K passerar orörd, vilket är formatet som kallas YCCK. Avkodaren måste ångra exakt den konvertering kodaren tillämpade. Ångra en som aldrig skedde, eller hoppa över en som gjorde det, och varje pixel flyttar på sig. Den svarta kanalen överlever ändå, vilket är varför en trasig CMYK-bild ofta behåller skarp svart text medan allt annat driver

Inverteringsproblemet är historiskt. Photoshop och andra Adobe-verktyg skriver CMYK-JPEG:er med varje sample lagrat som 255 - value, och de märker filen med en Adobe APP14-markör. Ingenting i JPEG-standarden säger så; det är en konvention, och varje konsument måste känna till den. I en PDF förväntas producenten uttrycka inversionen i image-dictionaryn i stället för att lämna den till gissningar

Vad berättar /ColorTransform för avkodaren?

/ColorTransform är uttalandet på PDF-nivå om vilken konvertering avkodaren måste ångra. ISO 32000-1 §7.4.8, Table 13, definierar två värden. Noll betyder ingen transformation: avkodaren returnerar samplesna som komprimerade. Ett betyder att kodaren tillämpade standardkonverteringen, så en trekanalsbild går från YCbCr tillbaka till RGB och en fyrkanalsbild går från YCCK tillbaka till CMYK. Parametern ignoreras för en- och tvåkanalsbilder. Table 13 sätter också en precedence-regel som är lätt att missa: när den kodade datan bär en Adobe-markör med egen transformkod är det koden som gäller och dictionary-posten ignoreras. Bara en JPEG utan den markören faller tillbaka till dictionaryn, och när posten saknas också faller en trekanalsbild tillbaka på transformen och en fyrkanalsbild på ingen

DCTDecode ColorTransform-beslutsmatris för Delphi: Adobe APP14-markören styr över dictionaryn, och odefinierade värden avvisas av HotPDF
Adobe-markören inuti JPEG-datan rankas över dictionary-posten, och bara en JPEG utan en faller tillbaka till /ColorTransform eller komponentantalsstandard

Det lätta misstaget är en bildväg som accepterar parameterdictionaryn men aldrig räcker den till JPEG-avkodaren. En JPEG med en pixel i RGB som håller färgen (128, 63, 31) avslöjar det direkt: en sådan avkodare returnerar [128, 63, 31] för både /ColorTransform 0 och /ColorTransform 1, medan det rätta svaret för noll är den råa YCbCr-tripletten [79, 101, 163]

HotPDF tillämpar parametern på bytenivå. HPDFDecodeHeadlessTerminalImage går först igenom JPEG-segmentstrukturen fram till start-of-scan-markören och letar efter ett Adobe APP14-segment; byte som bara liknar ett inuti ett kommentarssegment räknas inte. Bär datan markören är det dess transformkod som avgör och dictionary-värdet ignoreras. Gör den inte det bygger avkodaren en privat kopia av JPEG:en med ett APP14-segment insatt precis före start-of-scan-markören, med transformkod 0, 1, eller, för en fyrkanalsbild med värdet 1, YCCK-koden 2. Närhelst den effektiva transformen för en trekanalsbild är 0 oskadliggör kopian också JFIF-huvudet, för ett JFIF-huvud i sig säger åt libjpeg-turbo att anta YCbCr. Anroparens buffert rörds aldrig, och när ett segment sätts in debiteras dess 16 extra byte mot arbetsbytesbudgeten innan avkodaren allokerar något

Att bara inserta när ingen markör finns spelar roll på grund av en libjpeg-detalj: den använder det sista APP14-segmentet den ser. Inserta ett segment efter en befintlig markör och ett explicit dictionary-värde tyst överskrider en motsägelsefull markör i datan, vilket är exakt baklänges mot Table 13. HotPDF tillämpar Table 13-ordningen på hela image XObject-vägen och på inline-bildvägen likaså

Du kan anropa samma terminalavkodare direkt när en enskild bild ser fel ut och du vill se dess samples utan att rendera sidan. Funktionen bor i uniten HPDFHeadlessImageCodecs, tar filternamnet, de kodade bytena, en valfri parameterdictionary, och geometrin från image-dictionaryn, som måste matcha JPEG-framhuvudet

uses
  SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;

// ColorTransform < 0 betyder ”post saknas”: låt JPEG-markörerna avgöra
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;

Avkodaren är strikt med vad som räknas som lagligt värde. En explicit -1, en 2, eller ett icke-heltal som 0.5 kastar EHPDFHeadlessImageError i stället för att falla tillbaka på en standard, för ett värde specifikationen inte definierar ger avkodaren inget förtroendevärt att agera på. Kontrollen består även när en Adobe-markör i datan gör posten irrelevant, för ett odefinierat värde är ett strukturellt fel i dokumentet självt. En budget för liten för den transformerade kopian faller på samma sätt, med ett meddelande som namnger arbetsbytesbudgeten

En explicit null är ett annat fall, för den är inte ett värde alls. ISO 32000-1 §7.3.9 säger att en dictionary-post vars värde är null är likvärdig med en frånvarande post, så /ColorTransform null trillar ner till markören eller komponentantalsstandarden exakt som om nyckeln saknades. HotPDF behandlar den så på varje väg, headless och VCL, ström och inline likaså

En standard till förtjänar omnämnande, för det är där HotPDF medvetet går bortom Table 13:s bokstav. En JPEG med tre komponenter, utan vare sig JFIF-huvud eller Adobe-markör, vars framhuvud etiketterar sina komponenter R, G och B, säger i sina egna byte att den håller RGB. Table 13 skulle fortfarande falla tillbaka på transformen, men libjpeg och läsarna byggda på den avkodar den otransformerad, och det gör HotPDF. Både headless- och VCL-avkodarna behandlar den så, så röd och blå stannar där datan satte dem. En explicit dictionary 0 eller 1 överskrider ändå den slutsatsen

uses
  System.IOUtils;

var
  Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
  // råa DCTDecode-byte från en 640 x 480 CMYK-bild, sparade från PDF-strömmen
  Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
  try
    // Med en Adobe APP14-markör i datan är de tre anropen överens:
    // markören avgör och dictionary-värdet ignoreras
    Stored    := DecodeDCT(Jpeg, 640, 480, 4, 0);   // ingen markör: samples som komprimerade
    Converted := DecodeDCT(Jpeg, 640, 480, 4, 1);   // ingen markör: YCCK omgjort till CMYK
    ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1);  // ingen markör: fyrkanalsstandard, 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 fyrkanals referenspixel visar vad du ska leta efter. En CMYK-pixel på (10, 20, 30, 40) lagrad som YCCK kommer tillbaka som [245, 235, 225, 215] när YCCK-konverteringen inte ångrats och som [0, 116, 0, 215] när den är ångrad. K-byten, 215, är detsamma i båda. Om K-planet ser rätt ut och de andra tre inte är en felmatchad transform den första misstänkta

Den inverterade CMYK-konventionen hör till /Decode

HotPDF:s terminalavkodare returnerar CMYK-samples exakt som libjpeg-turbo producerar dem och vänder dem inte på en Adobe-markörs blotta anseende. Siffrorna ovan gör konventionen synlig: 245, 235, 225, 215 är helt enkelt 255 minus 10, 20, 30, 40, den inverterade form Adobe-liknande kodare lagrar. Att vända det tillbaka till bläckmängder är image-dictionaryns jobb, där en producent som bäddar in en sådan JPEG borde skriva /Decode [1 0 1 0 1 0 1 0]. Renderaren tillämpar /Decode-arrayen på varje komponent, så en korrekt skriven fil skrivs ut korrekt

Motstycket är värt att säga rakt ut. En PDF som bäddar in en Adobe-inverterad CMYK JPEG utan matchande /Decode-array renderas som ett negativ i HotPDF, för filen själv beskriver bilden så. Vissa viewers upptäcker APP14-markören och kompenserar tyst. HotPDF lägger inte till den heuristiken, så fixen för en sådan fil hör hemma i mjukvaran som skrev den

Varför är en DecodeParms-dictionary för två filter ett fel?

En ström vars /Filter är en array av flera steg måste leverera /DecodeParms som en array av samma längd, en parameterdictionary eller null per steg. ISO 32000-1 §7.3.8.2, Table 5, anger den regeln, och HotPDF driver den: en flerfilterström som levererar en gemensam dictionary rapporteras som ett ogiltigt dokument i stället för att avkodas

Det tillmötesgående alternativet är värre än det ser ut. En avkodare som kontrollerar längden bara när /DecodeParms redan är en array återanvänder, givet en dictionary och två filter, tyst den dictionaryn på varje steg. Det är harmlöst när dictionaryn är tom och förödande när den inte är det: en /Predictor 12 avsedd för det inre Flate-steget skulle även tillämpas på det yttre, och utdata blir trovärdigt seende skräp i stället för ett rent fel. Eftersom avsikten hos en sådan fil inte kan återvinnas vägrar HotPDF den, även i tomafallet, som en sidinnehållsström med /Filter [/FlateDecode /FlateDecode] och /DecodeParms << >>

DecodeParms-former HotPDF accepterar för flerfilterströmmar i Delphi: anpassade arrayer passrar, medan en gemensam dictionary eller felaktig arraylängd avvisas
en flerfilterström behöver en dictionary eller null per steg, och en gemensam dictionary vägras även när den är tom

De accepterade och avvisade formerna är lätta att räkna upp:

  • Accepterat: /DecodeParms utelämnad, eller null, för hur många filter som helst
  • Accepterat: en array vars längd är lika med filterantalet, varje post en dictionary eller null
  • Accepterat: en enda dictionary när /Filter namnger exakt ett filter
  • Avvisat: en enda dictionary delad av två eller fler filter, med diagnosen ”Stream filter chain requires array DecodeParms”
  • Avvisat: en array vars längd skiljer sig från filterantalet, med ”DecodeParms array must match the filter chain”

Samma kontroll körs för vanliga innehållsströmmar och för bildströmmar vars sista filter är en terminalcodec som DCTDecode, så en felformad kedja faller på samma ställe oavsett vad strömmen bär. Interaktionen med dekomprimeringsgränser tas upp i artikeln om filterkedjebudgeter och PDF-dekodningsbomber. Från applikationskod syns felet när sidan renderas

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-bilder behåller sin explicita /Decode

En JPEG 2000-bild ignorerar /Decode bara när dess dictionary saknar /ColorSpace och den inte är en bildmask. ISO 32000-1 §8.9.5 gör det villkorligt, och HotPDF följer det. Att kasta /Decode för varje icke-stencil-JPX-bild är den enkla genvägen, och den gör att en explicit inversion helt enkelt försvinner: en DeviceGray JPX-sample på 64 under /Decode [1 0] ska renderas som 191, inte 64

Att få det ena villkoret rätt räcker inte i sig. Färgrymdsgrenar som Lab och ICCBased som sätter standardkomponentintervall närhelst bilden är JPX skulle skriva över en legitimt tillämpad array en andra gång, så renderaren registrerar om en giltig /Decode-array faktiskt tillämpades, och fyller i standardintervall bara när ingen gjorde det. Explicita Lab- och ICCBased-intervall överlever alltså, och kodströmmens färgdeklaration styr fortfarande när dictionaryn är tyst. Avkodningssidan av formatet beskrivs i artikeln om JPEG 2000 och JPXDecode

Vilka avkodningsvägar följer de här reglerna?

Båda pipelineerna följer dem. På headless-sidan betyder det THPDFHeadlessDocument-laddning, den gemensamma strömavkodaren och den Cairo-baserade sidrenderaren bakom HPDFRenderHeadlessPagePNGFile. På VCL-sidan betyder det extraktion av laddade bilder till en TBitmap och VCL-sidrendering, som tillämpar samma Table 13-ordning på helbilds- och reducerad-region-avkodning likaså, så ett JFIF-huvud inte kan tvinga YCbCr över en Adobe-transform på 0, och ett olagligt värde gör bilden oavkodbar där också. Inline-DCT-bilder följer samma markör-, null-, standard- och avvisningsregler som image XObjects, med förkortade och fullständiga nyckelnamn likaså, inklusive en /DP << … >>-dictionary och förkortade filterarrayer som /F [/AHx /DCT], och en inline-bild med olagliga parametrar hoppas över medan resten av sidan renderas. Guiden till att extrahera bilder från en laddad PDF täcker resten av det API:ets filterhantering

Några praktiska notiser innan du bygger på det här beteendet:

  • JPEG-avkodning behöver ett kompatibelt TurboJPEG-bibliotek vid körning; HOTPDF_TURBOJPEG_LIBRARY överskrider standard turbojpeg.dll eller libturbojpeg.so.0
  • Strikta strukturella kontroller avvisar filer som tillmötesgående läsare öppnar, i synnerhet den gemensamma tomma /DecodeParms-dictionaryn, och fixen för de filerna ligger på producentsidan

För ett färghanterat tryckarbetsflöde är den praktiska lärdomen kort. Skriv CMYK-JPEG:er med en explicit /ColorTransform som stämmer med varje Adobe-markör i datan plus den /Decode-array som matchar deras lagring, ge varje flerfilterström en stage-anpassad /DecodeParms-array, och räkna med att HotPDF håller filer till vad de faktiskt säger

Headless-laddaren, strömavkodarna och Cairo-sidrenderaren som visas här följer med HotPDF Delphi PDF-komponenten för Delphi, C++Builder och Lazarus, tillsammans med den klassiska VCL-dokumentmotorn