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

    Введение


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

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


    • +18
    • 6,7k
    • 9
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

        Заголовок спойлера
        Печать двунаправленого текста
        • 0
          На тему двунаправленности у меня был пост—наглядная иллюстрация: https://habrahabr.ru/post/104493/
        • 0
          Вау… Сегодня целый день пытался разобраться с FreeType, HarfBuzz & Pango. А тут на хабре статья появилась. Спойлер — я не осилил. У меня проблема что и HarfBuzz и Pango тянут Каир (Cairo). У меня свой рендерер и мне нужен результат только как битмап в памяти. Ну, спасибо огромное за статью, очень интересно. Если можете подсказать как собрать HarfBuzz без Cairo, буду благодарен.
          • 0
            Отвечу вам личном сообщении попозже.
          • +1
            При активной работе с арабским удобным оказался текстовый редактор, который НЕ умеет писать Right-To-Left и отображает огласовки отдельными символами. SublimeText 3.

            image
            • 0
              Кстати, я попробовал, и у меня получилось написать свой рендерер слов используя только HarfBuzz и FreeType. Причина — лицензия. Ведь FriBidi под LGPL, что делает невозможным использования его, например, в iOS/Android игре. А есть какие-то более свободные имплементации?
              • 0
                К сожалению, я других библиотек не знаю. Можете постараться связаться с автором FriBidi, может быть он в следующей версии он добавит необходимые лицензии. Хотя это выглядит маловероятным.
                • +1
                  говорят что вместо FriBidi можно использовать часть ICU в которой реализован этот же алгоритм. У меня вообще есть план попытаться использовать ICU Parapraph Layout с HarfBuzz + FreeType, что бы получить прям полный набор с адекватной лицензией.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.