Рендеринг двунапревленого текста с поддержкой даикритиков

Введение


В этой статье я поделюсь опытом как в собственный 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 это было бы сделать легче.

Полезные ссылки


© Habrahabr.ru