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

Формування арабського та RTL тексту у PDF на Delphi з HotPDF

Передайте арабську фразу يوضح ملف PDF до TextOut і відкрийте результат. Літери йдуть у неправильному напрямку, і кожна з них знаходиться у своїй ізольованій формі з видимим проміжком перед наступною, ніби хтось надрукував англійською задом наперед і натиснув пробіл між кожним символом. Жодного винятку не виникло. Жодного попередження не надруковано. Вивід просто неправильний, і він неправильний тому, що дві окремі трансформації, від яких залежить арабська мова, так і не відбулися. Знання того, що це за дві трансформації, і який виклик їх виконує, — це більшість того, до чого зводиться вивід PDF зі складними скриптами

HotPDF — це нативний VCL PDF компонент для Delphi та C++Builder, і він робить роботу з текстом справа наліво (RTL) за вас за допомогою окремого виклику. Він також зупиняється в кількох конкретних місцях, про які вам потрібно знати, перш ніж ви зафіксуєте локаль, тому ця стаття описує концепції та чесні межі; практичне налаштування самого виклику міститься у довідковій статті про RtLTextOut

Чому правильний рядок все ще друкується неправильно

Unicode зберігає текст у логічному порядку, порядку, в якому ви його друкуєте та читаєте вголос. Рендерер має розміщувати гліфи у візуальному порядку. Для скриптів зліва направо ці порядки збігаються, і ніхто про це не думає. Для арабської та івриту це не так, і коли один рядок змішує напрямки, скажімо, арабське речення, що містить латинський токен "PDF", або ціна, написана цифрами, двонаправлений алгоритм Unicode (UAX #9) вирішує, як саме фрагменти зліва направо вкладаються всередині рядка справа наліво. Це перша трансформація, переупорядкування, і її пропуск — це те, що перевертає рядок

Друга — це контекстне формування. Арабська літера малюється по-різному залежно від того, де вона знаходиться у слові: на початку, в середині, в кінці або стоїть окремо. Кодова точка залишається незмінною протягом усього процесу; змінюється лише гліф. Конвеєр, який передає кожну кодову точку безпосередньо її типовому гліфу, видає саме той роз'єднаний вивід в ізольованій формі з початкового абзацу. Іврит пропускає цей крок, оскільки його літери не з'єднуються, але він все ще потребує переупорядкування. Арабська потребує обох, і саме тому арабська, а не іврит, є рядком, з яким ви тестуєте

На робочому столі нічого з цього не є вашою проблемою. Коли форма VCL малює арабську мову в TEdit, текстовий стек операційної системи тихо переупорядковує і формує її, що є саме тією причиною, чому рядок, який виглядає ідеально на екрані, виходить зламаним у наївному PDF. Потік контенту не зберігає редагований текст. Він зберігає позиційовані гліфи, тому той, хто випускає потік, успадковує завдання формування, з яким зазвичай справлялася ОС. RtLTextOut — це виклик, який бере цю роботу назад

Що RtLTextOut формує для вас

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

Одне правило використання має значення навіть на цьому рівні: ввід має бути у логічному порядку, оскільки RtLTextOut виконує розворот сам, і рядок, який ви вже перевернули вручну, виходить двічі перевернутим — довідкова стаття розглядає цю пастку та її виправлення. Що заслуговує на згадку тут, так це те, чому вона виживає під час тестування. Двічі перевернутий суто арабський рядок може виглядати абсолютно правильно, і розвалюється лише тоді, коли рядок містить латинське слово або число, оскільки ці вбудовані послідовності більше не вкладаються так, як диктує UAX #9. Помилка не в рендерингу; вона в тому, щоб згодувати алгоритму текст, який вже був наполовину оброблений

Та ж сама поведінка змішаного напрямку збиває з пантелику рецензентів більше, ніж збиває з пантелику код. Всередині рядка справа наліво цифри та вбудовані латинські слова все ще читаються зліва направо. Хтось, хто не працював з двонаправленим макетом, подивиться на відрендерений рахунок-фактуру, побачить, що номер рахунку читається в "неправильному" напрямку відносно арабської мови навколо нього, і запише це як помилку. Це правильний за специфікацією результат. Коротка примітка у ваших критеріях прийнятності, написана до першого проходу носієм мови, рятує від цього зайвого кола

Коли переупорядкування і з'єднання достатньо, а коли ні

Для потокового тексту арабською та івритом — звітів, рахунків-фактур, контрактів, листів — переупорядкування плюс контекстне з'єднання є всією роботою, і RtLTextOut несе її сам. Межа з'являється, коли типографіка вимагає більшого, ніж з'єднання. Відповідь HotPDF на арабській стороні — це шейпер (shaper) на стороні виробника, що підключається за бажанням: встановіть AutoShapeArabic := True, і компонент перезаписує послідовність у логічному порядку у форми представлення Unicode перед двонаправленим проходом, так що форми з'єднання обчислюються відносно логічних сусідів, а згортки лігатур запікаються в кодові точки, які PDF фактично несе, а не залишаються для вирішення програмі перегляду. Перемикач за замовчуванням вимкнений, і вивід є байт-стабільним, коли він залишається вимкненим, тому його вмикання є свідомим рішенням для кожного конвеєра документів, а не глобальним оновленням. Ця ж модель "opt-in" поширюється на інші скрипти справа наліво, що з'єднуються, які формує HotPDF: сирійська мова, нко, адлам і ханіфі-рохінджа — кожна має свій власний прапорець автоформатування, який віддзеркалює арабський

Опціональні функції OpenType — це знову ж таки інший механізм. Дискреційні лігатури та подібні функції одиничної заміни проходять через GetSingleSubstituteGlyph(GID, 'liga'), який вирішує одну заміну за раз — спочатку ідентифікатор вхідного гліфа, потім тег функції — і повертає вхідний гліф незмінним, коли функція не застосовується. Цього достатньо для керування відомим, кінцевим списком лігатур, який ви підтримуєте самі. Це не повноцінний движок GSUB, і різниця полягає саме в тому, де амбітні плани локалізації йдуть не так: конвеєр формування, який бездоганно обробляє арабську мову, продемонстрував переупорядкування та з'єднання, не більше

Охоплення за скриптами

Арабська мова використовує обидві трансформації, ось чому це рядок для тестування, і чому арабський прохід є найвагомішим єдиним доказом того, що конвеєр працює. Іврит потребує переупорядкування, але не з'єднання, оскільки його літери стоять окремо; якщо іврит рендериться правильно, але арабська мова виходить роз'єднаною, двонаправлена половина в порядку, а контекстна половина ніколи не запускалася. Перська та урду використовують арабський скрипт і успадковують його поведінку, хоча перевага урду щодо стилю Насталік (Nastaliq) є рішенням шрифту з наслідками для розбірливості, про які має судити носій мови

Тайська мова знаходиться зовсім по інший бік лінії. Вона йде зліва направо, тому їй не потрібна двонаправлена робота, і її літери не з'єднуються, тому їй не потрібен контекстний аналіз; тайські рядки проходять через звичайний шлях TextOut, як і латиниця. Що тайська мова дійсно має, так це накладені знаки — голосні та знаки тону над і під базовою приголосною — і чи сидять вони правильно, залежить від того, чи шрифт будує свої комбіновані знаки для накладання без допомоги движка формування. Більшість спеціальних тайських шрифтів роблять це. Тестуйте саме з тим шрифтом, який ви будете вбудовувати, а не з тим, що схожий

Деванагарі та решта індійської сім'ї є чесною жорсткою зупинкою. Їхні знаки голосних переупорядковуються навколо кластерів приголосних, а їхні кон'юнкти формуються через ланцюжки залежних від контексту замін, що є територією повноцінного GSUB, за межами переупорядкування та з'єднання. Якщо індійська локаль є в дорожній карті, запустіть реальний пілотний проект на справжніх клієнтських рядках, перш ніж обіцяти її — те, що працює арабська, не є доказом того, що працюватиме деванагарі. CJK-рядки, в'єтнамська з її накладеними діакритичними знаками та змішаний європейський текст — усі йдуть звичайним шляхом без двонаправленого аналізу, і варто тримати ці два шляхи фізично відокремленими в коді звіту: одна підпрограма для RTL-рядків і одна для всього іншого, щоб логіка локалі була видимою на місці виклику замість того, щоб бути прихованою за прапорцем, який хтось забуває встановити

Охоплення гліфів вирішується ще до запуску формування

Формування вибирає гліфи зі шрифту. Якщо шрифт їх не має, немає чого вибирати, ось чому класичний збій розгортання — бездоганно на машині розробника, порожні квадрати на сервері клієнта після тихої заміни шрифту — це проблема охоплення, а не проблема формування. Практичне лікування, реєстрація шрифту, який ви постачаєте, замість того, щоб довіряти тому, що встановлено на машині, крок за кроком розглядається в довідковій статті. Концептуальна суть полягає в тому, що охоплення має бути встановлено до того, як будь-яке питання формування взагалі матиме сенс, і що воно може бути встановлено програмно, а не шляхом огляду виводу на око

// 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, яке вказує загальний порядок читання документа. Воно не торкається жодного гліфа. Що воно робить, так це вказує програмі перегляду, як розташовувати розвороти на дві сторінки, з якого боку має починатися макет із суміжними сторінками, і в який бік має схилятися користувальницький інтерфейс читання. Нічого з цього не видно на одній сторінці, що є саме тією причиною, чому про це забувають

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

Встановлення Direction — це вся робота: сеттер властивості додає vpDirection до ViewerPreferences документа, тому один рядок переносить це налаштування у файл. Якщо текст виводиться через RtLTextOut, ви отримуєте це безкоштовно, оскільки виклик перевертає напрямок документа як побічний ефект — довідкова стаття охоплює випадки, коли для змішаного документа це потрібно скасувати. Випадок, коли ви повинні встановити це самостійно — це документ справа наліво, створений будь-яким іншим способом, наприклад, із вхідних даних, які ви попередньо сформували вище за течією і намалювали звичайним шляхом. Залиште його там, і односторінковий пробний відбиток, на який ви дивитесь, виглядає ідентично в обох випадках; потім хтось друкує дуплексний буклет, розвороти виходять дзеркальними, і причиною є відсутність одного рядка з кількатижневої давнини

Перевірка сформованого виводу

Перевіряйте від початку до кінця, тому що сторінка може виглядати правильно і все одно бути марною для всього, що знаходиться нижче за течією. Три перевірки знаходять більшість проблем. Скопіюйте текст назад з Acrobat і порівняйте кодові точки з вихідним рядком. Запустіть пошук у програмі перегляду за словом, яке ви бачите на сторінці. І відкрийте вивід на машині, яка не має ваших шрифтів розробки, тій, що найімовірніше виявить підміну. Ніщо з цього не замінює погляд носія мови на один реальний документ, який вловлює речі, які не вловить жоден синтетичний корпус. Внесіть цей огляд у календар до випуску формату

Вибирайте тестові рядки цілеспрямовано замість того, щоб повторно використовувати все, що надіслав перекладач минулого року. Робочий мінімум на локаль: речення суто одним скриптом, речення з вбудованими латинськими назвами брендів, рядок, що містить цифри та валюту, і імена з діакритичними або комбінованими знаками. Реальні імена клієнтів руйнують припущення, які текст-заповнювач залишає недоторканими, тому дозвольте набору для регресійного тестування рости на один рядок щоразу, коли випадок підтримки виявляє патерн, якого ви раніше не бачили

Реєстрація шрифтів, створення підмножин (subsetting) і повсякденний API для малювання тексту розглядаються у статті про вивід звітів, шрифти та зображення з HotPDF. Коли ці ж самі документи також повинні відповідати профілям доступності, тегування мови та правила структури у статті про валідацію PDF/A та PDF/UA знаходяться поверх роботи з формування, описаної тут

Описані вище API шрифтів справа наліво та Unicode постачаються з Компонентом HotPDF для Delphi та C++Builder; сторінка продукту містить посилання на повний довідник з виводу тексту