HotPDF rysuje kolorowe emoji do PDF przez THotPDF.DrawRegisteredColorGlyph, które czyta dane koloru fontu zarejestrowanego przez RegisterUnicodeTTF i emituje je jako natywną grafikę PDF: warstwy COLR v0 jako wypełnione kontury glifów, grafy malowania COLR v1 jako klipy, cieniowania i tryby mieszania, glify SVG jako Form XObjects, a bitmapy CBDT albo sbix jako obrazy. Czokolwiek, czego nie da się zmapować natywnie, idzie do zdarzenia OnColorGlyphRasterize, zamiast po cichu zamieniać się w czarny kształt
Ta ostatnia klauzula to cały powód istnienia tego kodu. Osadź font emoji zwyczajnie, a przeglądarka dostanie kontur z glyf albo CFF, wypełniony tym, czym akurat jest bieżący kolor wypełnienia. Uśmiechnięta buźka przychodzi jako czarna plama, flaga jako prostokąt, a nic w pipeline nie narzeka
Dlaczego kolorowe emoji drukuje się jako czarna sylwetka w PDF?
Program fontu w PDF nie ma pojęcia o kolorowych glifach. ISO 32000-1 traktuje glif jako kształt malowany bieżącym kolorem, a tabele koloru dodane później przez OpenType, czyli COLR/CPAL, SVG , CBDT/CBLC i sbix, nie są częścią modelu obrazowania PDF, więc żadna przeglądarka nie ma obowiązku czytać ich z osadzonego fontu. Kolor musi zostać przetłumaczony na treść strony w czasie generowania, póki producent wciąż trzyma bajty fontu i wie, którego glifa chce. To tłumaczenie różni się per format, a fonty emoji na wolności używają ich wszystkich: warstwowych wektorów, gradientowych grafów malowania, osadzonych dokumentów SVG i strike'ów PNG. HotPDF raportuje wynik jako THPDFOpenTypeColorFormat, z wartościami otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG i otcfSBIX, i sonduje font w sztywnej kolejności: najpierw COLR, potem SVG, potem CBDT, potem sbix. Dane wektorowe wygrywają z bitmapami, ilekroć font niesie oba, czego chcesz w dokumencie, który może być powiększany albo drukowany
Jedno wywołanie, pięć formatów: rozwiązywanie i rysowanie kolorowego glifa
THotPDF.GetRegisteredColorGlyphInfo odpowiada, którą ścieżkę pójdzie punkt kodowy, a DrawRegisteredColorGlyph ją realizuje. Obie szukają punktu kodowego w mapie znaków fontu ostatnio przekazanego do RegisterUnicodeTTF, więc font kolorowy musi być w chwili wywołania zarejestrowanym fontem Unicode. Funkcja rysująca zwraca False, gdy glif nie ma danych koloru albo żadna ścieżka nie umiała go wyrenderować, i zostawia fallback tobie
const
FormatNames: array[THPDFOpenTypeColorFormat] of string =
('none', 'COLR v0', 'COLR v1', 'CBDT', 'SVG', 'sbix');
var
Pdf: THotPDF;
Info: THPDFOpenTypeColorGlyphInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'emoji.pdf';
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\seguiemj.ttf');
// U+1F600, paleta CPAL 0, strike bitmapowy najbliższy 300 ppem
if Pdf.GetRegisteredColorGlyphInfo($1F600, 0, 300, Info) then
Writeln(Format('GID %d via %s',
[Info.GlyphID, FormatNames[Info.Format]]));
if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $1F600,
72, 144, 'Segoe UI Emoji', 36, 0, 300) then
begin
// Brak danych koloru: fallback na monochromatyczny kontur
Pdf.CurrentPage.SetFont('Segoe UI Emoji', [], 36, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 144, 0, WideString(#$D83D#$DE00));
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dwa parametry zasługują na uwagę. PaletteIndex wybiera paletę CPAL, więc font, który dowozi paletę pod ciemne tło, można przełączyć bez dotykania glifa. TargetPixelsPerEm ma znaczenie tylko dla fontów bitmapowych; zostawione na zerze daje domyślnie Round(FontSize * 96 / 72), rozdzielczość ekranową, dlatego przykład prosi o 300 przy wyjściu do druku. Uczciwy limit siedzi w sygnaturze: wywołanie bierze jeden punkt kodowy i mapuje go wyłącznie przez cmap. Sekwencje ZWJ, modyfikatory odcienia skóry i flagi regional-indicator to ligatury GSUB, więc ich składanie to problem shapingowy rodzaju opisanego w artykule o wariantach GSUB w OpenType, a nie coś, co ten punkt wejścia robi za ciebie
COLR v0: piętrzone warstwy glifów w kolorach z palety
COLR v0 to prosty przypadek i HotPDF renderuje go wprost: każdy glif bazowy wypisuje glify warstw z wpisem koloru CPAL, a każda warstwa staje się jedną zwyczajną operacją pokazywania tekstu z własnym kolorem wypełnienia, piętrowaną w kolejności tabeli. Warstwa z alfą poniżej 255 dostaje słownik parametrów stanu grafiki z pasującymi /ca i /CA (ISO 32000-1 §8.4.5), a każdy glif warstwy jest znacznikowany jako używany, więc subsetter zachowa jego kontur, choć żaden punkt kodowy nie mapuje się na niego wprost. Jeden szczegół zaskakuje: indeks wpisu palety 0xFFFF znaczy w specyfikacji OpenType „użyj koloru pierwszego planu tekstu”, a HotPDF rozwiązuje go na czarny, a nie na bieżący kolor wypełnienia strony. Dla fontów emoji to rzadko gra rolę; dla fontów ikonowych, które polegają na wpisie pierwszego planu, by barwić glifa, sprawdź wyjście, zanim założysz, że pójdzie za kolorem tekstu
Jak HotPDF zamienia graf malowania COLR v1 na operatory PDF?
Przez sparsowanie tabel malowania najpierw na płaski, ograniczony graf i dopiero potem mapowanie każdego węzła na konstrukcję PDF. Glif COLR v1 to nie lista warstw, tylko skierowany acykliczny graf rekordów malowania, w którym węzły bywają współdzielone przez PaintColrLayers i PaintColrGlyph. Parser tnie go na 4096 węzłów malowania, 64 poziomy głębokości i 1024 kolorostopy i prowadzi ewidencję każdego węzła jako aktywnego albo skończonego, więc odwołanie z powrotem do aktywnego węzła, cykl, jaki złośliwy font zbuduje z reużycia warstw, jest odrzucane, a nie rekursowane. Bazy offsetów to miejsce, gdzie pierwsza implementacja się wywraca. Offsety BaseGlyphPaintRecord są względem początku BaseGlyphList, offsety malowania LayerList są względem LayerList, a każdy Offset24 wewnątrz tabeli malowania jest względem tej tabeli malowania samej. Rozwiąż wszystkie trzy względem tej samej bazy, a całkowicie legalne glify padają na sprawdzeniu granic, co wygląda dokładnie jak uszkodzony font. Gdy graf stoi, mapowanie jest wprost:
PaintGlyphustawia kontur glifa jako klip z trybem renderowania tekstu 7 (ISO 32000-1 §9.3.6), po czym maluje swoje dziecko wewnątrz niego- Pełne malowania wypełniają przycięty prostokąt; gradienty liniowe stają się osiowymi cieniowaniami wielostopowymi, a gradienty promieniste dwukolorowymi cieniowaniami promienistymi (§8.7.4.5)
- Gradienty sweep nie mają odpowiednika w PDF, więc HotPDF przybliża je 96 klinami o płaskim kolorze, każdy próbkowany z linii koloru
- Transformacje są emitowane jako
cm, sprzężone wokół początku linii bazowej glifa, z przesunięciami skalowanymi przezFontSize / UnitsPerEm - Tryby
PaintCompositeod 13 do 27 mapują się na rozdzielalne i nierozdzielalne tryby mieszania PDF, takie jak/Multiply,/Screeni/Luminosity(§11.3.5), ustawiane wpisem/BMw ExtGState
Granica jest jawna. Tryby Porter-Duff od 5 do 12 (src_in, xor, plus i reszta) nie mają odpowiednika wśród trybów mieszania PDF, tryby rozszerzania repeat i reflect na gradientach liniowych i promienistych nie są emitowane, a gradienty, których stopy niosą różne wartości alfy, nie są imitowane pojedynczą przezroczystością. Gradienty promieniste z więcej niż dwoma stopami zachowują tylko pierwszy i ostatni kolor. HotPDF sprawdza cały graf względem tego obsługiwanego podzbioru, zanim napisze choćby jeden operator, więc nieobsługiwany glif zostawia stronę nietkniętą i przechodzi do fallbacku rastrowego, zamiast zostawiać połowę rysunku
Glify SVG i strike'e bitmapowe
Glify SVG idą przez ten sam ograniczony builder, którego HotPDF używa dla importowanych plików SVG, a wynik jest rejestrowany jako Form XObject (§8.10), dokładnie jak opisano w artykule o konwersji SVG na Form XObject. Dokument w tabeli SVG bywa skompresowany gzipem; dekompresja biegnie w kawałkach po 8 KB i zatrzymuje się, jak tylko rozmiar po rozwinięciu miałby przekroczyć 32 MB, zamiast najpierw rozdmuchiwać i sprawdzać potem, a samo skompresowane wejście ma limit 8 MB. Profil jest restrykcyjny z premedytacją: skrypty, osadzone obrazy, zewnętrzne URL-e, URI data: i odwołania nielokalne zawodzą w trybie fail-closed. Formularz jest skalowany tak, by jego dłuższy bok równał się rozmiarowi fontu, i kotwiczony na linii bazowej, co mapuje układ współrzędnych SVG y-w-dół na y-w-górę w PDF. Miej świadomość, że builder dostaje cały dokument SVG dla glifa, bez selekcji elementu glyphNNN, więc fonty pakujące wiele glifów w jeden współdzielony dokument warto przetestować, zanim im zaufasz
Fonty bitmapowe to kwestia wyboru strike'a i umiejscowienia. Dla CBDT HotPDF wybiera rozmiar CBLC, którego pionowe ppem jest najbliższe TargetPixelsPerEm, przyjmuje formaty obrazów 17, 18 i 19, a metryki formatu 19 czyta z podtabeli indeksu CBLC, bo ten format nie przechowuje żadnych własnych. Dla sbix offsety strike'ów są względem tabeli, a offsety glifów względem strike'a, a rekord dupe reużywa grafiki innego glifa, zachowując własne offsety początku; pozwolenie rekursji nadpisać zewnętrzny początek przesuwa obraz. Ładunki PNG i JPEG są dekodowane wewnętrznie, skalowane przez FontSize / PixelsPerEmY zamiast rozciągane do rozmiaru fontu, i zapisywane z miękką maską (§11.6.5.3), gdy tylko jakikolwiek piksel nie jest w pełni kryjący. Ładunki sbix TIFF nie są dekodowane i idą do zdarzenia
Co się dzieje, gdy glifa nie da się narysować natywnie?
HotPDF odpala OnColorGlyphRasterize i umieszcza cokolwiek, co zwróci twój handler jako bitmapę RGBA; jeśli nic nie jest przypisane albo handler zostawia Handled na false, DrawRegisteredColorGlyph zwraca False, a strona zostaje bez zmian. Zdarzenie odpala się dla grafu COLR v1 poza obsługiwanym podzbiorem, dokumentu SVG odrzuconego przez bezpieczny builder i ładunku bitmapowego, którego wewnętrzne dekodery nie czytają. Handler dostaje format, surowe bajty fontu, wyciągnięty zasób (dokument SVG, być może wciąż w gzipie, albo bajty bitmapy; pusto dla COLR v1), identyfikator glifa, paletę i docelowy rozmiar w pikselach
type
TEmojiFallback = class
public
procedure Rasterize(Sender: TObject;
Format: THPDFOpenTypeColorFormat; const FontBytes: TBytes;
const AssetData: TBytes; GlyphID: Word;
PaletteIndex, PixelSize: Integer;
out Width, Height: Integer; out RGBA: TBytes;
out Handled: Boolean);
end;
procedure TEmojiFallback.Rasterize(Sender: TObject;
Format: THPDFOpenTypeColorFormat; const FontBytes: TBytes;
const AssetData: TBytes; GlyphID: Word;
PaletteIndex, PixelSize: Integer;
out Width, Height: Integer; out RGBA: TBytes;
out Handled: Boolean);
begin
Width := 0;
Height := 0;
RGBA := nil;
// RenderWithOwnEngine to twój rasteryzer, a nie API HotPDF.
// Musi zwrócić dokładnie Width * Height * 4 bajtów RGBA.
Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;
// Podpięcie
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
HotPDF waliduje wyjście handlera, zanim dotknie strony: rozmiary zerowe, bufor o długości innej niż dokładnie Width * Height * 4 albo wymiary na tyle duże, by przepełnić się, są odrzucane, a wywołanie zwraca False. Fallback rastrowy to nadal raster, więc emoji wyrenderowane tą drogą traci wektorową ostrość; proś o PixelSize pasujący do rozdzielczości wyjściowej. Sparuj ścieżkę koloru z kontrolami pokrycia w czasie rysowania z artykułu o śledzeniu brakujących glifów, a pipeline obsługujący dowolny tekst użytkownika będzie umiał zgłosić i brakujące glify, i glify, które straciły kolor
Rasteryzer kolorowych glifów, stack shapingu OpenType i bezpieczny builder SVG trafiają razem do komponentu PDF HotPDF dla Delphi, dostępnego dla Delphi i C++Buildera