מאמר טכני

טקסט אנכי של CJK ב-PDF עם Delphi ו-HotPDF

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

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

A4 PDF page produced by HotPDF showing Chinese, Japanese, and Korean text stacked into vertical columns that read from right to left
עמוד A4 בודד עם טקסט סיני, יפני וקוריאני בעמודות אנכיות, שנוצר משגרת Delphi אחת.

המתג נמצא על SetFont

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

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// The 5th argument flips the current font into vertical mode.
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // horizontal
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // vertical

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

הארגומנט הרביעי הוא ערכת התווים של Windows, אותה אחת שלוקחת קריאה אופקית. העברת DEFAULT_CHARSET מאפשרת למערכת לפתור גליפים לכל מחרוזת, מה שחשוב כאן מכיוון שמסמך אנכי מערבב לעתים קרובות סקריפטים. כל השאר לגבי הגופן עדיין חל: הוא חייב להיות מותקן במכונת הבנייה, וכמעט תמיד תרצה FontEmbedding := True כדי שהקובץ יעבד את אותם גליפים של CJK על קורא שמעולם לא היה לו Arial Unicode MS

קריאת TextOut אחת היא עמודה אחת

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

var
  Pdf: THotPDF;
const
  ColTop = 760;   // y of the first glyph in every column (points)
  ColGap = 28;    // horizontal distance between columns
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // embed the CJK face for portable rendering
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // A horizontal heading first, in the ordinary writing mode.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 800, 0, 'Tang poem, vertical layout');

    // Switch the font into vertical mode; every TextOut below now stacks.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // Columns advance right to left, so X decreases with each call.
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

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

קואורדינטות: במורד העמוד, מימין לשמאל

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

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

גליפים שחייבים לשנות צורה

מספר תווים אינם פשוט עותקים מסובבים של העצמי האופקי שלהם; יש להחליף אותם כאשר הטקסט הופך לאנכי. האחד ש-HotPDF מטפל בו עבורך הוא סימן הצליל המוארך היפני (U+30FC), קו התנועה הארוכה שמופיע במילות קטאקאנה כמו コーヒー. כשהוא מצויר אופקית, זהו מקף קצר על קו הבסיס. אם היית עורם את אותו הגליף לעמודה, הוא היה שוכב שטוח לרוחב העמודה, וזה שגוי: ביפנית אנכית הסימן הופך לקו אנכי המחבר בין שני התווים שביניהם הוא יושב. HotPDF מזהה U+30FC בנתיב האנכי ומעבד אותו כקו אנכי (U+007C) כך שסימן התנועה הארוכה יצביע לכיוון הנכון ללא כל עבודה מצידך

החלפה בודדת זו מכסה את המקרה ששובר את רוב היישומים הנאיביים, אבל כדאי לדעת היכן הבעיה הכללית מעמיקה יותר. טיפוגרפיה אנכית מלאה גם מסובבת אותיות לטיניות ופיסוק מערבי בתשעים מעלות, מזיזה קאנה קטנה, וממקמת מחדש סוגריים ופסיקים לצורתם האנכית, ויישום שלם של כך שוכן בתכונות האנכיות של OpenType של גופן ולא בכללים קבועים. HotPDF יכול לנצל תכונות אלו כאשר הגופן נושא אותן: חלופות אנכיות (תכונות vert ו-vrt2 של GSUB) וקרנינג אנכי (בדיקות vkrn ו-vpal של GPOS) הם אופציונליים (opt-in), המיושמים לאורך הנתיב האנכי רק כאשר הגופן הפעיל אכן מגדיר אותם. עבור טקסט מעורב של CJK בגופן כיסוי רחב יחיד כמו Arial Unicode MS, הטיפול המובנה ב-U+30FC בתוספת צעד גובה שורה אחיד מספיק כדי לייצר עמודות נכונות וקריאות; תכונות OpenType חשובות כאשר אתה עובר לגופן שתוכנן להגדרות אנכיות עדינות ורוצה את מיקום הקאנה והריווח בין גליפים המקוריים שלו

ערבוב סקריפטים וכיוונים בעמוד אחד

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

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

מדוגמה לייצור

החלקים קטנים והם מתחברים באופן צפוי. הפעל את הדגל האנכי ב-SetFont, הנפק TextOut אחד לכל עמודה מ-Y עליון קבוע, והקטן את X בין הקריאות כך שהעמודות ייקראו מימין לשמאל. הטמע את הגופן כדי שגליפי ה-CJK ישרדו את המסע למכונה של קורא, ואפשר לרכיב לטפל בסימן התנועה הארוכה U+30FC ובמקום בו הגופן תומך בהם, בתכונות האנכיות של OpenType. משם, הכנסת הפריסה לייצור היא בעיקר אריתמטיקה: גזירת מיקומי ה-X של העמודות מרוחב בלוק מדוד, שבירת קטעים ארוכים לעמודות שמתאימות לגובה העמוד, והזמנת מקום לכותרות אופקיות ומספרי עמודים

למשטח הטקסט והגופן הרחב יותר שעל זה נבנה, כולל מוסכמות TextOut אופקיות ורישום גופן Unicode, ראה את דוגמת ה-Hello World הרב-לשונית ואת דוגמת ה-TextOut. המתג האנכי של SetFont וקריאות ה-TextOut המוצגות כאן הם חלק מ-רכיב ה-HotPDF עבור Delphi ו-C++Builder