עיצוב טקסט ברכיב PDFium עובר דרך object אחד שניתן להתקנה. ConfigureTextShaper מתקין את ה-shaper שכל נקודת כניסה לעיצוב מנתבת דרכו, מחליף ומשחרר את מה שהיה שם; ActiveTextShaper מחזיר את המותקן ויוצר את ברירת המחדל של הפלטפורמה בשימוש הראשון; ActiveTextShaperName מדווח איזה backend חי; ClearTextShaper מוריד את ההתקנה ומאפשר לברירת המחדל להיווצר שוב. ב-Windows ברירת המחדל היא TPdfUniscribeTextShaper. תחת Free Pascal יש TPdfHarfBuzzTextShaper, שקושר את libharfbuzz בזמן ריצה כך שספרייה חסרה היא מצב מדווח ולא כשל טעינה
ממשק אחד, שני backends שמחלקים את העבודה באופן שונה לחלוטין. הבנת אותה אסימטריה היא מה שמונע מהנתיב הנייד להפיק טקסט שמעוצב נכון וממוקם שגוי
למה ה-backend של Windows הוא מחלקה אחת והנייד שלושה חלקים?
כי Uniscribe הוא ארבעה APIs שמתחזים לאחד. ScriptItemize מפלח מחרוזת לפי סקריפט ופותר רמות דו-כיווניות; ScriptShape ממפה תווים ל-glyphs; ScriptPlace מחשב התקדמויות והיסטים; ScriptLayout שם את הריצות המתקבלות בסדר חזותי. backend שנבנה עליו לכן אין לו מה להוסיף, וזו הסיבה שה-shaper של Windows הוא מחלקה יחידה עם שיטה יחידה
HarfBuzz מכסה את שתי האמצעיות. הוא מעצב וממקם ריצה שאת הכיוון והסקריפט שלה הקורא כבר החליט, ואין לו דעה על איך פסקה מתפצלת לריצות או באיזה סדר הריצות האלה מופיעות. לכן ה-backend הנייד מספק את השאר: האלגוריתם הדו-כיווני פותר רמות הטמעה, פונקציות ה-Unicode של HarfBuzz מפלחות את הטקסט לפי סקריפט, והריצות מסודרות בסדר החזותי שכלל L2 של UAX #9 מפיק. החצי הדו-כיווני מהותי מספיק כדי להיות יחידה משלו, המתוארת במאמר רמות ההטמעה של UAX #9
ה-shaper אינו פותר גופנים, וזה מכוון
Uniscribe קורא את הבינארי של הגופן מהקשר מכשיר של GDI. אין מקבילה ניידת לכך, והמצאת אחת בתוך יחידת עיצוב פירושה להכריע בשם כל אפליקציה אם גופנים באים מ-fontconfig, מ-CoreText, מתיקיית גופנים של אפליקציה, או ממסד נתונים. לכן ה-backend של HarfBuzz מקבל resolver: callback שממפה שם גופן אל בייטי ה-TrueType או ה-OpenType. החזרת False מכשילה את בקשת העיצוב כפי שגופן GDI בלתי קריא מכשיל אותה ב-Windows
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// המדיניות שלכם: fontconfig, CoreText, תיקיית גופנים של אפליקציה, מסד נתונים
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// הבעלות עוברת ליחידה; קראו פעם אחת במהלך האתחול,
// לפני שמשהו מעצב טקסט
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// ב-Delphi ברירת המחדל של הפלטפורמה (Uniscribe) נוצרת לפי דרישה,
// ולכן שום התקנה אינה נחוצה כלל
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
השארת גילוי הגופנים מחוץ ל-shaper יש לה יתרון שני שמופיע בשרתים: אותו תהליך יכול לעצב עם קבוצת גופנים מוטמעת שאין לה שום קשר למה שמותקן על המכונה, מה שרוצים כשהפלט חייב להיות ניתן לשחזור בייטי על פני מארחים. הרכיב גם חושף ספק גופני מערכת של המארח עבור המקרים שבהם דווקא רוצים גופנים מותקנים, המכוסה במאמר ספק הגופנים של המערכת
רשומת התוצאה ניטרלית ל-backend, וה-clusters הם הסיבה
שני ה-backends ממלאים את אותו TPdfShapedText: הטקסט המקורי, שם הגופן, הגודל, בייטי הגופן, מערך ריצות, הרוחב הכולל, מניין ה-glyphs ומניין התווים הלוגי. כל TPdfShapedRun נושא את טווחו בטקסט המקורי, את מיקומו החזותי ב-X, את רוחבו, את רמתו הדו-כיוונית ודגל מימין-לשמאל, ועוד את ה-glyphs שלו. כל TPdfShapedGlyph נושא מזהה glyph, התקדמות, היסטי X ו-Y, ואת ה-cluster שאליו הוא שייך כהתחלה ואורך בטקסט המקורי
שדות ה-cluster האלה הם מה שהופך את הרשומה לשמישה ולא רק אינפורמטיבית. עיצוב אינו מיפוי אחד-לאחד: הברה של Devanagari הופכת ל-glyph אחד מארבעה תווים, ליגטורה של ערבית ממזגת שניים, ותו יחיד יכול להפיק כמה סימנים. בלי טווחי cluster אי אפשר למקם סימן צירוף, לבדוק פגיעה בקליק, או להדגיש בחירה, כי אי אפשר לומר לאילו תווים glyph שייך. איתם, האריתמטיקה מקומית ואותו קוד עובד עבור שני ה-backends
var
Shaped: TPdfShapedText;
R, G: Integer;
begin
if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
for R := 0 to High(Shaped.Runs) do
begin
// הריצות כבר מגיעות בסדר חזותי עם VisualX ממולא
X := Shaped.Runs[R].VisualX;
for G := 0 to High(Shaped.Runs[R].Glyphs) do
begin
EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
X + Shaped.Runs[R].Glyphs[G].OffsetX,
Shaped.Runs[R].Glyphs[G].OffsetY);
X := X + Shaped.Runs[R].Glyphs[G].Advance;
end;
end;
end;
תקציבים שייכים לרשומת האפשרויות
TPdfTextShapingOptions נושא כיוון ועוד שלוש תקרות: מקסימום תווים, מקסימום glyphs ומקסימום ריצות, עם פונקציית מחלקה Default שממלאת ערכים סבירים. התקרות אינן פרנויה לגבי קלט פגום; הן אריתמטיקה. עיצוב מתרחב: גופן עם החלפה הקשרית אגרסיבית יכול לפלוט יותר glyphs מתווי הקלט, ופסקה שמחליפה סקריפטים כל כמה תווים מפיקה ריצה לכל מעבר. מסמך שהורכב למקסם את שניהם הופך מחרוזת צנועה להקצאה גדולה, ושירות שמעצב טקסט מ-PDF לא מהימנים צריך מגבלה שהוא בחר ולא מגבלה שהמכונה כופה
קביעת הכיוון במפורש במקום להשאיר אותו על אוטומטי שווה לעשות בכל פעם שכבר ידוע לכם. האוטומטי מחיל את כללי כיוון הפסקה כדי לנחש מהתו החזק הראשון, מה שנכון לטקסט חופשי ושגוי עבור שדה טופס שכיוונו תכונה של השדה ולא של הערך שמישהו הקליד אליו
קישור בזמן ריצה, לא תלות בנייה
ה-backend של HarfBuzz טוען את הספרייה באופן דינמי. זו החלטת פריסה עם השלכות אמיתיות: בינארי אחד רץ על מכונה עם HarfBuzz ועל מכונה בלעדיו, מדווח על יכולת מופחתת במקרה השני במקום להיכשל בהתחלה. עבור ספרייה שנשלחת למפתחים אחרים זהו הסידור היחיד שעובד, כי אי אפשר לדרוש מכל צרכן של רכיב PDF להשיג ולהתאים גרסה של ספריית עיצוב שאולי אינו זקוק לה
הכלל המתאים לקוראים הוא לבדוק. ActiveTextShaper מחזיר nil כשלפלטפורמה אין ברירת מחדל ואף אחת לא הוגדרה, ונקודת הכניסה לעיצוב מדווחת על כך כ-shaper לא זמין ולא ככשל עיצוב. אלה בעיות שונות וראויות להודעות שונות: האחת היא פער פריסה, השנייה בעיית גופן או טקסט
התקינו פעם אחת, לפני שמשהו מעצב
ההתקנה מחליפה ומשחררת את ה-shaper הקודם, ולכן קריאה חוזרת בה בטוחה אך חסרת תועלת, וקריאה בה בזמן שתהליך אחר מעצב אינה בטוחה כלל. עשו זאת במהלך האתחול. אם עליכם לחזור לברירת המחדל של הפלטפורמה מאוחר יותר, העבירו nil, שהוא גם האופן שבו מבטלים כפיל בדיקה בסוף בדיקה
ברגע ש-backend מותקן, מדידה וגלישה מתנהגות זהה בשתי הפלטפורמות, מאחר שהן צורכות את מדדי הריצות וה-glyphs ולא קוראות לפלטפורמה ישירות; מודל הגלישה מתואר במאמר מדידת הטקסט וגלישת מילים. הפלטפורמות וה-toolchains הנתמכים עבור הרכיב מופיעים בדף המוצר של PDFium Delphi component