Technical Article

3D-kleuren-LUT's in PDF met Type 0 gesamplede functies

PDF-functies (PDF Functions) behoren tot de stillere uithoeken van de specificatie. De meeste ontwikkelaars komen ze één keer tegen, als datgene wat een Type 2 axiale arcering nodig heeft om tussen twee kleuren over te vloeien, en kijken er daarna nooit meer naar. Dat is jammer, want het functiemechanisme is een kleine evaluator voor algemeen gebruik die het formaat hergebruikt voor arceringen, overdrachtfuncties, halftoonsteunfuncties, scheidingstinten en overdrachtcurven voor zachte maskers (soft-masks). Van de vier functietypen is Type 0 de krachtigste en de minst begrepene. Het is een gesamplede functie: een meerdimensionaal grid van uitvoerwaarden waartussen de lezer interpoleert. Omdat het grid alle getallen kan bevatten die u erin stopt, kan een Type 0-functie een willekeurige niet-lineaire mapping uitdrukken, wat exact de vorm is van een kleurenzoektabel (color lookup table).

Dit artikel bespreekt de Type 0-dictionary zoals ISO 32000-1 deze definieert in §7.10.2, en laat vervolgens de twee situaties zien die het meest belangrijk zijn in een documenttraject: een RGB-naar-RGB-kleurcorrectie-LUT met drie ingangen, en een steunkleur-tinttransformatie met één ingang. Dezelfde builder voor gesamplede functies dient voor beide, en het verschil ertussen is louter een kwestie van hoeveel ingangen het grid heeft.

Een gesamplede functie is een grid dat de lezer interpoleert

Een Type 0-functie mapt een vector met m ingangen naar een vector met n uitgangen door samples op te slaan op een regelmatig grid en daartussen te interpoleren. ISO 32000-1 §7.10.2 somt de sleutels op die dat grid beschrijven. /Domain bevat twee getallen per ingang, de onder- en bovengrens van elke ingangsas. /Range bevat twee getallen per uitgangscomponent. /Size is een array van m integers die het aantal samples langs elke ingangsas aangeeft, dus een grid met twaalf samples per zijde in drie dimensies heeft /Size [12 12 12] en slaat 1.728 gridpunten op. /BitsPerSample stelt de precisie van elke opgeslagen waarde in; HotPDF accepteert 1, 2, 4, 8, 12, 16, 24 en 32 bits, wat overeenkomt met de waarden die tabel 38 toestaat.

De samplestroom wordt in een vaste volgorde gelezen. De eerste ingangsdimensie varieert het snelst, daarna de tweede, enzovoort, en bij elk gridpunt worden de n uitgangscomponenten op volgorde opgeslagen. Voor een RGB-naar-RGB-tabel zijn dat drie bytes per gridpunt bij acht bits, gerangschikt als rood-uitgang, groen-uitgang, blauw-uitgang, waarbij eerst de rode ingang wordt doorlopen. Nog twee sleutels mappen de continue wereld op het integer-grid. /Encode mapt elke ingang van zijn /Domain-interval naar het sample-indexbereik 0 tot Size[i] - 1, en /Decode mapt de ruwe opgeslagen integers terug naar de /Range-intervallen. Wanneer u deze op hun standaardwaarden laat staan, een ingang met het bereik [0 1] landt netjes op het volledige grid en decodeert een opgeslagen byte van 255 naar de bovenkant van het uitgangsbereik, wat is wat een [0,1]-genormaliseerde kleuren-LUT vereist.

Order 1 versus Order 3

Tussen gridpunten moet de lezer interpoleren, en /Order bepaalt hoe. /Order 1 is multilineaire interpolatie: lineair langs één as, bilineair over twee, trilineair over drie. Dit is snel, het is precies wat de hardware in de meeste viewers doet, en voor een vloeiende kleurtransformatie is het meestal niet te onderscheiden van iets complexers. /Order 3 vraagt om kubische spline-interpolatie, wat een vloeiendere curve door de samples trekt ten koste van meer rekenkracht en een groter ondersteuningsgebied rond elk geëvalueerd punt.

De afweging is griddichtheid versus curvevloeiendheid. De kubische volgorde bewijst zijn nut wanneer het grid grof is en de mapping een zichtbare kromming heeft, omdat een rechte lijn tussen twee verafgelegen samples een tooncurve kan afvlakken op een manier die het oog opmerkt bij verlopen. Zodra het grid dicht is, zijn de segmenten kort genoeg zodat lineaire interpolatie de curve nauwgezet volgt en kubische interpolatie weinig toevoegt. Een praktische regel is om alleen bij kleine grids of steile transformaties naar /Order 3 te grijpen, en het anders op de lineaire standaardwaarde te laten staan. Merk op dat /Order alleen van toepassing is op Type 0-functies, en HotPDF elke andere waarde dan 1 of 3 weigert.

De 3D-LUT: drie ingangen, drie uitgangen

Een RGB-naar-RGB-kleurcorrectie is het schoolvoorbeeld van een grid met drie ingangen, de klassieke 3D-LUT die wordt gebruikt bij kleurgradatie en apparaatafstemming. Elke as van de kubus is één ingangskanaal, elk gridpunt slaat de gecorrigeerde RGB-triplet op voor die ingangscoördinaat, en de lezer interpoleert trilineair de hoeksamples rond elke binnenkomende kleur. Drie ingangen zijn hier onvermijdelijk omdat het gecorrigeerde rood afhankelijk kan zijn van het ingangsgroen en -blauw, en niet alleen van het ingangsrood; een curve per kanaal kan geen kanaaloverspraak uitdrukken, maar een kubus kan dat wel.

HotPDF bouwt de Type 0-stroom via RegisterSampledFunction, die de /Domain, /Range, /Size, /BitsPerSample en sample-bytes rechtstreeks accepteert en het functie-object retourneert. Voor een standaard genormaliseerde kubus geeft u [0,1]-grenzen op voor alle drie de ingangsassen en alle drie de uitgangen, een N x N x N-grootte, en de platgeslagen sampletabel. De builder valideert of het aantal bytes overeenkomt met het grid: voor een byte-uitgelijnde diepte verwacht deze OutputCount x (BitsPerSample div 8) x the product of the sizes, and raises if the array is the wrong length, so a miscomputed stride fails loudly at registration rather than rendering as garbage later.

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],   // /Domain: three input axes on [0,1]
    [0,1, 0,1, 0,1],   // /Range:  three output channels on [0,1]
    [N, N, N],         // /Size:   the cube resolution per axis
    8,                 // /BitsPerSample
    Samples,
    1);                // /Order 1 = trilinear
end;

De colorimetrische juistheid van de kubus ligt in hoe u deze vult, niet in de PDF-functie. De juiste manier is om elk gridpunt te berekenen via een door ICC beheerde conversie, dezelfde engine die een soft-proof aanstuurt, zodat de getallen in het grid betekenis hebben ten opzichte van een gedefinieerd bron- en doelprofiel. Registreer de profielen die de conversie begrenzen met RegisterICCProfile, wat een op ICC gebaseerde kleurruimte (1, 3 of 4 componenten) registreert en een resourcenaam retourneert die u kunt koppelen aan de inhoud die de LUT voedt. De Type 0-functie bevat de interpolatietabel; het ICC-profiel bevat de betekenis van de eindpunten.

Het 1D-geval: een steunkleur-tinttransformatie

Separation-kleurruimten leunen op hetzelfde mechanisme voor een heel andere taak. Een Separation-ruimte, gedefinieerd in ISO 32000-1 §8.6.6.4, vertegenwoordigt een enkele kleurstof, een steunkleur (spot ink) zoals een Pantone of een vernis, door een naam te koppelen aan een tinttransformatie: een functie die de eendimensionale tintwaarde (0 voor geen inkt tot 1 voor volledige inkt) mapt op een alternatieve kleurruimte die het apparaat daadwerveel kan renderen, meestal CMYK. Dat tinttransformatie is vaak een Type 0-functie, en nu heeft het grid precies één ingangsas.

Dit is het duidelijke contrast met de 3D-LUT. Een steunkleur is één vrijheidsgraad, dus de bijbehorende tinttransformatie heeft één ingang nodig en het grid is een lijn van samples, die elk de CMYK-waarde (of andere alternatieve waarde) op dat tintniveau bevatten. De RGB-kubus heeft drie ingangen nodig omdat zijn domein driedimensionaal is en de kanalen elkaar beïnvloeden. Zelfde functietype, zelfde interpolatieregels, andere dimensionaliteit; de specificatie hergebruikt één evaluator en laat /Size bepalen of u een lijn, een vlak of een kubus doorloopt. HotPDF verpakt de hele scheiding in RegisterSeparationLUT, wat de Type 0-tinttransformatie met één ingang intern opbouwt uit een platte byte-array en de naam van de kleurruimtebron retourneert.

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',         // colorant name
    'DeviceCMYK',            // alternate color space
    [  0,   0,   0,   0,     // tint 0.00 -> 0,0,0,0
      90,  60,   0,   0,     // tint 0.33
     100,  80,   0,  10,     // tint 0.66
     100,  72,   0,  18]);   // tint 1.00 -> full ink build
  // Use SpotCS with SetFillColorSpace / SetFillColor on a page.
end;

Het aantal samples moet een heel aantal gridpunten zijn: een positief veelvoud van het aantal componenten van de alternatieve ruimte, en ten minste twee punten zodat er een segment is om te interpoleren. Als u drie bytes per punt doorgeeft ten opzichte van een CMYK-alternatief, weigert de aanroep dit, deelfde defensieve validatie die de 3D-builder toepast, en dat is precies wat u wilt van een functie die anders tijdens het afdrukken geruisloos zou mislukken.

Waar ditzelfde mechanisme opnieuw opduikt

Zodat u Type 0 beschouwt als een generieke interpolatietabel, twee andere apparaatbesturingsfuncties zien er niet langer uit als speciale gevallen. Een overdrachtfunctie (transfer function) past componentwaarden aan op hun weg naar het uitvoerapparaat, en is gewoon een functie per kanaal; HotPDF registreert dit als een ExtGState via RegisterTransferFunctionState, wat ofwel één gecombineerde functie of een array van functies per kanaal accepteert. Omdat die functies gewone functie-objecten zijn, kunt u er de THPDFStreamObject aan doorgeven die RegisterSampledFunction retourneert en zo een overdrachtcurve aansturen vanuit een gesamplede tabel in plaats van een formule.

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;

Zwartgeneratie (black generation) en UCR (undercolor removal) vallen binnen dezelfde familie. Wanneer een apparaat RGB naar CMYK converteert, bepaalt het hoeveel van de grijze component als zwarte inkt moet worden overgedragen. De specificatie drukt die beslissing uit als een functie: de /BG2- en /UCR2-vermeldingen van een graphics-state-dictionary, elk een curve met één ingang van het berekende grijs naar een hoeveelheid zwart. Dat zijn ook Type 0-functies wanneer u een gemeten curve wilt in plaats van een analytische, op dezelfde manier gebouwd via RegisterSampledFunction en in de grafische status geplaatst. De belangrijkste les is dat de PDF-functie nooit de plek is waar kleurbeheer plaatsvindt; het is de lookup-tabel die een beslissing bevat die u met een echte kleurengine hebt genomen, en Type 0 is het enige functietype dat flexibel genoeg is om elke beslissing te bevatten.

Voor het bredere plaatje van hoe lettertypen, afbeeldingen en kleurbronnen naar een definitief document worden weggeschreven, zie onze handleiding over rapportuitvoer met lettertypen en afbeeldingen. Wanneer de uitvoer een preflight-controle voor archivering of afdrukken moet doorstaan, bepalen de regels voor kleurruimte en uitvoerintentie (output-intent), behandeld in de validatiegids voor PDF/A, PDF/X en PDF/UA, welke van deze functies zijn toegestaan en hoe apparaatkleur moet worden gelabeld. Dit alles wordt geleverd in het HotPDF Component voor Delphi en C++Builder, naast de arcering-, ICC- en scheidings-API's die op dezelfde Type 0-kern bouwen.