Technikai cikk

Separation és DeviceN direkt színek renderelése Delphi-ben

A HotPDF a Separation és DeviceN direkt színeket (spot colors) úgy rendereli a betöltött PDF-oldalakon, hogy feloldja a színteret a HPDFResolveColorSpace segítségével, kiértékeli a színárnyalat-transzformációs (tint-transform) függvényt a HPDFEvalTintTransform-mal, majd az eredményt az alternatív színtéren keresztül RGB-vé konvertálja a képernyő számára. A v2.375.0 verzió óta ez a folyamat mind a négy PDF függvénytípust kiértékeli, beleértve a Type 4 PostScript kalkulátorokat is, így a Pantone tintákat tartalmazó nyomdakész fájl a valós színeit jeleníti meg egy helyettesítő helyett. Ez a cikk végigvezeti Önt azon, hogyan működik ez a folyamat, és ami szintén hasznos, hogyan hibázott a fejlesztése során.

A kiváltó ok mindig ugyanaz. Az ügyfél kap egy PDF-et a nyomdától: a címsor egy elnevezett direkt színnel (spot ink) van beállítva, a csomagolás grafikája kétfestékes DeviceN keveréket használ, a törzsszöveg pedig egyszerű fekete. Az Acrobatban tökéletesen mutat. Az Ön Delphi alkalmazásában azonban a címsor feketén renderelődik, vagy ami még rosszabb, egyáltalán nem jelenik meg, és az ügyfél nem a fájlra, hanem az Ön szoftverére jelent be hibát. A fájllal nincs gond. A renderelő egyszerűen nem ismeri azokat a színtereket, amelyeket a fájl használ.

Miért renderelődnek feketén a direkt színek a PDF-nézőben?

A direkt színek feketén renderelődnek vagy eltűnnek, ha a renderelő csak az eszköz-színoperátorokat (rg, g, k) valósítja meg, és figyelmen kívül hagyja az általánosakat. Az ISO 32000-1 §8.6 három színtércsoportot határoz meg: eszközszínterek (DeviceGray, DeviceRGB, DeviceCMYK), CIE-alapú színterek (CalGray, CalRGB, Lab, ICCBased) és speciális színterek (Indexed, Separation, DeviceN, Pattern). Az eszközcsoporton kívüli minden mást az általános operátorokkal jelölünk ki: a cs és CS név alapján választ színtért az oldal erőforrás-szótárából (resource dictionary), majd a sc, SC, scn és SCN megadják az összetevő értékeket. Azok az operátorok, amelyeket a renderelő kihagy, megőrzik a legutóbb beállított színt, ami egy olyan oldal esetében, amely direkt színű címsorral kezdődik, a kezdeti DeviceGray fekete lesz.

A HotPDF a v2.333.0 verzióban adta hozzá az általános operátorkészletet az oldalrenderelőjéhez, egy egységes feloldási útvonallal együtt: minden egyes /ColorSpace erőforrás-bejegyzés, legyen az egy egyszerű név, belső tömb vagy közvetett hivatkozás, egyetlen THPDFColorSpace rekorddá lesz elemezve, és minden kitöltési vagy körvonalazási színkérés egyetlen HPDFResolveColor hívásba fut be. A színtércsaládok felsorolása jól mutatja a lefedettséget.

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

Egyetlen tervezési döntés többszörösen is megtérül: a csfUnsupported egy elsőrangú színtércsalád, nem pedig hiba. Egy olyan színtér, amelyet a renderelő nem tud értelmezni, egy meghatározott tartalékra (fallback) esik vissza az oldal leállítása helyett, ami megegyezik a főbb megjelenítők működésével, és megakadályozza, hogy egyetlen különleges kitöltés miatt az egyébként renderelhető dokumentum üresen jelenjen meg.

Hogyan alakít át a színárnyalat-transzformáció egy tintaértéket valós színné?

A Separation színtér három információt hordoz: a tinta nevét, egy alternatív színteret és egy színárnyalat-transzformációs függvényt. A [/Separation /PANTONE485 /DeviceCMYK f] tömb azt mondja: amikor a tartalomfolyam 0.8 scn-t ír, táplálja be a 0,8-as árnyalatértéket az f függvénye, és fesse ki a kapott CMYK négyest. A DeviceN ezt terjeszti ki N tintára egy N-bemenetű függvénnyel. Maga a tinta neve a képernyőn csak tájékoztató jellegű; a színárnyalat-transzformáció jelenti a teljes renderelési szemantikát, így az a renderelő, amely elemzi a színteret, de kihagyja a függvényt, még semmi hasznosat nem csinált.

Az HPDFEvalTintTransform a lépés mögött álló függvénymotor. A v2.334.0 verzióban bevezetett modul kiértékeli a Type 2 exponenciális függvényeket (C0 + x^N * (C1 - C0) Domain és Range korlátozással), a Type 3 illesztési (stitching) függvényeket (Bounds-alapú részfüggvény-rekurzió Encode leképzéssel) és a Type 0 mintavételezett függvényeket 8, 16 és 32 bites mintákkal. A Type 0 ugyanaz a keresőtábla-struktúra (lookup-table), amelyet a létrehozó oldalról a Type 0 szín-LUT-ok építéséről szóló cikkben tárgyaltunk; a renderelő oldal ugyanezt a struktúrát járja be fordítva, a dekódolt mintabájtoktól visszafelé az összetevő értékekig.

A Type 4 PostScript kalkulátor függvények jelentették az utolsó bástyát. A v2.375.0 verzióig finom egy semleges helyettesítőre estek vissza; a v2.375.0 óta azonban a HPDFEvalPostScriptCalculator végrehajtja a teljes ISO 32000-1 §7.10.5 operátorkészletet egy korlátos operandus-vermen: aritmetikai, összehasonlító, logikai és bitenkénti operátorokat, veremmanipulációt (beleértve a roll-t is), valamint az if/ifelse feltételes elágazásokat. A határesetek szemantikája szigorúbb, mint amilyennek látszik. A PostScript round a felet a nagyobb érték felé kerekíti, így a Delphi bankárkerekítését használó Round függvény nem alkalmazható; a trigonometrikus operátorok fokban dolgoznak, az atan a [0, 360) tartományban ad vissza értékeket; és az exp egy kétoperandusú hatványozás, nem pedig a természetes exponenciális. Ugyanez a kiértékelő hajtja meg a függvényalapú színátmenet-kitöltéseket is, ezért kapott az axiális és radiális árnyékolások renderelése kalkulátor-vezérelt rámpákat ugyanebben a kiadásban.

// Kiértékeli a Separation/DeviceN színárnyalat-transzformációt (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript kalkulátor, ISO 32000-1 7.10.5 operátorkészlet.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

Őszinteség a pontosságról: a dupla pontossággal kiértékelt szoftveres kalkulátor nem fog bitre pontosan megegyezni egy RIP-pel (raster image processor), és a Range határán történő vágás (clamping) egy legkisebb helyiértékű bitben eltérhet más megvalósításoktól. Képernyős megjelenítéshez és regressziós rendereléshez ez lényegtelen; ha színkezelt próbanyomtatást (proofing) épít, a színárnyalat-transzformáció csak az első szakasz, és továbbra is szüksége van egy valós CMM-re (Color Management Module) a folyamat későbbi részében.

CalGray, CalRGB, Lab és ICCBased ICC motor nélkül

A CIE-alapú családok ugyanannak a feloldónak a másik ágát követik. A HotPDF a Lab értékeket a szabványos Lab -> XYZ -> sRGB láncon keresztül konvertálja, beleértve az inverz transzferfüggvény 6/29 töréspont-kockáját, a CalRGB-t a csatornánkénti gammával és a 3x3-as lineáris mátrixszal, a CalGray-t pedig az egyetlen gammával kezeli. A teljesítmény szempontjából lényeges részlet a fehérpont kezelése: az XYZ -> sRGB konverziós mátrix Bradford-adaptációval igazodik a színtérben deklarált fehérponthoz, és a feloldott THPDFColorSpace rekordban gyorsítótárazódik, így a képpontonkénti munka egyetlen 3x3-as szorzás marad, függetlenül attól, hogy milyen különleges a deklarált fényforrás.

Az ICCBased színterek szándékosan pragmatikus kezelést kapnak. A PDF specifikáció megköveteli, hogy minden ICCBased folyam deklaráljon egy /Alternate színteret, vagy egy implicit színteret az /N komponensszámán keresztül, éppen azért, hogy a színkezelő motorral nem rendelkező megjelenítők is ésszerűen tudjanak renderelni. A HotPDF az ICCBased színtereket ezen az alternatíván keresztül oldja fel, vagy az /N értéke (1, 3 vagy 4) alapján következtet a DeviceGray, DeviceRGB vagy DeviceCMYK színtérre, ha a bejegyzés hiányzik, és soha nem elemzi a profilbájtokat. Ez azt jelenti, hogy nincs lcms függőség és nincs profilkeresési költség, a színhelyesség áráért cserébe: az az ICCBased színtér, amelynek profilja erősen eltér az alternatívájától, az alternatív renderelést fogja mutatni. Képernyős megtekintéshez és bélyegképekhez ez ugyanaz a kompromisszum, amelyet minden könnyűsúlyú megjelenítő megköt, és ezt a határt érdemes egyértelműen rögzíteni a saját dokumentációjában.

A hiba, amely miatt minden elnevezett színtér-keresés sikertelen volt

A v2.333.0 operátor-megvalósítás tartalmazott egy olyan hibát, amely negyvenkét kiadáson keresztül láthatatlan maradt: a cs és CS kezelők a vezető perjellel együtt keresték vissza az operandusukat (/CS0) az erőforrás-szótár perjel nélkül tárolt kulcsaival szemben (CS0). Minden elnevezett színtér-keresés sikertelen volt az esetek száz százalékában, és a kód csendben visszalépett az alapértelmezett DeviceGray szintre. A látható tünet a legrosszabb módon volt finom: a Separation kitöltés 1 scn-je DeviceGray 1.0 lett, ami fehéret fest, és a fehér lapon lévő fehér tinta nem olyan renderelési hiba, amiről bárki képernyőképet készítene. A v2.375.0 verzióban bevezetett javítás egy megosztott név-normalizáló segédfüggvény volt, amelyet minden operandus-erőforrás kereséskor alkalmazunk.

Ugyanebből a vizsgálatból két kapcsolódó hiba is felszínre került. Először is, a tömb értékű objektumokra való közvetett hivatkozások feloldatlanul tértek vissza: a renderelő dokumentum-hozzáférése csak a folyamokhoz (streams) és szótárakhoz (dictionaries) rendelkezett típusos feloldókkal, így egy önálló [/Separation ...] tömbre mutató /CS0 5 0 R nyers hivatkozásként tért vissza, és a színtér unsupported-ként lett elemezve. Másodszor, a HPDFReadNumericArray szigorú hosszemantikát kényszerített ki, megkövetelve, hogy a PDF tömb legalább olyan hosszú legyen, mint a megadott puffer. Egy Type 2 függvény /C0 és /C1 értékének négyelemű pufferbe történő beolvasása ezért meghiúsult az egy- és háromkomponensű alternatívák esetében, mindkét tömböt nullázva hagyva, így minden nem CMYK exponenciális árnyalat feketén renderelődött, amíg a v2.376.0 be nem vezette az engedékenyebb HPDFReadNumericArrayUpTo olvasót. A levonható tanulság: minden olyan PDF-kulcsot, amely változó hosszúságú numerikus tömböt tartalmaz, olyan olvasóval kell beolvasni, amely azt tölti fel, ami létezik, mert a szigorú egyezést megkövetelő, rögzített méretű puffer a különben érvényes fájlokat csendben nullákká változtatja.

Hogyan tesztelheti a direkt színek renderelését anélkül, hogy becsapná magát?

A kellemetlen kérdés az, hogy a tesztcsomag miért maradt végig zöld mindezek ellenére. Az eredeti regresszió, a SeparationRendersDistinguishable, csak azt ellenőrizte, hogy a renderelt bitkép nem teljesen fekete-e. Egy olyan folyamat, amely minden direkt színt DeviceGray-re vetett vissza, szürke és fehér kimenetet eredményezett, ami nem fekete, így az ellenőrzés sikeres volt, miközben a teljes funkció nem működött. A „nem üres”, „nem teljesen fekete” vagy „a kivonat nem nulla” formájú gyenge ellenőrzések nem tudják megkülönböztetni a működő renderelőt a hibástól, mert szinte bármilyen hibaállapot eredményez valamilyen képpontot.

Az az ellenőrzési stílus, amely valóban elcsípi ezeket a hibákat, rögzíti az elvárt színt: rendereljen egy kézzel készített minimális PDF-et, amelynek Separation tintája ismert színárnyalatra oldódik fel, majd számolja meg azokat a képpontokat, amelyekben ez a színárnyalat dominál. A vizsgálathoz szükséges bitképbe renderelés ugyanazt a RenderLoadedPageToBitmap belépési pontot használja, amelyet a lap-bitkép renderelési útmutatóban leírtunk.

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Rögzítse a várt tintát: követeljen meg egy valós vörös-domináns
        // képpontterületet, soha ne érje be a „nem teljesen fekete” eredménnyel.
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

A tesztfájl (fixture) ugyanolyan fontos, mint maga az ellenőrzés. Egy kézzel készített, néhány száz bájt hosszú PDF, amelyben egyetlen Separation kitöltés található és semmi más, nem hagy kétséget afelől, hogy mi a várt kimenet; egy valós fájl több kódot mozgat meg, de nem tudja megmondani, melyik szakasz hibásodott meg. Mostantól az elvárt színű képpontok számlálását tekintjük a minimális elvárásnak minden renderelési füstteszt (smoke test) esetében, mert ez az egyetlen ellenőrzési stílus, amely felszínre hozta ezeket a hibákat.

A direkt színek és a CIE színtér renderelése a betöltött dokumentum-feldolgozási folyamat része a Delphi és C++Builder rendszerekhez készült HotPDF Component szoftverben, a fent bemutatott függvénymotorral, árnyékolás-rendereléssel és bitkép-exportálási útvonalakkal együtt.