מאמר טכני

חלופות סגנוניות של OpenType GSUB ב-Delphi טהור

מעצב בוחר גופן עם a מקומה אחת (single-story) לכותרות, או אפס עם קו נטוי עבור טבלאות, או סט של אותיות רישיות מצועצעות (swash) לכריכה. גליפים אלו כבר קיימים בגופן. הם פשוט אינם ברירת המחדל. ה-a של ברירת המחדל מופה מהתו דרך טבלת ה-cmap לגליף אחד, והחלופה יושבת כמה מזהי גליפים משם, וניתן להגיע אליה רק דרך כלל החלפה. הפקת חלופה זו ב-PDF משמעותה קריאת הכלל ופליטת הגליף החלופי בזרם התוכן. מאמר זה עוסק בקריאת כללים אלה, מסוג ההחלפה הבודדת, ב-Object Pascal ללא ספריית עיצוב (shaping) מקורית מתחת

ההיקף צר בכוונה. ערכות וחלופות סגנוניות הן החלפות של גליף-בודד-נכנס, גליף-בודד-יוצא. הן החלק של פריסת OpenType שניתן לפתור עם הליכת טבלה קטנה ודטרמיניסטית, מה שהופך אותן למתאימות היטב למנוע Pascal שרוצה להישאר חופשי מתלויות ב-C

מדוע Delphi טהור ולא HarfBuzz

HarfBuzz היא התשובה הברורה ל"עצב את הטקסט הזה", ועבור עיצוב מלא דו-כיווני, הודי, או ערבי זו התשובה הנכונה. זוהי גם ספריית C. קשירתה למוצר Delphi או C++Builder משמעותה משלוח אובייקט מקורי (native) עבור כל פלטפורמת יעד וארכיטקטורה, התאמה למוסכמות הקריאה שלה, מעקב אחר קצב השחרור שלה, וקריאת תנאי הרישיון שלה מול שלך. שום דבר מזה אינו קשה בפני עצמו. כל זה הוא חיכוך שלעולם אינו נעלם, והוא אינו קונה דבר כאשר הדרישה בפועל היא "תן לי את צורת ה-ss01 של האות הזו"

החלפה בודדת אינה זקוקה למנוע עיצוב (shaping engine). היא זקוקה למנתח (parser) עבור קומץ פורמטים של תת-טבלאות GSUB וחיפוש בינארי אחד או שניים. כתיבת זה ב-Pascal שומרת את כל שרשרת הכלים בתוך מהדר אחד. המגבלה הכנה היא שגישה זו מטפלת בבדיקות החלפת גליפים ולא בשום דבר אחר. זו אינה רזולוציית bidi, זה אינו סידור מחדש של שפות הודיות, וזה אינו עיצוב הקשרי אוטומטי. היכן שאלה נדרשים, הם נדרשים, ושאילתת החלפה בודדת לא תמלא את מקומם

היררכיית ה-GSUB, מלמעלה למטה

טבלת החלפת הגליפים מאורגנת כשרשרת של הפניות עקיפות (indirections), ושאילתת החלפה הולכת לאורך השרשרת מלמעלה. בראש נמצאת ה-ScriptList. תגית סקריפט כמו latn בוחרת רשומה, והתגית המיוחדת DFLT היא סקריפט ברירת המחדל שחל כאשר אף סקריפט ספציפי יותר אינו מתאים. רשומת הסקריפט מצביעה על LangSys, מערכת השפה, עם LangSys ברירת מחדל למקרה הנפוץ וכאלה עם שם אופציונלי עבור שפות שזקוקות להתנהגות שונה. טורקית היא הדוגמה הרגילה, שבה ה-i עם וללא הנקודה דורשים טיפול משלהם

ה-LangSys מציין אוסף של אינדקסי תכונות. כל אינדקס מצביע לתוך ה-FeatureList, שם רשומת תכונה נושאת תגית של ארבעה בתים, ss01 ביניהם, ורשימה של אינדקסי בדיקה (lookup indices). אינדקסים אלה מצביעים לבסוף אל תוך ה-LookupList, שם נמצאות תת-טבלאות ההחלפה בפועל. לכן פתרון ss01 משמעותו: מצא את הסקריפט, מצא את ה-LangSys שלו, מצא את התכונה שהתגית שלה היא ss01, אסוף את הבדיקות שהיא מציינת, ויישם אותן. HotPDF משתמש כברירת מחדל בסקריפט DFLT וב-LangSys של ברירת המחדל, שזה מה שרובם המכריע של עיצובי הטקסט הלטיני מספקים, והוא חושף דרך לדרוס את תגית הסקריפט כאשר גופן מחווט את התכונות שלו תחת סקריפט ספציפי במקום זאת

טבלאות כיסוי מחליטות מי משתתף

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

כיסוי מגיע בשני פורמטים. פורמט 1 הוא רשימה של מזהי גליפים הממוינים בסדר עולה. אתה מוצא גליף בעזרת חיפוש בינארי, ומיקומו ברשימה הוא אינדקס הכיסוי שלו. פורמט 2 הוא רשימה של רשומות טווח, כל אחת היא גליף התחלה, גליף סיום, ואינדקס הכיסוי שאליו ממופה גליף ההתחלה. גליף בתוך טווח מקבל את אינדקס הכיסוי שלו על ידי היסט מתחילת הטווח. פורמט 1 הוא קומפקטי כאשר הגליפים המשתתפים מפוזרים, פורמט 2 כאשר הם נופלים לרצפים רציפים. שניהם ממוינים, ולכן שניהם נסרקים בזמן לוגריתמי, ושניהם מחזירים או אינדקס כיסוי או "לא מכוסה" נקי המאפשר למנוע לעזוב את הגליף בשקט

החלפה בודדת, שני הפורמטים

החלפה בודדת (Single Substitution) היא LookupType 1, והיא ממפה גליף אחד לתחליף אחד בדיוק. יש לה גם שני פורמטים, והפיצול הוא אופטימיזציה של שטח. פורמט 1 מאחסן דלתא (delta) חתומה בודדת. מזהה הגליף של הפלט הוא מזהה גליף הקלט בתוספת אותה דלתא, מודולו 65536. כך גופן מקודד החלפה שבה כל גליף משתתף יושב באותו היסט קבוע מהחלופה שלו, למשל בלוק של ספרות ישורות הממוקמות במרחק קבוע מספרות הסגנון הישן (oldstyle) התואמות. טבלת הכיסוי אומרת אילו גליפים עומדים בדרישות, והדלתא האחת משמשת את כולם

פורמט 2 מאחסן מערך מפורש של מזהי גליפים חלופיים. אינדקס הכיסוי מטבלת הכיסוי הוא האינדקס לתוך מערך זה, כך שגליף באינדקס כיסוי 0 הופך לרשומה הראשונה במערך, אינדקס כיסוי 1 לשנייה, וכן הלאה. פורמט 2 משמש כאשר החלופות אינן בהיסט אחיד, שזהו המקרה הנפוץ עבור ערכות סגנוניות שנבנו ידנית. השאילתה זהה מצד הקורא בכל מקרה. קח את גליף הקלט, העבר אותו דרך Coverage, ואם הוא מכוסה, יישם את הדלתא או קרא את חריץ המערך

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

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

מה המשמעות של תגיות התכונות הסגנוניות

תגית התכונה היא כל אוצר המילים של איזו חלופה אתה מבקש, והתגיות הרלוונטיות לעבודה סגנונית הן רשימה קצרה. צמד הכותרות הוא salt, חלופות סגנוניות (stylistic alternates), הגישה הכוללת לצורות החלופיות של גליף, ו-ss01 עד ss20, עשרים הערכות הסגנוניות הממוספרות שגופן יכול להגדיר, כל אחת היא חבילת החלפות בעלת שם שהמעצב מקבץ יחד. גופן עשוי לשים a מקומה אחת ו-R עם רגל ישרה תחת ss03, למשל, כך שהפעלת סט אחד זה מעצבת מחדש את שניהם

מסביב לאלו יושבות עוד כמה תגיות של החלפה בודדת. aalt היא גישה-לכל-החלופות, האיחוד של כל חלופה שיש לגליף, המוצגת בדרך כלל כתכונה של פלטת גליפים. titl בוחר אותיות רישיות לכותרת שנחתכו עבור גדלים גדולים. subs ו-sups מחליפים פנימה ספרות כתב תחתי ועילי אמיתיות במקום ברירות מחדל מוקטנות. ordn מפיק צורות סודרות, האותיות המוגבהות ב-1st ו-2nd. frac בונה שברים, אם כי שברים אלכסוניים מלאים נשענים גם על ליגטורה והיגיון הקשרי שחורג מהחלפה בודדת פשוטה. עבור מקרי הגליף הבודד, המנגנון זהה ל-ss01: העבר את התגית לשאילתת ההחלפה וקרא בחזרה את הגליף החלופי

// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

cmap פורמט 12 והמישורים המשלימים

לפני שכל החלפה יכולה לרוץ, תו חייב להפוך לגליף, וזו העבודה של טבלת cmap. שאילתת ההחלפה מתחילה ממזהה גליף, כך שהנתיב הוא תמיד מתו לגליף דרך cmap, ואז מגליף לחלופה דרך GSUB. החלק המעניין ב-cmap הוא טווח ההגעה שלו. תת-טבלה בפורמט 4 מכסה את המישור הרב-לשוני הבסיסי (BMP), 65536 נקודות הקוד הראשונות, וזה מספיק לרוב הטקסט הלטיני. זה אינו מספיק לנקודות קוד מ-U+10000 ומעלה, המישורים המשלימים, ששם נמצאים כעת אלפאנומריים מתמטיים, סמלים רבים, וכמה סקריפטים חיים

פורמט 12 הוא תת-הטבלה המכסה את הטווח המלא של U+0000 עד U+10FFFF. זוהי רשימה ממוינת של קבוצות, כל קבוצה היא נקודת קוד התחלה, נקודת קוד סיום, ומזהה גליף התחלה, כך שריצה רציפה של נקודות קוד ממופה לריצה רציפה של גליפים. HotPDF פותר נקודות קוד באסטרטגיה היברידית שמתאימה לאופן שבו הנתונים מעוצבים. נקודות קוד ב-BMP מוגשות ממערך ישיר המאונדקס על ידי נקודת הקוד, בדיקה יחידה ללא חיפוש. נקודות קוד במישורים המשלימים מוגשות מטבלה דלילה הממוינת לפי נקודת קוד ונסרקת בחיפוש בינארי. התוצאה היא ש-GetUnicodeGlyphForCodepoint לוקח Cardinal מלא ועונה נכונה לאורך כל הטווח, תוך החזרת מזהה גליף 0, הגליף .notdef, עבור כל נקודת קוד שהגופן אינו ממפה

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

היכן שהשאילתות הללו נעצרות

ממשקי ה-API של החלפה בודדת עונים על צורה אחת של שאלה, וכדאי להיות ברורים לגבי מה שהם לא עונים. LookupType 1 הוא אחד משמונה סוגי החלפה. השאילתה אינה מטפלת בהחלפה מרובה של LookupType 2, שבה גליף אחד הופך לכמה, ולא בהחלפת ליגטורה של LookupType 4, שבה כמה גליפים הופכים לאחד. היא אינה מטפלת בסוגים ההקשריים ושרשור-הקשרי, LookupTypes 5 ו-6, שמופעלים רק כאשר גליף מופיע בסביבה מסוימת, וגם לא בסוגי ההרחבה ושרשור-הפוך. שבר אלכסוני, עיצור קשור בדוונאגרי, או מפל התחלי-אמצעי-סופי בערבית היא בעיית רצף, ובדיקת החלפה בודדת לכל גליף אינה יכולה לבטא אותה

הוא גם אינו מבצע עיצוב אוטומטי (automatic shaping). שום דבר כאן אינו בודק ריצת טקסט, מחליט אילו תכונות להפעיל, ומיישם אותן בסדר שהסקריפט דורש. הקורא בוחר את תגית התכונה ומיישם אותה גליף אחר גליף. זהו בדיוק הכלי הנכון עבור ערכות וחלופות סגנוניות, שהן אופציונליות ומקומיות, ובדיוק הכלי הלא נכון עבור סקריפט שצריך סידור מחדש. שמירה על גבול חד היא מה שמאפשרת לנתיב ההחלפה להישאר קטן וצפוי

עבור המקרים שכן זקוקים לעבודה ברמת הרצף, סיפור הסקריפט המורכב מטופל במאמר שלנו על עיצוב טקסט של סקריפט מורכב ב-Delphi. אם ההחלפות שלך הן חלק מעבודת דיווח גדולה יותר שגם ממקמת תמונות וגופנים אחרים בעמוד, המדריך לפלט דוחות עם גופנים ותמונות מכסה כיצד החלקים הללו משתלבים יחד. כל אלה רצים על אותו מנוע, רכיב HotPDF עבור Delphi ו-C++Builder, שנושא את שאילתות ההחלפה של GSUB לצד ממשקי ה-API של הטמעת גופנים, יצירת תת-קבוצות וטקסט המכוסים במקומות אחרים בבלוג זה