Технічна стаття

Вертикальний текст CJK у PDF з Delphi та HotPDF

Розгорніть японський роман на сторінці, і перше, що ви помітите, це те, що текст іде вниз по стовпцю, а не вздовж рядка, і що стовпці просуваються від правого краю аркуша вліво. Читачу, який виріс на цьому, горизонтальний текст здається дещо незвичним. Інженерна проблема полягає в тому, що PDF, як і майже кожна цифрова текстова система, був побудований навколо горизонтальної базової лінії, яка росте зліва направо, і потік вмісту не має поняття "записати цей абзац вниз". Тому, коли програма на Delphi повинна створити сертифікат, вірш, вивіску або юридичний документ традиційного формату для тайванського, японського чи корейського читача, макет доводиться створювати власноруч: один символ під іншим, один стовпець ліворуч від попереднього

HotPDF надає вам перемикач, який виконує посимвольний облік замість вас. Налаштований вами шрифт містить прапорець IsVertical, і як тільки його ввімкнено, єдиний виклик TextOut накладає весь рядок у вертикальний стовпець, а не пускає його вздовж базової лінії. Розміщення стовпців, порядок справа наліво та одна досить важлива заміна гліфів будуть розглянуті в решті цієї статті

Сторінка A4 у форматі PDF, створена за допомогою HotPDF, яка показує китайський, японський та корейський текст, складений у вертикальні стовпці, що читаються справа наліво
Одна сторінка 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 збільшується вгору в пунктах. Тому вертикальний стовпець починається з високого значення Y (біля верхнього краю аркуша), а символи спускаються звідти, оскільки HotPDF віднімає висоту рядка для кожного з них. Ви встановлюєте цей початковий Y один раз для кожного стовпця, а компонент бере на себе обробку спуску

Горизонтальною віссю ви керуєте вручну. Кожен стовпець знаходиться на своєму X, а наступні стовпці зміщуються до менших значень X, оскільки порядок читання відбувається справа наліво. Розумним ритмом буде вибрати X крайнього правого стовпця, а потім віднімати фіксований інтервал між стовпцями для кожного наступного виклику, як це зроблено в прикладі з ColGap. Цей інтервал ви обираєте самостійно; якщо він занадто малий, сусідні стовпці стикаються, якщо занадто великий, блок виглядає розрідженим. Для основного тексту розміром від 12 до 18 пунктів інтервал трохи більший за розмір шрифту читається комфортно

Гліфи, які повинні змінити форму

Деякі символи не є просто повернутими копіями їх горизонтальних версій; їх потрібно замінювати, коли текст стає вертикальним. Символ, який HotPDF обробляє для вас, це знак японського подовженого звуку (U+30FC), риска довгої голосної, яка з'являється у словах катаканою, таких як コーヒー. Намальована горизонтально, вона виглядає як коротке тире на базовій лінії. Якщо ви покладете цей самий гліф у стовпець, він буде лежати пластом поперек стовпця, що неправильно: у вертикальній японській мові цей знак стає вертикальним штрихом, що з'єднує два символи, між якими він знаходиться. HotPDF виявляє U+30FC на вертикальному шляху і відображає його як вертикальну риску (U+007C), тому знак довгої голосної вказує в правильному напрямку без будь-яких зусиль з вашого боку

Ця єдина заміна охоплює випадок, який ламає більшість наївних реалізацій, але варто знати, де загальна проблема стає глибшою. Повноцінна вертикальна типографіка також повертає латинські літери та західну пунктуацію на дев'яносто градусів, зсуває малу кану та переміщує дужки й коми у їхні вертикальні форми, і повна реалізація цього живе у вертикальних функціях OpenType шрифту, а не у фіксованих правилах. HotPDF може використовувати ці функції, коли шрифт їх містить: вертикальні альтернативи (функції GSUB vert та vrt2) і вертикальний кернінг (пошуки GPOS vkrn та vpal) є опціональними, вони застосовуються вздовж вертикального шляху лише тоді, коли активний шрифт фактично їх визначає. Для змішаного тексту 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 Component для Delphi та C++Builder