Technical Article

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

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

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

מדוע Delphi טהור במקום HarfBuzz

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

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

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

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

ה-LangSys מציין קבוצה של אינדקסי תכונות (feature indices). כל אינדקס מצביע לתוך ה-FeatureList, שבה רשומת תכונה נושאת תג בן ארבעה בתים, ss01 ביניהם, ורשימה של אינדקסי חיפוש. האינדקסים הללו מצביעים לבסוף לתוך ה-LookupList, שבה חיות תתי-טבלאות ההחלפה בפועל. So resolving ss01 means: find the script, find its LangSys, find the feature whose tag is ss01, collect the lookups it names, and apply them. HotPDF defaults to the DFLT script and the default LangSys, which is what the vast majority of Latin text designs ship, and it exposes a way to override the script tag when a font wires its features under a specific script instead

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

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

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

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

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

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

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

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

סביב אלה יושבים עוד מספר תגי החלפה בודדת. aalt הוא גישה לכל החלופות (access-all-alternates), איחוד של כל חלופה שיש לגליף, המוצג בדרך כלל כתכונת פלטת גליפים. titl בוחר אותיות רישיות לכותרות (titling capitals) המיועדות לגדלים גדולים. subs ו-sups מחליפים לספרות תחתונות ועליונות אמיתיות במקום ברירות מחדל מוקטנות. ordn מייצר צורות סידוריות (ordinal forms), האותיות המורמות ב-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 מכסה את מישור השפות הבסיסי (Basic Multilingual Plane), 65536 נקודות הקוד הראשונות, וזה מספיק לרוב הטקסט הלטיני. זה לא מספיק עבור נקודות קוד מ-U+10000 ומעלה, מישורי העזר (supplementary planes), שבהם חיים כיום תווים אלפאנומריים מתמטיים, סמלים רבים ומספר כתבים פעילים

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

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

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