HotPDF Component tekent COLRv1 repeat- en reflect-gradients als vector-PDF-shadings in plaats van de glyph te rasteren. De geometrie wordt uitgerekt om een geheel aantal color-line-cycli te dekken, elke cyclus krijgt zijn eigen axiale of radiale shading met Extend uitgeschakeld, kleuren worden geïnterpoleerd in linear-light CalRGB, en de Porter-Duff in/out composite modes worden uitgedrukt met luminosity soft masks
Toen we behandelden hoe HotPDF COLR v1- en SVG-kleuremoji-fonts in Delphi tekent, stonden de repeat- en reflect-extend modes op de lijst van dingen die het native pad niet uitzond, en zulke glyphs gingen naar de raster-fallback. Nieuwere HotPDF-versies tekenen ze als vectoren. Daar komen kostte meerdere valse starts, de meeste veroorzaakt door een plausibel ogende aanname over PDF die in ten minste één veelgebruikte viewer onjuist bleek. Deze post loopt die aannames langs, want iedereen die CSS-, SVG- of OpenType-gradients op PDF mapt, ontmoet dezelfde
Waarom kan een PDF-viewer de gradient niet voor u herhalen?
Een PDF-shading heeft geen repeat- of reflect-modus, dus de producent moet elke cyclus zelf uitzetten. ISO 32000-1 §8.7.4.5.3 en §8.7.4.5.4 definiëren /Extend voor axiale en radiale shadings, en alles wat het doet is de kleur bij t0 of t1 doorlaten lopen voorbij het einde van de as. Dat is COLRv1 EXTEND_PAD, en niets anders. OpenType COLR v1 definieert ook EXTEND_REPEAT, waarbij de color line opnieuw begint telkens als de parameter een geheel getal passeert, en EXTEND_REFLECT, waarbij om de andere cyclus achterstevoren loopt
De eerste HotPDF-poging probeerde de functie het periodieke werk te laten doen. Die registreerde één Type 0 sampled function (§7.10.2) met een breed domein, ruwweg [-12.73, 12.73] voor een span van 25 cycli, vulde hem met gevouwen samples en zette Extend aan. Binnen de eerste cyclus rendert een minimaal testbestand correct; erbuiten schilderde MuPDF één gemiddelde kleur en Poppler deed het niet beter. Beide viewers evalueren de functie langs de shading-parameter en vullen voorbij de as aan, en geen van beide gaat terug om een periodieke functie in het uitgebreide gebied te bevragen. Het brede domein was bovendien een mislezing van de spec: de shading-parameter t wordt rechtstreeks aan de functie doorgegeven, en volgens §7.10.1 wordt een invoer buiten het functiedomein geclipt, dus een functie wiens domein niet matcht met de /Domain van de shading (default [0 1]) krijgt hooguit een deel van zijn samples gelezen
Het ontwerp dat werkt heeft drie delen, alle geverifieerd tegen MuPDF- en Poppler-renders. De shading-geometrie dekt fysiek een geheel aantal N cycli, dus geen enkele pixel hangt van Extend af. De sampled function houdt het default domein [0 1] aan, matchend met wat de viewer werkelijk doorgeeft. En de sample-reeks zelf is per cyclus gevouwen, met 256 intervallen per cyclus, zodat de functie de repeat- of reflect-kleurdata vasthoudt in plaats van op een willekeurige viewer-feature te leunen
Een color line vouwen tot Type 0 samples
Vouwen betekent de color line één keer per cyclus samplen en de reeks op oneven cycli spiegelen voor de reflect-modus. De helper hieronder is kale Delphi, geen HotPDF-API, en hij doet twee dingen die de naive versie verkeerd doet. Hij interpoleert in linear light in plaats van op de opgeslagen sRGB-bytes, en hij lost hard stops (twee stops op dezelfde offset) op naar de kleur aan de verre kant, wat de OpenType- en CSS-semantiek is
uses
System.SysUtils, System.Math;
type
TColorStop = record
Offset: Single; // 0..1 binnen één cyclus, oplopend gesorteerd
R, G, B: Byte; // sRGB-bytes in CPAL-stijl
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;
// Eén cyclus van een color line als 8-bit RGB-tripels. Reverse = True
// spiegelt de cyclus, wat reflect mode op oneven cycli doet
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;
// >= kiest de achterzijde van een 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;
De COLRv1-renderer in HotPDF gaat verder dan deze schets. Hij schrijft big-endian 16-bit lineaire samples in een CalRGB-functie en bewaart alpha als een aparte 16-bit grijze sample, terwijl de publieke helpers hieronder 8-bit DeviceRGB-tripels nemen, en daarom converteert de schets terug naar sRGB-bytes. Voor de theorie van wat een reader met die samples doet, behandelt het artikel over Type 0 sampled functions als 3D color LUTs het interpolatiemodel in detail
Waarom heeft elke cyclus zijn eigen shading nodig?
Elke cyclus heeft zijn eigen shading nodig, omdat readers een sampled function niet exact evalueren; ze rekenen vooraf een kleurentabel, en de tabel interpoleert over de overgang heen van de laatste kleur van één cyclus naar de eerste kleur van de volgende. MuPDF bewaart de eendimensionale functie van een shading als een kleurentabel van 256 entries. Propt u 11 cycli in één functie van 2816 samples, dan kan elke opgeslagen sample exact zijn terwijl MuPDF toch 12 tot 14 niveaus kanaalfout laat zien, want de 256 tabelentries liggen uitgesmeerd over het hele span en de harde repeat-naad wordt gladgestreken tot een ramp. Poppler en de analytische interpolatie van hetzelfde bestand waren allebei correct, dus dit is een precisielimiet van de reader, geen bestandsfout, en meer samples toevoegen helpt niet
De fix is elke reader-tabel binnen één cyclus houden. HotPDF zendt periodieke lineaire gradients uit als één onafhankelijke axiale shading per originele cyclus, met beide Extend-flags op false, zodat de sprong van einde naar begin op de grens tussen twee shadings valt in plaats van binnen één tabel. Voor reflect gebruikt de oneven-toets het originele signed cyclustal, inclusief negatieve cycli vóór de start van de color line, en wordt de shadingrichting omgedraaid in plaats van de samples te herberekenen. Alle cycli worden in één linear-light geïsoleerd vlak getekend en de paint-alpha wordt een keer op het eind toegepast, dus een translucide gradient verdubbelt zijn opaciteit niet langs de naden
De publieke methoden THotPDF.RegisterSampledAxialGradient en THotPDF.RegisterSampledRadialGradient leggen de sampled-shading-bouwsteen bloot voor uw eigen tekenwerk. Beide nemen (..., DomainT0, DomainT1: Single; const Samples: TBytes; SampleCount, Cycles: Integer) na de geometrie, eisen PDF 1.3, gooien een exception tenzij Samples exact SampleCount * 3 bytes bevat, en geven een pattern-naam terug voor THPDFPage.SetFillPattern. Ze zetten Extend aan beide einden en leggen Cycles vast als een custom /Cycles-patternsleutel die viewers negeren. Om de regel één-cyclus-per-shading ermee te volgen, registreert u één pattern per cyclus en laat u een bandrechthoek hem clippen
// Horizontale repeat- of reflect-gradient opgebouwd uit één shading per cyclus.
// Pattern-coördinaten staan in default PDF space; een horizontale as houdt
// dit voorbeeld onafhankelijk van de van-boven-naar-beneden-Y-conventie van de pagina
procedure PaintPeriodicBands(Pdf: THotPDF; const Stops: array of TColorStop;
X, Y, Width, Height, CycleLength: Single; Reflect: Boolean);
const
SamplesPerCycle = 257; // 256 intervallen per cyclus
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]: de shading-parameter komt onveranderd bij de functie aan
PatternName := Pdf.RegisterSampledAxialGradient(X0, 0, X1, 0, 0, 1,
Samples, SamplesPerCycle, 1);
if PatternName = '' then
Exit; // documentversie vastgezet onder PDF 1.3
Pdf.CurrentPage.SetFillPattern(PatternName);
// De band clipt de shading op exact één cyclus, dus Extend komt nooit tevoorschijn
Pdf.CurrentPage.Rectangle(X0, Y, Math.Min(X1, X + Width) - X0, Height);
Pdf.CurrentPage.Fill;
end;
end;
Zeer lange spans hebben een andere truc nodig, want één shading per cyclus betekent één resource per cyclus. Voor periodieke lineaire COLRv1-gradients schakelt HotPDF over op een verdubbelschema: een leaf form bevat een repeat-primitive van één cyclus of een reflect-primitive van twee cycli, en elk Form XObject-niveau tekent het vorige niveau twee keer, verschoven om zijn breedte. Het aantal forms groeit logaritmisch met het span, terwijl de viewer toch elke zichtbare cyclus schildert, dus de dekking na fase-uitlijning is gekapt op 65536 cyclus-eenheden. Een eerdere versie die de primitive in een Type 1 tiling pattern stopte is geschrapt, omdat readers de tile-afstand op het device-raster bijstellen en Poppler uit de fase dreef op een cyclus die geen geheel aantal pixels was
Het parameterbereik voor radiale cirkelfamilies oplossen
Een radiale gradient dekt de glyph pas als het cyclusbereik voor de juiste cirkelfamilie is berekend, en COLRv1 radiale paints vallen in drie families met verschillende wiskunde. Met D = c1 - c0 en dr = r1 - r0 heeft een streng geneste familie |dr| > |D|, een niet-geneste kegel |D|² > dr², en een tangentfamilie |D| = |dr|. HotPDF meet de parameter vanaf het nulstraalpunt van de originele familie en houdt centrum en straal van elke uitgezonden cirkel verbonden aan dezelfde originele parameter, dus gehermapte negatieve stralen worden nooit onafhankelijk geclampt
- Geneste families snijden de fill box monotoon, dus het parameterbereik komt uit de vier hoeken plus een bisectie voor de eerste cirkel die de box raakt. Tot 64 cycli worden in één shading gesampled met 256 samples per cyclus, gekapt op 4096 samples; diepere spans schakelen over op slices per cyclus
- Kegels en gelijke-straal-stroken gebruiken aparte korte radiale shadings op de originele cyclusgrenzen, elk met 257 samples en
Extenduit, gecomposited in originele parametervolgorde binnen een geïsoleerde non-knockout-groep zodat de grootste geldige parameter wint waar cirkels overlappen - Tangentfamilies reduceren de cirkelvergelijking tot een lineaire. Relatief aan het gedeelde raakpunt is de root
q = |W|² / (2·(W·V)), en het gedekte gebied is het open halfvlak vóór dat punt
Het tangentgeval heeft een echte singulariteit. Kruist de tangentlijn de fill box, dan is het aantal cycli dat nodig is om pixels bij die lijn te bereiken onbegrensd, dus geen enkel eindig aantal shadings dekt hem. HotPDF besteedt een budget van 64 cycli vanaf het raakpunt en laat de resterende dunne strook transparant. Zitten alle vier de hoeken van de box achter het raakpunt, dan is de paint als geheel transparant en tekent de rest van de glyph gewoon. Het eindige kegelpad accepteert tot 4096 vereiste cycli, met dekking gekapt op 1024 slices en alles voorbij de kap ongeverfd; een glyph van 1024 slices komt op zo'n 1.8 MB
Linear-light kleur en de valkuil van twee decimals
COLRv1 interpoleert en composits in linear light, dus HotPDF decodeert elke CPAL-byte met de omgekeerde sRGB-overdrachtsfunctie en verft solids, shadings en interne groepen in één CalRGB-space met D65-wit, sRGB-primaries en /Gamma [1 1 1] (§8.6.5.3). Alleen het afgeronde rootvlak converteert naar de blend space van de aanroeper. In plaats daarvan opgeslagen sRGB-bytes interpoleren geeft zichtbaar donkerdere middenpunten, de klassieke gammafout die elke gradient-engine één keer maakt
De stap naar linear light legde een oude formatter-bug bloot. Een donkere CPAL-kanaalwaarde van 12 wordt in linear light ongeveer 0.0036765, en de gedeelde getallenformatter achter scn hield maar twee decimals aan, dus die component werd als 0 weggeschreven nog voordat een viewer het bestand aanraakte. Het symptoom leek precies op reader-quantisatie, en de eerste ingeving was MuPDF's 8-bit group buffers de schuld te geven. Fill- en stroke-componenten gaan nu door een decimal-formatter zonder exponent die kleine waarden vasthoudt en de process-locale negeert. De les generaliseert: wanneer een donkere kleur verdwijnt, dump de content stream en controleer de weggeschreven operand voordat u de viewer gaat onderzoeken
De viewer telt daarna nog steeds mee. MuPDF 1.28.2 verliest nabij-nul lineaire kanalen bij het compositen van een geïsoleerde transparency group die een expliciete blend color space draagt, en Poppler 26.07.0 Splash negeert group blend spaces volledig. De opt-in eigenschap THotPDF.COLRv1DeviceSpaceGroupBlend (default False) zendt COLRv1-groepen uit zonder de blend color space, dus ze compositen in de overgeërfde device space terwijl contentkleuren, shadings en functies linear-light CalRGB blijven. De afweging is dat overlappende translucide lagen dan in device space blenden in viewers die group color spaces wel eren
Welke COLRv1 composite modes kan PDF exact uitdrukken?
HotPDF rendert COLRv1 composite modes 5 tot 8 (SRC_IN, DEST_IN, SRC_OUT, DEST_OUT) native, en modes 9 tot 12 (SRC_ATOP, DEST_ATOP, XOR, PLUS) gaan naar OnColorGlyphRasterize. De in- en out-modes zijn producten van het ene vlak met de alpha van het andere, precies wat een soft mask berekent (§11.6.5). Het overlevende vlak wordt als een gewone form vastgelegd; het gemaskeerde vlak wordt in een alpha-pas vastgelegd als platte content binnen één geïsoleerde DeviceGray-groep en gebruikt als /Luminosity-mask met /BC [0]. De out-modes schrijven 1 − α voor elke alpha en zetten een dekkende witte afdekking onder de maskcontent, zodat de overlevende zichtbaar blijft waar het andere vlak nooit verfde
De andere vier vereisen premultiplied optelling van twee vlakken. In het source-over-model van PDF wordt de backdrop-term altijd geattenuëerd door (1 − α) van de bron, en er is geen primitive die twee vlakken optelt of de backdrop alleen naar de dekking van de bron schaalt, dus atop, xor en plus zijn niet als vectoren te schrijven. Nog een beperking kwam uit kale test-PDF's: beide viewers rekenen soft-mask-groepen waarvan de content een form Do nest verkeerd, dus het gemaskeerde vlak moet plat zijn. Solide kleuren, pad-gradients en een PaintGlyph-wikkel eromheen kwalificeren; lagen, transformaties en periodieke gradients, die allemaal interne forms opleveren, sturen de glyph naar de fallback
var
Pdf: THotPDF;
Fallback: TEmojiFallback; // handler-klasse met de eventsignatuur hieronder
begin
Pdf := THotPDF.Create(nil);
Fallback := TEmojiFallback.Create;
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'periodic-icons.pdf';
// Opt-in: device-space group compositing voor MuPDF en Poppler
Pdf.COLRv1DeviceSpaceGroupBlend := True;
// Ontvangt atop/xor/plus-glyphs en al het andere buiten de subset:
// (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;
Waar het vectorpad nog tekortschiet
Vectoroutput is niet hetzelfde als identieke pixels in elke viewer, en de HotPDF-acceptatieregistraties zeggen dat onverbloemd. Samples binnen een of twee stappen van een cyclusgrens worden gladgestreken door reader-interpolatie, dus pixelcontroles samplen weg van de naden en functiestreams worden geverifieerd door ze te decoderen. Apart opgeslagen rechte RGB- en alpha-samples interpoleren is een benadering bij lage-alpha-eindpunten en hard stops; de gemeten fout is tweede orde bij 256 samples per cyclus, maar het is niet exact. Poppler Splash samplet aan de linkerrand van een pixel in plaats van in het centrum, wat op de steilste ramps als kleine verschillen naar voren komt
De budgets zijn ook echte limieten. Geneste radiale families weigeren voorbij spans van 4096 cycli, kegels en tangentfamilies voorbij 4096 vereiste cycli, en de tangentstrook bij de singuliere lijn blijft leeg. Wisselende stop-alpha vereist PDF 1.4-transparantie, dus hij staat niet aan onder PDF/A-1, PDF/X-1a of PDF/X-3. Valt een glyph buiten een van deze limieten, dan weigert DrawRegisteredColorGlyph de hele graaf voordat er een operator wordt geschreven, hetzelfde alles-of-niets-gedrag als voorheen
FAQ
Rastert HotPDF COLRv1 repeat- en reflect-gradients?
Nee. Nieuwere HotPDF-versies zenden ze uit als vector-axiale en -radiale shadings die hele cycli dekken. Alleen grafen buiten de ondersteunde subset, zoals atop-, xor- en plus-composites, gaan naar OnColorGlyphRasterize
Waarom niet één sampled function voor alle cycli?
Readers rekenen vooraf een kleurentabel uit de functie, en de tabel van MuPDF heeft 256 entries. Uitgesmeerd over veel cycli interpoleert hij over elke repeat-naad heen. Eén shading per cyclus houdt elke tabel binnen één enkele cyclus
Wanneer moet ik COLRv1DeviceSpaceGroupBlend aanzetten?
Zet hem aan wanneer nabij-zwarte kanalen in COLRv1-glyphs in MuPDF naar nul zakken. Laat hem uit wanneer overlappende translucide lagen in linear light moeten blenden in viewers die group color spaces eren
Native COLRv1-gradients, kleuremoji, sampled shadings en transparency groups maken allemaal deel uit van de HotPDF Component for Delphi and C++Builder, die PDF-documenten rechtstreeks uit Delphi- en C++Builder-code genereert