HotPDF renderar separations- och DeviceN-dekorfärger (spot colors) på inlästa PDF-sidor genom att lösa upp färgrymden via HPDFResolveColorSpace, utvärdera tint-transformationsfunktionen med HPDFEvalTintTransform och konvertara resultatet via den alternativa färgrymden till RGB för skärmen. Sedan version 2.375.0 utvärderar den pipelinen alla fyra PDF-funktionstyper, inklusive Typ 4 PostScript-kalkylatorer, så att en tryckfärdig fil med Pantone-färger visar sina verkliga färger istället för en platshållare. Den här artikeln går igenom hur pipelinen fungerar och, vilket är minst lika användbart, hur den misslyckades under tiden vi byggde den
Utlösande faktor är alltid densamma. En kund får en PDF från ett tryckeri: rubriken är satt i en namngiven dekorfärg, förpackningsdesignen använder en DeviceN-blandning av två färger och brödtexten är helt svart. I Acrobat ser det perfekt ut. I din Delphi-applikation renderas rubriken som svart, eller ännu värre, renderas inte alls, och kunden anmäler en bugg mot din programvara snarare än mot filen. Det är inget fel på filen. Renderaren talar bara inte de färgrymder som filen använder
Varför renderas dekorfärger som svarta i en PDF-visare?
Dekorfärger renderas som svarta, eller försvinner helt, när renderaren endast implementerar enhetsfärgoperatorerna (rg, g, k) och ignorerar de generiska operatorerna. ISO 32000-1 §8.6 definierar tre grupper av färgrymder: enhetsfärgrymder (DeviceGray, DeviceRGB, DeviceCMYK), CIE-baserade färgrymder (CalGray, CalRGB, Lab, ICCBased) och speciella färgrymder (Indexed, Separation, DeviceN, Pattern). Allt utanför enhetsgruppen väljs med de generiska operatorerna: cs och CS väljer en färgrymd efter namn från sidans resursordlista, och därefter tillhandahåller sc, SC, scn och SCN komponentvärdena. En renderare som hoppar över dessa operatorer behåller den färg som senast ställdes in, vilket för en sida som inleds med en rubrik i dekorfärg innebär den ursprungliga svarta färgen i DeviceGray
HotPDF lade till den generiska operatorsamlingen i sin sidrenderare i version 2.333.0, tillsammans med en enhetlig upplösningssökväg: varje /ColorSpace-resurspost, oavsett om det är ett rent namn, en infogad array eller en indirekt referens, tolkas till en THPDFColorSpace-post, och varje begäran om fyllnings- eller linjefärg kanaliseras genom ett enda anrop till HPDFResolveColor. Uppräkningen av färgrymdsfamiljer visar täckningen i korthet
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;
Ett designbeslut har visat sig värdefullt gång på gång: csfUnsupported är en familj av första klass, inte ett fel. En färgrymd som renderaren inte kan tolka degraderas till en definierad fallback istället för att avbryta renderingen av sidan, vilket stämmer överens med hur etablerade visare beter sig och förhindrar att en enskild exotisk fyllning tömmer ett i övrigt renderbart dokument
Hur omvandlar en tint-transformation ett färgtonsvärde till en verklig färg?
En Separations-färgrymd innehåller tre delar information: bläckets namn (färgtonens namn), en alternativ färgrymd samt en tint-transformationsfunktion. Arrayen [/Separation /PANTONE485 /DeviceCMYK f] säger: när innehållsströmmen skriver 0.8 scn, mata in färgtonsvärdet 0.8 i funktionen f och rita det resulterande CMYK-värdet. DeviceN generaliserar detta till N färgtoner med en funktion som tar N indata. Själva bläcknamnet är endast rådgivande på skärmen; tint-transformationen utgör hela renderingssemantiken, så en renderare som tolkar färgrymden men hoppar över funktionen har inte åstadkommit något användbart än
HPDFEvalTintTransform är funktionsmotorn bakom detta steg. Den introducerades i version 2.334.0 och utvärderar Typ 2-exponentiella funktioner (C0 + x^N * (C1 - C0) med begränsning av Domain och Range), Typ 3-sammanfogningsfunktioner (gränsvald subfunktionsrekursion med Encode-mappning) och Typ 0-samplade funktioner med 8-, 16- och 32-bitars samplar. Typ 0 är samma uppslagstabellsmekanism som vi täckte från skapandesidan i artikeln om att bygga Typ 0 färg-LUT:ar; renderingssidan går igenom samma struktur i omvänd ordning, från avkodade samplingsbyte tillbaka till komponentvärden
Typ 4 PostScript-kalkylatorfunktioner var den sista utposten. Fram till version 2.375.0 degraderades de till en neutral platshållare; sedan version 2.375.0 exekverar HPDFEvalPostScriptCalculator hela operatoruppsättningen enligt ISO 32000-1 §7.10.5 på en begränsad operandstack: aritmetik, jämförelse, booleska och bitvisa operatorer, stackmanipulering inklusive roll samt villkorssatserna if/ifelse. Villkorssatserna i gränsfallen är striktare än de verkar. PostScript round avrundar halva värden uppåt (mot det större värdet), så Delphis bankavrundning via Round kan inte användas; de trigonometriska operatorerna arbetar i grader, där atan returnerar värden i [0, 360), och exp är en potens med två operander, inte den naturliga exponentialfunktionen. Samma utvärderare driver också funktionsbaserade övertoningsfyllningar, vilket är anledningen till att rendering av axiell och radiell skuggning fick kalkylatordrivna färgramper i samma utgåva
// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
const Inputs: THPDFColorComps; InputCount: Integer;
out AltComps: THPDFColorComps): Boolean;
// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
const Inputs: THPDFColorComps; InputCount: Integer;
var Outputs: THPDFColorComps; OutCount: Integer): Boolean;
Ärlighet gällande precision: en mjukvarukalkylator som utvärderas med dubbelprecision kommer inte att matcha en RIP bit för bit, och begränsning vid Range-gränsen kan skilja sig med en minst betydande bit från en annan implementering. För skärmvisning och regressionstestning är detta irrelevant; om du bygger färgstyrt provtryck (proofing) är tint-transformationen bara det första steget och du behöver fortfarande en riktig CMM nedströms
CalGray, CalRGB, Lab och ICCBased utan en ICC-motor
De CIE-baserade familjerna tar den andra grenen av samma upplösningsmotor. HotPDF konverterar Lab-värden genom standardkedjan Lab till XYZ till sRGB, inklusive 6/29-brytpunktskuben i den omvända överföringsfunktionen, och hanterar CalRGB med dess gamma per kanal plus en linjär 3x3-matris samt CalGray med dess enkla gamma. Det prestandarelevanta detaljen är hanteringen av vitpunkt: konverteringsmatrisen för XYZ till sRGB är Bradford-anpassad till den vitpunkt som deklarerats i färgrymden och cachelagras i den upplösta THPDFColorSpace-posten, så att arbetet per pixel förblir en enkel 3x3-multiplikation oavsett hur exotic den deklarerade ljuskällan är
ICCBased-färgrymder får en medvetet pragmatisk behandling. PDF-specifikationen kräver att varje ICCBased-ström deklarerar en alternativ färgrymd (/Alternate) eller en underförstådd sådan via sitt komponentantal /N, just för att visare utan färgstyrmotor ändå ska kunna rendera på ett förnuftigt sätt. HotPDF löser ICCBased via det alternativet, eller gissar DeviceGray, DeviceRGB eller DeviceCMYK baserat på om /N är 1, 3 eller 4 när posten saknas, och tolkar aldrig profilens byte. Det innebär inget lcms-beroende och ingen profiluppslagningskostnad, på bekostnad av färgprecision: en ICCBased-rymd vars profil avviker kraftigt från sitt alternativ kommer att visa den alternativa renderingen. För skärmvisning och miniatyrbilder är detta samma kompromiss som alla lätta visare gör, och det är en begränsning som bör framgå tydligt i din egen dokumentation
Buggen som gjorde att varje namngiven färgrymdsuppslagning misslyckades
Operatorkopplingen i version 2.333.0 levererades med en defekt som förblev osynlig under fyrtiotvå utgåvor: hanterarna för cs och CS slog upp sin operand med dess inledande snedstreck intakt (/CS0) mot resursordlistans nycklar som lagrades utan snedstrecket (CS0). Varje namngiven färgrymdsuppslagning misslyckades, hundra procent av gångerna, och koden föll tyst tillbaka på standardvärdet DeviceGray. Det synliga symptomet var subtilt på värsta tänkbara sätt: en Separationsfyllning på 1 scn blev DeviceGray 1.0, vilket ritar vitt, och vitt bläck på en vit sida är inte ett renderingsfel som någon tar skärmdumpar av. Lösningen, i version 2.375.0, var en delad namnnormaliseringshjälpare som tillämpades vid varje uppslagning av operand till resurs
Två relaterade defekter upptäcktes under samma undersökning. För det första returnerades indirekta referenser till array-värda objekt oupplösta: renderarens dokumentåtkomst hade endast typade resolvers for strömmar och ordlistor, så /CS0 5 0 R som pekar på en fristående [/Separation ...]-array kom tillbaka som den råa referensen och färgrymden tolkades som unsupported. För det andra upprätthöll HPDFReadNumericArray strikt längdsemantik, vilket krävde att PDF-arrayen var minst lika lång som den tillhandahållna bufferten. Att läsa in en Typ 2-funktions /C0 och /C1 i en buffert med fyra element misslyckades därför för alternativ med en och tre komponenter, vilket lämnade båda arrayerna nollställda. Varje icke-CMYK-exponentiell färgton renderades som svart tills version 2.376.0 introducerade den mer toleranta läsaren HPDFReadNumericArrayUpTo. Lärdomen att ta med sig: varje PDF-nyckel som dokumenteras innehålla en numerisk array med rörlig längd måste läsas med en läsare som fyller det som finns, eftersom en buffert med fast storlek och strikt matchning förvandlar giltiga filer till tysta nollor
Hur testar du dekorfärgsrendering utan att lura dig själv?
Den obekväma frågan är varför testsviten förblev grön under allt detta. Den ursprungliga regressionstestningen, SeparationRendersDistinguishable, försäkrade endast att den renderade bitmappen inte var helt svart. En pipeline som lät varje dekorfärg falla tillbaka till DeviceGray producerade gråa och vita utdata, vilket inte är svart, så påståendet godkändes trots att hela funktionen var död. Svaga påståenden av typen "inte tom", "inte helt svart" eller "kontrollsumman är inte noll" kan inte skilja en fungerande renderare från en trasig, eftersom nästan alla felscenarier ändå producerar några pixlar
Det påstående som faktiskt fångar dessa fel låser den förväntade färgen: rendera en handbyggd minimal PDF vars Separationsbläck löses upp till en känd nyans, och räkna sedan pixlar som är dominerande i den nyansen. Rendering till en bitmapp för granskning använder samma RenderLoadedPageToBitmap-ingångspunkt som beskrivs i guiden för rendering från sida till bitmapp
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;
// Lock the expected ink: demand a real area of red-dominant
// pixels, never settle for "not all black".
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Fixturen (teststrukturen) är lika viktig som påståendet. En handbyggd PDF på några hundra byte, med en enda Separationsfyllning och inget annat, lämnar inget utrymme för tvetydighet om vad de förväntade utdata är; en fil från verkliga livet testar mer kod men kan inte tala om vilket steg som misslyckades. Vi behandlar nu pixelräkning av förväntad färg som minimikravet för alla röktest (smoke tests) gällande rendering, eftersom det är den enda formen av påstående som tvingade ut dessa fel i ljuset
Rendering av dekorfärg och CIE-färgrymder är en del av pipelinen för inlästa dokument i HotPDF Component för Delphi och C++Builder, tillsammans med funktionsmotorn, skuggningsrendering samt de bitmap-exportvägar som visas ovan