מאמר טכני

רינדור גופני PDF מוטמעים ב-Delphi עם HotPDF

התוכנה HotPDF מרנדרת גופני PDF מוטמעים ב-Delphi מבלי להתקין דבר על המחשב: צינור רינדור הגליפים המוטמעים בקובץ HPDFGlyphRender.pas מנתח את תוכניות הגופנים השמורות בתוך קובץ ה-PDF עצמו — קווי מתאר TrueType glyf מתוך FontFile2, מחרוזות תווים CFF Type 2 מתוך FontFile3, וזרמי תוכן של גליפים מסוג Type 3 — ומפעיל אותם מחדש כנתיבים וקטוריים מלאים של GDI. מאמר זה מהווה צלילה עמוקה לנאמנות הגופנים שמאחורי רינדור דפי PDF למפת סיביות (TBitmap) עם HotPDF: מאמר זה מכסה את המרנדר בכללותו, ואילו מאמר זה מכסה כיצד הטקסט בדפים אלו מקבל את צורותיו המדויקות

מדוע קובץ PDF מרונדר עם מלבנים במקום טקסט?

מלבנים, תווים ריקים או תווים שגויים במקצת בפלט ה-PDF המרונדר פירושם כמעט תמיד שהמרנדר ביקש ממערכת ההפעלה גופן במקום להשתמש בזה המוטמע בקובץ. התלונה מגיעה באותו אופן בכל פעם: המסמך נראה מושלם במחשב שיצר אותו, ואז לקוח פותח אותו בשרת נקי או במחשב שולחני נעול והחשבונית ביפנית מציגה ריבועים ריקים (tofu), או שהחלפת גופן דומה מסיטה כל מעבר שורה. הגופנים מעולם לא היו על אותו מחשב — רק בתוך ה-PDF — ומרנדר שעוצר בהחלפת גופן מערכת אינו יכול לראות אותם. גופנים מקוצצים (subset fonts) מחמירים את המצב: תת-קבוצה עשויה לשאת ארבעים גליפים תחת קודי תווים שהוקצו באופן פרטי לקובץ הבודד הזה, הקצאה שאף גופן מותקן אינו משתף

תקן ISO 32000-1 §9.9 מגדיר שלושה נשאים עבור תוכנית גופנים מוטמעת במתאר הגופן (font descriptor): FontFile מחזיק בתוכנית המקורית מסוג Type 1,‏ FontFile2 מחזיק בתוכנית TrueType, ו-FontFile3 מחזיק בטבלת CFF חשופה (Type1C או CIDFontType0C) או בעטיפת OpenType. סוג רביעי, גופן Type 3 של תקן ISO 32000-1 §9.6.5, אינו מטמיע קובץ בינארי כלל — כל גליף הוא זרם תוכן PDF קטן המבוצע במקום. שלושת הנשאים נבדלים במתמטיקת קווי המתאר (B-splines ריבועיים לעומת מחרוזות תווים מעוקבות לעומת אופרטורים שרירותיים של דפים), ולכן מרנדר נאמן זקוק למפרש נפרד לכל אחד, בתוספת שכבת קידוד ההופכת קודי תווים לאינדקס הגליף הנכון לפני שנוגעים בקו מתאר כלשהו

כיצד HotPDF ממירה קווי מתאר TrueType glyf לנתיבי GDI?

המחלקה THPDFEmbeddedTTF בקובץ HPDFGlyphRender.pas קוראת את טבלת loca כדי לאתר כל רשומת גליף, פוסעת לאורך קווי המתאר של glyf נקודה אחר נקודה ומפיקה נתיב GDI. שתי מוסכמות של TrueType דורשות טיפול מפורש. ראשית, נקודות רצופות מחוץ לעקומה (off-curve) מרמזות על נקודה על העקומה (on-curve) בנקודת האמצע שלהן, ועקומה שכל נקודותיה מחוץ לעקומה מתחילה בנקודת האמצע של הנקודות האחרונה והראשונה שלה — דלגו על אחד הכללים הללו והגליפים המעוגלים יקבלו פאות שטוחות או יקרסו. שנית, עקומות TrueType הן עקומות בזייה ריבועיות בעוד שפקודת GDI בשם PolyBezierTo מקבלת עקומות מעוקבות (cubics), ולכן כל מקטע ריבועי מועלה דרגה במדויק במקום להיות מיושר למקטעי קווים

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

העלאת הדרגה היא ללא אובדן נתונים: העקומה המעוקבת מתווה את אותה עקומה, כך שקו המתאר המרונדר תואם למה שמציג תואם מצייר מאותה טבלה, בכל זום. העבודה הנותרת היא המיקום. כל גליף מוגדר ביחידות גופן (בדרך כלל רשת של 1000 או 2048 יחידות לכל em), והמרנדר מרכיב את מטריצת קנה המידה, מטריצת הטקסט ומטריצת הטרנספורמציה הנוכחית לטרנספורמציה יחידה של גליף-להתקן לפני מילוי הנתיב. הסדר כאן חשוב יותר מכפי שנראה: הרכיבו את שלוש המטריצות הללו בסדר הפוך וכל גליף יקרוס לכיוון ראשית הצירים — דף בעל מראה שגוי שהבאג האמיתי שלו הוא שורה אחת של אלגברה של מטריצות

כיצד מפרש מחרוזות התווים Type 2 מטפל בגופני CFF

המחלקה THPDFEmbeddedCFF מעניקה לתוכניות FontFile3 מפרש מחרוזות תווים Type 2 אמיתי: היא מנתחת את מבני ה-CFF INDEX, את ה-Top DICT ואת ה-Private DICT, ואז מריצה כל מחרוזת תווים ומפיקה מקטעי נתיב ישירות ל-GDI. עטיפת OpenType (מכל ה-OTTO) מוסרת תחילה כדי להגיע לטבלת ה-CFF החשופה; זרמי CIDFontType0C ו-Type1C חשופים נצרכים ישירות. מחרוזות תווים הן שפת מחסנית קומפקטית, ושלוש מהמוסכמות שלה קובעות האם המפרש נשאר מסונכרן עם זרם הבייטים. קידומת הרוחב האופציונלית פירושה שלאופרטור הראשון שמנקה את המחסנית עשוי להיות אופרנד מוביל אחד נוסף. האופרטור hintmask מרמז על vstemhm כאשר האופרנדים עדיין על המחסנית, ומספר בייטי המסכה שיש לדלג עליהם תלוי במספר גבעולי הרמז (stem count) שנצברו — טעו בספירה פעם אחת וכל אופקוד הבא ייקרא בצורה שגויה. וקריאות לתת-שגרה (subroutine calls) מוסיפות היסט לאינדקס שלהן (107, 1131 או 32768 בהתאם למספר תת-השגרות) לפני החיפוש, כך שקריאה ללא היסט נוחתת על תת-שגרה שגויה לחלוטין

גופן CFF המבוסס על מפתח CID מוסיף רמת עקיפין אחת שמכשילה מימושים פשוטים: קוד התו בוחר CID, אך אינדקס מחרוזת התווים הוא GID, וערכת התווים (charset) של הגופן ממפה GID ל-CID — ולכן המרנדר בונה את מיפוי ה-CID-ל-GID ההפוך לפני הציור, ובוחר את ה-Private DICT לכל גליף דרך FDSelect עבור גופנים הנושאים מספר כאלה. תוכניות Type1C המבוססות על מפתח שם, הנשא הנפוץ לגופני Type 1 פשוטים, מפענחות במקום זאת קודים של בייט אחד דרך הקידוד המובנה של תוכנית ה-CFF או דרך מנגנון הקידוד ברמת ה-PDF המתואר בהמשך. אזהרה כנה אחת: המפרש קורא אופרטורים של רמזים (hint operators) כדי לשמור על סנכרון הזרם אך אינו מבצע רמיזה (hinting), מגבלה שנדונה בסוף

מהו גופן Type 3 וכיצד הוא מצויר?

גליף Type 3 אינו קו מתאר כלל — תקן ISO 32000-1 §9.6.5 מגדיר אותו כזרם תוכן, ולכן HotPDF מרנדרת אותו על ידי דחיפת מצב הגרפיקה, הרכבת מטריצת הגופן, גודל הגופן ומטריצת הטקסט על גבי ה-CTM, והרצת פרוצדורת הגליף דרך אותו מפרש אופרטורים שמצייר דפים, כאשר ה-/Resources של הגופן עצמו בטווח. שני פרטי מפרט חשובים לצורך תקינות העבודה. ערכי /Widths של Type 3 מבוטאים במרחב הגליף ולא במרחב הטקסט של 1/1000 שבו משתמש כל סוג גופן אחר, ולכן התקדמויות חייבות לעבור דרך /FontMatrix — גופני ברקוד עם מטריצה של 0.01 יצעדו אחרת באופן שגוי בסדר גודל שלם. ופרוצדורת גליף שנפתחת עם האופרטור d1 נותנת שתי הבטחות שהמרנדר אוכף: הציור מוגבל לתיבה החוסמת המוצהרת, ולפי תקן ISO 32000-1 §9.6.5.2 הגליף מתעלם מאופרטורי הצבע שלו ומצייר עם צבע המילוי הנוכחי של הקורא, כך שפקודות rg,‏ g,‏ k ותאומותיהן המתוות בתוך הפרוצדורה מנוטרלות למשך אותו גליף. דלגו על כלל הצבע וגופן ברקוד d1 שהוחתם בכחול על ידי הדף ייצא שחור; דלגו על החיתוך וגליף פגום יצייר מחוץ לתא שלו

כיצד קודי תווים הופכים לזהות גליף (glyph ID)

מפרשי קווי מתאר הם רק חצי מהעבודה, מכיוון שהבייטים במחרוזת טקסט ב-PDF הם קודי תווים ולא אינדקסים של גליפים, ותקן ISO 32000-1 מקדיש שני סעיפי משנה למיפוי. עבור גופנים פשוטים, סעיף 9.6.6 קובע עדיפות קשיחה: מערך /Differences דורס את קידוד הבסיס (WinAnsiEncoding,‏ MacRomanEncoding או StandardEncoding), שדורס את המפה של תוכנית הגופן עצמה. HotPDF מפענחת שרשרת זו לטבלת קוד-ל-GID בת 256 רשומות, ומתרגמת שמות גליפים לאינדקסים של גליפים בשלושה נתיבים: התאמת charset מדויקת בתוך תוכנית CFF, שמות מספריים gNN/glyphNN הנלקחים כאינדקסים מילוליים, ותרגום שם-ל-Unicode של רשימת הגליפים של אדובי (Adobe Glyph List) ולאחריו חיפוש cmap עבור תוכניות TrueType. עבור גופנים מורכבים, סעיף 9.7 מציב את CIDToGIDMap כאחראי: המקרה הנפוץ הוא /Identity, אך הרשומה עשויה להיות זרם של זוגות big-endian באינדקס לפי CID — ופלט ה-Unicode של HotPDF משתמש בדיוק בצורת זרם זו עבור תת-קבוצות קומפקטיות, כך שנתיב הזרם אינו מקרה קצה אקזוטי

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

כאשר נדרש חיפוש cmap של TrueType,‏ HotPDF עוברת לאורך שרשרת גיבוי במקום לבטוח בטבלת משנה אחת: טבלאות המשנה של Windows Unicode (פורמט 4, ואז פורמט 12 עבור מישורים משלימים) מגיעות ראשונות, לאחר מכן טבלת המשנה של סמלים (3,0) עם מוסכמת ה-Pua F000 שלה הממופה למטה לבייט הנמוך — הסיבה שגופן סמלים כמו Wingdings עונה לקודי ASCII רגילים — ואז פורמטים ישנים 6 ו-0. טבלאות משנה בפורמט 2 אינן מפורשות במכוון: הן ממפות דפי קוד מרובי-בייטים מיושנים כמו Shift-JIS ו-Big5, ולא Unicode, וגופני CJK מודרניים נושאים ממילא טבלת משנה בפורמט 4 או 12. כל קוד שאינו שורד אף אחד מהנתיבים הללו נסוג לציור GDI עבור אותו גליף בודד, כך שתו אחד שאינו ניתן למיפוי פוגע בגליף אחד בלבד, ולא בכל ריצת הטקסט

מה שהנתיב המוטמע אינו עושה

ראוי לציין את המגבלות בבירור. רמיזה (hinting) אינה מבוצעת — קווי המתאר ממולאים כפי שהוגדרו, דבר שאינו ניתן להבחנה מפלט בעל רמיזה ב-150 DPI ומעלה, אך יכול להיות שונה בפיקסל ממרנדר בעל רמיזה בגדלים קטנים מאוד. תוכניות Type 1 מקוריות ב-FontFile (מחרוזות תווים מוצפנות eexec) אינן מפורשות, וצירי גופנים משתנים של OpenType אינם מוחלים; שני המקרים הללו, כמו תוכנית גופנים פגומה או טבלת glyf ללא cmap שמיש, נסוגים לציור גופני מערכת במקום להכשיל את הדף. אותה גישת נאמנות תחילה מתרחבת למקומות אחרים במרנדר — תבניות הצללה ציריות ורדיאליות מקבלות את אותו טיפול שמגיע למעברי צבע — ולצד הייצור יש סיפור גופנים משלו באופן שבו EndDoc מסדר תת-קבוצות גופנים

השימוש בצינור אינו דורש שום קוד ספציפי לגופנים — כל מנגנון למעלה מופעל אוטומטית בתוך קריאת רינדור הדף

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

התוצאה המעשית היא זו שחשובה למשתמשים שלכם: קובץ PDF הנושא את גופניו מרונדר עם אותם גופנים, בשרת בנייה, במכל Windows או במחשב לקוח שמעולם לא ראה את הגופן הזה. צינור רינדור הגליפים המוטמע מגיע כחלק מרכיב HotPDF עבור Delphi ו-C++Builder — ספריית VCL מקורית המכסה יצירת PDF, עריכה, חילוץ טקסט ורינדור דפים ללא תלות בקובצי DLL חיצוניים