HotPDF малює кольорові emoji в PDF через THotPDF.DrawRegisteredColorGlyph, який читає кольорові дані шрифту, зареєстрованого через RegisterUnicodeTTF, і випромінює їх як нативну PDF-графіку: шари COLR v0 як залиті контури гліфів, paint-графи COLR v1 як кліпи, шейдінги та blend modes, SVG-гліфи як Form XObject-и, а бітмапи CBDT чи sbix — як зображення. Усе, що не мапиться нативно, іде в подію OnColorGlyphRasterize замість того, щоб тихо перетворитися на чорну фігуру
Останнє зауваження — вся причина, з якої цей код існує. Вбудуйте emoji-шрифт звичайним способом, і переглядач отримає контур із glyf чи CFF, залитий тим, яким випадково виявиться поточний колір заливки. Усміхнене обличчя приїжджає чорною кляксою, прапор — прямокутником, і ніщо в конвеєрі не скаржиться
Чому кольоровий emoji друкується чорним силуетом у PDF?
Шрифтова програма PDF не має поняття про кольорові гліфи. ISO 32000-1 трактує гліф як фігуру, залиту поточним кольором, а кольорові таблиці, які OpenType додав пізніше, а саме COLR/CPAL, SVG , CBDT/CBLC і sbix, не є частиною PDF-моделі зображення, тож жоден переглядач не зобов'язаний читати їх із вбудованого шрифту. Колір мусить бути перекладений у вміст сторінки в момент генерації, поки продюсер ще тримає байти шрифту і знає, який гліф йому потрібен. Той переклад різниться за форматом, і emoji-шрифти в реальних даних використовують усі: нашаровані вектори, градієнтні paint-графи, вбудовані SVG-документи і PNG-страйки. HotPDF звітує результат як THPDFOpenTypeColorFormat зі значеннями otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG і otcfSBIX і пробує шрифт у фіксованому пріоритеті: спершу COLR, потім SVG, потім CBDT, потім sbix. Векторні дані перемагають бітмапи щоразу, коли шрифт несе обидва, — саме те, чого хочеш у документі, який можуть зумити чи надрукувати
Один виклик, п'ять форматів: розв'язання і малювання кольорового гліфа
THotPDF.GetRegisteredColorGlyphInfo відповідає, який шлях обере кодова точка, а DrawRegisteredColorGlyph ним малює. Обидва шукають кодову точку в таблиці символів шрифту, останнього переданого в 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 strike, найближчий до 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
// Немає кольорових даних: fallback на монохромний контур
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-лігатури, тож їхнє складання — проблема шейпінгу того роду, що розібрана в статті про GSUB-альтернати OpenType, а не те, що ця точка входу робить за вас
COLR v0: нашаровані шари гліфів з палітровими кольорами
COLR v0 — простий випадок, і HotPDF рендерить його прямо: кожен базовий гліф перелічує шарові гліфи з кольоровим записом CPAL, і кожен шар стає однією звичайною операцією показу тексту з власним кольором заливки, складеною в порядку таблиці. Шар з альфою нижче 255 отримує словник параметрів графічного стану з відповідними /ca і /CA (ISO 32000-1 §8.4.5), а кожен шаровий гліф позначається як ужитий, тож сабсетер тримає його контур, хоча жодна кодова точка не мапиться на нього прямо. Одна деталь дивує: індекс запису палітри 0xFFFF означає в специфікації OpenType "використай колір тексту на передньому плані", а HotPDF розв'язує його в чорний, а не в поточний колір заливки сторінки. Для emoji-шрифтів це рідко має значення; для іконкових шрифтів, які покладаються на передньопланний запис, щоб тінити гліф, перевіряйте вивід, перш ніж сподіватися, що він піде за кольором вашого тексту
Як HotPDF перетворює paint-граф COLR v1 на PDF-оператори?
Спершу paint-таблиці розбираються в плаский обмежений граф, і лише потім кожен вузол мапиться на PDF-конструкцію. Гліф COLR v1 — не список шарів, а орієнтований ациклічний граф paint-записів, де вузли можуть ділитися через PaintColrLayers і PaintColrGlyph. Парсер обмежує його 4096 paint-вузлами, 64 рівнями глибини і 1024 колірними стопами і трекає кожен вузол як активний чи завершений, щоб посилання назад на активний вузол — цикл, який зловмисний шрифт може збудувати з повторного використання шарів, — відкидалося, а не рекурсувалося. Основи зсувів — місце, де перша реалізація помиляється. Зсуви BaseGlyphPaintRecord відносні до початку BaseGlyphList, paint-зсуви LayerList відносні до LayerList, а кожен Offset24 усередині paint-таблиці відносний до самої тієї paint-таблиці. Розв'язати всі три від однієї основи — і цілком легальні гліфи провалюють перевірку меж, що виглядає рівно як пошкоджений шрифт. Щойно граф збудовано, відображення пряме:
PaintGlyphставить контур гліфа як кліп із режимом рендерингу тексту 7 (ISO 32000-1 §9.3.6), а потім малює всередині нього свою дитину- Суцільні фарби заливають обрізаний прямокутник; лінійні градієнти стають багатостоповими осьовими шейдінгами, а радіальні — двокольоровими радіальними шейдінгами (§8.7.4.5)
- Sweep-градієнти не мають PDF-еквівалента, тож HotPDF апроксимує їх 96 клинами суцільного кольору, кожен зразкований з колірної лінії
- Трансформації випромінюються як
cm, спряжені навколо початку базової лінії гліфа, зі зсувами, відмасштабованими наFontSize / UnitsPerEm - Режими
PaintCompositeз 13 по 27 відображаються на роздільні та нероздільні PDF blend modes на кшталт/Multiply,/Screenі/Luminosity(§11.3.5), що ставляться через запис/BMв ExtGState
Межа явна. Режими Porter-Duff з 5 по 12 (src_in, xor, plus та решта) не мають PDF-аналога серед blend modes, режими розширення repeat і reflect на лінійних та радіальних градієнтах не випромінюються, а градієнти, чиї стопи несуть різні значення альфи, не фейкаться однією прозорістю. Радіальні градієнти з більш ніж двома стопами тримають лише перший і останній кольори. HotPDF звіряє весь граф із цією підтримуваною підмножиною, перш ніж написати хоч один оператор, тож непідтримуваний гліф лишає сторінку недоторканою і переходить до raster-фолбеку замість того, щоб лишити позаду половину малюнка
SVG-гліфи та bitmap-страйки
SVG-гліфи йдуть крізь той самий обмежений білдер, який HotPDF використовує для імпортованих SVG-файлів, а результат реєструється як Form XObject (§8.10), рівно як описано в статті про SVG у Form XObject. Документ у таблиці SVG може бути стиснутий gzip; декомпресія йде шматками по 8 КБ і зупиняється щойно розгорнутий розмір мав би пройти 32 МБ, замість надувати спершу і перевіряти потім, а сам стиснутий вхід обмежений 8 МБ. Профіль обмежувальний навмисно: скрипти, вбудовані зображення, зовнішні URL, URI data: і нелокальні посилання падають закрито. Форма масштабується так, щоб її довший бік дорівнював розміру шрифту, і якориться на базовій лінії, що відображає систему координат SVG з y вниз на PDF-івську з y вгору. Майте на увазі: білдер отримує весь SVG-документ гліфа, без вибору елемента glyphNNN, тож шрифти, які пакують багато гліфів в один спільний документ, варто протестувати, перш ніж покладатися на них
Бітмапні шрифти — питання вибору страйка і розміщення. Для CBDT HotPDF вибирає розмір CBLC, чий вертикальний ppem найближчий до TargetPixelsPerEm, приймає формати зображень 17, 18 і 19 і читає метрики формату 19 із індексної субтаблиці CBLC, бо той формат не зберігає власних. Для sbix зсуви страйків відносні до таблиці, а зсуви гліфів — до страйка, і запис dupe перевикористовує графіку іншого гліфа, тримаючи власні зсуви початку; дозволити рекурсії перезаписати зовнішній origin — значить зсунути зображення. Payload-и PNG і JPEG декодуються всередині, масштабуються на FontSize / PixelsPerEmY, а не розтягуються до розміру шрифту, і пишуться з soft mask (§11.6.5.3), щоразу коли якийсь піксель не повністю непрозорий. TIFF payload-и sbix не декодуються і йдуть у подію
Що буває, коли гліф неможливо намалювати нативно?
HotPDF піднімає OnColorGlyphRasterize і розміщує той RGBA-бітмап, який поверне ваш обробник; якщо нічого не присвоєно або обробник лишає Handled хибним, DrawRegisteredColorGlyph повертає False, і сторінка лишається без змін. Подія спрацьовує на графі COLR v1 поза підтримуваною підмножиною, на SVG-документі, який безпечний білдер відмовив, і на bitmap payload-і, який внутрішні декодери не читають. Обробник отримує формат, сирі байти шрифту, витягнутий asset (SVG-документ, можливо досі в gzip, або байти бітмапа; порожньо для COLR v1), glyph ID, палітру і цільовий розмір у пікселях
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-фолбек — все одно растр, тож emoji, відрендерений так, втрачає векторну різкість; просіть PixelSize, що відповідає вашій роздільності виводу. Поєднайте кольоровий шлях із перевірками покриття під час малювання з статті про трекінг відсутніх гліфів, і конвеєр, що обробляє довільний користувацький текст, зможе звітувати і відсутні гліфи, і гліфи, що втратили колір
Рендерер кольорових гліфів, стек шейпінгу OpenType і безпечний SVG-білдер усі виходять у HotPDF Delphi PDF component, доступному для Delphi і C++Builder