מאמר טכני

הפחתת (subsetting) גופני TrueType דרך fontsub.dll ב-Delphi

HotXLS, רכיב ה-Excel של Delphi ו-C++Builder, חותכת את גודל גופני ה-PDF המשובצים דרך הפחתת גופן (subsetting) TrueType: בזמן ייצוא PDF היא קוראת לפונקציית CreateFontPackage של ספריית המערכת של Windows, ‏fontsub.dll, כדי לבנות מחדש גופן TrueType משובץ סביב רק נקודות הקוד של יוניקוד שגיליון עבודה בפועל השתמש בהן, במקום לשלוח את קובץ הגופן השלם. דוח עם מאתיים שורות של שמות מוצר סיניים אולי זקוק רק לכמה מאות תווי האן (Han) ייחודיים, אבל גופני CJK ש-Windows שולח רגילים לרוץ 5 עד 20 מגה-בייט לכל אחד. שבץ אחד שלם, והגופן לבדו יכול לשקול יותר מכל אובייקט אחר ב-PDF ביחד

fontsub.dll אינה ספרייה שרוב מפתחי Delphi אי-פעם שמעו עליה, ויש לכך סיבה: מיקרוסופט משלחת אותה כ-DLL עזר קטן ומתועד באופן דליל ולא כ-API כותרתי של Win32. HotXLS מתייחסת אליה כיכולת אופציונלית, לא תלות קשיחה, כך שאיך שהמייצא טוען אותה, קורא לה, ונופל בחזרה כשהיא חסרה אומר לא פחות על תכנות Windows הגנתי מאשר על פורמטי גופן, ושתי מחצית הסיפור הזה שוות סיור

למה טקסט יוניקוד מנפח ייצוא PDF של HotXLS?

מייצא ה-PDF של HotXLS פונה לגופן TrueType משובץ רק כאשר טקסט גיליון-העבודה נופל מחוץ ל-WinAnsi, ונשאר על משפחת Helvetica המובנית בשאר הזמן, נתיב ברירת המחדל שסיור ייצוא גיליון-עבודה ל-PDF מכסה לעומק. WinAnsi מכסה טקסט מערב-אירופי טוב מספיק שהרבה חוברות עבודה אף פעם לא מפעילות שיבוץ גופן בכלל: ה-PDF פשוט מפנה ל-Helvetica בשם והקורא מספק אותו מקומית, כך שהקובץ נשאר קטן. ברגע שתא מחזיק משהו ש-WinAnsi לא יכול לייצג, שם מוצר סיני, הערה קוריאנית, סמל תועה בהערה, המייצא חייב לשבץ תוכנית גופן ממשית, משום שלקורא PDF אין מקור גליף חלופי עבור תווים מחוץ לארבעה-עשר הגופנים התקניים

HotXLS מאתרת את הגופן הזה אוטומטית, סורקת את תיקיית Fonts של Windows עבור רשימה קצרה של מועמדים מותקנים, כולל הגופנים בעלי-יכולת-CJK ש-Windows שולח עבור עיבוד סינית וקוריאנית, אלא אם מאפיין UnicodeFontFile של המייצא כבר מצביע על קובץ ספציפי, ובאיזה גופן שהיא נוחתת משובץ שלם לפני שהפחתה אי-פעם רצה. דרישת השיבוץ הזו ספציפית ל-PDF: נתיבי הייצוא 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, נשלחת מאז 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 של גופן, וקבלת כל זה נכון על פני כל גופן מוזר שמחשב לקוח במקרה מותקן עליו. מיקרוסופט כבר פתרה את הבעיה הזו ומשלחת את הפתרון כחלק מ-Windows עצמה, כך שקריאה ל-DLL מערכת שהיא מתחזקת, בודקת מול מחסנית עיבוד-הגופנים שלה עצמה, ומפיצה לכל מחשב בחינם עולה ל-HotXLS טעינה דינמית והפניית פונקציה; מימוש מחדש של אותה לוגיקה היה אומר להחזיק פענוח (parser) עבור פורמט בינארי עם עשרות שנות מקרי-קצה, עבור תכונה שחשובה רק כשגופן במקרה גדול

בניית רשימת-השמירה מגליפים שבאמת עובדו

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;

בזמן ה-finalize, HotXLS עוברת על אותה מפה פעם שנייה כדי לבנות את רשימת-השמירה ש-CreateFontPackage מצפה לה, מערך פשוט של נקודות קוד יוניקוד לשימור בצורת 16-סיביות שארגומנט רשימת-השמירה של ה-API דורש. מכיוון שהארגומנט הזה הוא מערך מילים בנות 16 סיביות, הוא מכתובת את המישור הרב-לשוני הבסיסי (BMP) בנקיות, שמכסה CJK רגיל, קירילית, יוונית, וטקסט ערבי ללא סיבוך; גיליון עבודה שנשען על תווי מישור-משלים, אימוג'י מסוימים או כתבי-יד היסטוריים נדירים, יושב מחוץ למה שרשומת רשימת-שמירה בודדת יכולה לכנות ישירות, וזה גבול ששווה לדעת עליו ולא פגם, שכן הרוב המכריע של גיליונות אלקטרוניים עסקיים כבדי-יוניקוד אף פעם לא מתקרבים למישור הזה מלכתחילה

מה קורה כאשר fontsub.dll חסרה?

HotXLS אף פעם לא מניחה ש-fontsub.dll נוכחת, וייצוא ה-PDF אף פעם לא נכשל בגלל שהיא לא. הספרייה נטענת דינמית ברגע שהפחתה נדרשת, עם SafeLoadLibrary ו-GetProcAddress במקום ייבוא סטטי, בדיוק משום ש-fontsub.dll אינה API ציבורי מתועד, מובטח-נוכחות, באופן ש-kernel32.dll הוא: היא כלי שיבוץ-גופן ארוז, ושום דבר בחוזה של מיקרוסופט לא מבטיח שהוא שורד על כל SKU, כל ענף שירות, או כל שכבת תאימות שמנסה לחקות את Windows

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 חסר, ייצוא חסר, קוד החזרה שאינו אפס, או גופן שטבלת ה-OS/2 שלו אוסרת הפחתה דרך סיביות הרשאת-השיבוץ שלה, HotXLS פשוט שומרת על הגופן השלם שכבר שיבצה וממשיכה הלאה. שום דבר לא זורק חריגה, שום דבר לא מבטל את הייצוא, והקוד הקורא אף פעם לא צריך לעטוף אופטימיזציית גופן בטיפול חריגות משלו; ה-PDF המיוצא תקין בכל מקרה, והמשתנה היחיד הוא אם הוא מסתיים קטן או קצת יותר גדול

כמה יותר קטן ה-PDF בפועל הופך?

הפחתת גופן TrueType של HotXLS בדרך כלל מכווצת PDF מיוצא כבד-יוניקוד של גיליון עבודה למקום שבין עשרים-ואחד לשמינית מגודלו הבלתי-מופחת, הפחתה פי 8 עד 20 שהסולם שלה עוקב אחר כמה מגופן מלא מסמך נתון בפועל נוגע: הזמנת רכש שבנויה סביב כמה מאות תווים סיניים ייחודיים שומרת רק את כמה מאות הגליפים הללו מתוך עשרות האלפים שגופן CJK שולח, בעוד גיליון שמשתרע על תמהיל רחב יותר של תווים שומר יחסית יותר. HotXLS מוסיפה שכבת דחיסת Flate נוספת מעל בייטי הגופן המופחת לפני כתיבתם לתוך זרם ה-/FontFile2 של ה-PDF, אותה דחיסה ששאר זרמי התוכן של המסמך כבר עוברים, ושום דבר מזה לא מבקש שום דבר נוסף מהקוד הקורא: גיליון עבודה שאף פעם לא יוצא מ-WinAnsi אף פעם לא נוגע בנתיב הזה וממשיך לייצא דרך Helvetica פשוט, בעוד גיליון עבודה שכן מפעיל את נתיב גופן-היוניקוד מקבל הפחתה אוטומטית, ללא מאפיין להגדיר וללא קריאה נפרדת לבצע, והמאפיין היחיד המעורב, UnicodeFontFile, רק בוחר איזה גופן משובץ ומופחת, לא אם הפחתה קורית

הפחתת גופן היא פרט אחד בתוך משטח ייצוא ה-PDF הרחב יותר של רכיב ה-Excel Delphi של HotXLS, לצד עימוד, מטא-נתוני הדפסת גיליון-עבודה, ונתיבי הייצוא CSV, ‏HTML, ו-RTF שהיא נשלחת איתם