Teknisk artikel

Rendering af separationsfarver og DeviceN spotfarver i Delphi

HotPDF renderer separationsfarver og DeviceN spotfarver på indlæste PDF-sider ved at opløse farveområdet via HPDFResolveColorSpace, evaluere farvetonetransformationsfunktionen med HPDFEvalTintTransform og konvertere resultatet via det alternative område til RGB for skærmen. Siden v2.375.0 har denne pipeline evalueret alle fire PDF-funktionstyper, inklusive Type 4 PostScript-beregnere, så en trykklar fil med Pantone-blæk viser sine reelle farver i stedet for en pladsholder. Denne artikel gennemgår, hvordan pipelinen fungerer, og lige så nyttigt, hvordan den fejlede, ment vi byggede den

Klagen ankommer på samme måde hver gang: Dokumentet ser perfekt ud på den maskine, der producerede det, og derefter åbner en kunde det på en ren server eller et låst skrivebord, og den japanske faktura viser tofu-firkanter, eller en substitueret lignende skrifttype forskyder hvert linjeskift. Skrifttyperne var aldrig på den maskine — kun inde i PDF'en — og en renderer, der stopper ved system-skrifttypesubstitution, kan ikke se dem. Delmængde-skrifttyper gør det værre: En delmængde kan bære fyrre glyfer under tegnkoder, der er tildelt privat til netop den ene fil, en tildeling som ingen installeret skrifttype deler

Hvorfor renderer spotfarver sort i en PDF-fremviser?

Spotfarver renderer sort, eller forsvinder, når rendereren kun implementerer enhedsfarveoperatorerne (rg, g, k) og ignorerer de generiske. ISO 32000-1 §8.6 definerer tre grupper af farveområder: Enhedsområder (DeviceGray, DeviceRGB, DeviceCMYK), CIE-baserede områder (CalGray, CalRGB, Lab, ICCBased) og specielle områder (Indexed, Separation, DeviceN, Pattern). Alt uden for enhedsgruppen vælges med de generiske operatorer: cs og CS vælger et område efter navn fra sidens ressourceordbog, og derefter leverer sc, SC, scn og SCN komponentværdierne. En renderer, der springer disse operatorer over, beholder den farve, der sidst blev indstillet, hvilket for en side, der starter med en overskrift i spotfarve, er den oprindelige DeviceGray sort

HotPDF tilføjede det generiske operatorsæt til sin siderenderer i v2.333.0, sammen med en samlet opløsningssti: Hver /ColorSpace-ressourcepost, uanset om det er et råt navn, et inline-array eller en indirekte reference, analyseres til én THPDFColorSpace-post, og enhver anmodning om udfyldnings- eller stregfarve kanaliseres gennem et enkelt HPDFResolveColor-kald. Familieopregningen viser dækningen på et øjeblik

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;

Én designbeslutning betaler sig selv mange gange: csfUnsupported er en førsteklasses familie, ikke en fejl. Et område, som rendereren ikke kan fortolke, degraderes to et defineret fallback i stedet for at afbryde siden, hvilket matcher, hvordan gængse fremvisere opfører sig, og forhindrer, at en enkelt eksotisk udfyldning tømmer et ellers renderbart dokument

Hvordan forvandler en farvetonetransformation en enkelt blækværdi til en reel farve?

Et separationsområde bærer tre stykker information: Blæknavnet, et alternativt farveområde og en farvetonetransformationsfunktion. Arrayet [/Separation /PANTONE485 /DeviceCMYK f] siger: Når indholdsstrømmen skriver 0.8 scn, skal farvetoneværdien 0,8 fødes ind i funktionen f og male den resulterende CMYK-firedobbelte værdi. DeviceN generaliserer dette til N blæktyper med en funktion med N-input. Selve blæknavnet er kun vejledende på skærmen; farvetonetransformationen er hele renderingssemantikken, så en renderer, der analyserer området, men springer funktionen over, har endnu ikke gjort noget nyttigt

HPDFEvalTintTransform er funktionsmotoren bag dette trin. Den blev introduceret i v2.334.0 og evaluerer Type 2 eksponentielle funktioner (C0 + x^N * (C1 - C0) med Domain- og Range-fastspænding), Type 3 sammensyede funktioner (Bounds-valgt underfunktionsrekursion med Encode-genkortlægning) og Type 0 samplede funktioner med 8-, 16- og 32-bit samples. Type 0 er det samme opslagstabel-maskineri, som vi dækkede fra oprettelsessiden i artiklen om opbygning af Type 0 farve-LUTs; renderingssiden gennemgår den identiske struktur i omvendt rækkefølge, fra afkodede samplebytes tilbage til komponentværdier

Type 4 PostScript-beregningsfunktioner var den sidste bastion. Indtil v2.375.0 blev de degraderet yndefuldt til en neutral pladsholder; siden v2.375.0 har HPDFEvalPostScriptCalculator udført det fulde operatorsæt fra ISO 32000-1 §7.10.5 på en afgrænset operandstak: Aritmetik, sammenligning, boolske og bitvise operatorer, stakmanipulation inklusive roll og if/ifelse-betingelser. Grænsesemantikken er strengere, end den ser ud til. PostScript round runder halvvejs mod den største værdi, så Delphis bankmandsafrunding Round kan ikke bruges; de trigonometriske operatorer arbejder i grader, hvor atan returnerer værdier i [0, 360); og exp er en potens med to operander, ikke den naturlige eksponentiel. Den samme evaluator driver også funktionsbaserede gradientudfyldninger, hvilket er grunden til, at aksial og radial skyggerendering fik beregnerdrevne ramper i samme udgivelse

// 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;

Ærlighed om præcision: En softwareberegner evalueret i dobbelt præcision vil ikke matche en RIP bit for bit, og fastspænding ved Range-grænsen kan afvige med en mindst betydende bit fra en anden implementering. For skærmvisning og regressionsrendering er det irrelevant; hvis du bygger farvestyret korrektur, er farvetonetransformationen kun det første trin, og du har stadig brug for en ægte CMM nedstrøms

CalGray, CalRGB, Lab og ICCBased uden en ICC-motor

De CIE-baserede familier tager den anden gren af den samme opløser. HotPDF konverterer Lab-værdier via standardkæden fra Lab till XYZ til sRGB, inklusive 6/29-knækpunktsterningen i den omvendte overførselsfunktion, og håndterer CalRGB med dens per-kanal-gamma plus en lineær 3x3-matrix samt CalGray med dens enkelte gamma. Den ydeevnerelevante detalje er håndtering af hvidpunkt: Konverteringsmatrixen fra XYZ til sRGB er Bradford-tilpasset til det hvidpunkt, der er erklæret i farveområdet, og cachet i den opløste THPDFColorSpace-post, så arbejde pr. pixel forbliver en enkelt 3x3-multiplikation, uanset hvor eksotisk den erklærede lyskilde er

ICC-baserede områder får en bevidst pragmatisk behandling. PDF-specifikationen kræver, at enhver ICCBased-strøm erklærer et /Alternate-område eller et underforstået område via sit komponentantal /N, netop for at fremvisere uden en farvestyringsmotor stadig kan rendere fornuftigt. HotPDF opløser ICCBased via dette alternativ, eller gætter DeviceGray, DeviceRGB eller DeviceCMYK ud fra, om /N er 1, 3 eller 4, når posten mangler, og analyserer aldrig profilbytes. Det betyder ingen lcms-afhængighed og ingen profilopslagskostnad, på bekostning af kolorimetrisk nøjagtighed: Et ICCBased-område, hvis profil afviger stærkt fra sit alternativ, vil vise den alternative rendering. For skærmvisning og miniaturebilleder er dette det samme kompromis, som alle lette fremvisere indgår, og det er den grænse, du skal angive tydeligt i din egen dokumentation

Fejlen, der fik alle navngivne farveområdeopslag til at fejle

Håndteringene af cs og CS slog deres operand op med dens foranstillede skråstreg intakt (/CS0) mod ressourceordbogs-nøgler gemt uden skråstreg (CS0). Hvert navngivent opslag af farveområde kiksede, hundrede procent af tiden, og koden faldt lydløst tilbage til standarden DeviceGray. Det synlige symptom var subtilt på den værste måde: En separationsudfyldning af 1 scn blev til DeviceGray 1.0, som maler hvidt, og hvidt blæk på en hvid side er ikke en renderingsfejl, som nogen tager et screenshot af. Løsningen, i v2.375.0, var en fælles navnenormaliserings-hjælper, der anvendes ved hvert opslag fra operand til ressource

To relaterede fejl faldt ud af den samme undersøgelse. For det første blev indirekte referencer til objekter med array-værdi returneret uoplyste: Renderens dokumentadgang havde kun typede opløsere for strømme og ordbøger, så /CS0 5 0 R, der pegede på et fritstående [/Separation ...]-array, kom tilbage som den rå reference, og området blev fortolket som ikke understøttet. For det andet havde HPDFReadNumericArray en streng længdesemantik, der krævede, at PDF-arrayet var mindst lige så langt som den medfølgende buffer. Indlæsning af en Type 2-funktions /C0 og /C1 i en fire-elementers buffer mislykkedes derfor for en- og tre-komponentalternativer, hvilket efterlod begge arrayer nulstillet, og enhver ikke-CMYK eksponentiel farvetone renderede sort, indtil v2.376.0 introducerede den mere overbærende HPDFReadNumericArrayUpTo-læser. Læren, der kan overføres: Enhver PDF-nøgle, der er dokumenteret som indeholdende et numerisk array af variabel længde, skal læses med en læser, der fylder det, der findes, fordi en buffer med fast størrelse og streng matching forvandler gyldige filer til tavse nuller

Hvordan tester du spotfarverendering uden at snyde dig selv?

Det ubehagelige spørgsmål er, hvorfor testsuiten forblev grøn gennem alt dette. Den oprindelige regression, SeparationRendersDistinguishable, hævdede kun, at det renderede bitmap ikke var helt sort. En pipeline, der lod enhver spotfarve falde til DeviceGray, producerede grå og hvid output, som ikke er sort, så påstanden bestod, mens hele funktionen var død. Svage påstande af formen "ikke tom", "ikke helt sort" eller "digest er forskellig fra nul" kan ikke skelne en fungerende renderer fra en ødelagt, fordi næsten enhver fejltilstand stadig producerer nogle pixels

Den påstands-stil, der fanger disse fejl, fastlåser den forventede farve: Render en håndbygget minimal PDF, hvis separationsblæk opløses til en kendt nuance, og tæl derefter pixels, der er dominerende i den nuance. Rendering til et bitmap til inspektion bruger det samme RenderLoadedPageToBitmap-indgangspunkt, som er beskrevet i vejledningen til rendering fra side til bitmap

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;

Testfixturen betyder lige så meget som påstanden. En håndbygget PDF på nogle få hundrede bytes med én separationsudfyldning og intet andet efterlader ingen tvivl om, hvad det forventede output er; en fil fra den virkelige verden aktiverer mere kode, men kan ikke fortælle dig, hvilket trin der fejlede. Vi behandler nu tælling af pixels med forventet farve som minimumskravet for enhver renderings-smoketest, fordi det er den eneste påstands-stil, der tvang disse fejl frem i lyset

Rendering af spotfarver og CIE-farveområder er en del af loaded-document-pipelinen i HotPDF Component til Delphi og C++Builder, sammen med funktionsmotoren, skyggerenderingen og bitmap-eksportstierne vist ovenfor