юникод

Поддержка Юникода во «Флиппере Зеро»

Если помните, какое-то время назад я разбирался как обстоит дело с поддержкой Юникода во «Флиппере Зеро». Если вкратце: плохо, но жить можно. Исследование этой проблемы даже подтолкнуло меня к написанию игры, в которой я мужественно поборол все трудности, связанные с ограниченностью АПИ в этом вопросе.

Видно, что авторы не подумали о поддержке Юникода, поэтому он там работает кое-как и по чистой случайности, просто потому что его поддерживает сторонний фреймворк «U∞g2», используемый для вывода на экран.

Ситуацию можно было бы немного улучшить простым патчем — надо всего лишь поменять в функции canvas_glyph_width тип входного параметра с char на uint16_t (эта функция возвращает ширину символа в пикселях) и она начнёт работать с кодировкой UCS-2.

Две недели назад я подумал — почему бы не попробовать предложить этот патч авторам прошивки. Если не примут, то может быть хоть в дискуссию вступят, всё польза. Скачал разработческую версию ядра, сделал правку и закинул патч.

Сегодня патч приняли без лишних слов. Вот это да, не ожидал! Справедливости ради, это небольшое изменение, которое не должно ничего сломать. Теперь хорошо бы подумать как сделать так, чтобы в остальном АПИ поддержка Юникода заработала. А там, кажется, малой кровью не обойдёшься.

Скриншот GitHub-запроса #3226: пользователь bolknote предлагает добавить поддержку кодировки UCS-2 в функцию canvas_glyph_width, приводя примеры кода для кириллицы.
Внезапно мой небольшой коммит для поддержки Юникода функцией вычисления ширины символа приняли

Оставить комментарий

Пишем на «Флиппере Зеро» по-русски ★

Вкратце: в русским текстом работать можно, но сложно, код лежит на «Гитхабе».

Не мог не обратить внимание, что несмотря на приличное количество русскоязычных авторов программ для «Флиппера Зеро» и российское происхождение устройства, в софте под него нигде не обнаружилось ни одной буквы по-русски.

Flipper Zero

Стало интересно — а есть ли вообще в устройстве поддержка русского языка? Сел вечерком за исходники прошивки и оказалось, что однозначного ответа на заданный вопрос нет. Частичная поддержка есть и её можно заставить работать, но она работает как будто по недосмотру — никто её не собирался делать, так случайно вышло.

На экране Flipper Zero отображается текст: «Привет, мир! Пишем по-русски на Флиппере Зеро. Евгений Степаницев, 2023». Устройство показано в оранжевом стиле на тёмном фоне.
Вывод русских букв (трансляция с экрана через оболочку qFlipper)

Сейчас расскажу, что я имею ввиду и постараюсь покороче, никто сейчас не любит длинные тексты.

Внутри «Флиппера» довольно много всякого АПИ для вывода текста, от незатейливого, выводящего строку в заданные координаты, до довольно навороченного, умеющего переносить и выравнивать текст. К сожалению, с русскими буквами оно работать не умеет.

Под капотом у этого АПИ — очень известная в своей нише библиотека U∞g2, у которой по историческим, видимо, причинам есть два набора функций для работы со строками. Одни принимают только однобайтовую кодировку ASCII, другие работают в Юникоде.

АПИ «Флиппера», к сожалению, использует функции, которые с Юникодом работать не умеют. К счастью, по какой-то случайности, упомянутое АПИ содержит два вызова, которые помогают решить эту проблему, правда придётся помучаться.

Первый из них — canvas_set_custom_u8g2_font.

Чтобы что-то написать русскими буквами, нужен шрифт, который их содержит. Стандартный вывод canvas_set_font позволяет выбрать один из нескольких шрифтов, ни одни из которых кириллицу не поддерживает. Тут и приходит на помощь canvas_set_custom_u8g2_font. Он позволяет выбрать произвольный шрифт пакета U∞g2, — внутри их целая куча и некоторые в названии содержат слово «cyrillic».

Вот как я это делаю:

#include <u8g2/u8g2_fonts.c>
// …
canvas_set_custom_u8g2_font(canvas, u8g2_font_haxrcorp4089_t_cyrillic);

Файл u8g2/u8g2_fonts.c внутри себя содержит шрифты в особом формате, можно открыть его прямо в текстовом виде и посмотреть как всё устроено.

Шрифт мы выбрали, теперь надо что-нибудь вывести. Тут на помощь приходит canvas_draw_glyph. «Под капотом» у него лежит u8g2_DrawGlyph, которая понимает символы Юникода почти без танцев с бубном. Думаю, это чистое везение — наверняка эта функция появилась сильно позднее, поэтому у неё нет двух вариантов для работы с разными кодировками.

Плохая новость в том, что строки в коде мы обычно пишем в кодировке UTF-8, а canvas_draw_glyph ожидает их в UCS-2. Но есть и хорошая новость — в прошивке доступно АПИ для перекодирования, как обычно, великолепно документированное на языке Си.

Но canvas_draw_glyph, как можно понять из названия, выводит только один символ. Как же вывести строку? Достаточно просто — надо вывести её посимвольно, сдвигая координату на ширину символа. Звучит просто, но как это сделать?

Тут нас ждёт очередная засада. Дело в том, что canvas_glyph_width, которая должна бы это делать, принимает на вход только char, при том, что нижележащая u8g2_GetGlyphWidth преспокойно работает с Юникодом.

Поэтому я сделал просто — выдрал реализацию u8g2_GetGlyphWidth себе в исходники. В итоге мякотка моего решения выглядит вот так:

void unicode_draw_utf8_str(Canvas* canvas, uint8_t x, uint8_t y, char* str) {
    FuriStringUTF8State state = FuriStringUTF8StateStarting;
    FuriStringUnicodeValue value = 0;

    for(; *str; str++) {
        furi_string_utf8_decode(*str, &state, &value);
        if(state == FuriStringUTF8StateError) furi_crash(NULL);

        if(state == FuriStringUTF8StateStarting) {
            canvas_draw_glyph(canvas, x, y, value);
            x += unicode_GetGlyphWidth(&canvas->fb, value);
        }
    }
}

Тут unicode_GetGlyphWidth — скопированная реализация u8g2_GetGlyphWidth, а furi_string_utf8_decode используется для кодирования UTF-8-строки в последовательные символы UCS-2.

2 комментария

«⠀» (Braille Pattern Blank)

Недавно один из символов Юникода немного потрепал мне нервы — в одном из структурированных файлов, содержащих пользовательский ввод встретилось что-то, что не смогло нормально сконвертироваться из UTF-8 в CP1251. Зачем там нужна именно эта кодировка оставим за скобками — ну вот такие требования.

Случаи, когда что-то не может сконвертироваться мы специально отлавливаем, так как иногда оказывается, что всё-таки можно. Особенно показателен случай с «й», когда вместо одной буквы пришла «и» скомбинированная с отдельной краткой — в Юникоде так можно. Нормализация почему-то эту пару не объединила и конвертация падала на этой странной букве.

Скриншот терминала: предупреждение о Ruby в macOS и команды irb, проверяющие символ \xE2\xA0\x80 как «BRAILLE PATTERN BLANK» через unicode/name.
Выводим символ — пусто; проверяем — пробельный ли? нет; смотрим его название

Вот и в этот раз на чём-то упало, но увидеть это что-то не удалось — выглядел виновник как обычный пробельный символ. Я довольно быстро вычленил его код, по нему в стандарте нашли символ — это оказался пробел в азбуке Брайля. Если вдруг вы не знаете, это специальная тактильная точечная азбука для слепых, визуализация которой есть в Юникоде.

Этот пробел — странный символ, библиотеки для работы с Юникодом не считают его пробельным, но во многих шрифтах он визуально неотличим от пробела. Как его ввёл пользователь — непонятно, может случайно через какую-нибудь специальную раскладку.

Библиотека libiconv конвертировать его отказалась, хотя, как по мне, он вполне может быть сконвертирован в обычный пробел, что мы теперь и делаем.

2 комментария

Политкорректный Unicode 8

Я всё ждал, когда это появится, и вот наконец в восьмом Юникоде обнаружились модификаторы цвета кожи. Модификаторов в Юникоде — пруд пруди, это специальные символы, которые сами по себе ничего не значат — они привносят что-то новое в символ, с которым комбинируются. Простой пример — модификатор ударения, вот вам ударная буква «й»: «й́».

Модификаторы цвета (57.52КиБ)

Вот восьмой Юникод вводит модификаторы цвета (на картинке выше), при комбинировании с которыми цветные «эмоджи», которые уже давно входят в Юникод, могут менять свой цвет:

Смена цвета (12.86КиБ)

Юникод всё усложняется и усложняется, чувствую, однажды они придут к чем-то похожему на ХТМЛ+ЦСС — будут базовые элементы и стилевые таблицы для их изменения.

6 комментариев

I � Unicode

Давайте я вам про Unicode ещё раз расскажу?

Как известно, в памяти компьютера числа представлены битами, которые группируются в байты. Один байт может хранить одно из 256 значений (поскольку состоит из восьми бит, каждый из которых может хранить одно из двух). Следовательно, числа, значения которых > 255 хранятся в больше, чем одном байте.

Например, числа до 65535 можно уместить уже в двух байтах: в так называемом старшем записывается сколько раз полных 256 содержится в числе, а в младшем — остаток: старший × 256 + младший.

В зависимости от типа процессора, порядок, в котором записаны в памяти старшие и младшие байты, различается. Собственно, мне хорошо известны только две системы: младший записывается первым (как в процессорах Intel) и старший записывается первым (в процессорах ARM, которые стоят в смартфонах). Есть ещё смешанная, но с ней я не сталкивался. Системы эти носят имена: little-endian и big-endian (системы со смешанным порядком называются middle-endian и термин не указывает на то как именно «мешается» этот порядок). Краткая запись названий — LE и BE.

Есть ещё системы, которые умеют переключать порядок (те же ARM) и называются bi-endian.

Термины little-endian и big-endian пришли к нам из «Приключений Гулливера» и на русский переводятся как «тупоконечный» и «остроконечный». Те, кто читали, те помнят (война по поводу того с какой стороны разбивать яйца). Информатика тут какбэ намекает. Хотя у каждой системы есть свои достоинства и (не удержался) мне ближе LE.

Сюрприз для непрограммистов: буквы в памяти компьютера тоже представлены числом. Это просто номер по порядку в компьютерном алфавите. Так девочки в нашем классе «кодировали» записки: вместо букв ставили номер позиции в алфавите. В чём-то они были правы, но только не в том, что это шифр.

Когда-то компьютеры победивших сейчас систем использовали всего 256 символов и всем было хорошо — туда умещались все символы, которые присутствовали в том мире, где эти компьютеры создавались. Экспансия привела к тому, что 256 значений для символов перестало хватать.

Было принято очевидное решение — выделять на символ не один байт, а несколько. Так появился стандарт Unicode, где огромному количеству символов дано своё число и закреплены начертания, стандарт дополняется и новые версии выходят почти каждый год.

Система кодирования Unicode, где выделяются два байта, называется UTF-16 (16 бит на символ), там где четыре байта — UTF-32 (32 бита). Название UCS-4 (четыре байта) является синонимом UTF-32, а UCS-2 (два байта) подмножеством UTF-16. UCS-2 отличается от UTF-16 отсутствием так называемых «суррогатных пар» (которые появились только в Unicode 2.0, вы не хотите знать что это) и является устаревшим стандартом, можете про него забыть.

Так как способов хранения чисел, не умещающихся в памяти у нас несколько, то системы подразделяются на UTF-16BE, UTF-16LE, UTF-32BE и UTF-32LE. Отсюда видно, что UCS-4LE это тоже, что и UTF-32LE. Если порядок байт не указан, то принято считать, что используется big-endian.

Первого апреля 2005-го года были предложены шуточные «стандарты» UTF-9 и UTF-18, отношения к рассматриваемой проблеме они имеют. Для телеграфа и прочего слоновьего гуано, разрабатывались UTF-5 и UTF-6, но о их судьбе мне ничего не известно. Так же есть UTF-7, который в стандарт не вошёл, но реально применяется (в модифицированном виде) внутри почтового протокола IMAP4, про него я рассказывать не буду, мне он стал известен из-за оригинального способа его использования для XSS-атак в IE (в частности, решением этой проблемы я занимался в PEAR PHP классе HTML_Safe). Можно упомянуть ещё UTF-1, но с ней я не сталкивался в работе.

BOM. BOM расшифровывается как «byte order mark» (признак порядка байт) и ставится внутри файлов упомянутых двух- и четырёхбайтных кодировок. Если BOM внутри файла не встретился, принимается порядок big-endian. У BOM есть значание. В UCS-2 это 65279 (для программистов — FEFF), для UCS-4/UTF-32 — это 4278124544 (FEFF0000). Число выбрано так, чтобы старшие и младшие байты у них не совпадали и по их порядку можно было бы определить какой порядок байт используется. К сожалению, BOM не даёт возможности определить использутеся двух- или четырёхбайтная кодировка.

Теперь непрограммистам будет трудно.

Пока всё было достаточно просто, но человечество придумало ещё одну кодировку — UTF-8, с плавающим размером. Хорошие новости заключаются в том, что порядок следования байт тут определён и никаких LE и BE рядом с UTF-8 не ставится. Соотвественно и BOM тут не нужен. Он может использоваться только для того, чтобы указать программе, что это именно UTF-8 и имеет номер 15711167 (EF BB BF). Откуда можно сделать вывод (дорогие писатели редакторов), что использование в UTF-8 BOM от UTF-16 — ошибка.

Трудность в том, что UTF-8, по сути это ещё один способ записи многобайтовых чисел (а каждая буква в стандарте Unicode — многобайтовое число). У системы есть целых два плюса (ирония!): старая однобайтовая кодировка совместима с UTF-8, а значит буржуинам не нужно переделывать свои программы, если они не используют в них буквы и других языков (например, на любом старом англоязычном сайте как бы уже используется кодировка UTF-8), второй плюс — латиница записывается компактнее (в один символ). Минусы — чисто программисткие: работа с кодировкой требует больше ресурсов из-за плавающего размера.

Итак. Каждый символ в кодировке занимает от 1 до 4-х байт. Вообще, формат устроен так, что можно было бы взять и более длинные цепочки, но в Unicode нет столько символов, чтобы записывать их более длинными последовательностями.

Тут надо вспомнить что такое биты. Бит — единица информации, мельче не бывает, у него всего два значения — 0 или 1. Байт состоит из 8 битов, биты очень удобно записывать в позиционной двоичной системе: 00001011. «Позиционная» тут означает, что значение числа зависит от его позиции. Кстати, это привычная нам система. В числе «22» две двойки, но у первой значение в десять раз больше, чем у второй. Это десятичная позиционная система. В двоичной, каждая более левая однёрка будет больше в два раза своей соседки.

Таким образом число 1011 расшифровывается из двоичной как 1 × 23 + 0 × 22 + 1 × 21 + 1 × 20 = 1 × 8 + 1 × 2 + 1 × 1 = 8 + 2 + 1 = 11 в десятичной системе.

UTF-8 устроен следующим образом. Пусть, мы двигаемся по строке, содержащей два байта: 208 и 159. В битах это 11010000 и 10011111. (Немного осталось, потерпите).

В первом символе нужно посчитать количество бит со значением «1» до первого нуля. Это общее количество байт, которым записан данный символ. Если количество байт — один (это вроде как специальный признак), то вы нашли не первый байт символа.

У нас в примере количество бит до первого нуля — два. Значит, буква записана двумя символами — первый это тот, на которым мы находимся и второй — который следует за ним. Каждый байт в UTF-8 разбит на две части — до первого нулевого бита. Первая часть — общая длина байт последовательности, а оставшаяся — значение. Биты из значения записывают последовательно (у нас это 10000 011111) и смотрят какое число получилось (у нас это — 1055, это номер буквы «П» в Unicode).

Могу рассказать про UTF-7 и UTF-1, если интересно. Или про суррогатные пары.

41 комментарий

Интересный символ в Unicode

Однолетнее травянистое растение из семейства шелковичных (1.40КиБ)

Как вы думаете, если ли в Unicode символ однолетного травянистого растения из семейства шелковичных, который большая часть альтернативно настроенных к обществу подростков носит в виде узнаваемого символа, а некоторые даже и пробовали курить?

Естественно, символа, в упомянутой мной узнаваемой форме, нет. И не будет. Но на планете Земля есть иерогифические языки, где многие базовые слова состоят из одного символа. В Unicode эти символы нужно включать. Так вот. Альтернативно настроенным повезло — в таблице есть символ, который в переводе обозначает это упорно неназываемое мной слово. Его же можно увидеть на картинке слева.

Оставить комментарий