Technický článek

Gradienty COLRv1 repeat/reflect jako PDF shadings v Delphi

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

Mapování COLRv1 na PDF shading v HotPDF pro Delphi: EXTEND_PAD odpovídá příznaku pokračování /Extend axial a radial shadings, zatímco EXTEND_REPEAT a EXTEND_REFLECT nemají PDF ekvivalent a musí se postavit z geometrie shadingu po cyklu
PDF shadings jen udrží barvy na t0 a t1, takže gradient COLRv1 repeat nebo reflect musí producent rozložit cyklus po cyklu, ne přepnout prohlížeči

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á

Diagram švů repeat gradientu COLRv1 v HotPDF pro Delphi: jedna vzorkovaná funkce přes 11 cyklů dovolí 256položkové tabulce barev MuPDF vyhladit každý repeat šev do rampy, zatímco jeden axial shading na cyklus udrží každou tabulku uvnitř jediného cyklu a zachová tvrdý skok barvy
Čtečky předpočítávají pevnou tabulku barev na shading, takže oprava je geometrická: jeden shading na cyklus s vypnutým Extend posune repeat šev na hranici, kterou žádná tabulka nikdy neinterpoluje přes

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ášť

Rozhodovací strom radiálního gradientu COLRv1 v HotPDF pro Delphi: srovnání dr s D oddělí vnořené rodiny kruhů řešené bisekcí, kužely řešené krátkými radiálními shadings po cyklu a tečné rodiny redukované na lineární kořen q = |W|^2 / (2(W*V))
Nerovnost mezi |dr| a |D| rozhoduje o cyklové matematice: vnořené rodiny půlí za prvním se dotýkajícím kruhem, kužely dostanou jeden krátký shading na cyklus a tečné linie nechají singularní pás průhledný
  • 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