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

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 з явним лічильником кодових одиниць, і це та форма, яку варто вживати, коли текст надходить з API, а не з рядка Delphi. У старіших версіях Delphi, що передують розв'язанню перевантажень для цих типів, рядкова форма відкрита під іменем RtLTextOutStr з ідентичним переліком параметрів

Розподіл праці між двома викликами виводу є суворим. TextOut малює кодові позиції в тому порядку, у якому ви їх передали, що правильно для латиниці, кирилиці та CJK і неправильно для арабської та івриту. RtLTextOut спершу перевпорядковує кожен рядок у візуальний порядок справа наліво, а потім малює, лишаючи вкраплені латинські слова та цифри читабельними зліва направо всередині рядка. HotPDF навмисно тримає ці два методи окремо, замість того щоб вгадувати напрямок із символів, тож вибір того, який із них викликати, є вибором тієї поведінки писемності, яку ви отримаєте; беріть RtLTextOut для серій справа наліво, TextOut для всього іншого й ніколи не пропускайте один через інший. Чому перевпорядкування взагалі існує, що насправді роблять двонаправлений алгоритм Unicode і контекстне зчеплення арабських літер і де зупиняється шейпінг у HotPDF - це теми супутнього матеріалу про шейпінг арабського та RTL-тексту в HotPDF; усе, що нижче, є практичним налаштуванням

Схема того, як RtLTextOut перевпорядковує змішаний рядок з арабським і латинським текстом у візуальний порядок справа наліво, перш ніж намалювати його в PDF
RtLTextOut перевпорядковує кожен рядок у візуальний порядок перед малюванням: серії справа наліво зберігають свою послідовність, тоді як вкраплені латинські слова й цифри читаються зліва направо всередині рядка

Аргумент charset вирішує, яка це писемність

Те, що каже RtLTextOut, чи компонує він арабську, чи іврит, є не методом, а шрифтом. SetFont приймає набір символів Windows як свій четвертий аргумент, і це значення заносить правила писемності у виклик справа наліво: 178 обирає арабську, 177 обирає іврит. Задайте набір символів, потім малюйте - і два рядки нижче вийдуть у правильному порядку читання без жодного додаткового налаштування

// Арабська: 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, бо поточний шрифт разом із набором символів не переживає розриву сторінки. Забудете повторити - і друга сторінка відкотиться до того шрифту, який був активним, а для арабської це зазвичай означає порожні прямокутники

Він не перевертає текст, який ви вже перевернули

Єдина помилка, яка з'їдає тут найбільше часу на налагодження, - це подавання в 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, вбудовується, а обробка вбудованого Unicode в HotPDF потребує документа версії PDF 1.5 або пізнішої; це кусає лише тоді, коли щось нижче за течією наполягає на PDF 1.4, але коли кусає, збій є мовчазним. Друга межа радше юридична, ніж технічна: файли TrueType несуть біти дозволів на вбудовування, і накреслення, яке чудово виглядає на екрані, може бути ліцензоване так, що забороняє постачати його всередині документів клієнтів. Підтвердьте ліцензію до вбудовування, а не після скарги

Повний консольний приклад

Складаючи частини разом, ось самодостатня програма, яка пише одну сторінку з арабським рядком, рядком на івриті та змішаним рядком, що несе латинську назву продукту. Кожен блок задає свій набір символів, а потім малює в логічному порядку

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 викликали без набору символів для письма справа наліво. Малюйте через RtLTextOut і передавайте 178 для арабської чи 177 для івриту четвертим аргументом SetFont
  • Порожні прямокутники на машині клієнта — Windows підставила шрифт без покриття арабської чи івриту. Перестаньте називати встановлені шрифти; зареєструйте накреслення, яке постачаєте, через RegisterUnicodeTTF і задайте його в SetFont за цим іменем
  • Друга сторінка рендериться не тим шрифтом — поточний шрифт не переживає AddPage. Повторюйте виклик SetFont разом із набором символів після кожного розриву сторінки
  • Двосторонні розвороти друкуються дзеркально в переважно LTR-документі — перший виклик RtLTextOut перекинув Direction документа як побічний ефект. Задайте Pdf.Direction := LeftToRight після серії справа наліво
  • Вбудований текст Unicode мовчки деградує нижче за течією — щось у конвеєрі змушує PDF 1.4, а обробка вбудованого Unicode в HotPDF потребує 1.5 або пізнішої. Підніміть версію документа або приберіть обмеження нижче за течією

Перш ніж формат піде в реліз, перевіряйте не лише на око: скопіюйте текст назад із переглядача, проженіть пошук усередині документа, відкрийте файл на машині без ваших шрифтів розробки й покладіть один справжній документ перед носієм мови. Повний чекліст перевірки, карта покриття по писемностях і корпус тестових рядків, який варто побудувати, живуть у супутній статті про шейпінг арабського та RTL-тексту в HotPDF

Показані тут виклики RtLTextOut, SetFont та RegisterUnicodeTTF є частиною HotPDF Delphi Component для Delphi та C++Builder