Technical Article

3D Color LUT:er i PDF med Type 0 Sampled Functions

PDF-funktioner är ett av de mer undangömda hörnen av specifikationen. De flesta utvecklare stöter på dem en gång, som den sak en Type 2 axial shading behöver för att tona mellan två färger, och tittar aldrig mer på dem. Det är synd, eftersom funktionsmaskineriet är en liten allmän beräknare som formatet återanvänder för skuggningar (shadings), överföringsfunktioner (transfer functions), halvtonspunktfunktioner, separationsnyanser och överföringskurvor för mjuka masker. Av de fyra funktionstyperna är Type 0 den mest kraftfulla och den minst förstådda. Det är en samplad funktion (sampled function): ett flerdimensionellt rutnät av utdatavärden som läsaren interpolerar mellan. Eftersom rutnätet kan innehålla vilka tal du än stoppar i det, kan en Type 0-funktion uttrycka en godtycklig icke-linjär mappning, vilket är den exakta formen för en färguppslagstabell (color lookup table).

Denna artikel går igenom Type 0-ordboken så som ISO 32000-1 definierar den i §7.10.2, och visar sedan de två fall som är mest betydelsefulla i ett dokumentflöde: en RGB-till-RGB-färgkorrigerings-LUT med tre indata, och en dekorfärgtonskonvertering (spot-color tint transform) med en indata. Samma byggare av samplade funktioner tjänar båda, och skillnaden mellan dem är helt en fråga om hur många indata rutnätet har.

En samplad funktion är ett rutnät som läsaren interpolerar

En Type 0-funktion mappar en vektor med m indata till en vektor med n utdata genom att lagra samplade värden på ett regelbundet rutnät och interpolera mellan dem. ISO 32000-1 §7.10.2 listar de nycklar som beskriver det rutnätet. /Domain innehåller två tal per indata, den nedre och övre gränsen för varje indataaxel. /Range innehåller två tal per utdatakomponent. /Size är en array med m heltal som anger antalet samplade punkter längs varje indataaxel, så ett rutnät som är tolv samplade punkter på en sida i tre dimensioner har /Size [12 12 12] och lagrar 1 728 rutnätspunkter. /BitsPerSample sätter precisionen för varje lagrat värde; HotPDF accepterar 1, 2, 4, 8, 12, 16, 24 och 32 bitar, vilket matchar de värden som tabell 38 tillåter.

Strömmen av samplade värden läses i en fast ordning. Den första indatadimensionen varierar snabbast, sedan den andra, och så vidare, och vid varje rutnätspunkt lagras de n utdatakomponenterna i ordning. För en RGB-till-RGB-tabell är det tre byte per rutnätspunkt vid åtta bitar, upplagda som röd utdata, grön utdata, blå utdata, med svep över den röda indatan först. Ytterligare två nycklar mappar den kontinuerliga världen på heltalsrutnätet. /Encode mappar varje indata från dess /Domain-intervall till samplingsindexintervallet 0 till Size[i] - 1, och /Decode mappar de råa lagrade heltalen tillbaka till /Range-intervallen. När du lämnar dem på sina standardvärden landar en indata som spänner över [0 1] landar rent på hela rutnätet och en lagrad byte på 255 avkodas till toppen av dess utdataområde, vilket är vad en [0,1]-normaliserad färg-LUT vill ha.

Order 1 mot Order 3

Mellan rutnätspunkterna måste läsaren interpolera, och /Order chooses how. /Order 1 är multilinjär interpolation: linjär längs en axel, bilinjär över två, trilinjär över tre. Det är snabbt, det är exakt vad hårdvaran i de flesta visningsprogram gör, och för en mjuk färgtransformation är det vanligtvis omöjligt att skilja från något mer avancerat. /Order 3 begär kubisk spline-interpolation, vilket anpassar en mjukare kurva genom samplade värden till kostnaden av mer arbete och ett bredare stödområde kring varje utvärderad punkt.

Avvägningen är tätheten i rutnätet mot kurvans mjukhet. Kubisk ordning gör nytta när rutnätet är grovt och mappningen har en synlig krökning, eftersom en rät linje mellan två avlägsna samplingspunkter kan platta till en tonkurva på ett sätt som ögat upptäcker på gradienter. När rutnätet väl är tätt är segmenten korta nog för att linjär interpolation ska följa kurvan nära, och kubisk tillför lite. En praktisk regel är att sträcka sig efter /Order 3 endast vid små rutnät eller branta transformationer, och annars lämna den på det linjära standardvärdet. Notera att /Order endast gäller för Type 0-funktioner, och HotPDF avvisar alla andra värden än 1 eller 3.

3D LUT: tre indata, tre utdata

En RGB-till-RGB-färgkorrigering är skolboksexemplet för ett rutnät med tre indata, den klassiska 3D-LUT:en som används vid färggradering och enhetsmatchning. Varje axel i kuben är en indatakanal, varje rutnätspunkt lagrar den korrigerade RGB-trippeln för den indatakoordinaten, och läsaren interpolerar trilinjärt de hörnprover som omger varje inkommande färg. Tre indata är oundvikliga här eftersom den korrigerade röda färgen kan bero på indatan för grönt och blått, inte bara på indatan för rött; en kurva per kanal kan inte uttrycka överhörning (crosstalk) mellan kanaler, men en kub kan.

HotPDF bygger Type 0-strömmen genom RegisterSampledFunction, som tar /Domain, /Range, /Size, /BitsPerSample och samplingsbyten direkt samt returnerar funktionsobjektet. För en standardiserad normaliserad kub skickar du [0,1]-gränser på alla tre indataaxlar och alla tre utdata, en N x N x N-storlek och den platta samplingstabellen. Byggaren validerar att byteantalet matchar rutnätet: för ett byte-justerat djup förväntar den sig OutputCount x (BitsPerSample div 8) x produkten av storlekarna, och kastar ett undantag om arrayen har fel längd, så att en felberäknad datasteglängd (stride) misslyckas tydligt vid registreringen snarare än att renderas som skräp senare.

const
  N = 17;  // 17 x 17 x 17 cube, the common ICC LUT resolution
var
  LutFn: THPDFStreamObject;
  Samples: TBytes;
begin
  // Fill Samples with N*N*N grid points, 3 bytes each (R,G,B output),
  // red input varying fastest. Build the corrected triple for each
  // grid coordinate with your ICC-managed conversion, then store it.
  SetLength(Samples, N * N * N * 3);
  BuildCorrectedCube(Samples, N);   // your color-managed fill

  LutFn := Pdf.RegisterSampledFunction(
    [0,1, 0,1, 0,1],
    [0,1, 0,1, 0,1],
    [N, N, N],
    8,
    Samples,
    1);
end;

Kubens kolorimetriska korrekthet ligger i hur du fyller den, inte i PDF-funktionen. Den korrekta vägen är att beräkna varje rutnätspunkt via en ICC-hanterad konvertering, samma motor som driver en skärmkorrektur (soft-proof), så att siffrorna i rutnätet betyder något mot en definierad käll- och målprofil. Registrera profilerna som begränsar konverteringen med RegisterICCProfile, vilket sparar ett ICCBased färgrymd (1, 3 eller 4 komponenter) och returnerar ett resursnamn som du kan koppla till det innehåll som LUT:en matar. Type 0-funktionen bär interpolationstabellen; ICC-profilen bär betydelsen av ändpunkterna.

1D-fallet: en dekorfärgtonskonvertering

Separationsfärgrymder lutar sig mot samma maskineri för ett helt annat jobb. En separationsfärgrymd (Separation space), definierad i ISO 32000-1 §8.6.6.4, representerar ett enskilt färgämne, en dekorfärg (spot ink) som en Pantone eller en lack, genom att para ihop ett namn med en tonskonvertering (tint transform): en funktion som mappar det endimensionella tonvärdet, 0 för ingen färg till 1 för full färg, till en alternativ färgrymd som enheten faktiskt kan rendera, vanligtvis CMYK. Den tonskonverteringen är ofta en Type 0-funktion, och nu har rutnätet exakt en indataaxel.

Detta är den tydliga kontrasten mot en 3D-LUT. En dekorfärg har en frihetsgrad, så dess tonskonvertering behöver en indata och rutnätet är en linje av samplingspunkter, där var och en innehåller CMYK-värdet (eller annat alternativt värde) vid den tonnivån. RGB-kuben behöver tre indata eftersom dess domän är tredimensionell och kanalerna samverkar. Samma funktionstyp, samma interpolationsregler, olika dimensionalitet; specifikationen återanvänder en beräknare och låter /Size avgöra om du vandrar längs en linje, ett plan eller en kub. HotPDF slår in hela separationen i RegisterSeparationLUT, som bygger den Type 0-tonskonvertering med en indata från en platt byte-array internt och returnerar resursnamnet för färgrymden.

var
  SpotCS: AnsiString;
begin
  // Four CMYK output bytes per tint grid point, tint domain [0..1].
  // Here 0% ink -> all zero, 100% ink -> a rich spot build,
  // with two interior steps; the tint transform interpolates between.
  SpotCS := Pdf.RegisterSeparationLUT(
    'PANTONE 286 C',
    'DeviceCMYK',
    [  0,   0,   0,   0,
      90,  60,   0,   0,
     100,  80,   0,  10,
     100,  72,   0,  18]);
  // Use SpotCS with SetFillColorSpace / SetFillColor on a page.
end;

Antalet samplade punkter måste vara ett helt antal rutnätspunkter: en positiv multipel av den alternativa färgrymdens komponentantal, och minst två punkter så att det finns ett segment att interpolera. Skickar du tre byte per punkt mot ett CMYK-alternativ avvisar anropet det, samma defensiva validering som 3D-byggaren tillämpar, vilket är vad du vill ha av en funktion som annars skulle misslyckas tyst vid utskrift.

Där samma maskineri dyker upp igen

När du väl ser Type 0 som en generisk interpolationstabell, slutar ytterligare två enhetskontrollfunktioner att se ut som specialfall. En överföringsfunktion (transfer function) justerar komponentvärden på deras väg till utmatningsenheten, och det är bara en funktion per kanal; HotPDF registrerar den som en ExtGState via RegisterTransferFunctionState, vilket accepterar antingen en kombinerad funktion eller en array av funktioner per kanal. Eftersom de funktionerna är vanliga funktionsobjekt kan du ge den exakt det THPDFStreamObject som RegisterSampledFunction returnerar och driva en överföringskurva från en samplad tabell snarare än en formel.

var
  ToneFn: THPDFStreamObject;
  GsName: AnsiString;
begin
  // A single-input, single-output sampled tone curve on [0,1].
  ToneFn := Pdf.RegisterSampledFunction(
    [0,1], [0,1], [256], 8, ToneCurveBytes, 1);

  // Apply it to all channels as a combined /TR2 transfer function.
  GsName := Pdf.RegisterTransferFunctionState(ToneFn, []);
  // Select GsName on the page before drawing the affected content.
end;

Svartgenerering (black generation) och underfärgsborttagning (undercolor removal) ligger i samma familj. När en enhet konverterar RGB till CMYK bestämmer den hur mycket av gråkomponenten som ska bäras som svart bläck, och specifikationen uttrycker det beslutet som en funktion, posterna /BG2 och /UCR2 i en grafiklägesordbok (graphics-state dictionary), vardera en kurva med en indata från den beräknade gråheten till en svartmängd. De är också Type 0-funktioner när du vill ha en uppmätt kurva snarare än en analytisk, byggda på samma sätt via RegisterSampledFunction och placerade i grafikläget. Lärdomen värd att minnas är att PDF-funktionen aldrig är där färghanteringen sker; den är uppslagstabellen som bär ett beslut du fattat med en riktig färgmotor, och Type 0 är den enda funktionstyp som är flexibel nog att bära vilket beslut som helst.

För den bredare bilden av hur typsnitt, bilder och färgresurser skickas ut i ett färdigt dokument, se vår genomgång av rapportutdata med typsnitt och bilder. När utdatan måste överleva en preflight-kontroll för arkivering eller utskrift styr reglerna för färgrymd och avsedd utmatning (output intent), vilka täcks i valideringsguiden för PDF/A, PDF/X och PDF/UA, vilka av dessa funktioner som är tillåtna och hur enhetsfärg måste taggas. Allt levereras i HotPDF Component för Delphi och C++Builder, tillsammans med API:erna för skuggning, ICC och separation som bygger på samma Type 0-kärna.