مقاله فنی

زیرمجموعه‌سازی فونت TrueType از طریق fontsub.dll در Delphi

HotXLS، کامپوننت Excel در Delphi و C++Builder، اندازه‌ی فونت PDF جاسازی‌شده را از طریق زیرمجموعه‌سازی فونت TrueType کاهش می‌دهد: در زمان export به PDF، تابع CreateFontPackage از کتابخانه‌ی سیستمی ویندوز fontsub.dll را فرا می‌خواند تا یک فونت TrueType جاسازی‌شده را فقط حول نقاط کد یونیکدی که یک برگ‌کاری واقعاً استفاده کرده بازسازی کند، به‌جای اینکه کل فایل قلم را منتقل کند. یک گزارش با دویست ردیف از نام‌های محصولات چینی شاید فقط به چند صد نویسه‌ی متمایز هان نیاز داشته باشد، اما فونت‌های CJKای که ویندوز عرضه می‌کند به‌طور معمول هرکدام ۵ تا ۲۰ مگابایت هستند. کل آن را جاسازی کنید، و همان فونت به‌تنهایی می‌تواند از هر شیء دیگری در PDF روی‌هم رفته سنگین‌تر شود

fontsub.dll کتابخانه‌ای نیست که اغلب توسعه‌دهندگان Delphi تا به‌حال شنیده باشند، و دلیلی برای این وجود دارد: مایکروسافت آن را به‌عنوان یک DLL ابزاری کوچک و به‌ندرت مستندشده عرضه می‌کند نه یک API برجسته‌ی Win32. HotXLS آن را به‌عنوان یک قابلیت اختیاری در نظر می‌گیرد، نه یک وابستگی سخت، پس اینکه صادرکننده چطور آن را بار می‌کند، فرا می‌خواند، و وقتی نباشد به آن فروبازمی‌گردد، به‌همان‌اندازه درباره‌ی برنامه‌نویسی دفاعی ویندوز می‌گوید که درباره‌ی قالب‌های فونت، و هر دو نیمه‌ی آن داستان ارزش گذشتن از آن‌ها را دارند

چرا متن یونیکدی یک export PDF از HotXLS را باد می‌کند؟

صادرکننده‌ی PDF در HotXLS فقط زمانی به‌سراغ یک فونت TrueType جاسازی‌شده می‌رود که متن برگ‌کاری از WinAnsi فراتر برود، و بقیه‌ی مواقع روی خانواده‌ی Helvetica توکار می‌ماند، مسیر پیش‌فرضی که راهنمای export از برگ‌کاری به PDF به‌طور کامل پوشش می‌دهد. WinAnsi متن اروپای غربی را به‌اندازه‌ی کافی خوب پوشش می‌دهد که خیلی از کاربرگ‌ها هرگز اصلاً یک جاسازی فونت را ماشه نمی‌کشند: PDF صرفاً به Helvetica با نام ارجاع می‌دهد و خواننده آن را محلی تأمین می‌کند، پس فایل کوچک می‌ماند. همان لحظه‌ای که یک سلول چیزی داشته باشد که WinAnsi نتواند نمایش دهد، یک نام محصول چینی، یک یادداشت کره‌ای، یک نماد سرگردان در یک نظر، صادرکننده باید یک برنامه‌ی فونت واقعی را جاسازی کند، چون یک خواننده‌ی PDF هیچ منبع گلیف جایگزینی برای نویسه‌های خارج از ۱۴ فونت استاندارد ندارد

HotXLS آن فونت را به‌طور خودکار پیدا می‌کند، با اسکن‌کردن پوشه‌ی Fonts ویندوز به‌دنبال یک فهرست کوتاه از کاندیدهای نصب‌شده، از جمله قلم‌های توانمند-CJKای که ویندوز برای رندر چینی و کره‌ای عرضه می‌کند، مگر اینکه ویژگی UnicodeFontFile صادرکننده از پیش به یک فایل مشخص اشاره کند، و هر فونتی که روی آن بنشیند پیش از اینکه زیرمجموعه‌سازی اصلاً اجرا شود، کامل جاسازی می‌شود. آن الزام جاسازی مختص PDF است: مسیرهای export به RTF و HTML در HotXLS متن یونیکدی را با escapeکردن نقاط کد درون جریان بایت دست‌نخورده نگه می‌دارند به‌جای اینکه یک برنامه‌ی فونت منتقل کنند، به همین دلیل مسئله‌ی اندازه‌ای که این مقاله پوشش می‌دهد در آن دو قالب معادلی ندارد

uses
  lxHandle, lxPDF;

var
  Book: TXLSWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('catalog-cn.xlsx');
    Exporter := TXLSPDFExport.Create;
    try
      // Optional: pin a specific CJK-capable font instead of the
      // exporter's automatic Windows\Fonts scan.
      Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
      Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

fontsub.dll چیست، و چرا یک زیرمجموعه‌ساز از صفر ننویسیم؟

fontsub.dll یک کتابخانه‌ی سیستمی کوچک ویندوز است، عرضه‌شده از زمان Windows XP، که یک تابع مرتبط با اینجا را در معرض دید می‌گذارد: CreateFontPackage. بایت‌های یک فونت منبع TrueType و فهرستی از نقاط کد یونیکد برای نگه‌داشتن به آن بدهید، و یک فونت حداقلی پس می‌دهد که همچنان هر محدودیت قالب فونت را برآورده می‌کند: شماره‌های گلیف دوباره‌شماره‌گذاری‌شده، glyf و loca فقط حول کانتورهای نگه‌داشته‌شده بازساخته‌شده، hmtx و cmap بازنویسی‌شده تا تطبیق کنند. HotXLS نوع اشاره‌گر تابع را مستقیماً در برابر آن قرارداد اعلان می‌کند

const
  TTFCFP_FLAGS_SUBSET = 1;
  TTFMFP_SUBSET = 0;
  TTFCFP_MS_PLATFORMID = 3;
  TTFCFP_UNICODE_CHAR_SET = 1;

type
  TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
    var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
    var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
    usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
    pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
    lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;

نوشتن کار CreateFontPackage دستی به‌جای فراخوانی آن، یعنی پیاده‌سازی یک زیرمجموعه‌ساز درست TrueType: پیمایش گلیف‌های ترکیبی برای کشیدن هر گلیف مؤلفه‌ای که یک گلیف نگه‌داشته‌شده به آن ارجاع می‌دهد، بازسازی افست‌های loca پس از حذف‌شدن کانتورها، رعایت بیت‌های مجوز-جاسازی در جدول OS/2 یک فونت، و درست‌گرفتن همه‌ی این‌ها در برابر هر فونت عجیبی که ممکن است روی دستگاه یک مشتری نصب شده باشد. مایکروسافت از پیش آن مسئله را حل کرده و راه‌حل را به‌عنوان بخشی از خودِ ویندوز عرضه می‌کند، پس فراخوانی یک DLL سیستمی که آن را نگه‌داری می‌کند، در برابر پشته‌ی رندر فونت خودش تست می‌کند، و به هر دستگاهی رایگان توزیع می‌کند، برای HotXLS هزینه‌ی یک بارگذاری پویا و یک اشاره‌گر تابع دارد؛ دوباره پیاده‌سازی همان منطق یعنی مالک‌بودن یک تجزیه‌گر برای یک قالب باینری با ده‌ها سال مورد لبه، برای یک ویژگی که فقط زمانی اهمیت دارد که یک فونت اتفاقاً بزرگ باشد

ساخت فهرست-نگه‌داری از گلیف‌هایی که واقعاً رندر شده‌اند

HotXLS فهرست-نگه‌داری زیرمجموعه‌سازی را از یک نگاشتی می‌سازد که از پیش به دلیل دیگری نگه‌داری می‌کرد، پس این حسابداری هیچ هزینه‌ی اضافی ندارد. هر بار که کد رندر صفحه یک نویسه که به فونت یونیکد جاسازی‌شده نیاز دارد رسم می‌کند، شماره‌ی گلیف آن نویسه را جستجو می‌کند و آن جفت را در FUnicodeGlyphMap، یک جدول گلیف-به-کدنقطه که CMap از نوع ToUnicode در PDF را هم هدایت می‌کند تا کپی-و-چسباندن از سند نهایی متن اصلی را برگرداند نه شناسه‌های خام گلیف، ثبت می‌کند. تا زمانی که جریان‌های محتوای صفحه تمام شوند، آن نگاشت از پیش دقیقاً همان مجموعه از نقاط کد یونیکدی را فهرست می‌کند که سند استفاده کرده، نه بیشتر و نه کمتر

var
  keepList: array of Word;
  keepCount, i: Integer;
  codePoint: LongWord;
begin
  SetLength(keepList, FUnicodeGlyphMap.Count);
  keepCount := 0;
  for i := 0 to FUnicodeGlyphMap.Count - 1 do
  begin
    codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
    if codePoint > 0 then
    begin
      keepList[keepCount] := Word(codePoint);
      Inc(keepCount);
    end;
  end;
end;

در زمان نهایی‌سازی، HotXLS همان نگاشت را برای دومین‌بار می‌پیماید تا فهرست-نگه‌داری‌ای را بسازد که CreateFontPackage انتظار دارد، یک آرایه‌ی ساده از نقاط کد یونیکد برای نگه‌داری به شکل ۱۶بیتی‌ای که آرگومان فهرست-نگه‌داری API نیاز دارد. چون آن آرگومان یک آرایه از کلمات ۱۶بیتی است، صفحه‌ی چندزبانه‌ی پایه را تمیز آدرس‌دهی می‌کند، که CJK معمولی، سیریلیک، یونانی، و متن عربی را بدون پیچیدگی پوشش می‌دهد؛ یک برگ‌کاری که به نویسه‌های صفحه‌ی مکمل تکیه می‌کند، برخی ایموجی‌ها یا رسم‌خط‌های تاریخی نادر، خارج از چیزی می‌نشیند که یک ورودی فهرست-نگه‌داری تکی بتواند مستقیماً نام ببرد، که یک مرز است ارزش دانستن، نه یک نقص، چون اکثریت قریب به اتفاق صفحه‌گسترده‌های کسب‌وکاری یونیکد-محور هرگز اصلاً نزدیک آن صفحه نمی‌روند

وقتی fontsub.dll نباشد چه اتفاقی می‌افتد؟

HotXLS هرگز فرض نمی‌کند fontsub.dll حاضر است، و export PDF هرگز به این دلیل که حاضر نیست شکست نمی‌خورد. کتابخانه در همان لحظه‌ای که یک زیرمجموعه لازم است، با SafeLoadLibrary و GetProcAddress به‌جای یک import ایستا، پویا بار می‌شود، دقیقاً چون fontsub.dll یک API عمومی مستند و تضمین‌شده‌حاضر مثل kernel32.dll نیست: این ابزار همراه جاسازی فونت است، و هیچ چیزی در قرارداد مایکروسافت وعده نمی‌دهد که در هر SKU، هر شاخه‌ی servicing، یا هر لایه‌ی سازگاری‌ای که تلاش می‌کند ویندوز را شبیه‌سازی کند، زنده می‌ماند

var
  hFontSub: HMODULE;
  CreateFontPackage: TCreateFontPackage;
begin
  hFontSub := SafeLoadLibrary('FontSub.dll');
  if hFontSub = 0 then
    Exit; // no subsetting available - keep the full embedded font
  try
    @CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
    if not Assigned(CreateFontPackage) then
      Exit;
    // ... call CreateFontPackage, check its return code ...
  finally
    FreeLibrary(hFontSub);
  end;
end;

هر مسیر شکست به همان نتیجه تا می‌شود. یک DLL گم‌شده، یک export گم‌شده، یک کد بازگشت ناصفر، یا فونتی که جدول OS/2اش زیرمجموعه‌سازی را از طریق بیت‌های مجوز-جاسازی‌اش ممنوع می‌کند، HotXLS صرفاً فونت کاملی را که از پیش جاسازی کرده بود نگه می‌دارد و ادامه می‌دهد. هیچ چیزی throw نمی‌کند، هیچ چیزی export را متوقف نمی‌کند، و کد فراخواننده هرگز مجبور نیست یک بهینه‌سازی فونت را در exception handling خودش بپیچد؛ PDF صادرشده در هر دو حالت معتبر است، و تنها متغیر این است که آیا در نهایت کوچک درمی‌آید یا کمی بزرگ‌تر

PDF واقعاً چقدر کوچک‌تر می‌شود؟

زیرمجموعه‌سازی فونت TrueType در HotXLS معمولاً PDF صادرشده‌ی یک برگ‌کاری یونیکد-محور را به جایی بین یک‌بیستم و یک‌هشتم اندازه‌ی زیرمجموعه‌نشده‌اش کوچک می‌کند، یک کاهش ۸ تا ۲۰ برابری که مقیاسش پیگیری می‌کند که یک سند مشخص چقدر از یک فونت کامل را واقعاً لمس می‌کند: یک سفارش خرید ساخته‌شده حول چند صد نویسه‌ی متمایز چینی فقط همان چند صد گلیف را از ده‌ها هزاری که یک قلم CJK عرضه می‌کند نگه می‌دارد، درحالی‌که یک برگه‌ای که ترکیب گسترده‌تری از نویسه‌ها را پوشش می‌دهد، به‌نسبت بیشتر نگه می‌دارد. HotXLS یک پاس فشرده‌سازی Flate اضافی روی بایت‌های فونت زیرمجموعه‌شده پیش از نوشتنشان درون جریان /FontFile2 PDF لایه می‌کند، همان فشرده‌سازی‌ای که بقیه‌ی جریان‌های محتوای سند از پیش از آن عبور می‌کنند، و هیچ‌کدام از این‌ها چیز اضافه‌ای از کد فراخواننده نمی‌خواهد: برگ‌کاری‌ای که هرگز WinAnsi را ترک نمی‌کند هرگز این مسیر را لمس نمی‌کند و همچنان از طریق Helvetica ساده export می‌کند، درحالی‌که برگ‌کاری‌ای که واقعاً مسیر فونت یونیکد را ماشه می‌کشد به‌طور خودکار زیرمجموعه‌سازی می‌گیرد، بدون هیچ ویژگی‌ای برای تنظیم و بدون هیچ فراخوانی جداگانه‌ای برای انجام، و تنها ویژگی درگیر، UnicodeFontFile، فقط انتخاب می‌کند کدام فونت جاسازی و زیرمجموعه‌سازی شود، نه اینکه آیا زیرمجموعه‌سازی اتفاق می‌افتد یا نه

زیرمجموعه‌سازی فونت یک جزئیات درون سطح گسترده‌تر export PDF در کامپوننت Excel از HotXLS برای Delphi است، در کنار صفحه‌بندی، فراداده‌ی چاپ برگ‌کاری، و مسیرهای export به CSV، HTML، و RTF که با آن‌ها عرضه می‌شود