Teknisk artikel

COLRv1 repeat/reflect-gradients som PDF-shadings i Delphi

HotPDF Component maler COLRv1 repeat- og reflect-gradients som vektor-PDF-shadings i stedet for at rasterisere glyphen. Geometrien strækkes, så den dækker et helt antal color line-cyklusser, hver cyklus får sin egen axiale eller radiale shading med Extend slået fra, farver interpoleres i linear-light CalRGB, og Porter-Duff in/out-compositemodes udtrykkes med luminosity soft masks

Da vi gennemgik hvordan HotPDF tegner COLR v1- og SVG-color emoji-fonts i Delphi, var repeat- og reflect-extend-modes på listen over ting, den native sti ikke udsendte, og sådanne glyphs gik til raster-fallback. Nyere HotPDF-versioner tegner dem som vektorer. At komme dertil krævede flere forkerte starter, de fleste forårsaget af en plausibelt udseende antagelse om PDF, der viste sig forkert i mindst én udbredt viewer. Dette indlæg gennemgår de antagelser, for enhver, der mapper CSS-, SVG- eller OpenType-gradients over på PDF, møder de samme

Hvorfor kan en PDF-viewer ikke gentage gradienten for dig?

En PDF-shading har ingen repeat- eller reflect-mode, så producenten selv må lægge hver cyklus ud. ISO 32000-1 §8.7.4.5.3 og §8.7.4.5.4 definerer /Extend for axiale og radiale shadings, og alt, den gør, er at fortsætte farven ved t0 eller t1 forbi aksens ende. Det er COLRv1 EXTEND_PAD, og intet andet. OpenType COLR v1 definerer også EXTEND_REPEAT, hvor color linen starter forfra, hver gang parameteren passerer et heltal, og EXTEND_REFLECT, hvor hver anden cyklus kører baglæns

HotPDF COLRv1-til-PDF-shading-mapping i Delphi: EXTEND_PAD matcher /Extend continue-flagget for axiale og radiale shadings, mens EXTEND_REPEAT og EXTEND_REFLECT intet PDF-modstykke har og må bygges af pr. cyklus-shading-geometri
PDF-shadings padder kun farverne ved t0 og t1, så en COLRv1 repeat- eller reflect-gradient må lægges ud cyklus for cyklus af producenten, ikke slås til for vieweren

Det første HotPDF-forsøg prøvede at få funktionen til at klare det periodiske arbejde. Den registrerede én Type 0 sampled function (§7.10.2) med et bredt domæne, groft [-12.73, 12.73] for et 25-cyklus-spænd, fyldte den med foldede samples og slog Extend til. Inden for første cyklus renderede en minimal testfil korrekt; udenfor malede MuPDF én enkelt gennemsnitsfarve, og Poppler var ikke bedre. Begge viewers evaluerer funktionen langs shading-parameteren og padder ud over aksen, og ingen af dem går tilbage og spørger en periodisk funktion i det udvidede område. Det brede domæne var også en fejl-læsning af spec'en: shading-parameteren t sendes direkte til funktionen, og efter §7.10.1 clips et input uden for funktionsdomænet, så en funktion, hvis domæne ikke matcher shadingens /Domain (default [0 1]), kun får en del af sine samples læst

Designet, der virker, har tre dele, alle verificeret mod MuPDF- og Poppler-renders. Shading-geometrien dækker fysisk et heltal N af cyklusser, så ingen pixel afhænger af Extend. Den sampledte funktion beholder default-domænet [0 1], matchende det, vieweren reelt giver med. Og samplesekvensen selv foldes pr. cyklus, med 256 intervaller pr. cyklus, så funktionen rummer repeat- eller reflect-farvedata i stedet for at læne sig op ad en viewer-funktion

At folde en color line ind i Type 0-samples

At folde betyder at samplere color linen én gang pr. cyklus og spejle sekvensen på ulige cyklusser for reflect-mode. Hjælperen nedenfor er almindelig Delphi, ikke en HotPDF-API, og den gør to ting, den naive version får forkert. Den interpolerer i linear light i stedet for på de gemte sRGB-bytes, og den resolver hard stops (to stops ved samme offset) til farven på den fjerne side, hvilket er OpenType- og CSS-semantikken

uses
  System.SysUtils, System.Math;

type
  TColorStop = record
    Offset: Single;  // 0..1 inden for én cyklus, sorteret stigende
    R, G, B: Byte;   // CPAL-agtige sRGB-bytes
  end;

function SRGBToLinear(V: Byte): Double;
var
  C: Double;
begin
  C := V / 255;
  if C <= 0.04045 then
    Result := C / 12.92
  else
    Result := Power((C + 0.055) / 1.055, 2.4);
end;

function LinearToSRGB(L: Double): Byte;
var
  C: Double;
begin
  L := EnsureRange(L, 0, 1);
  if L <= 0.0031308 then
    C := L * 12.92
  else
    C := 1.055 * Power(L, 1 / 2.4) - 0.055;
  Result := Round(C * 255);
end;

// Én cyklus af en color line som 8-bit RGB-tripler. Reverse = True
// spejler cyklussen, hvilket er det, reflect-mode gør på ulige cyklusser
function SampleCycle(const Stops: array of TColorStop; Count: Integer;
  Reverse: Boolean): TBytes;
var
  I, S: Integer;
  T, F, Span: Double;
begin
  SetLength(Result, Count * 3);
  for I := 0 to Count - 1 do
  begin
    T := I / (Count - 1);
    if Reverse then
      T := 1 - T;
    S := 0;
    // >= vælger den bageste side af et hard stop
    while (S < High(Stops) - 1) and (T >= Stops[S + 1].Offset) do
      Inc(S);
    Span := Stops[S + 1].Offset - Stops[S].Offset;
    if Span > 0 then
      F := EnsureRange((T - Stops[S].Offset) / Span, 0, 1)
    else
      F := 1;
    Result[I * 3] := LinearToSRGB(SRGBToLinear(Stops[S].R) +
      (SRGBToLinear(Stops[S + 1].R) - SRGBToLinear(Stops[S].R)) * F);
    Result[I * 3 + 1] := LinearToSRGB(SRGBToLinear(Stops[S].G) +
      (SRGBToLinear(Stops[S + 1].G) - SRGBToLinear(Stops[S].G)) * F);
    Result[I * 3 + 2] := LinearToSRGB(SRGBToLinear(Stops[S].B) +
      (SRGBToLinear(Stops[S + 1].B) - SRGBToLinear(Stops[S].B)) * F);
  end;
end;

COLRv1-rendereren inde i HotPDF går videre end dette udkast. Den skriver big-endian 16-bit lineære samples ind i en CalRGB-funktion og gemmer alpha som en separat 16-bit grå sample, mens de offentlige hjælpere, der vises herunder, tager 8-bit DeviceRGB-tripler, hvilket er grunden til, at udkastet konverterer tilbage til sRGB-bytes. For teorien om, hvad en reader gør ved de samples, dækker artiklen om Type 0 sampled functions som 3D color LUTs interpolationsmodellen i detaljer

Hvorfor har hver cyklus brug for sin egen shading?

Hver cyklus har brug for sin egen shading, fordi readers ikke evaluerer en sampled funktion eksakt; de forberegner en colortabel, og tabellen interpolerer hen over wrap'et fra en cyklus' sidste farve til den næstes første farve. MuPDF gemmer en shadings endimensionelle funktion som en colortabel med 256 indgange. Pak 11 cyklusser ind i én enkelt 2816-sample-funktion, og hver gemte sample kan være eksakt, mens MuPDF stadig viser 12 til 14 niveauer af kanalfejl, fordi 256 tabelindgange spredes over hele spændet, og den hårde repeat-søm udglattes til en rampe. Poppler og den analytiske interpolation af samme fil var begge korrekte, så det er en reader-præcisionsgrænse, ikke en filfejl, og flere samples hjælper ikke

HotPDF COLRv1 repeat-gradient-sømsdiagram til Delphi: én sampled funktion henover 11 cyklusser lader MuPDF's colortabel med 256 indgange udglatte hver repeat-søm til en rampe, mens én axial shading pr. cyklus holder hver tabel inden for én enkelt cyklus og bevarer det hårde farvespring
Readers forberegner en fast colortabel pr. shading, så fixet er geometrisk: én shading pr. cyklus med Extend slået fra flytter repeat-sømmen ud på en grænse, ingen tabel nogensinde interpolerer hen over

Fixet er at holde hver reader-tabel inden for én cyklus. HotPDF udsender periodiske lineære gradients som én uafhængig axial shading pr. original cyklus, med begge Extend-flags false, så springet fra slut- til startfarve lander på grænsen mellem to shadings i stedet for inde i én tabel. For reflect bruger ulige-testen det oprindelige signede cyklusnummer, inklusive negative cyklusser før color linens start, og shading-retningen byttes i stedet for at genberegne samples. Alle cyklusser tegnes ind i én linear-light isoleret flade, og paint-alphaen anvendes én gang til sidst, så en halvgennemsigtig gradient ikke fordobler sin opacity langs sømmene

De offentlige metoder THotPDF.RegisterSampledAxialGradient og THotPDF.RegisterSampledRadialGradient eksponerer sampled-shading-byggeklodsen til din egen tegning. Begge tager (..., DomainT0, DomainT1: Single; const Samples: TBytes; SampleCount, Cycles: Integer) efter geometrien, kræver PDF 1.3, rejser en exception, medmindre Samples rummer præcis SampleCount * 3 bytes, og returnerer et pattern-navn til THPDFPage.SetFillPattern. De sætter Extend i begge ender og registrerer Cycles som en custom /Cycles-pattern-nøgle, som viewers ignorerer. For at følge én-cyklus-pr.-shading-reglen med dem, registrér ét pattern pr. cyklus og lad et båndrektangel clippe det

// Vandret repeat- eller reflect-gradient bygget af én shading pr. cyklus.
// Pattern-koordinater er i default PDF space; en vandret akse holder
// dette eksempel uafhængigt af sidens top-ned-Y-konvention
procedure PaintPeriodicBands(Pdf: THotPDF; const Stops: array of TColorStop;
  X, Y, Width, Height, CycleLength: Single; Reflect: Boolean);
const
  SamplesPerCycle = 257;  // 256 intervaller pr. cyklus
var
  Cycle, CycleCount: Integer;
  X0, X1: Single;
  Samples: TBytes;
  PatternName: AnsiString;
begin
  CycleCount := Ceil(Width / CycleLength);
  for Cycle := 0 to CycleCount - 1 do
  begin
    X0 := X + Cycle * CycleLength;
    X1 := X0 + CycleLength;
    Samples := SampleCycle(Stops, SamplesPerCycle, Reflect and Odd(Cycle));
    // Domain [0, 1]: shading-parameteren når funktionen uændret
    PatternName := Pdf.RegisterSampledAxialGradient(X0, 0, X1, 0, 0, 1,
      Samples, SamplesPerCycle, 1);
    if PatternName = '' then
      Exit;  // dokumentversion låst under PDF 1.3
    Pdf.CurrentPage.SetFillPattern(PatternName);
    // Båndet clipper shadingen til præcis én cyklus, så Extend aldrig vises
    Pdf.CurrentPage.Rectangle(X0, Y, Math.Min(X1, X + Width) - X0, Height);
    Pdf.CurrentPage.Fill;
  end;
end;

Meget lange spænd kræver et andet trick, fordi én shading pr. cyklus betyder én ressource pr. cyklus. Til periodiske lineære COLRv1-gradients skifter HotPDF til et fordoblingsskema: en bladeform rummer en ét-cyklus repeat- eller to-cyklus reflect-primitiv, og hvert Form XObject-niveau tegner det forrige niveau to gange, forskudt med dets bredde. Antallet af former vokser logaritmisk med spændet, mens vieweren stadig maler hver synlig cyklus, så dækning efter fasejustering er loftet til 65536 cyklusenheder. En tidligere version, der lagde primitivet i et Type 1 tiling pattern, blev droppet, fordi readers justerer tile-afstanden til device-gitteret, og Poppler drev ud af fase på en cyklus, der ikke var et helt antal pixels

At løse parametrområdet for radiale cirkelfamilier

En radial gradient dækker glyphen kun, når cyklusintervallet beregnes for den rigtige cirkelfamilie, og COLRv1 radiale paints falder i tre familier med forskellig matematik. Med D = c1 - c0 og dr = r1 - r0 har en strengt indlejret familie |dr| > |D|, en ikke-indlejret kegle har |D|² > dr², og en tangentfamilie har |D| = |dr|. HotPDF måler parameteren fra nul-radius-punktet i den oprindelige familie og holder center og radius for hver udsendt cirkel bundet til samme oprindelige parameter, så remappede negative radier aldrig clamps uafhængigt

HotPDF COLRv1 radial gradient-beslutningstræ til Delphi: at sammenligne dr med D adskiller indlejrede cirkelfamilier løst med bisektion, kegler løst med korte pr. cyklus-radiale shadings og tangentfamilier reduceret til den lineære rod q = |W|^2 / (2(W*V))
Uligheden mellem |dr| og |D| afgør cyklusmatematikken: indlejrede familier bisekterer for den første berørende cirkel, kegler får én kort shading pr. cyklus, og tangentlinjer efterlader et singulært bånd transparent
  • Indlejrede familier skærer fyldboksen monoton, så parametrområdet kommer fra de fire hjørner plus en bisektion for den første cirkel, der rører boksen. Op til 64 cyklusser samples ind i én shading ved 256 samples pr. cyklus, loftet ved 4096 samples; dybere spænd skifter til pr. cyklus-slices
  • Kegler og ens-radius-bånd bruger separate korte radiale shadings ved de oprindelige cyklusgrænser, hver med 257 samples og Extend slået fra, compositeret i oprindelig parameterorden inde i en isoleret, ikke-knockout-gruppe, så den største gyldige parameter vinder, hvor cirkler overlapper
  • Tangentfamilier reducerer cirkelligningen til en lineær. I forhold til det fælles tangentpunkt er roden q = |W|² / (2·(W·V)), og det dækkede område er den åbne halvplan foran det punkt

Tangenttilfældet har en ægte singularitet. Krydser tangentlinjen fyldboksen, er antallet af cyklusser, der skal til for at nå pixels nær den linje, ubegrænset, så intet endeligt antal shadings dækker den. HotPDF bruger et budget på 64 cyklusser fra tangentpunktet og efterlader det resterende tynde bånd transparent. Ligger alle fire hjørner af boksen bag tangentpunktet, er paintet transparent som helhed, og resten af glyphen tegnes stadig. Den endelige keglesti accepterer op til 4096 påkrævede cyklusser, med dækning lofter ved 1024 slices og alt forbi loftet efterladt umalt; en 1024-slice-glyph kommer på omkring 1,8 MB

Linear-light-farve og to-decimaler-fælden

COLRv1 interpolerer og compositerer i linear light, så HotPDF decoder hver CPAL-byte med sRGB's inverse transferfunktion og maler solide farver, shadings og interne grupper i ét CalRGB-space med D65-hvid, sRGB-primals og /Gamma [1 1 1] (§8.6.5.3). Kun den færdige rodflade konverterer til kalderens blend space. At interpolere gemte sRGB-bytes i stedet giver synligt mørkere mellempunkter, hvilket er den klassiske gamma-fejl, enhver gradientmotor laver én gang

Skiftet til linear light udstillede en gammel formatter-fejl. En mørk CPAL-kanalværdi på 12 bliver omkring 0,0036765 i linear light, og den delte talformatter bag scn beholdt kun to decimaler, så den komponent blev skrevet som 0, før nogen viewer rørte filen. Symptomet lignede præcis reader-kvantisering, og den første refleks var at bebrejde MuPDF's 8-bit gruppebuffers. Fill- og stroke-komponenter går nu gennem en eksponentfri decimalformatter, der bevarer små værdier og ignorerer processens locale. Lektionen generaliserer: når en mørk farve forsvinder, så dump content streamen og tjek den skrevne operand, før du undersøger viewer'en

Vieweren betyder stadig noget efter det fix. MuPDF 1.28.2 taber nær-nul lineære kanaler, når den compositerer en isoleret transparency group, der bærer et eksplicit blend color space, og Poppler 26.07.0 Splash ignorerer gruppe-blend spaces helt. Egenskaben THotPDF.COLRv1DeviceSpaceGroupBlend (opt-in, default False) udsender COLRv1-grupper uden blend color space, så de compositerer i det arvede device space, mens indholdsfarver, shadings og funktioner forbliver linear-light CalRGB. Afvejningen er, at overlappende halvgennemsigtige lag så blender i device space i viewers, der faktisk respekterer gruppe color spaces

Hvilke COLRv1 compositemodes kan PDF udtrykke eksakt?

HotPDF renderer COLRv1 compositemodes 5 til 8 (SRC_IN, DEST_IN, SRC_OUT, DEST_OUT) nativt, og modes 9 til 12 (SRC_ATOP, DEST_ATOP, XOR, PLUS) går til OnColorGlyphRasterize. In- og out-modes er produkter af én flade med den andens alpha, hvilket er præcis, hvad en soft mask beregner (§11.6.5). Den overlevende operand fanges som en almindelig form; den maskerede operand fanges i et alpha-pass som fladt indhold inde i én isoleret DeviceGray-gruppe og bruges som en /Luminosity-maske med /BC [0]. Out-modes skriver 1 − α for hver alpha og lægger et opak hvidt dæk under maskeringsindholdet, så den overlevende forbliver synlig, hvor den anden operand aldrig malede

De andre fire kræver premultiplieret addition af to flader. I PDF's source-over-model dæmpes backdrop-leddet altid med kildens (1 − α), og der findes ingen primitiv, der adderer to flader eller skalerer backdrop kun efter kildens dækning, så atop, xor og plus kan ikke skrives som vektorer. Én begrænsning mere kom fra nøgne test-PDF'er: begge viewers beregner soft mask-grupper forkert, hvis indhold indlejrer en form-Do, så den maskerede operand skal være flad. Solide farver, pad-gradients og en PaintGlyph-wrapper omkring dem kvalificerer; layers, transforms og periodiske gradients, som alle producerer interne former, sender glyphen til fallback'en

var
  Pdf: THotPDF;
  Fallback: TEmojiFallback;  // handler-klasse med event-signaturen nedenfor
begin
  Pdf := THotPDF.Create(nil);
  Fallback := TEmojiFallback.Create;
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'periodic-icons.pdf';
    // Opt-in: device-space gruppe-compositing til MuPDF og Poppler
    Pdf.COLRv1DeviceSpaceGroupBlend := True;
    // Modtager atop/xor/plus-glyphs og alt andet uden for subsættet:
    // (Sender; Format; const FontBytes, AssetData: TBytes; GlyphID: Word;
    //  PaletteIndex, PixelSize: Integer; out Width, Height: Integer;
    //  out RGBA: TBytes; out Handled: Boolean)
    Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('Fonts\GradientIcons.ttf');
    if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $E001,
      72, 144, 'Gradient Icons', 48) then
      Writeln('U+E001 was neither drawn natively nor rasterized');
    Pdf.EndDoc;
  finally
    Fallback.Free;
    Pdf.Free;
  end;
end;

Hvor vektorstien stadig kommer til kort

Vektor-output er ikke det samme som identiske pixels i enhver viewer, og HotPDF's acceptansedokumenter siger det ligeud. Samples inden for ét eller to trin af en cyklusgrænse udglattes af reader-interpolation, så pixeltjek sampler væk fra sømmene, og funktionsstreams verificeres i stedet ved at decode dem. At interpolere separat gemte straight RGB- og alpha-samples er en approksimation nær lav-alpha-endepunkter og hard stops; den målte fejl er af anden orden ved 256 samples pr. cyklus, men den er ikke eksakt. Poppler Splash sampler ved en pixels venstre kant i stedet for dens center, hvilket viser sig som små forskelle på de stejleste ramper

Budgetterne er også reelle grænser. Indlejrede radiale familier takker nej ud over 4096 cyklusspænd, kegler og tangentfamilier ud over 4096 påkrævede cyklusser, og tangentbåndet nær den singulære linje forbliver tomt. Varierende stop-alpha kræver PDF 1.4-transparens, så den er ikke slået til under PDF/A-1, PDF/X-1a eller PDF/X-3. Ligger en glyph uden for nogen af disse grænser, takker DrawRegisteredColorGlyph nej til hele grafen, før en operator skrives, hvilket er samme alt-eller-intet-adfærd som før

FAQ

Rasteriserer HotPDF COLRv1 repeat- og reflect-gradients?

Nej. Nyere HotPDF-versioner udsender dem som vektor-axiale og -radiale shadings, der dækker hele cyklusser. Kun grafer uden for det understøttede subsæt, som atop-, xor- og plus-composites, går til OnColorGlyphRasterize

Hvorfor ikke bruge én sampled funktion til alle cyklusser?

Readers forberegner en colortabel ud fra funktionen, og MuPDF's tabel har 256 indgange. Spredt over mange cyklusser interpolerer den hen over hver repeat-søm. Én shading pr. cyklus holder hver tabel inden for én enkelt cyklus

Hvornår skal jeg slå COLRv1DeviceSpaceGroupBlend til?

Slå den til, når nær-sorte kanaler i COLRv1-glyphs falder til nul i MuPDF. Lad den være fra, når overlappende halvgennemsigtige lag skal blende i linear light i viewers, der respekterer gruppe color spaces

Native COLRv1-gradients, color emoji, sampledte shadings og transparency groups er alle en del af HotPDF Component for Delphi og C++Builder, som genererer PDF-dokumenter direkte fra Delphi- og C++Builder-kode