مقاله فنی

ایموجی رنگی در PDF: COLR v1 و SVG و بیت‌مپ با Delphi

HotPDF ایموجی‌های رنگی را از طریق THotPDF.DrawRegisteredColorGlyph در PDF رسم می‌کند، که دادهٔ رنگی یک فونت ثبت‌شده با RegisterUnicodeTTF را می‌خواند و به‌صورت گرافیک بومی PDF صادر می‌کند: لایه‌های COLR v0 به‌شکل خطوط دور گلایف پرشده، گراف‌های paint مربوط به COLR v1 به‌شکل clip و shading و blend mode، گلایف‌های SVG به‌شکل Form XObject، و بیت‌مپ‌های CBDT یا sbix به‌شکل تصویر. هر چیزی که نتواند به‌صورت بومی نگاشت شود به‌جای آنکه بی‌سروصدا یک شکل سیاه شود، به event مربوط به OnColorGlyphRasterize می‌رود

همان عبارت آخر کل دلیل وجود این کد است. یک فونت ایموجی را به روش معمول embed کن و viewer خطوط دور را از glyf یا CFF می‌گیرد و با هر رنگ fill جاری که باشد پر می‌کند. صورت خندان به‌شکل یک لکهٔ سیاه می‌رسد، پرچم به‌شکل یک مستطیل، و هیچ‌جای پایپ‌لاین گله نمی‌کند

چرا یک ایموجی رنگی در PDF به‌شکل سیلوئت سیاه چاپ می‌شود؟

یک font program در PDF مفهومی به نام color glyph ندارد. ISO 32000-1 گلایف را شکلی می‌گیرد که با رنگ جاری رنگ می‌شود، و جدول‌های رنگی که OpenType بعداً اضافه کرد، یعنی COLR/CPAL و SVG و CBDT/CBLC و sbix، بخشی از مدل تصویربرداری PDF نیستند، پس هیچ viewerی ملزم نیست از یک فونت embed شده آن‌ها را بخواند. رنگ باید در لحظهٔ تولید به محتوای صفحه ترجمه شود، در حالی که تولیدکننده هنوز بایت‌های فونت را دارد و می‌داند کدام گلایف را می‌خواهد. این ترجمه به‌ازای هر فرمت متفاوت است و فونت‌های ایموجی در دنیای واقعی از همه‌شان استفاده می‌کنند: وکتورهای لایه‌ای، گراف‌های paint گرادیانی، سندهای SVG نهفته و strikeهای PNG. HotPDF نتیجه را به‌شکل THPDFOpenTypeColorFormat گزارش می‌کند، با مقادیر otcfNone و otcfCOLRv0 و otcfCOLRv1 و otcfCBDT و otcfSVG و otcfSBIX، و فونت را با یک اولویت ثابت probe می‌کند: اول COLR، بعد SVG، بعد CBDT، بعد sbix. دادهٔ وکتوری هر وقت فونت هر دو را داشته باشد بر بیت‌مپ برتری دارد، که در سندی که ممکن است زوم یا چاپ شود همان چیزی است که می‌خواهی

نمودار probe گلایف رنگی در HotPDF: یک font program در PDF خطوط دور گلایف را با رنگ جاری رنگ می‌کند، پس جدول‌های رنگی OpenType یعنی COLR و SVG و CBDT و sbix باید در لحظهٔ تولید به محتوای صفحه ترجمه شوند، و HotPDF فونت ثبت‌شده را با اولویت ثابت probe می‌کند: اول COLR، بعد SVG، بعد CBDT، بعد sbix، و THPDFOpenTypeColorFormat را از otcfCOLRv0 تا otcfSBIX گزارش می‌دهد
دادهٔ وکتوری هر وقت فونت هر دو را داشته باشد بر بیت‌مپ برتری دارد، که در سندی که ممکن است زوم یا چاپ شود همان چیزی است که می‌خواهی، و گلایفی که مسیر رنگی ندارد به fallback تو واگذار می‌شود

یک فراخوانی، پنج فرمت: resolve کردن و رسم یک color glyph

THotPDF.GetRegisteredColorGlyphInfo جواب می‌دهد یک code point کدام مسیر را خواهد رفت و DrawRegisteredColorGlyph همان مسیر را می‌رود. هر دو code point را در 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 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 می‌خواهد. محدودیت صادقانه در امضا نشسته: فراخوانی یک code point می‌گیرد و فقط از cmap عبورش می‌دهد. توالی‌های ZWJ و مودیفایرهای رنگ پوست و پرچم‌های regional-indicator لیگاچرهای GSUB هستند، پس ترکیب کردنشان یک مسئلهٔ shaping از همان جنس است که در مقالهٔ جایگزین‌های GSUB در OpenType پوشش داده شده، نه چیزی که این نقطهٔ ورود برایت انجام بدهد

COLR v0: لایه‌های گلایف روی‌هم‌چیده با رنگ‌های پالت

COLR v0 مورد ساده است و HotPDF مستقیم رندرش می‌کند: هر گلایف پایه گلایف‌های لایه را با یک درایهٔ رنگ CPAL فهرست می‌کند و هر لایه یک عملیات نمایش متن معمولی با رنگ fill خودش می‌شود که به ترتیب جدول روی هم چیده می‌شود. لایه‌ای با آلفای زیر 255 یک dictionary پارامتر وضعیت گرافیک با /ca و /CA متناظر می‌گیرد (ISO 32000-1 §8.4.5) و هر گلایف لایه استفاده‌شده علامت می‌خورد تا subsetter خطوط دورش را نگه دارد هرچند هیچ code pointی مستقیم به آن نگاشت نمی‌شود. یک جزئیات آدم را غافلگیر می‌کند: ایندکس درایهٔ پالت 0xFFFF در spec مربوط به OpenType یعنی «از رنگ پیش‌زمینهٔ متن استفاده کن» و HotPDF آن را به سیاه resolve می‌کند نه به رنگ fill جاری صفحه. برای فونت‌های ایموجی به‌ندرت مهم است؛ برای فونت‌های آیکونی که به درایهٔ پیش‌زمینه برای رنگ‌آمیزی گلایف تکیه دارند، قبل از اینکه فرض کنی از رنگ متن تو پیروی می‌کند خروجی را بررسی کن

HotPDF چطور گراف paint مربوط به COLR v1 را به operatorهای PDF تبدیل می‌کند؟

با parse کردن جدول‌های paint اول به یک گراف تخت و محدود و بعد نگاشت هر گره به یک construct در PDF. یک گلایف COLR v1 فهرستی از لایه‌ها نیست بلکه یک گراف جهت‌دار بدون چرخه از رکوردهای paint است که گره‌هایش می‌توانند از طریق PaintColrLayers و PaintColrGlyph مشترک باشند. parser سقفش را روی 4096 گرهٔ paint و 64 سطح عمق و 1024 color stop می‌گذارد و هر گره را active یا done دنبال می‌کند تا ارجاعی به یک گرهٔ فعال — چرخه‌ای که یک فونت مخرب از بازیابی لایه می‌سازد — به‌جای بازگشت درونش رد شود. پایه‌های offset جایی است که پیاده‌سازی اول گول می‌خورد. offsetهای BaseGlyphPaintRecord نسبت به شروع BaseGlyphList هستند، offsetهای paint در LayerList نسبت به LayerList، و هر Offset24 داخل یک جدول paint نسبت به همان جدول paint. هر سه را با یک پایه واحد resolve کن و گلایف‌های کاملاً قانونی بررسی مرزها را رد می‌کنند، که دقیقاً شبیه یک فونت خراب به نظر می‌رسد. وقتی گراف ساخته شد، نگاشت مستقیم است:

  • PaintGlyph خطوط دور گلایف را با حالت رندر متن 7 به‌عنوان clip ست می‌کند (ISO 32000-1 §9.3.6) و بعد فرزندش را داخلش رنگ می‌کند
  • paintهای توپر یک مستطیل بریده را پر می‌کنند؛ گرادیان‌های خطی shadingهای axial چند-توقفی می‌شوند و گرادیان‌های شعاعی shadingهای شعاعی دونرنگی (§8.7.4.5)
  • گرادیان‌های sweep معادل PDF ندارند، پس HotPDF با 96 گوهٔ تک‌رنگ تقریب‌شان می‌زند که هر کدام از خط رنگ نمونه‌برداری شده
  • ترنسفورم‌ها به‌شکل cm صادر می‌شوند که دور مبدأ خط کرسی گلایف مزدوج شده‌اند و جابه‌جایی‌هایشان با FontSize / UnitsPerEm مقیاس شده
  • حالت‌های 13 تا 27 در PaintComposite به blend modeهای جداشدنی و ناجداشدنی PDF مثل /Multiply و /Screen و /Luminosity نگاشت می‌شوند (§11.3.5) که از طریق درایهٔ /BM در ExtGState ست می‌شوند

مرز صریح است. حالت‌های Porter-Duff از 5 تا 12 (src_in و xor و plus و بقیه) همتای blend mode در PDF ندارند، حالت‌های extend تکرار و بازتاب روی گرادیان‌های خطی و شعاعی صادر نمی‌شوند و گرادیان‌هایی که stopهایشان آلفای متفاوت دارند با یک opacity واحد جعل نمی‌شوند. گرادیان‌های شعاعی با بیش از دو stop فقط رنگ اول و آخرشان را نگه می‌دارند. HotPDF کل گراف را قبل از نوشتن حتی یک operator با این زیرمجموعهٔ پشتیبانی‌شده می‌سنجد، پس یک گلایف پشتیبانی‌نشده صفحه را دست‌نخورده می‌گذارد و به fallback رستری می‌رود به‌جای آنکه نصف یک رسم را جا بگذارد

نمودار تبدیل COLR v1 در HotPDF: گراف paint به یک گراف محدود parse می‌شود با سقف 4096 گره و 64 سطح عمق و 1024 color stop و رد کردن چرخه‌ها، بعد PaintGlyph به یک clip با حالت 7 تبدیل می‌شود، گرادیان‌های خطی و شعاعی به shadingهای axial و شعاعی، گرادیان‌های sweep به 96 گوه و حالت‌های 13 تا 27 در PaintComposite به blend modeهای PDF
کل گراف قبل از نوشتن اولین operator با زیرمجموعهٔ پشتیبانی‌شده سنجیده می‌شود، پس یک گلایف پشتیبانی‌نشده صفحه را دست‌نخورده می‌گذارد و به fallback رستری می‌رود به‌جای آنکه نصف یک رسم جا بگذارد

گلایف‌های SVG و strikeهای بیت‌مپی

گلایف‌های SVG از همان builder محدودی می‌گذرند که HotPDF برای فایل‌های SVG واردشده استفاده می‌کند و نتیجه به‌عنوان Form XObject ثبت می‌شود (§8.10)، دقیقاً همان‌طور که در مقالهٔ SVG تا Form XObject توضیح داده شد. سند داخل جدول SVG ممکن است gzip شده باشد؛ تجزیه در قطعه‌های 8 KB اجرا می‌شود و همان لحظه که اندازهٔ بازشده بخواهد از 32 MB بگذرد متوقف می‌شود، به‌جای اینکه اول inflate کند بعد بررسی، و خود ورودی فشرده سقفش 8 MB است. پروفایل عمداً محدودکننده است: اسکریپت‌ها، تصاویر نهفته، URLهای خارجی، URIهای data: و ارجاع‌های غیرمحلی با شکست امن رد می‌شوند. فرم طوری مقیاس می‌شود که ضلع بلندترش برابر اندازهٔ فونت شود و روی خط کرسی لنگر می‌افتد، که دستگاه مختصات y-روبه‌پایین SVG را روی y-روبه‌بالای PDF می‌نگارد. توجه کن builder کل سند SVG مربوط به گلایف را می‌گیرد، بدون انتخاب عنصر glyphNNN، پس فونت‌هایی که گلایف‌های زیادی را در یک سند مشترک بسته‌بندی می‌کنند قبل از اتکا به آن‌ها لایق تست‌اند

فونت‌های بیت‌مپی مسئلهٔ انتخاب strike و جای‌گذاری‌اند. برای CBDT، HotPDF اندازهٔ CBLCای را انتخاب می‌کند که ppem عمودی‌اش به TargetPixelsPerEm نزدیک‌تر باشد، قالب‌های تصویر 17 و 18 و 19 را می‌پذیرد و متریک‌های قالب 19 را از زیرجدول ایندکس CBLC می‌خواند چون آن قالب هیچ متریک خودکاری ندارد. برای sbix، offsetهای strike نسبت به جدول و offsetهای گلایف نسبت به strike هستند و یک رکورد dupe گرافیک گلایف دیگری را با نگه داشتن offsetهای مبدأ خودش بازیابی می‌کند؛ اجازه دادن به بازگشت که مبدأ بیرونی را بازنویسی کند تصویر را جابه‌جا می‌کند. payloadهای PNG و JPEG داخلی decode می‌شوند، با FontSize / PixelsPerEmY مقیاس می‌شوند نه اینکه به اندازهٔ فونت کشیده شوند، و هر وقت پیکسلی کاملاً مات نباشد با soft mask نوشته می‌شوند (§11.6.5.3). payloadهای TIFF مربوط به sbix decode نمی‌شوند و به event می‌روند

وقتی یک گلایف را نمی‌شود بومی رسم کرد چه اتفاقی می‌افتد؟

HotPDF رویداد OnColorGlyphRasterize را صدا می‌زند و هر بیت‌مپی RGBA که هندلرت برگرداند را می‌گذارد؛ اگر چیزی assign نشده باشد یا هندلر Handled را false بگذارد، DrawRegisteredColorGlyph مقدار False برمی‌گرداند و صفحه بدون تغییر می‌ماند. event برای یک گراف COLR v1 بیرون از زیرمجموعهٔ پشتیبانی‌شده، یک سند SVG که builder امن رد کرده و یک payload بیت‌مپی که decoderهای داخلی نمی‌خوانند عمل می‌کند. هندلر فرمت و بایت‌های خام فونت و asset استخراج‌شده (سند SVG، شاید هنوز gzip‌شده، یا بایت‌های بیت‌مپ؛ برای COLR v1 خالی) و شناسهٔ گلایف و پالت و اندازهٔ پیکسل هدف را می‌گیرد

نمودار fallback رستری در HotPDF: رویداد OnColorGlyphRasterize برای یک گراف COLR v1 بیرون از زیرمجموعهٔ پشتیبانی‌شده یا یک سند SVG که builder امن رد کرده یا یک payload بیت‌مپی که decoderها نمی‌خوانند عمل می‌کند و فرمت و بایت‌های فونت و asset و GlyphID و PaletteIndex و PixelSize را پاس می‌دهد، و بافر RGBA برگشتی فقط وقتی پذیرفته می‌شود که طولش دقیقاً Width ضربدر Height ضربدر 4 باشد
اندازه‌های صفر، طول بافر غلط یا ابعاد سرریزشده قبل از دست زدن به صفحه رد می‌شوند و بدون هندلر یا با 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 رسترایزر خود توست، نه یک 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 برمی‌گرداند. یک fallback رستری باز هم رستر است، پس ایموجی‌ای که این‌طور رندر شده تیزی وکتوری‌اش را از دست می‌دهد؛ یک PixelSize بخواه که با وضوح خروجیت بخواند. مسیر رنگی را با بررسی‌های پوشش در زمان رسم از مقالهٔ ردیابی گلایف‌های غایب جفت کن و پایپ‌لاینی که متن دلخواه کاربر را هندل می‌کند می‌تواند هم گلایف‌های غایب و هم گلایف‌هایی که رنگشان را از دست داده‌اند را گزارش کند

رندرکنندهٔ color glyph، پشتهٔ shaping مربوط به OpenType و builder امن SVG همه در کامپوننت PDF در Delphi از HotPDF عرضه می‌شوند، برای Delphi و C++Builder