Техническая статья

Формирование арабского текста и текста с направлением справа налево в PDF Delphi с помощью HotPDF

Передайте арабскую фразу يوضح ملف PDF в TextOut и откройте результат. Буквы идут не в ту сторону, и каждая из них находится в своей изолированной форме с видимым пробелом перед следующей, как если бы кто-то набирал английский текст задом наперед и нажимал пробел между каждым символом. Исключение не сработало. Предупреждение не напечатано. Вывод просто неверен, и он неверен, потому что два отдельных преобразования, от которых зависит арабский язык, так и не произошли. Понимание того, что это за два преобразования и какой вызов их выполняет, — это большая часть того, к чему сводится вывод сложных сценариев в PDF

HotPDF — это нативный VCL PDF компонент для Delphi и C++Builder, и он выполняет работу справа налево для вас через отдельный вызов. Он также останавливается в нескольких конкретных местах, о которых вы захотите узнать, прежде чем использовать локаль, поэтому в этой статье отображаются концепции и честные границы; практическая настройка самого вызова описана в справочной статье о RtLTextOut

Почему правильная строка все равно печатается неправильно

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

Второе — это контекстное формирование. Арабская буква рисуется по-разному в зависимости от того, где она находится в слове: в начале, в середине, в конце или стоит отдельно. Кодовая точка остается неизменной; меняется только глиф. Конвейер, который передает каждую кодовую точку прямо в ее глиф по умолчанию, производит именно тот бессвязный вывод в изолированной форме из первого абзаца. Иврит пропускает этот шаг, так как его буквы не соединяются, но ему все равно нужно переупорядочивание. Арабскому языку нужно и то, и другое, и именно поэтому вы тестируете строку на арабском языке, а не на иврите

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

Что формирует для вас RtLTextOut

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

На этом уровне имеет значение одно правило использования: ввод должен быть в логическом порядке, потому что RtLTextOut выполняет обращение самостоятельно, и строка, которую вы уже перевернули вручную, выходит дважды перевернутой — справочная статья рассказывает об этой ловушке и о том, как ее избежать. Что заслуживает упоминания этой ловушки здесь, так это то, почему она выживает при тестировании. Дважды перевернутая чисто арабская строка может выглядеть совершенно правильной и рассыпается только тогда, когда строка содержит латинское слово или число, потому что эти внедренные фрагменты больше не вкладываются так, как того требует UAX #9. Ошибка не в рендеринге; она заключается в том, что в алгоритм скармливается уже наполовину обработанный текст

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

Когда переупорядочивания и соединения достаточно, а когда нет

Для арабского и ивритского непрерывного текста — отчетов, счетов-фактур, договоров, писем — переупорядочивание плюс контекстное соединение — это вся работа, и RtLTextOut выполняет ее в одиночку. Граница возникает, когда типографика требует большего, чем соединение. Ответ HotPDF для арабской стороны — это необязательный формирователь на стороне производителя: установите AutoShapeArabic := True, и компонент перепишет прогон в логическом порядке в формы представления Unicode перед двунаправленным проходом, чтобы формы соединения вычислялись относительно логических соседей, а сгибы лигатур "запекались" в кодовые точки, которые фактически несет PDF, а не оставлялись на усмотрение зрителя. По умолчанию переключатель отключен, и вывод остается стабильным на уровне байтов, если он остается отключенным, поэтому его включение — это преднамеренное решение для конвейера документа, а не глобальное обновление. Эта же модель включения распространяется и на другие присоединяющиеся алфавиты с направлением письма справа налево, которые формирует HotPDF: у сирийского, N'Ko, адлама и ханифи рохинджа есть свой собственный флаг автоматического формирования, зеркально отражающий арабский

Необязательные функции OpenType — это уже другой механизм. Дискреционные лигатуры и аналогичные функции одиночной замены проходят через GetSingleSubstituteGlyph(GID, 'liga'), который разрешает одну замену за раз — сначала идентификатор входного глифа, затем тег функции — и возвращает входной глиф без изменений, если функция не применяется. Этого достаточно, чтобы управлять известным конечным списком лигатур, который вы поддерживаете сами. Это не полноценный движок GSUB, и эта разница — именно то место, где ошибаются амбициозные планы локализации: конвейер формирования, который безупречно обрабатывает арабский язык, продемонстрировал переупорядочивание и объединение, и не более того

Поддержка различных алфавитов

Арабский язык выполняет оба преобразования, поэтому именно на этой строке следует проводить тестирование, и именно поэтому арабский проход является самым веским доказательством того, что конвейер работает. Ивриту нужно переупорядочивание, но не соединение, так как его буквы стоят отдельно; если иврит отображается правильно, но арабский получается бессвязным, двунаправленная половина работает нормально, а контекстная половина никогда не запускалась. Персидский язык и урду используют арабский алфавит и наследуют его поведение, хотя предпочтение урду стилю насталик является решением в области шрифтов, последствия которого для удобочитаемости должен оценивать носитель языка

Тайский язык находится совершенно на другой стороне баррикад. Он пишется слева направо, поэтому ему не нужна двунаправленная работа, и его буквы не соединяются, поэтому ему не нужен контекстный анализ; тайские строки проходят через обычный путь 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 и сравните кодовые точки с вашей исходной строкой. Запустите внутридокументный поиск программы просмотра по слову, которое вы видите на странице. И откройте результат на машине, на которой нет шрифтов для разработки — той, где, скорее всего, обнаружится замена. Ничто из этого не заменит носителя языка, просматривающего один реальный документ, который улавливает то, чего не уловит ни один синтетический корпус. Внесите эту проверку в календарь до выпуска формата

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

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

API для шрифтов с направлением письма справа налево и Unicode, описанные выше, поставляются вместе с компонентом HotPDF для Delphi и C++Builder; на странице продукта есть ссылка на полный справочник по выводу текста