Введение


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

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


Комментарии (9)


  1. Randl
    19.02.2016 00:18
    +1

    Печатать текст на иврите и английском в одном абзаце — боль. Угадать что выкинет очередной редактор или браузер нереально. Иврит слева направо, все английские слова справа, английский справа налево (sic!). Пунктуация судя по всему расстанавливается просто рандомно, а не там где я ее поставил. Причем грешит этим даже Word.
    Думаю и с арабским не лучше дело обстоит.


    1. UnickSoft
      19.02.2016 00:36
      +3

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

      Заголовок спойлера
      Печать двунаправленого текста


    1. tyomitch
      19.02.2016 10:49

      На тему двунаправленности у меня был пост—наглядная иллюстрация: https://habrahabr.ru/post/104493/


  1. Shchvova
    19.02.2016 04:55

    Вау… Сегодня целый день пытался разобраться с FreeType, HarfBuzz & Pango. А тут на хабре статья появилась. Спойлер — я не осилил. У меня проблема что и HarfBuzz и Pango тянут Каир (Cairo). У меня свой рендерер и мне нужен результат только как битмап в памяти. Ну, спасибо огромное за статью, очень интересно. Если можете подсказать как собрать HarfBuzz без Cairo, буду благодарен.


    1. UnickSoft
      19.02.2016 13:30

      Отвечу вам личном сообщении попозже.


  1. aTwice
    19.02.2016 14:24
    +1

    При активной работе с арабским удобным оказался текстовый редактор, который НЕ умеет писать Right-To-Left и отображает огласовки отдельными символами. SublimeText 3.

    image


  1. Shchvova
    24.02.2016 23:41

    Кстати, я попробовал, и у меня получилось написать свой рендерер слов используя только HarfBuzz и FreeType. Причина — лицензия. Ведь FriBidi под LGPL, что делает невозможным использования его, например, в iOS/Android игре. А есть какие-то более свободные имплементации?


    1. UnickSoft
      25.02.2016 19:16

      К сожалению, я других библиотек не знаю. Можете постараться связаться с автором FriBidi, может быть он в следующей версии он добавит необходимые лицензии. Хотя это выглядит маловероятным.


      1. Shchvova
        25.02.2016 19:39
        +1

        говорят что вместо FriBidi можно использовать часть ICU в которой реализован этот же алгоритм. У меня вообще есть план попытаться использовать ICU Parapraph Layout с HarfBuzz + FreeType, что бы получить прям полный набор с адекватной лицензией.