מאמר טכני

עיצוב טקסט בערבית ושפות ימין-לשמאל ב-PDF ב-Delphi עם HotPDF

העבירו את הביטוי בערבית יוضح ملف PDF ל-TextOut ופתחו את התוצאה. האותיות ירוצו בכיוון הלא נכון, וכל אחת מהן תופיע בצורתה המבודדת עם רווח גלוי לפני הבאה אחריה, כאילו מישהו הקליד אנגלית לאחור ולחץ על רווח בין כל תו. לא נזרקה חריגה. שום אזהרה לא הודפסה. הפלט פשוט שגוי, והוא שגוי משום ששתי טרנספורמציות נפרדות שערבית תלויה בהן מעולם לא קרו. לדעת מהן שתי הטרנספורמציות הללו, ואיזו קריאה מבצעת אותן, זה רוב מה שמסתכם בו פלט PDF עבור שפות מורכבות

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

מדוע מחרוזת נכונה עדיין מודפסת שגוי

Unicode שומר טקסט בסדר לוגי, הסדר שבו אתם מקלידים אותו וקוראים אותו בקול. רנדרר חייב להניח גליפים בסדר חזותי. עבור שפות של שמאל-לימין סדרים אלו חופפים ואף אחד לא חושב על כך. עבור ערבית ועברית הם לא, וכאשר שורה אחת משלבת כיוונים, למשל משפט בערבית הנושא את האסימון הלטיני "PDF" או מחיר הכתוב בספרות, אלגוריתם הדו-כיווניות של Unicode (UAX #9) מחליט בדיוק כיצד מקטעי השמאל-לימין מקננים בתוך שורת הימין-לשמאל. זוהי הטרנספורמציה הראשונה, סידור מחדש, ודילוג עליה הוא מה שהופך את השורה

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

על שולחן העבודה שום דבר מזה אינו בעיה שלכם. כאשר טופס VCL מצייר ערבית לתוך TEdit, מחסנית הטקסט של מערכת ההפעלה מסדרת מחדש ומעצבת אותו בשקט, וזו בדיוק הסיבה שהמחרוזת שנראית מושלמת על המסך יוצאת שבורה ב-PDF נאיבי. זרם תוכן (content stream) אינו שומר טקסט הניתן לעריכה. הוא שומר גליפים ממוקמים, ולכן מי שפולט את הזרם יורש את עבודת העיצוב שמערכת ההפעלה נהגה לטפל בה. RtLTextOut היא הקריאה שלוקחת את העבודה הזו בחזרה

מה RtLTextOut מעצב עבורכם

HotPDF שומר את הנתיב הלטיני ואת הנתיב של השפות המורכבות כשתי שיטות שונות. TextOut מדפיס את מה שאתם נותנים לו בסדר שאתם נותנים לו. RtLTextOut מבצע קודם את שתי הטרנספורמציות — סידור מחדש דו-כיווני לאורך כל השורה, וניתוח הקשרי עבור השפות המתחברות — ואז מדפיס. החוקים של איזו שפה חלים נכנסים דרך סט התווים (charset) של הגופן ולא דרך הקריאה עצמה, כך שהכיוון הוא בחירה מפורשת בכל אתר קריאה במקום ניחוש שנעשה מתוך התווים. ההגדרה של פרמטר אחר פרמטר, ערכי סט התווים, שלבי רישום הגופן ודוגמה שלמה הניתנת להידור נמצאים כולם במאמר ההפניה של RtLTextOut; חלק זה נשאר עם מה שהטרנספורמציות אומרות, היכן הן עוצרות וכיצד להוכיח שהן עבדו

כלל שימוש אחד חשוב אפילו בגובה הזה: הקלט חייב להיות בסדר לוגי, מכיוון ש-RtLTextOut מבצע את ההיפוך בעצמו, ומחרוזת שכבר הפכתם ביד תצא הפוכה פעמיים — מאמר ההפניה עובר על המלכודת הזו והניקוי שלה. מה שמזכה את המלכודת באזכור כאן הוא הסיבה שהיא שורדת בדיקה. מחרוזת בערבית טהורה שהופכה פעמיים יכולה להיראות נכונה לחלוטין, ומתפרקת רק כאשר שורה נושאת מילה לטינית או מספר, מכיוון שהרצפים המוטמעים האלה כבר לא מקננים כפי ש-UAX #9 מכתיב. הבאג אינו ברינדור; הוא בהזנת האלגוריתם בטקסט שכבר חצי מעובד

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

מתי סידור מחדש וחיבור מספיקים, ומתי לא

עבור טקסט רץ בערבית ובעברית — דוחות, חשבוניות, חוזים, מכתבים — סידור מחדש פלוס חיבור הקשרי הם כל העבודה, ו-RtLTextOut נושא זאת לבדו. הגבול מופיע כאשר הטיפוגרפיה דורשת יותר מחיבור. התשובה של HotPDF בצד הערבי היא מעצב בצד המפיק הדורש הצטרפות (opt-in): הגדירו AutoShapeArabic := True והרכיב משכתב את הרצף בסדר לוגי לצורות תצוגה של Unicode (Unicode Presentation Forms) לפני המעבר הדו-כיווני, כך שצורות מחברות מחושבות מול שכנים לוגיים וקפלי ליגטורה נאפים לתוך נקודות הקוד שה-PDF למעשה נושא, במקום להשאיר לצופה (viewer) לפתור. המתג כבוי כברירת מחדל והפלט יציב ברמת הבתים כאשר הוא נשאר כבוי, ולכן הפעלתו היא החלטה מכוונת לכל צינור נתונים של מסמך, ולא שדרוג גלובלי. אותו מודל opt-in מתרחב לשפות ימין-לשמאל המתחברות האחרות ש-HotPDF מעצב: סורית, נ'קו, אדלאם, ורוהינגיה הניפי, שלכל אחת מהן דגל עיצוב אוטומטי משלה המשקף את זה של הערבית

תכונות OpenType אופציונליות הן שוב מנגנון שונה. ליגטורות לפי שיקול דעת ותכונות דומות של החלפה יחידה עוברות דרך GetSingleSubstituteGlyph(GID, 'liga'), אשר פותר החלפה אחת בכל פעם — מזהה גליף (ID) קלט ראשון, תג תכונה שני — ומחזיר את גליף הקלט ללא שינוי כאשר התכונה אינה חלה. זה מספיק כדי להניע רשימת ליגטורות ידועה וסופית שאתם מתחזקים בעצמכם. זהו אינו מנוע GSUB מלא, וההבדל הוא בדיוק המקום שבו תוכניות אזוריות (locale plans) שאפתניות משתבשות: צינור נתונים של עיצוב המטפל בערבית ללא רבב הוכיח סידור מחדש וחיבור, שום דבר מעבר לכך

כיסוי פני שפות

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

תאילנדית יושבת בצד השני של הקו לחלוטין. היא רצה משמאל לימין, ולכן היא אינה זקוקה לעבודה דו-כיוונית, ואותיותיה אינן מתחברות, ולכן היא אינה זקוקה לניתוח הקשרי; מחרוזות תאילנדיות עוברות דרך הנתיב הרגיל של TextOut כמו לטינית. מה שכן יש לתאילנדית הם סימנים מוערמים — תנועות וסימני טון מעל ומתחת לעיצור הבסיס — והאם אלו יושבים נכון תלוי בכך שהגופן בונה את הסימנים המשלבים שלו להיערם ללא עזרת מנוע עיצוב. רוב הגופנים הייעודיים לתאילנדית עושים זאת. בדקו עם הגופן המדויק שתטמיעו, לא עם מראה דומה

דוונאגרי ושאר המשפחה ההודית הם העצירה הקשה והכנה. סימני התנועות שלהם מסתדרים מחדש סביב אשכולות עיצורים והעיצורים המחוברים שלהם נוצרים דרך שרשראות של החלפות תלויות הקשר, שזה שטח GSUB מלא, מעבר לסידור מחדש וחיבור. אם שפה הודית נמצאת במפת הדרכים, הפעילו פיילוט אמיתי על מחרוזות לקוח אמיתיות לפני שאתם מבטיחים זאת — ערבית עובדת אינה ראיה לכך שדוונאגרי תעבוד. מחרוזות CJK, וייטנאמית עם הדיאקריטיקה המוערמת שלה, וטקסט אירופאי מעורב, כולם לוקחים את הנתיב הרגיל ללא ניתוח דו-כיווני, ומשתלם לשמור את שני הנתיבים נפרדים פיזית בקוד הדוח, שגרה אחת לרצפי RTL ואחת לכל השאר, כך שההיגיון של האזור גלוי באתר הקריאה במקום מוסתר מאחורי דגל שמישהו שוכח להגדיר

כיסוי גליפים נקבע עוד לפני שהעיצוב מופעל

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

// After RegisterUnicodeTTF, audit coverage for the
// codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

הרישום עצמו נושא שני אילוצים — רצפת PDF 1.5 לטיפול מובנה ב-Unicode וסיביות הרשאת ההטמעה של הגופן — שניהם מכוסים יחד עם שלבי ההגדרה במאמר ההפניה של RtLTextOut. מה ששייך לכאן הוא הרגל הביקורת: GetUnicodeGlyphForCodepoint היא מערכת ההתראה המוקדמת שלכם. עברו על טווחי נקודות הקוד שהנתונים שלכם משתמשים בהם בפועל כאשר השירות מופעל ורשמו אילו מזהי גליפים חוזרים. פער כיסוי יופיע אז כשורה ביומן הפעלה במהלך הפריסה, ולא כתווים חסרים בחשבונית שכבר הגיעה ללקוח

סדר הקריאה שייך למסמך, לא לגליפים

קבלת כל גליף נכון עדיין משאירה דבר אחד לא עשוי. תקן ISO 32000-1 §12.2 מגדיר העדפת צופה בשם /Direction הקובעת את סדר הקריאה הכולל של המסמך. זה לא נוגע בשום גליפים. מה שזה כן עושה זה להגיד לצופה כיצד לסדר פריסות של שני עמודים (two-up spreads), מאיזה צד פריסת עמודים מול עמודים צריכה להתחיל, ולאיזה כיוון ממשק הקריאה צריך לנטות. שום דבר מזה לא מופיע בעמוד בודד, וזו בדיוק הסיבה שזה נשכח

// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft;  // adds vpDirection to ViewerPreferences

הגדרת Direction היא כל העבודה: מגדיר המאפיין מוסיף vpDirection ל-ViewerPreferences של המסמך, כך ששורה אחת מכניסה את ההעדפה לקובץ. אם הטקסט יוצא דרך RtLTextOut אתם מקבלים את זה בחינם, מכיוון שהקריאה הופכת את כיוון המסמך כתופעת לוואי — מאמר ההפניה מכסה מתי מסמך מעורב צריך לבטל זאת. המקרה שבו עליכם להגדיר זאת בעצמכם הוא מסמך ימין-לשמאל שהופק בכל דרך אחרת, למשל מקלט שעיצבתם מראש במעלה הזרם וציירתם דרך הנתיב הרגיל. השאירו זאת בחוץ וההוכחה בעמוד בודד שאתם מסתכלים עליה תיראה זהה כך או כך; ואז מישהו מדפיס חוברת דו-צדדית, הפריסות יוצאות הפוכות, והסיבה היא שורת קוד אחת חסרה מלפני שבועות

אימות פלט מעוצב

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

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

רישום גופנים, יצירת תת-קבוצות ו-API הציור היומיומי של טקסט מכוסים במאמר על פלט דוחות, גופנים ותמונות עם HotPDF. כאשר אותם מסמכים צריכים לעמוד גם בפרופילי נגישות, תיוג השפה וכללי המבנה במאמר האימות של PDF/A ו-PDF/UA יושבים על גבי עבודת העיצוב כאן

ממשקי ה-API של ימין-לשמאל וגופני Unicode שתוארו לעיל נשלחים עם ה-HotPDF Component עבור Delphi ו-C++Builder; דף המוצר מקשר להפניה המלאה של פלט טקסט