HotPDF Component kreslí gradienty COLRv1 repeat a reflect jako vektorové PDF shadings místo rasterizace glyfu. Geometrie se natáhne, aby pokryla celý počet cyklů barevné linie, každý cyklus dostane vlastní axial nebo radial shading s vypnutým Extend, barvy se interpolují v lineárním světle CalRGB a kompozitní režimy Porter-Duff in/out se vyjádří luminosity soft maskami
Když jsme popisovali jak HotPDF kreslí fonty COLR v1 a SVG barevné emoji v Delphi, extend režimy repeat a reflect byly na seznamu věcí, které nativní cesta nevysílala, a takové glyfy šly na raster fallback. Novější verze HotPDF je kreslí jako vektory. Dostat se tam znamenalo několik falešných startů, většinou způsobených rozumně vypadajícím předpokladem o PDF, který se ukázal jako špatný aspoň v jednom široce používaném prohlížeči. Tenhle postup provede ty předpoklady, protože se stejnými se potká kdokoli, kdo mapuje gradienty CSS, SVG nebo OpenType na PDF
Proč vám prohlížeč PDF gradient nezopakuje sám?
PDF shading nemá režim repeat ani reflect, takže producent si musí rozložit každý cyklus sám. ISO 32000-1 §8.7.4.5.3 a §8.7.4.5.4 definují /Extend pro axial a radial shadings a jediné, co dělá, je pokračování barvy na t0 nebo t1 za koncem osy. To je COLRv1 EXTEND_PAD a nic jiného. OpenType COLR v1 definuje i EXTEND_REPEAT, kde se barevná linie začíná znovu pokaždé, když parametr projde integerem, a EXTEND_REFLECT, kde každý druhý cyklus běží pozpátku
První pokus HotPDF se snažil přimět funkci, aby periodickou práci dělala sama. Zaregistrovala jednu vzorkovanou funkci Type 0 (§7.10.2) s širokou doménou, zhruba [-12.73, 12.73] pro rozsah 25 cyklů, naplnila ji složenými vzorky a zapla Extend. Uvnitř prvního cyklu se minimální testovací soubor vyrenderoval správně; mimo něj MuPDF namaloval jednu zprůměrovanou barvu a Poppler nebyl lepší. Oba prohlížeče vyhodnocují funkci podél parametru shadingu a za osou vypolstrují, a ani jeden se nevrací dotazovat periodickou funkci v rozšířené oblasti. Široká doména byla i špatným čtením specifikace: parametr shadingu t jde rovnou do funkce a podle §7.10.1 se vstup mimo doménu funkce ořízne, takže funkce, jejíž doména neodpovídá /Domain shadingu (výchozí [0 1]), má přečtenou jen část svých vzorků
Design, který funguje, má tři části, všechny ověřené proti renderům MuPDF a Poppler. Geometrie shadingu fyzicky pokrývá celé číslo N cyklů, takže žádný pixel nezávisí na Extend. Vzorkovaná funkce drží výchozí doménu [0 1], přesně to, co prohlížeč skutečně předá. A vzorkovací sekvence je sama složená po cyklu, s 256 intervaly na cyklus, takže funkce drží data repeat nebo reflect barev místo spoléhání na jakoukoli funkci prohlížeče
Skládání barevné linie do vzorků Type 0
Skládání znamená vzorkovat barevnou linii jednou za cyklus a zrcadlit sekvenci na lichých cyklech pro režim reflect. Helper níže je čisté Delphi, ne API HotPDF, a dělá dvě věci, které naivní verze pokazí. Interpoluje v lineárním světle místo na uložených bajtech sRGB a tvrdé zastávky (dvě zastávky na stejném offsetu) řeší barvou na vzdálenější straně, což je sémantika OpenType a CSS
uses
System.SysUtils, System.Math;
type
TColorStop = record
Offset: Single; // 0..1 uvnitř jednoho cyklu, seřazené vzestupně
R, G, B: Byte; // sRGB bajty ve stylu CPAL
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;
// Jeden cyklus barevné linie jako 8bitové RGB trojice. Reverse = True
// zrcadlí cyklus, což reflect režim dělá na lichých cyklech
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;
// >= volí zadní stranu tvrdé zastávky
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 renderer uvnitř HotPDF jde dál než tahle skica. Zapisuje big-endian 16bitové lineární vzorky do CalRGB funkce a ukládá alfu jako samostatný 16bitový šedý vzorek, zatímco veřejné helpery ukázané dál berou 8bitové trojice DeviceRGB, proto skica převádí zpět na bajty sRGB. O teorii, co čtečka s těmi vzorky dělá, pojednává článek o vzorkovaných funkcích Type 0 jako 3D barevných LUT a detailně rozebírá interpolační model
Proč každý cyklus potřebuje vlastní shading?
Každý cyklus potřebuje vlastní shading, protože čtečky nevyhodnocují vzorkovanou funkci přesně; předpočítají tabulku barev a tabulka interpoluje přes přechod od poslední barvy jednoho cyklu k první barvě dalšího. MuPDF ukládá jednorozměrnou funkci shadingu jako tabulku barev o 256 položkách. Zabalte 11 cyklů do jedné 2816vzorkové funkce a každý uložený vzorek může být přesný, zatímco MuPDF pořád ukazuje 12 až 14 úrovní chyby kanálu, protože 256 položek tabulky se rozprostře přes celý rozsah a tvrdý repeat šev se vyhladí do rampy. Poppler i analytická interpolace téhož souboru byly správné, takže jde o limit přesnosti čtečky, ne o chybu souboru, a přidávání dalších vzorků nepomáhá
Oprava spočívá v tom, udržet každou tabulku čtečky uvnitř jednoho cyklu. HotPDF vysílá periodické lineární gradienty jako jeden nezávislý axial shading na původní cyklus s oběma příznaky Extend false, takže skok barvy od konce k začátku dopadne na hranici mezi dvěma shadings místo dovnitř jedné tabulky. U reflect používá lichý test původní znaménkové číslo cyklu, včetně negativních cyklů před začátkem barevné linie, a směr shadingu se prohodí místo přepočítávání vzorků. Všechny cykly se kreslí do jedné izolované plochy v lineárním světle a alfa barvy se aplikuje jednou na konci, takže poloprůhledný gradient nezdvojnásobí krytí podél švů
Veřejné metody THotPDF.RegisterSampledAxialGradient a THotPDF.RegisterSampledRadialGradient vystavují stavební blok vzorkovaného shadingu pro vlastní kreslení. Obě berou (..., DomainT0, DomainT1: Single; const Samples: TBytes; SampleCount, Cycles: Integer) za geometrií, vyžadují PDF 1.3, vyhodí výjimku, pokud Samples nedrží přesně SampleCount * 3 bajtů, a vrací název patternu pro THPDFPage.SetFillPattern. Nastavují Extend na obou koncích a zaznamenávají Cycles jako vlastní klíč patternu /Cycles, který prohlížeče ignorují. Abychom dodrželi pravidlo jeden-cyklus-na-shading, zaregistrujte jeden pattern na cyklus a nechte pásový obdélník ho oříznout
// Vodorovný repeat nebo reflect gradient složený z jednoho shadingu na cyklus.
// Souřadnice patternu jsou ve výchozím PDF prostoru; vodorovná osa udrží
// tenhle příklad nezávislý na konvenci Y shora dolů na stránce
procedure PaintPeriodicBands(Pdf: THotPDF; const Stops: array of TColorStop;
X, Y, Width, Height, CycleLength: Single; Reflect: Boolean);
const
SamplesPerCycle = 257; // 256 intervalů na 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));
// Doména [0, 1]: parametr shadingu dorazí do funkce beze změny
PatternName := Pdf.RegisterSampledAxialGradient(X0, 0, X1, 0, 0, 1,
Samples, SamplesPerCycle, 1);
if PatternName = '' then
Exit; // verze dokumentu uzamčena pod PDF 1.3
Pdf.CurrentPage.SetFillPattern(PatternName);
// Pás ořízne shading přesně na jeden cyklus, takže Extend se nikdy neukáže
Pdf.CurrentPage.Rectangle(X0, Y, Math.Min(X1, X + Width) - X0, Height);
Pdf.CurrentPage.Fill;
end;
end;
Velmi dlouhé rozsahy potřebují jiný trik, protože jeden shading na cyklus znamená jeden resource na cyklus. U periodických lineárních gradientů COLRv1 přepíná HotPDF na schéma zdvojování: listový form drží primitiv s jedním cyklem repeat nebo dvěma cykly reflect a každá úroveň Form XObject kreslí předchozí úroveň dvakrát, posunutou o svou šířku. Počet formů roste logaritmicky s rozsahem, zatímco prohlížeč pořád maluje každý viditelný cyklus, takže pokrytí po zarovnání fáze se stropuje na 65536 cyklových jednotek. Dřívější verze, která dávala primitiv do tiling patternu Type 1, byla zahozena, protože čtečky přizpůsobují rozestup dlaždic mřížce zařízení a Poppler se na cyklu, který nebyl celým počtem pixelů, dostal z fáze
Řešení rozsahu parametru pro rodiny radiálních kruhů
Radiální gradient pokryje glyf jen tehdy, když se rozsah cyklů spočte pro správnou rodinu kruhů, a radiální barvy COLRv1 padají do tří rodin s odlišnou matematikou. S D = c1 - c0 a dr = r1 - r0 má přísně vnořená rodina |dr| > |D|, nevnořený kužel |D|² > dr² a tečná rodina |D| = |dr|. HotPDF měří parametr od bodu s nulovým poloměrem původní rodiny a drží střed i poloměr každého vysílaného kruhu vázané na stejný původní parametr, takže přemapované záporné poloměry se nikdy neupravují každý zvlášť
- Vnořené rodiny protínají výplňový box monotónně, takže rozsah parametru pochází ze čtyř rohů plus bisekce za prvním kruhem, který se boxu dotkne. Až 64 cyklů se vzorkuje do jednoho shadingu po 256 vzorcích na cyklus, stropované na 4096 vzorků; hlubší rozsahy přecházejí na řezy po cyklu
- Kužely a pásy se stejným poloměrem používají oddělené krátké radiální shadings na původních hranicích cyklů, každý s 257 vzorky a vypnutým
Extend, skládané v původním pořadí parametrů uvnitř izolované non-knockout skupiny, takže největší platný parametr vyhraje tam, kde se kruhy překrývají - Tečné rodiny redukují rovnici kruhu na lineární. Vzhledem ke sdílenému tečnému bodu je kořen
q = |W|² / (2·(W·V))a pokrytá oblast je otevřená polorovina před tím bodem
Tečný případ má skutečnou singularitu. Když tečná linie protne výplňový box, počet cyklů potřebných k dosažení pixelů u té linie je neomezený, takže žádný konečný počet shadings ho nepokryje. HotPDF vynaloží rozpočet 64 cyklů od tečného bodu a zbývající tenký pás nechá průhledný. Když všech čtyři rohy boxu sedí za tečným bodem, je barva průhledná jako celek a zbytek glyfu se pořád nakreslí. Cesta konečného kuželu přijme až 4096 vyžadovaných cyklů, s pokrytím stropovaným na 1024 řezů a čímkoli za stropem nenamalovaným; glyf s 1024 řezy vyjde zhruba na 1,8 MB
Barva v lineárním světle a past dvou desetinných míst
COLRv1 interpoluje a skládá v lineárním světle, takže HotPDF dekóduje každý bajt CPAL inverzní přenosovou funkcí sRGB a maluje plné barvy, shadings i vnitřní skupiny v jednom prostoru CalRGB s bílým D65, primárkami sRGB a /Gamma [1 1 1] (§8.6.5.3). Jen hotová kořenová plocha se převede na blend prostor volajícího. Interpolace uložených bajtů sRGB místo toho dává viditelně tmavší středy, což je klasická gamma chyba, kterou udělá každý gradientní engine jednou
Přechod na lineární světlo odhalil starý bug formátovače. Tmavá hodnota kanálu CPAL 12 se v lineárním světle stane zhruba 0,0036765 a sdílený formátovač čísel za scn držel jen dvě desetinná místa, takže ta složka se zapsala jako 0, než se na soubor podíval jakýkoli prohlížeč. Symptom vypadal přesně jako kvantizace čtečky a první instinkt byl obvinit 8bitové skupinové buffery MuPDF. Složky výplně a tahu teď procházejí desetinným formátovačem bez exponentu, který drží malé hodnoty a ignoruje locale procesu. Lekce se zobecňuje: když zmizí tmavá barva, vydumpujte content stream a zkontrolujte zapsaný operand, než budete zkoumat prohlížeč
Prohlížeč po té opravě pořád hraje roli. MuPDF 1.28.2 ztrácí téměř nulové lineární kanály, když skládá izolovanou průhlednostní skupinu, která nese explicitní blend barevný prostor, a Poppler 26.07.0 Splash ignoruje skupinové blend prostory úplně. Volitelná vlastnost THotPDF.COLRv1DeviceSpaceGroupBlend (výchozí False) vysílá skupiny COLRv1 bez blend barevného prostoru, takže se skládají v děděném prostoru zařízení, zatímco barvy obsahu, shadings a funkce zůstávají v lineárním světle CalRGB. Kompromis je, že se překrývající poloprůhledné vrstvy pak skládají v prostoru zařízení v prohlížečích, které skupinové barevné prostory ctí
Které kompozitní režimy COLRv1 umí PDF vyjádřit přesně?
HotPDF vykresluje kompozitní režimy COLRv1 5 až 8 (SRC_IN, DEST_IN, SRC_OUT, DEST_OUT) nativně a režimy 9 až 12 (SRC_ATOP, DEST_ATOP, XOR, PLUS) posílá do OnColorGlyphRasterize. Režimy in a out jsou produkty jedné plochy s alfou druhé, což je přesně to, co spočítá soft maska (§11.6.5). Přežívající operand se zachytí jako obyčejný form; maskovaný operand se zachytí v alfa průchodu jako plochý obsah uvnitř jedné izolované skupiny DeviceGray a použije jako maska /Luminosity s /BC [0]. Režimy out zapisují 1 − α pro každou alfu a dávají neprůhledný bílý kryt pod obsah masky, takže přeživší zůstane vidět tam, kde druhý operand nikdy nemaloval
Ostatní čtyři potřebují premultiplied sčítání dvou ploch. V modelu source-over v PDF se term pozadí vždy utlumí (1 − α) zdroje a neexistuje primitiv, který by přičítal dvě plochy nebo škáloval pozadí jen krytím zdroje, takže atop, xor a plus se nedají napsat jako vektory. Jedno další omezení přišlo z holých testovacích PDF: oba prohlížeče špatně počítají skupiny soft masek, jejichž obsah vnořuje form Do, takže maskovaný operand musí být plochý. Kvalifikují plné barvy, pad gradienty a wrapper PaintGlyph kolem nich; vrstvy, transformace a periodické gradienty, které všechny produkují vnitřní formy, posílají glyf na fallback
var
Pdf: THotPDF;
Fallback: TEmojiFallback; // handler třída se signaturou události níže
begin
Pdf := THotPDF.Create(nil);
Fallback := TEmojiFallback.Create;
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'periodic-icons.pdf';
// Volitelné: skládání skupin v prostoru zařízení pro MuPDF a Poppler
Pdf.COLRv1DeviceSpaceGroupBlend := True;
// Přijímá glyfy atop/xor/plus a cokoli jiného mimo podmnožinu:
// (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;
Kde vektorová cesta pořád zaostává
Vektorový výstup není totéž jako identické pixely v každém prohlížeči a akceptační záznamy HotPDF to říkají nahlas. Vzorky do jednoho či dvou kroků od hranice cyklu se vyhlazují interpolací čtečky, takže pixelové kontroly vzorkují mimo švy a streamy funkcí se ověřují dekódováním. Interpolace odděleně uložených vzorků straight RGB a alfy je aproximace u nízkých alfa koncových bodů a tvrdých zastávek; změřená chyba je druhého řádu při 256 vzorcích na cyklus, ale není to přesnost. Poppler Splash vzorkuje na levé hraně pixelu místo jeho středu, což se projeví malými rozdíly na nejstrmějších rampách
I rozpočty jsou skutečné limity. Vnořené radiální rodiny se zdráhají nad rozsahy 4096 cyklů, kužely a tečné rodiny nad 4096 vyžadovanými cykly a tečný pás u singularní linie zůstává prázdný. Proměnná alfa zastávek potřebuje průhlednost PDF 1.4, takže není povolená pod PDF/A-1, PDF/X-1a ani PDF/X-3. Když glyf spadne mimo jakýkoli z těchto limitů, DrawRegisteredColorGlyph odmítne celý graf dřív, než zapíše operátor, což je stejné all-or-nothing chování jako dřív
FAQ
Rasterizuje HotPDF gradienty COLRv1 repeat a reflect?
Ne. Novější verze HotPDF je vysílají jako vektorové axial a radial shadings pokrývající celé cykly. Jen grafy mimo podporovanou podmnožinu, jako kompozity atop, xor a plus, jdou do OnColorGlyphRasterize
Proč nepoužít jednu vzorkovanou funkci pro všechny cykly?
Čtečky předpočítávají tabulku barev z funkce a tabulka MuPDF má 256 položek. Rozprostřená přes mnoho cyklů interpoluje přes každý repeat šev. Jeden shading na cyklus udrží každou tabulku uvnitř jediného cyklu
Kdy zapnout COLRv1DeviceSpaceGroupBlend?
Zapínejte ho, když téměř černé kanály v glyfech COLRv1 padají na nulu v MuPDF. Nechávejte ho vypnutý, když se musí překrývající se poloprůhledné vrstvy skládat v lineárním světle v prohlížečích, které ctí skupinové barevné prostory
Nativní gradienty COLRv1, barevná emoji, vzorkované shadings i průhlednostní skupiny jsou všechny součástí HotPDF Component for Delphi and C++Builder, který generuje PDF dokumenty přímo z kódu Delphi a C++Builderu