Uniscribe עושה יותר עבודה ממה שרוב הקוראים מבינים. ScriptItemize מבצע ניתוח דו-כיווני ופילוח סקריפטים במעבר אחד, ו-ScriptLayout מפיק את הסדר החזותי של הריצות המתקבלות. HarfBuzz, התחליף הנייד שאליו אנשים פונים, לא עושה אף אחד מהם: הוא מעצב ריצה בודדת שאת הכיוון והסקריפט שלה כבר הכריע מישהו אחר. לכן החלק הקשה בהעברת צינורית טקסט PDF של Windows אל Linux או macOS אינו קישור מנוע עיצוב. הוא אספקת האלגוריתם הדו-כיווני ש-Uniscribe סיפק בשקט, וברכיב PDFium זה בדיוק מה ש-FPdfBidi בשבילו
היחידה מממשת UAX #9 ישירות: הכללים P2 ו-P3 לכיוון פסקה, X1 עד X10 להטמעות ו-isolates מפורשים, W1 עד W7 לטיפוסים חלשים, N0 עד N2 לניטרליים ולסוגריים, I1 ו-I2 לרמות מרומזות, ו-L1 ו-L2 לסידור מחדש הסופי. שתי פונקציות נושאות אותו: PdfResolveBidiLevels מחזיר רמת הטמעה אחת לכל יחידת קוד UTF-16, ו-PdfBidiVisualOrder הופך את הרמות האלה לתמורה שמציבה יחידות קוד משמאל לימין
מה האלגוריתם נותן לכם, ומה שהוא לא
הוא נותן לכם מספרים. רמות זוגיות הן משמאל לימין, רמות אי-זוגיות הן מימין לשמאל, ורמתו של כל תו מקודדת את הקינון של הריצות הכיווניות שהתו יושב בתוכן. מהמספרים האלה L2 גוזר תמורה. מה שהאלגוריתם בכוונה אינו עושה הוא להכריע באיזה גופן להשתמש, ליצור ליגטורות, או לסדר מחדש glyphs בתוך cluster; אלה ענייני עיצוב ושייכים לשלב שאחרי זה
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto מחיל P2-P3: התו החזק הראשון מכריע
if PdfResolveBidiLevels(Text, pbdAuto, Levels, ParagraphLevel) then
begin
Order := PdfBidiVisualOrder(Text, Levels);
SetLength(Visual, Length(Order));
for I := 0 to High(Order) do
Visual[I + 1] := Text[Order[I] + 1];
// Visual קורא עכשיו משמאל לימין; Levels[] עדיין אומר אילו
// ריצות RTL כדי שניתן למסור ל-shaper כיוונים נכונים
end;
end;
טבלת מחלקות התווים מיוצרת, לא נכתבת
לכל נקודת קוד יש תכונת Bidi_Class, והאלגוריתם נועץ בה כל העת, ולכן הטבלה היא היסוד שהכול נשען עליו. היא מיוצרת ממסד הנתונים של התווים של Unicode ולא מתוחזקת ביד: השדה החמישי של UnicodeData.txt נותן את המחלקות המוקצות, והצהרות ה-@missing ב-DerivedBidiClass.txt נותנות את ברירות המחדל לנקודות קוד שהמסד אינו מקצה, וכך בלוקים לא מוקצים נופלים נכון לברירת המחדל R, AL, ET או BN ולא L
תרגיל הדחיסה הוא לפלוט רק את הטווחים שהמחלקה שלהם אינה L. כל דבר שנופל מחוץ לכל טווח הוא L, שהוא גם ברירת המחדל של Unicode וגם המחלקה של הרוב המוחלט של נקודות הקוד. זה לוקח טבלה שאחרת הייתה רצה לאלפי רשומות ומוריד אותה ל-745 טווחים וכ-6.7 KB. ההשלכה התפעולית שווה ניסוח: כשעוברים לגרסת Unicode חדשה, הריצו שוב את המחולל. עריכת קובץ ה-include ביד תעבוד, והיא גם תסטה בשקט מהמסד בשדרוג הבא
L2 חייב לסדר מחדש נקודות קוד, לא יחידות קוד UTF-16
זו הטעות שמפיקה פלט מושחת באמת, והמימוש הראשון עשה אותה. L2 אומר להפוך ריצות רצופות בכל רמה מהגבוהה ומטה אל הרמה האי-זוגית הנמוכה. נכתב מול מחרוזת UTF-16, "להפוך ריצה" פירושו באופן טבעי היפוך יחידות הקוד בה. עבור תווים במישור הרב-לשוני הבסיסי זה בסדר. עבור תו RTL במישור astral, כמו אלה בבלוקים הקפריסאי או הדרום-ערבי העתיק ליד U+10800, זה לא: התו הוא זוג surrogates, היפוך הריצה מציב את ה-surrogate הנמוך לפני הגבוה, והמחרוזת מכילה עכשיו שני surrogates לא מזווגים במקום תו אחד. שום דבר במורד הזרם לא יכול לשחזר אותו
התיקון הוא לעשות L2 על יחידות נקודות-קוד. המימוש ממזג יחידות קוד ליחידות נקודות-קוד, מבצע את ההיפוכים על אותן יחידות, ומרחיב את התוצאה חזרה לאינדקסי יחידות קוד בסוף. זו הסיבה ש-PdfBidiVisualOrder מקבל את הטקסט ולא רק את מערך הרמות: הוא לא יכול לדעת מהרמות לבדן היכן גבולות ה-surrogate. אותה משמעת של זוגות surrogate עוברת דרך ממשקי הטקסט באופן כללי, כפי שמתואר במאמר ה-emoji, ה-CJK וזוגות ה-surrogate
הירידה דרך הרמות חייבת לכלול רמות שאינן מתרחשות
הטעות השנייה עדינה יותר ואינה מפיקה קריסה, רק טקסט שאינו מסודר מחדש. L2 אומר להתחיל ברמה הגבוהה הקיימת ולרדת אל הרמה האי-זוגית הנמוכה. אופטימיזציה טבעית היא לאסוף את קבוצת הרמות שאכן מתרחשות ולעבור על אותה קבוצה. זה שגוי
שקלו שורת טקסט לטיני בתוך הטמעה מימין לשמאל. רמת הפסקה היא 0, ההטמעה דוחפת את התווים הלטיניים לרמה 2, ואף תו לא יושב ברמה 1. מעבר על הרמות המתרחשות מוצא רק 0 ו-2, ואין בכלל רמה אי-זוגית, ולכן הלולאה לא מבצעת היפוך. התשובה הזאת נכונה, אבל מסיבה שהאופטימיזציה אינה יודעת: היפוך ברמה 2 ואחריו היפוך ברמה 1 היו מתבטלים בדיוק, ולכן לא לבצע אף אחד הוא התוצאה הנכונה. שנו את הקלט מעט, כך שקיימים תווי רמה 1 ורמה 3 אבל רמה 2 אינה קיימת, והלולאה מבוססת-הקבוצה מדלגת על היפוך הרמה-2 שהאלגוריתם דורש
// נכון: עברו על כל רמה מהמקסימום ומטה אל הרמה האי-זוגית
// הנמוכה, כולל רמות שאף תו לא מחזיק בפועל
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // ללא פעולה כשאף ריצה אינה מתאימה
Dec(Level);
end;
נכתב כלולאה יורדת פשוטה ההתנהגות מתקבלת במתנה, ואיטרציות ה-no-op לא עולות דבר נמדד. זהו מקרה שבו האופטימיזציה המובנת אינה שגויה במעט, היא שגויה באופן תלוי-קלט שקורפוס בדיקה קטן לעולם לא יגלה
סוגריים: BD16 עם טבלה פרגמטית
הכלל N0 ואלגוריתם זוגות-הסוגריים BD16 קיימים כדי שסוגריים בטקסט בכיוונים מעורבים ייפתרו לכיוון של מה שהם סוגרים ולא לכיוון של מה שבמקרה סמוך. זה דורש טבלה של זוגות סוגריים. המימוש נושא את הזוגות בשימוש כללי ולא את כל תכולת קובץ הסוגריים של Unicode: ASCII, CJK, fullwidth, סוגריים מתמטיים ודקורטיביים
סוגריים שאינם רשומים אינם שגיאה. הם נפתרים כניטרליים רגילים דרך N1 ו-N2, שהוא בדיוק ההתנהגות שכל מימוש החזיק לפני ש-Unicode 6.3 הכניס את N0. לכן הגבול הוא "משוכלל פחות עבור סוגריים נדירים", ולא "שגוי". פרט אחד כן זקוק לטיפול מפורש: השקילות הקנונית בין סוגרי הזווית ב-U+2329 וב-U+232A לאלה ב-U+3008 וב-U+3009 חייבת להתקפל בעת התאמת זוגות, או שסוגר פותח שנכתב בדרך אחת לא ימצא זיווג עם סוגר סוגר שנכתב בדרך האחרת
איך בודקים שלושים כללים מתקשרים
לא עם קורפוס גדול, לפחות לא קודם. הגישה הפרודוקטיבית הייתה שישה עשר מקרים מאומתים ביד, כל אחד נבחר להפעיל כלל ספציפי וכל אחד נבדק מול הרמות ש-UAX #9 אומר שהוא צריך להפיק: זיהוי כיוון פסקה תחת P2 ו-P3, כללי הטיפוסים החלשים W2, W3 ו-W7, כללי הרמה המרומזת I1 ו-I2, הטמעה מפורשת דרך X2 ו-X7, isolates דרך X5a ו-X6a, איפוס ה-L1 של רווחים עוקבים ומפרידים, מקרה סוגריים של N0, ומקרה אחד עם תו astral לנעילת הטיפול ב-surrogate
שישה עשר מקרים עם רמות צפויות ידועות-נכונות תופסים יותר מאלף ושישה מאות מקרים עם פלט שנראה סביר, כי מצב הכשל של מימוש דו-כיווני הוא טקסט שקורא כמעט נכון. ברגע שאלה עוברים, קורפוס שימושי למציאת פערי טבלה ובעיות ביצועים, שהם מחלקות פגמים שונות
בתוך רכיב PDFium הרמות מזינות שני צרכנים. בצד הכתיבה הן אומרות ל-backend העיצוב את הכיוון של כל ריצה, שהוא הקלט ש-HarfBuzz דורש. בצד הקריאה הן מאירות את גאומטריית הבחירה וסדר הקריאה, מאחר שקליק בטקסט RTL חייב להימפה אל מיקום לוגי ולא חזותי; המיפוי הזה מכוסה במאמר בחירת שורות חזותיות ומודל סדר הקריאה בבלוקי טקסט מובנים וסדר קריאה. פרטי התמיכה בפלטפורמות עבור הרכיב נמצאים בדף המוצר של PDFium Delphi component