Техническа статия

Цветни emoji шрифтове в PDF: COLR v1, SVG, битмапи в Delphi

HotPDF рисува цветни emoji в PDF чрез THotPDF.DrawRegisteredColorGlyph, който чете цветовите данни на шрифт, регистриран с RegisterUnicodeTTF, и ги излъчва като нативна PDF графика: COLR v0 слоеве като запълнени glyph контури, COLR v1 paint графи като клипове, shadings и blend режими, SVG глифове като Form XObjects, а CBDT или sbix битмапи като изображения. Каквото не може да се мапне нативно, отива на събитието OnColorGlyphRasterize, вместо тихо да се превърне в черна форма

Последната крачка е цялата причина този код да съществува. Вградете emoji шрифт по обичайния начин и viewer-ът получава контура от glyf или CFF, запълнен с какъвто и да е текущият fill цвят. Усмихнатото лице пристига като черно петно, знамето като правоъгълник, а нищо в линията не се оплаква

Защо цветно emoji излиза като черен силует в PDF?

PDF font програма няма понятие за цветни глифове. ISO 32000-1 третира глифа като форма, рисувана с текущия цвят, а цветовите таблици, които OpenType добави по-късно — COLR/CPAL, SVG , CBDT/CBLC и sbix — не са част от PDF imaging модела, така че нито един viewer не е длъжен да ги чете от вграден шрифт. Цветът трябва да се преведе в съдържание на страницата още при генерирането, докато producer-ът още държи байтовете на шрифта и знае кой глиф иска. Този превод е различен за всеки формат, а emoji шрифтовете в дивото ползват всички: наслоени вектори, paint графи с градиенти, вградени SVG документи и PNG strikes. HotPDF докладва резултата като THPDFOpenTypeColorFormat, със стойности otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG и otcfSBIX, и пробва шрифта в фиксиран приоритет: първо COLR, после SVG, после CBDT, после sbix. Векторните данни бият битмапите, щом шрифтът носи и двете, което е точно това, което искате в документ, който може да бъде увеличаван или печатан

Диаграма на probe-а за цветни глифове в HotPDF: PDF font програма рисува glyph контурите с текущия цвят, така че OpenType цветовите таблици COLR, SVG, CBDT и sbix трябва да се преведат в съдържание на страницата още при генерирането, а HotPDF пробва регистриран шрифт във фиксирания приоритет COLR, после SVG, после CBDT, после sbix, докладвайки THPDFOpenTypeColorFormat от otcfCOLRv0 до otcfSBIX
Векторните данни бият битмапите, щом шрифтът носи и двете — точно това искате в документ, който може да бъде увеличаван или печатан — а глиф без цветен път се оставя на вашия fallback

Едно извикване, пет формата: разрешаване и рисуване на цветен глиф

THotPDF.GetRegisteredColorGlyphInfo отговаря кой път ще вземе една кодова точка, а DrawRegisteredColorGlyph го поема. И двете търсят кодовата точка в character map-а на шрифта, подаден най-скоро на RegisterUnicodeTTF, така че цветният шрифт трябва да е регистрираният Unicode шрифт в момента на извикването. Draw функцията връща 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, битмап 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
      // Няма цветни данни: връщане към монохромния контур
      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 последователности, skin-tone модификатори и regional-indicator знамена са GSUB лигатури, така че сглобяването им е shaping проблем от рода, покрит в статията за OpenType GSUB алтернативи, а не нещо, което този входен пункт върши вместо вас

COLR v0: наслагвани glyph слоеве с палитрени цветове

COLR v0 е простият случай и HotPDF го рендира директно: всеки базов глиф изброява layer глифове с CPAL цветови запис, а всеки слой става една обикновена операция за показване на текст със собствен fill цвят, наслагвана в реда на таблицата. Слой с алфа под 255 получава речник с параметри на graphics state със съответстващи /ca и /CA (ISO 32000-1 §8.4.5), а всеки layer глиф се маркира като ползван, така че subsetter-ът пази контура му, макар никоя кодова точка да не сочи него директно. Една подробност изненадва хората: палитреният индекс 0xFFFF значи „ползвай цвета на текста отпред" в OpenType спецификацията, а HotPDF го разрешава на черно, а не на текущия fill цвят на страницата. За emoji шрифтове това рядко има значение; за icon шрифтове, разчитащи на foreground записа, за да оцветят глиф, проверете изхода, преди да предполагате, че ще следва цвета на текста ви

Как HotPDF превръща COLR v1 paint графика в PDF оператори?

Като първо парсва paint таблиците в плоска, ограничена графика и чак после мапва всеки възел в PDF конструкция. COLR v1 глиф не е списък от слоеве, а насочен ацикличен граф от paint записи, при който възли могат да се споделят през PaintColrLayers и PaintColrGlyph. Parser-ът го капва на 4096 paint възела, 64 нива дълбочина и 1024 color stop-а, и следи всеки възел като активен или свършен, така че референция обратно към активен възел — цикъл, който злонамерен шрифт може да построи от преизползване на слоеве — се отхвърля, вместо да се рекурсира в него. Offset базите са мястото, където първата имплементация се проваля. BaseGlyphPaintRecord offset-ите са спрямо началото на BaseGlyphList, paint offset-ите на LayerList са спрямо LayerList, а всеки Offset24 вътре в paint таблица е спрямо самата тази paint таблица. Разрешите ли и трите срещу една и съща база, напълно легални глифове падат на bounds проверката, което изглежда точно като развален шрифт. Щом графиката е изградена, мапването е директно:

  • PaintGlyph задава glyph контура като клип с text rendering режим 7 (ISO 32000-1 §9.3.6), после рисува своето дете вътре в него
  • Solid paint-ове запълват отрязан правоъгълник; линейните градиенти стават многостъпкови axial shadings, а радиалните градиенти — двуцветни радиални shadings (§8.7.4.5)
  • Sweep градиентите нямат PDF еквивалент, затова HotPDF ги приближава с 96 клина с плътен цвят, всеки семплиран от цветовата линия
  • Трансформациите се излъчват като cm, конюгирани около baseline произхода на глифа, с транслации, мащабирани по FontSize / UnitsPerEm
  • PaintComposite режими 13 до 27 се мапват към разделимите и неразделимите PDF blend режими като /Multiply, /Screen и /Luminosity (§11.3.5), задавани през ExtGState запис /BM

Границата е изрична. Porter-Duff режимите 5 до 12 (src_in, xor, plus и останалите) нямат PDF blend-mode съответствие, repeat и reflect extend режимите на линейни и радиални градиенти не се излъчват, а градиенти, чиито stop-ове носят различни алфа стойности, не се фалшифицират с единична плътност. Радиални градиенти с повече от два stop-а пазят само първия и последния си цвят. HotPDF проверява цялата графика срещу този поддържан поднабор, преди да напише един-единствен оператор, така че неподдържан глиф оставя страницата недокосната и минава към растерния fallback, вместо да остави половин рисунка след себе си

Диаграма на COLR v1 конверсията в HotPDF: paint графиката се парсва в ограничена графика, капирана на 4096 възела, 64 нива дълбочина и 1024 color stop-а с отхвърляне на цикли, после PaintGlyph става клип режим 7, линейни и радиални градиенти стават axial и радиални shadings, sweep градиенти стават 96 клина, а PaintComposite режими 13 до 27 стават PDF blend режими
Цялата графика се проверява срещу поддържания поднабор, преди да е написан първият оператор, така че неподдържан глиф оставя страницата недокосната и минава към растерния fallback, вместо да остави половин рисунка

SVG глифове и битмап strikes

SVG глифовете минават през същия ограничен builder, който HotPDF ползва за импортирани SVG файлове, а резултатът се регистрира като Form XObject (§8.10), точно както е описано в статията за SVG към Form XObject. Документът в таблицата SVG може да е gzip-компресиран; декомпресията върви на парчета по 8 KB и спира веднага щом разширен размер би минал 32 MB, вместо първо да се надува и после да се проверява, а самият компресиран вход е капиран на 8 MB. Профилът е рестриктивен нарочно: скриптове, вградени изображения, външни URL-и, data: URI-и и нелокални референции провалят затворено. Формата се мащабира така, че по-дългата ѝ страна да равнява размера на шрифта, и се закотвя на baseline-а, което мапва y-надолу SVG координатната система върху y-нагоре PDF такава. Имайте предвид, че builder-ът получава целия SVG документ за глифа, без селекция на елемента glyphNNN, така че шрифтове, опаковащи много глифове в един споделен документ, си заслужават тестване, преди да им се доверите

Битмап шрифтовете са въпрос за избор на strike и поставяне. За CBDT HotPDF избира CBLC размера, чийто вертикален ppem е най-близо до TargetPixelsPerEm, приема image формати 17, 18 и 19, и чете метриките на формат 19 от CBLC index subtable-а, защото този формат не пази свои собствени. За sbix strike offset-ите са спрямо таблицата, а glyph offset-ите — спрямо strike-а, а запис dupe преизползва графиката на друг глиф, пазейки собствените си origin отмествания; ако рекурсията презапише външния origin, изображението се измества. PNG и JPEG payload-и се декодират вътрешно, мащабират се по FontSize / PixelsPerEmY, вместо да се разтегнат до размера на шрифта, и се записват със soft mask (§11.6.5.3), щом някой пиксел не е напълно непрозрачен. sbix TIFF payload-и не се декодират и отиват на събитието

Какво става, когато глиф не може да се нарисува нативно?

HotPDF вдига OnColorGlyphRasterize и поставя какъвто RGBA битмап върне вашият handler; ако нищо не е закачено, или handler-ът остави Handled false, DrawRegisteredColorGlyph връща False и страницата си остава непроменена. Събитието се включва за COLR v1 графика извън поддържания поднабор, за SVG документ, който safe builder-ът е отказал, и за битмап payload, който вътрешните декодери не четат. Handler-ът получава формата, суровите шрифтови байтове, извлечения asset (SVG документът, евентуално още gzipped, или битмап байтовете; празно за COLR v1), glyph ID-то, палитрата и целевия пикселен размер

Диаграма на растерния fallback в HotPDF: OnColorGlyphRasterize се включва за COLR v1 графика извън поддържания поднабор, за SVG документ, отказан от safe builder-а, или за битмап payload, който декодерите не четат, подавайки формата, шрифтовите байтове, asset-а, GlyphID, PaletteIndex и PixelSize, а върнатият RGBA буфер се приема само когато дължината му е точно Width пъти Height пъти 4
Нулеви размери, грешна дължина на буфера или преливащи размери се отхвърлят, преди страницата да е пипната, а без handler или с Handled false извикването връща False и страницата си остава непроменена
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 е вашият растеризатор, а не HotPDF API.
  // Трябва да върне точно Width * Height * 4 байта RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

// Свързване
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;

HotPDF валидира изхода на handler-а, преди да пипне страницата: нулеви размери, буфер с дължина, различна от точно Width * Height * 4, или размери, достатъчно големи да прелеят, се отхвърлят и извикването връща False. Растерният fallback пак е растр, така че emoji, изрендериран така, губи векторната си острота; искайте PixelSize, съответстващ на изходната ви резолюция. Сдвоете цветния път с проверки за покритие при рисуване от статията за проследяване на липсващи глифове и линия, обработваща произволен потребителски текст, може да докладва и липсващи глифове, и глифове, изгубили цвета си

Рендерът за цветни глифове, OpenType shaping стекът и safe SVG builder-ът излизат всичките в HotPDF Delphi PDF компонента, наличен за Delphi и C++Builder