Введение
В этой статье я поделюсь опытом как в собственный TextBox была добавлена поддержка двунаправленного текста с правильным отображением диакритиков с использованием FriBidi и HarfBuzz. Это вторая статья на эту тему, а первой была Добавление поддержки двунаправленного текста в собственный TextBox. В ней я описывал особенности добавления арабского в собственный текст с использованием FriBidi.
В чём проблема?
Диакрити?ческие зна?ки (диакри?тики (профессионально-жаргонное)) в типографике — элементы письменности, модифицирующие начертание знаков и обычно набираемые отдельно. В предыдущем предложении знаки ударения над и? и а? — это диакритические знаки. Например, в русском языке диакритиками можно считать две точки над «ё» и кратка над «й». Но добавление этих диакритиков привело к созданию новых букв, хотя для ё две точки часто опускаются.
В большинстве языков при работе с текстом особых проблем с рендерингом диакритиков не возникает (если конечно вы не указываете ударение над каждой буквой), т.к. буквы с диакритиком — это или отдельная буква в алфавите или в файлах шрифтов они идут как отдельный символ. Другими словами, TextBox-у не надо отдельно размещать диакритики над буквами.
Но в арабском языке (и например, в хинди) не всё так просто. В арабском языке огласо?вки являются диакритическими знаками. Они могут использоваться почти с каждой буквой и даже у одной буквы может быть несколько огласок.
Чёрным цветом изображены буквы арабского алфавита, серым — огласовки (диакритики).
Как вы понимаете, никто не перебирал все возможные комбинации букв и огласок и не заводил для каждой комбинации отдельный символ. То есть, для правильного рендера арабского текста необходимо отрисовать арабскую букву и отдельно над или под ней отрисовать диакритик.
FreeType, который мы использовали, позволяет получить изображение диакритика из файла шрифта и даже сообщает нам сдвиги. Но эти сдвиги некорректные, т.е. по одному символу невозможно понять, как расположить диакритик. Ниже показательный пример — несколько диакритиков над буквой. Для правильного позиционирования необходимо проанализировать весь текст.
Для вычисления позиции диакритиков над буквами мы использовали библиотеку HarfBuzz. Библиотека позволяет получить номера глифов в шрифте и их сдвиги для дальнейшей отрисовки.
Как использовать HarfBuzz
HarfBuzz получает на вход шрифт и строку, а возвращает позицию каждой буквы и дополнительную информацию (например, номер глифа).
hb_buffer_t *buf; // harfbuzz буфер. hb_buffer_create/hb_buffer_destroy
hb_font_t *hb_ft_font; // harfbuzz шрифт, для создания используйте hb_font_create, для уничтожения hb_font_destroy
hb_script_t script; // Скрипт текущего текста. Используйте hb_unicode_script для получения скрипта.
hb_direction_t dir = hb_script_get_horizontal_direction(script);
hb_buffer_set_direction(buf, dir); // Справа налево или слева на право
hb_buffer_set_script(buf, script);
hb_buffer_add_utf32(buf, (const uint32_t*)text,length, 0,length); // Добавляем наш текст в harfbuzz буффер.
hb_shape(hb_ft_font, buf, NULL, 0); // расчёт
unsigned int glyph_count = 0;
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count); // Получаем информацию о глифах.
hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count); // Получаем позицию глифов.
Стоит отметить, что приведённый код необходимо применять только к тексту, использующему одинаковый шрифт и имеющий одинаковый скрипт. Для реализации разбиения текста на такие части можно использовать функцию hb_unicode_script, которая возвращает скрипт символа.
Так как перед нами стояла задача поддержки не просто арабского, но и двунаправленного текста (например, арабский и латинский может присутствовать в одной строчке), мы использовали FriBidi для правильного позиционирования. Но это более подробно было описано в первой статье Добавление поддержки двунаправленного текста в собственный TextBox.
Изменения в TextBox-е
Итак, Текст бокс уже поддерживал двунаправленный текст. Символы хранятся в памяти в порядке ввода, но каждому из них соответствовала позиция в порядке рендера.
С добавлением даикритиков ситуация немного усложнилась, т.к. одной букве при рендере могло соответствовать несколько введённых символов. Для того чтобы код работы с позиционированием курсора мог работать независимо от диакритиков, буквы пришлось немного усложнить. Теперь каждая буква хранила в себе список глифов, которые в неё входят.
При таком подходе упростилась реализация функций редактирования, включая копирование и вставку. Но такой подход не даёт возможности удалить отдельный диакритик, так как курсор можно поставить только перед или за буквой.
Пример
Пример рендера двунаправленного текста вы можете найти здесь GitHub/ex-sdl-freetype-harfbuzz-fribidi. В примере используется: SDL2 — для создания окна визуализации; Freetype — для рендера букв; fribidi — для правильного позиционирования; harfbuzz — для получения глифов и их позиций.
Disclaimer
Да, мы пишем свой велосипед, поэтому реализуем свой TextBox с нуля. И мы не использовали Pango, потому что с ним был неудачный опыт раньше. Может быть, с Pango это было бы сделать легче.
Полезные ссылки
- behdad.org/text — о рендеринге текста
- www.freedesktop.org/wiki/Software/HarfBuzz — библиотека HarfBuzz.
- fribidi.org — библиотека FriBidi.
- ru.wikipedia.org/wiki/Огласовки_в_арабском_письме — о диакритиках в арабском письме.
- ru.wikipedia.org/wiki/Диакритические_знаки — о диакритических знаках.
- ex-sdl-freetype-harfbuzz-fribidi — мой пример использования fridibi + harfbuzz, был основан на https://github.com/lxnt/ex-sdl-freetype-harfbuzz.
- www.unicode.org/reports/tr9 — алгоритм отображения двунаправленного текста.
- www.pango.org — библиотека Pango.
Randl
Печатать текст на иврите и английском в одном абзаце — боль. Угадать что выкинет очередной редактор или браузер нереально. Иврит слева направо, все английские слова справа, английский справа налево (sic!). Пунктуация судя по всему расстанавливается просто рандомно, а не там где я ее поставил. Причем грешит этим даже Word.
Думаю и с арабским не лучше дело обстоит.
UnickSoft
Да, неподготовленному пользователю набирать двунаправленный текст очень тяжело. В предыдущей статье делал гифки, которые это демонстрируют.
tyomitch
На тему двунаправленности у меня был пост—наглядная иллюстрация: https://habrahabr.ru/post/104493/
Shchvova
Вау… Сегодня целый день пытался разобраться с FreeType, HarfBuzz & Pango. А тут на хабре статья появилась. Спойлер — я не осилил. У меня проблема что и HarfBuzz и Pango тянут Каир (Cairo). У меня свой рендерер и мне нужен результат только как битмап в памяти. Ну, спасибо огромное за статью, очень интересно. Если можете подсказать как собрать HarfBuzz без Cairo, буду благодарен.
UnickSoft
Отвечу вам личном сообщении попозже.
aTwice
При активной работе с арабским удобным оказался текстовый редактор, который НЕ умеет писать Right-To-Left и отображает огласовки отдельными символами. SublimeText 3.
Shchvova
Кстати, я попробовал, и у меня получилось написать свой рендерер слов используя только HarfBuzz и FreeType. Причина — лицензия. Ведь FriBidi под LGPL, что делает невозможным использования его, например, в iOS/Android игре. А есть какие-то более свободные имплементации?
UnickSoft
К сожалению, я других библиотек не знаю. Можете постараться связаться с автором FriBidi, может быть он в следующей версии он добавит необходимые лицензии. Хотя это выглядит маловероятным.
Shchvova
говорят что вместо FriBidi можно использовать часть ICU в которой реализован этот же алгоритм. У меня вообще есть план попытаться использовать ICU Parapraph Layout с HarfBuzz + FreeType, что бы получить прям полный набор с адекватной лицензией.