21 июля 2015 в 00:26

Добавление поддержки двунаправленного текста в собственный TextBox

C++*

Введение


Давно хотел поделиться опытом добавления поддержки двунаправленного текста в собственный редактор текста, но подвигли меня к этому корыстные соображения. В этой статье я напишу как интегрировал GNU FriBidi в свой TextBox для поддержки арабского языка. Надеюсь, моя статья окажется полезной, так как хороших материалов по поддержке арабского текста сложно найти.

Что мы имели


К моменту возникновения необходимости добавления поддержки арабского языка, самописный контрол TextBox уже умел многое: редактировать текст, управлять курсором, выделять часть текста, вставлять, вырезать, поддерживать много строк, выравнивание и т.д. Конечно, с Word не сравнится, но базовые вещи он умел. Также TextBox использовался в приложении для Windows и Mac OS X.

Привет Хабру

Для получения изображения глифов использовался FreeType, а для вывода OpenGL, хотя это не важно. Контрол от FreeType получал метрику для глифов, чтобы правильно рассчитать их позицию.

Метрика символов FreeType

Какую задачу поставили


Ребята, давайте добавим поддержку арабского и побыстрее,
— такова была задача.

На тот момент об арабском языке я знал, что он пишется справа налево, да и что у них буквы в словах соединяются, чтобы получалась вязь. Но в действительности всё оказалось немного сложнее.
Для этого функционала необходимо выбрать библиотеку. Обычно для этих целей используются следующие: GNU FriBidi, Pango, HarfBuzz. Выбрали GNU FriBidi, т.к. оно показалось самым простым и требовало минимальных изменений.

Некоторые особенности арабского языка


На первый взгляд арабский язык (اللغة العربية) кажется очень непохожим на русский или английский. Но различия не столь велики, как кажется на первый взгляд. При реализации я столкнулся со следующими особенностями:

  1. Арабский язык пишется справа налево, т.е. первым символом является самый правый символ. При этом нажатие на клавиши вправо или влево, смещает курсор также вправо или влево. В отличии от русского, если нажать вправо, то курсор увеличивает свою позицию в строке, а в арабском уменьшает.
    Del удаляет следующий за курсором символ, а backspace предыдущий. Для арабского Del удаляет правый символ, а backspace — левый.
    Но самое интересное начинается, когда арабский и русский смешаны в одной строке.
    Перемещение курсора в смешаного текста
    Также это касается выделения текста мышкой. Попробуйте выделить мышкой текст ниже:
    اللغة العربية Русский язык اللغة العربية English language اللغة العربية

  2. Вторая особенность — это вязь. Чтобы слова выглядели как вязь, почти каждая буква имеет разные Unicode символы для разных позиций в слове: в конце, в начале, в середине. Кому интересно, есть хорошая таблица на Википедии.
  3. Лигатуры. Если две буквы идут одна за одной, они могут заменяться на одну букву. Например, вот эти два символа "ل‎" "آ" преобразуются в этот لآ.
  4. Диакритики. Для русского языка диакритики — это "¨" над ё или "˘" над й. С этими диакритиками особых проблем нет, т.к. они уже «вшиты» в глиф этих букв. Т.е. «И» и «Й» — это отдельные символы в шрифте, и не надо отдельно брать галочку и добавлять её к букве И, чтобы получить Й. В рабском диакритики намного разнообразнее и не «вшиты» в символы.
    Пример диакритиков в Арабском
    Чёрным цветом изображены буквы арабского алфавита, серым — огласовки (диакритики).
    Как вы можете видеть на картинке есть один интересный случай:
    2 диакритика над одной буквой
    Два диакритика одновременно над одной буквой.
  5. Я уверен, что есть ещё особенности, но мы с ними не столкнулись и пользователи не зарепортили.

Реализация


Как использовать GNU FriBidi

Использовать GNU FriBidi довольно просто. Библиотека принимает Unicode строку символов и после вызова ряда функций возвращает Unicode строку с учётом позиции букв в слове, легатур и позиции букв в строке.
int nLength; // длина строки  
uint* pInputLine; // входная/выходная строка Unicode   
FriBidiCharType* pBidiTypes; // типы каждого символа в строке  
FriBidiLevel *pEmbeddingLevels; // Embedding Levels 
FriBidiJoiningType *pJtypes; // типы соединения букв 
FriBidiArabicProp  *pArProps; // дополнительные свойства для арабских букв 
FriBidiStrIndex *pPositionLogicToVisual; // позиции букв для отображения 
------------------------- 
// Получаем тип для каждой буквы. 
fribidi_get_bidi_types(pInputLine, nLength, pBidiTypes);  
// Ищем Resolving Embedding Levels (http://www.unicode.org/reports/tr9/#Resolving_Embedding_Levels) для каждой буквы. 
FriBidiParType baseDirection = FRIBIDI_PAR_RTL;  
FriBidiLevel   resolveParDir = fribidi_get_par_embedding_levels(pBidiTypes, nLength, &baseDirection, pEmbeddingLevels);  
// Получаем типы соединения для каждой буквы  
fribidi_get_joining_types(pInputLine, nLength, pJtypes);  
// получаем соединения для арабских букв.  
memcpy(pArProps, pJtypes, nLength * sizeof(FriBidiJoiningType));  
fribidi_join_arabic(pBidiTypes, nLength, pEmbeddingLevels, pArProps);  
// получаем корректные Unicode символы для арабских букв. Для лигутур эта функция один символ заменяет на лигатуру, а другой на пустой символ. 
fribidi_shape (FRIBIDI_FLAG_SHAPE_MIRRORING | FRIBIDI_FLAG_SHAPE_ARAB_PRES | FRIBIDI_FLAG_SHAPE_ARAB_LIGA,  
pEmbeddingLevels, nLength, pArProps, pInputLine);  
// получаем позицию букв в строке для случаев арабского и не арабского текста. 
FriBidiLevel res = fribidi_reorder_line(FRIBIDI_FLAGS_ARABIC, pBidiTypes, nLength,  
0, baseDirection, pEmbeddingLevels, pInputLine, pPositionLogicToVisual); 

В текс бокс добавил вызов GNU FriBidi перед обновлением позиции букв и курсора.

Изменения в существующем коде

Для упрощения расчёта позиции букв пришлось немного усложнить нашу структуру данных. Первоначально был список букв, по которому перемещался свой курсор, этот же список использовался для расчёта позиции каждой буквы.
Позиция 0 1 2 3 4 5
Буква П р и в е т

Для арабского пришлось добавить 2 списка, первый список — это логическое хранение букв, то есть номера по порядку ввода пользователем. А второй список — это буквы в порядке их отрисовки, начиная с левой и до правой (даже для арабского). С таким подходом проще реализовать выравнивание параграфа.
Пример для смешаного текста:
Пример смешаного текста
Позиция 0 1 2 3 4 5 6 7 8
Порядок ввода خ ط أ О ш и б к а
Порядок отображения О ш и б к а أ ط خ

По большому счёту GNU FriBidi использовалась для построения списка отображения букв.
Таким образом вся работа курсора производилась со списком букв в порядке ввода: вставка символа, выделение, удаление, перемещение. А для отображения и выравнивания использовался список букв в порядке отображения. Кстати для русского языка оба списка одинаковые.

Результат

В результате довольно быстро удалось добавить поддержку арабского. Всё вроде бы работало.
Мы сделали это!
Но потом нам пришел репорт от арабского пользователя, о том что диакритики отображаются не правильно. FreeType не смог справиться со сложными случаями, когда диакритики добавляются отдельно. FreeType не хватает информации, т.к. позиция деакритика зависит от буквы, с которой он используется.

Что дальше...


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

Disclaimer


Да, мы пишем свой велосипед, поэтому реализуем свой TextBox с нуля. И мы не использовали Pango, потому что с ним был неудачный опыт раньше. Может быть с Pango было бы сделать легче. Арабским не владею, может быть что-то и упустил.

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


@UnickSoft
карма
16,0
рейтинг 0,0
Пользователь
Самое читаемое Разработка

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

  • 0
    удалён
  • 0
    HarfBuzz используют очень многие для рендеринга complex scripts (куда входит арабский, Thai, и другие). Его надо было брать с самого начала, остальные библиотеки не настолько хороши, либо очень «жирные».

    Кроме того, FreeType может рендерить только тривиальные случаи — например, «отрендерить данную English строку пользуясь вот этим шрифтом», где один символ строки — это один глиф из шрифта. Для complex scripts необходимо делать промежуточное преобразование входной Unicode строки в последовательность «кластеров глифов». Эту задачу и выполняет HarfBuzz. Полученные кластера можно будет скармливать FreeType, так как вся необходимая информация для правильного рендеринга уже получена.
    • 0
      Да в конечном варианте мы используем HarfBuzz вместе с FriBidi. Как и для чего добавили HarfBuzz напишу в следующей статье.
      Но HarfBuzz не получилось использовать без FriBidi, так как ему необходимо подавать строчки одного скрипта. Т.е. ему нельзя подавать строку где английские и арабские слова в перемешку, их необходимо разбить и подавать отдельно.
      Про FreeType вы правильно написали, так и работает.

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