מאמר טכני

RtLTextOut ב-HotPDF: טקסט PDF מימין לשמאל ב-Delphi

שלח את המשפט הערבי يوضح ملف PDF هذا אל TextOut רגילה והעמוד שחוזר שגוי בשתי דרכים בו זמנית. המילים רצות משמאל לימין במקום מימין לשמאל, והאותיות יושבות מופרדות בצורותיהן המבודדות במקום להתחבר למילים מחוברות. שום דבר אינו שוגה. ה-Delphi מתקמפל, הקובץ נפתח, ומבקר שקורא ערבית אומר לך שהפלט אינו שמיש. התיקון הוא קריאה אחת, לא החלפת ספרייה: HotPDF מנתב טקסט מימין לשמאל דרך מתודה נפרדת, RtLTextOut, שמטפלת בסידור מחדש ש-TextOut רגילה לא תעשה. העמוד הזה הוא המדריך המעשי למתודה הזו: החתימה והפרמטרים שלה, ארגומנט ה-charset שבוחר את הכתב, תופעת הלוואי ברמת המסמך, הגדרת הגופן שחייבת לבוא קודם, והכשלים שבאמת מגיעים לתמיכה, כל אחד עם התיקון שלו

חתימה ופרמטרים

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

X ו-Y מעגנים את הרצף במערכת הצירים של העמוד עצמו, נמדדים מהפינה השמאלית התחתונה כאשר Y גדל כלפי מעלה, אותה ראשית שכל קריאת TextOut משתמשת בה; RtLTextOut משנה את סדר התווים, לא את המקום שממנו העמוד נמדד. angle מסובב את קו הבסיס בדיוק כפי שהוא עושה ב-TextOut, ולכן 0 מצייר קו אופקי. Text הוא המחרוזת בסדר לוגי, הסדר שבו היית מקליד אותה, וההעמסה השנייה מקבלת את אותם נתוני UTF-16 כחוצץ PWORD גולמי עם ספירת יחידות קוד מפורשת, וזו הצורה שכדאי להשתמש בה כשהטקסט מגיע מממשק ולא ממחרוזת Delphi. בגרסאות Delphi ישנות שקדמו לפתרון העמסות עבור הטיפוסים האלה, צורת המחרוזת נחשפת תחת השם RtLTextOutStr עם רשימת פרמטרים זהה

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

תרשים של האופן שבו RtLTextOut מסדרת מחדש שורה מעורבת בערבית ובלטינית לסדר חזותי מימין לשמאל לפני שהיא מציירת אותה אל PDF
RtLTextOut מסדרת כל שורה מחדש לסדר חזותי לפני הציור: רצפים מימין לשמאל שומרים על רצפם בעוד מילים לטיניות וספרות מוטמעות נקראות משמאל לימין בתוך השורה

ארגומנט ה-charset מכריע את הכתב

מה שאומר ל-RtLTextOut אם היא פורסת ערבית או עברית אינו המתודה, אלא הגופן. SetFont מקבלת charset של Windows כארגומנט הרביעי שלה, והערך הזה נושא את כללי הכתב אל תוך הקריאה מימין לשמאל: 178 בוחר ערבית, 177 בוחר עברית. קבע את ה-charset, אחר כך צייר, ושתי השורות שלהלן יוצאות בסדר קריאה נכון בלי שום תצורה נוספת

// ערבית: charset 178 אומר ל-RtLTextOut להחיל כללי ערבית
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// עברית: charset 177 מחליף את הכללים לעברית
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

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

היא אינה הופכת טקסט שכבר הפכת

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

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

תופעת הלוואי של Direction שכדאי להכיר

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

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

// RtLTextOut כבר קבעה את כיוון המסמך ל-RightToLeft;
// שחזר משמאל לימין אם המסמך ברובו LTR
Pdf.Direction := LeftToRight;

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

רשום את הגופן שאתה מפיץ, לא את זה שאתה מקווה שמותקן

שום דבר מהסידור מחדש אינו חשוב אם לגופן אין תווים לצייר. הכשל הקלאסי הוא דוח שמשורטט ללא רבב במכונת המפתח, שבה Arial Unicode MS במקרה קיים, ויוצא כשורות של ריבועים ריקים בשרת של לקוח שבו Windows החליף בשקט לגופן ללא שום כיסוי ערבי. התרופה היא להפסיק לסמוך על גופני מערכת מותקנים ולרשום גופן שאתה מפיץ עם היישום

// הפץ גופן ערבי ידוע ורשום אותו לפני הציור
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

שני גבולות נוסעים יחד עם הרישום. גופן שהובא דרך RegisterUnicodeTTF מוטמע, והטיפול של HotPDF ב-Unicode מוטמע דורש שהמסמך יהיה ב-PDF 1.5 או מאוחר יותר; זה נושך רק אם משהו במורד הזרם מתעקש על PDF 1.4, אבל כשזה קורה הכשל שקט. השני הוא משפטי ולא טכני: קובצי TrueType נושאים סיביות הרשאת הטמעה, וגופן שנראה מצוין על המסך יכול להיות מורשה באופן שאוסר להפיץ אותו בתוך מסמכי לקוחות. ודא את הרישיון לפני שאתה מטמיע, לא אחרי תלונה

דוגמת קונסולה מלאה

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

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // היחידה הראשית של HotPDF

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // כותרת לטינית עוברת בנתיב TextOut הרגיל
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // ערבית: charset 178, סדר לוגי, RtLTextOut מבצעת את הסידור מחדש
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // עברית: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // שורה מעורבת: המילה הלטינית המוטמעת עדיין נקראת משמאל לימין
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

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

שגיאות נפוצות והתיקונים שלהן

כל כשל שלהלן הופיע בשרשור תמיכה אמיתי, וכל אחד מהם מתחקה בחזרה אל אחד הפרקים שלמעלה

  • הפלט נקרא הפוך או מתערבב בשורות מעורבות — המחרוזת הופכה ידנית לפני הקריאה, בדרך כלל שריד של מעקף מניסיון TextOut. מחק כל היפוך ידני והעבר סדר לוגי; RtLTextOut הופכת פנימית
  • האותיות מודפסות מנותקות בצורות מבודדות — הטקסט עבר דרך TextOut רגילה, או ש-SetFont נקראה בלי charset של ימין לשמאל. צייר עם RtLTextOut והעבר 178 לערבית או 177 לעברית כארגומנט הרביעי של SetFont
  • ריבועים ריקים במכונת הלקוח — Windows החליף לגופן ללא כיסוי ערבי או עברי. הפסק לנקוב בשמות גופנים מותקנים; רשום גופן שאתה מפיץ דרך RegisterUnicodeTTF וקרא לו ב-SetFont בשם הזה
  • העמוד השני משורטט בגופן שגוי — הגופן הנוכחי אינו שורד את AddPage. חזור על קריאת ה-SetFont, כולל ה-charset, אחרי כל מעבר עמוד
  • פריסות דו-צדדיות מודפסות משוקפות במסמך שברובו LTR — קריאת ה-RtLTextOut הראשונה הפכה את ה-Direction של המסמך כתופעת לוואי. קבע Pdf.Direction := LeftToRight אחרי הרצף מימין לשמאל
  • טקסט Unicode מוטמע מתדרדר בשקט במורד הזרם — משהו בצינור כופה PDF 1.4, והטיפול של HotPDF ב-Unicode מוטמע דורש 1.5 או מאוחר יותר. העלה את גרסת המסמך או הסר את האילוץ שבמורד הזרם

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

הקריאות RtLTextOut, SetFont ו-RegisterUnicodeTTF שמוצגות כאן הן חלק מ-HotPDF Delphi Component עבור Delphi ו-C++Builder