HotPDF рисует цветные emoji в PDF через THotPDF.DrawRegisteredColorGlyph, который читает цветовые данные шрифта, зарегистрированного RegisterUnicodeTTF, и выпускает их как нативную PDF-графику: слои COLR v0 как заполненные контуры глифов, графы отрисовки COLR v1 как клипы, shading и blend mode, SVG-глифы как Form XObject, а битмапы CBDT или sbix как изображения. Всё, что не удаётся отобразить нативно, уходит в событие OnColorGlyphRasterize вместо того, чтобы молча превратиться в чёрную кляксу
Последняя оговорка — вся причина существования этого кода. Встройте emoji-шрифт обычным способом, и вьювер возьмёт контур из glyf или CFF, залитый тем, каким случилось быть текущему цвету заливки. Улыбающееся лицо приходит чёрной кляксой, флаг — прямоугольником, и ни одна ступень конвейера не жалуется
Почему цветной emoji печатается чёрным силуэтом в PDF?
У шрифтовой программы PDF нет понятия цветных глифов. ISO 32000-1 считает глиф фигурой, закрашенной текущим цветом, а цветовые таблицы, добавленные в OpenType позже, — COLR/CPAL, SVG , CBDT/CBLC и sbix — не входят в imaging model PDF, так что ни один вьювер не обязан читать их из встроенного шрифта. Цвет приходится переводить в содержимое страницы в момент генерации, пока производитель ещё держит байты шрифта и знает, какой глиф ему нужен. Перевод у каждого формата свой, и дикие emoji-шрифты используют их все: слоистые векторы, градиентные графы отрисовки, встроенные SVG-документы и PNG-страйки. HotPDF сообщает результат как THPDFOpenTypeColorFormat со значениями otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG и otcfSBIX и прощупывает шрифт в фиксированном приоритете: сначала COLR, затем SVG, затем CBDT, затем sbix. Векторы бьют битмапы, когда шрифт несёт и то и другое, — именно это вам нужно в документе, который могут увеличить или распечатать
Один вызов, пять форматов: разрешаем и рисуем цветной глиф
THotPDF.GetRegisteredColorGlyphInfo отвечает, каким путём пойдёт кодовая точка, а DrawRegisteredColorGlyph этим путём её ведёт. Оба ищут кодовую точку в character map шрифта, последним переданного в RegisterUnicodeTTF, так что цветной шрифт на момент вызова обязан быть зарегистрированным Unicode-шрифтом. Рисующая функция возвращает False, когда у глифа нет цветовых данных или ни один путь не смог его отрисовать, и оставляет fallback вам
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, палитра CPAL 0, bitmap-страйк, ближайший к 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
// Цветовых данных нет: откат к монохромному контуру
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;
Два параметра заслуживают внимания. PaletteIndex выбирает палитру CPAL, так что шрифт с палитрой под тёмный фон переключается, не трогая глиф. TargetPixelsPerEm важен только для битмапных шрифтов; при нуле он берёт Round(FontSize * 96 / 72) — экранное разрешение, почему пример и просит 300 для печатного вывода. Честный предел сидит в сигнатуре: вызов берёт одну кодовую точку и отображает её только через cmap. ZWJ-последовательности, модификаторы тона кожи и флаги из regional indicator — это лигатуры GSUB, так что их сборка — проблема шейпинга из статьи об альтернатах OpenType GSUB, а не то, что эта точка входа делает за вас
COLR v0: слои глифов стопкой с цветами из палитры
COLR v0 — простой случай, и HotPDF рендерит его напрямую: каждый базовый глиф перечисляет глифы-слои с записью цвета CPAL, и каждый слой становится одной обычной операцией показа текста со своей заливкой, сложенной в порядке таблицы. Слой с альфой ниже 255 получает словарь параметров графического состояния с совпадающими /ca и /CA (ISO 32000-1 §8.4.5), и каждый глиф-слой помечается как используемый, чтобы сабсеттер сохранил его контур, хоть к нему и не ведёт ни одна кодовая точка. Одна деталь удивляет: индекс записи палитры 0xFFFF по спецификации OpenType значит «использовать цвет переднего плана текста», и HotPDF разрешает его в чёрный, а не в текущую заливку страницы. Для emoji-шрифтов это редко важно; для иконочных шрифтов, живущих записью переднего плана, чтобы тонировать глиф, проверяйте вывод, прежде чем рассчитывать, что он пойдёт за цветом вашего текста
Как HotPDF превращает граф отрисовки COLR v1 в PDF-операторы?
Тем, что сперва разбирает таблицы отрисовки в плоский ограниченный граф и лишь затем отображает каждый узел в PDF-конструкцию. Глиф COLR v1 — не список слоёв, а направленный ациклический граф записей отрисовки, где узлы могут разделяться через PaintColrLayers и PaintColrGlyph. Парсер ограничивает его 4096 узлами отрисовки, 64 уровнями глубины и 1024 color stop, и ведёт каждый узел как активный или завершённый, так что ссылка назад на активный узел — цикл, который зловредный шрифт может собрать из переиспользования слоёв, — отвергается, а не рекурсируется. Смещения баз — то место, где ошибается первая реализация. Смещения BaseGlyphPaintRecord отсчитываются от начала BaseGlyphList, смещения отрисовки LayerList — от LayerList, а каждый Offset24 внутри таблицы отрисовки — от самой этой таблицы. Разрешить все три от одной базы — и совершенно легальные глифы валятся на проверке границ, что выглядит точь-в-точь как битый шрифт. Когда граф построен, отображение прямое:
PaintGlyphставит контур глифа как клип с режимом рендеринга текста 7 (ISO 32000-1 §9.3.6), затем красит своего ребёнка внутри него- Сплошные заливки красят обрезанный прямоугольник; линейные градиенты становятся многостоповыми axial shading, а радиальные — двухцветными radial shading (§8.7.4.5)
- Sweep-градиенты не имеют PDF-эквивалента, поэтому HotPDF аппроксимирует их 96 клиньями плоских цветов, каждый сэмплится с цветовой линии
- Трансформации выпускаются как
cm, сопряжённые вокруг начала базовой линии глифа, со сдвигами, промасштабированными наFontSize / UnitsPerEm - Режимы
PaintCompositeс 13 по 27 отображаются на разделимые и неразделимые PDF blend mode вроде/Multiply,/Screenи/Luminosity(§11.3.5), выставляемые записью/BMв ExtGState
Граница объявлена явно. Режимы Porter-Duff с 5 по 12 (src_in, xor, plus и остальные) не имеют PDF-аналога среди blend mode, режимы расширения repeat и reflect на линейных и радиальных градиентах не выпускаются, а градиенты, чьи стопы несут разные альфы, не подделываются одной непрозрачностью. Радиальные градиенты с более чем двумя стопами сохраняют только первый и последний цвета. HotPDF сверяет весь граф с этим поддерживаемым подмножеством до записи первого оператора, поэтому неподдерживаемый глиф оставляет страницу нетронутой и переходит к raster fallback, вместо того чтобы бросить полкартинки
SVG-глифы и bitmap-страйки
SVG-глифы идут через тот же ограниченный билдер, которым HotPDF пользуется для импортируемых SVG-файлов, и результат регистрируется как Form XObject (§8.10) — ровно как описано в статье о SVG в Form XObject. Документ в таблице SVG может быть сжат gzip; декомпрессия идёт чанками по 8 KB и останавливается, как только развёрнутый размер перевалил бы за 32 MB, вместо того чтобы раздуть и потом проверить, а сам сжатый вход ограничен 8 MB. Профиль ограничен намеренно: скрипты, встроенные изображения, внешние URL, data: URI и нелокальные ссылки отказывают закрыто. Форма масштабируется так, чтобы её длинная сторона равнялась размеру шрифта, и якорится на базовой линии, что отображает систему координат SVG с y вниз на PDF-овскую с y вверх. Учтите: билдер получает весь SVG-документ глифа без выборки элемента glyphNNN, поэтому шрифты, пакующие много глифов в один общий документ, стоит протестировать, прежде чем полагаться на них
Битмапные шрифты — вопрос выбора страйка и размещения. Для CBDT HotPDF берёт размер CBLC, чей вертикальный ppem ближе всех к TargetPixelsPerEm, принимает форматы изображений 17, 18 и 19, а метрики формата 19 читает из подтаблицы индекса CBLC, потому что сам этот формат своих не хранит. Для sbix смещения страйков отсчитываются от таблицы, смещения глифов — от страйка, а запись dupe переиспользует графику другого глифа, храня собственные смещения origin; дать рекурсии затереть внешний origin — значит сдвинуть изображение. Нагрузки PNG и JPEG декодируются внутри, масштабируются на FontSize / PixelsPerEmY, а не растягиваются до размера шрифта, и пишутся с soft mask (§11.6.5.3), когда хоть один пиксель не полностью непрозрачен. Нагрузки sbix TIFF не декодируются и уходят в событие
Что происходит, когда глиф нельзя нарисовать нативно?
HotPDF поднимает OnColorGlyphRasterize и кладёт тот RGBA-битмап, который вернёт ваш хендлер; если ничего не назначено или хендлер оставил Handled ложным, DrawRegisteredColorGlyph возвращает False, и страница остаётся без изменений. Событие срабатывает для графа COLR v1 вне поддерживаемого подмножества, для SVG-документа, отвергнутого безопасным билдером, и для битмапной нагрузки, которую внутренние декодеры не читают. Хендлер получает формат, сырые байты шрифта, извлечённый ассет (SVG-документ, возможно ещё сжатый gzip, или байты битмапа; пусто для COLR v1), идентификатор глифа, палитру и целевой размер в пикселях
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 — ваш растеризатор, а не API HotPDF.
// Он обязан вернуть ровно Width * Height * 4 байт RGBA.
Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;
// Подключение
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
HotPDF валидирует вывод хендлера до прикосновения к странице: нулевые размеры, буфер, чья длина не ровно Width * Height * 4, или размерности, способные переполниться, отвергаются, и вызов возвращает False. Raster fallback — всё равно растр, так что emoji, отрисованный так, теряет векторную резкость; запрашивайте PixelSize, соответствующий вашему разрешению вывода. Поставьте цветовой путь рядом с проверками покрытия при отрисовке из статьи об отслеживании отсутствующих глифов, и конвейер, переваривающий произвольный пользовательский текст, сможет сообщать и о недостающих глифах, и о глифах, потерявших цвет
Рендерер цветных глифов, стек шейпинга OpenType и безопасный SVG-билдер выходят в HotPDF Delphi PDF component для Delphi и C++Builder