Переодически возникает необходимость вывести какие-то буковки на экран. Иногда это экран компудастера, иногда это экран микроконтроллера. Все больше и больше популярности в нашей стране приобретает набор для образовательных целей «Ардуино», который уже достаточно широко распространен в школах, детских больницах, хосписах и гаражных кооперативах. К сожалению, наша молодеж и автолюбители сталкиваются с проблемой вывода кириллицы на свои экраны, так как наборчик Ардуино хоть и снабжен многочисленными примерами работы с текстом, но не учитывает потребности малых народов и узких языковых групп. Поэтому мы взяли на себя тяжелый труд по ликвидации колоссального языкового разрыва и восстановлению языковой справедливости.
В основном шрифты бывают трех типов:
- Пиксельные. Так как обычно экранчики пиксельные, то логично и шрифты хранить в этом же формате и просто быстро-быстро копировать эти самые пиксели. Обычно именно этот тип используется в микроконтроллерах, в библиотеках Arduino и древних компьютерах, вроде ZX-Spectrum. Недостатком этих шрифтов является то, что нельзя произвольно менять размер отрисовки: на экране или появятся артефакты масштабирования, или нужно будет добавлять все символы под новый размер.
- Векторные штрихи. Если требуется рисовать большие буквы, а то и вообще в произвольном масштабе, то лучше использовать шрифты на основе векторных штрихов. Каждая линия такого шрифта - это штрих минимальной толщины. Подобные шрифты отлично подходят, если надо научить лазерный ЧПУ-станок делать надписи. На сегодняшний день практически не встречаются. Примером может выступать шрифт GOST type A (plotter)
- Векторные контуры. Самый распространенный тип шрифтов на сегодняшний день. В этих шрифтах каждая линия задана парой контуров, внутренности которых закрашиваются. И хотя этот формат сегодня является доминирующим на компьютерах, в микроконтроллерах почти не используется из-за сложности реализации и фактической ненужности.
Есть и другие типы шрифтов, например, шрифты для emoji или цветные шрифты. Где-то используются PNG-картинки, почти как в древних пиксельных шрифтах, только теперь в цвете, где-то используются сложные системы растеризации и внутренние виртуальные машины, что позволяет задавать оттенки букв. Но в эту степь мы сегодня не пойдем.
Пример шрифта из штрихов:
Пример шрифта из контуров:
С этого момента будем считать, что нам надо где-то взять ПИКСЕЛЬНЫЕ ШРИФТЫ, желательно с кириллицей.
- Нарисовать самим
- Стырить из винды
- Стырить из Linux
- Стырить из древних компьютеров вроде C64 / ZX-Spectrum / демок под msdos
- Растеризовать из ttf
Так как шрифтов ttf в мире крайне много, то первым делом в голову приходит сделать растеризацию векторных шрифтов. Увы, но в общем и целом задача не имеет решения.
Если попробовать написать программу "в лоб", которая рисует отдельные глифы на экране, то мы получим огромное количество артефактов:
Шрифт Fifaks 1.0 dev1 явно сделан пиксельным, но сетка символов иногда не вмещает символы целиком:
Шрифт Fixedsys Excelsior font with programming ligatures по большей части пиксельный, но некоторые символы выглядят размазанными, другие тоже не влезают в свои знакоместа:
Поэтому если очень хочется, то можно взять такую заготовку, взять древний mspaint.exe
и ручками доработать шрифт там, где он выглядит совсем отвратительным. Или трансплантировать отдельные глифы из других шрифтов, где они выглядят лучше. Например, я ручками доработал шрифт Fifaks 1.0 dev1
и получил такую картинку:
Для растеризации можно использовать прилагающийся скрипт ttf2png.pl
или rasterizer.pl
, оба скрипта крайне кривые и сделаны из говна и палок, но они могут служить отправной точкой (нет). Как ими пользоваться, читатель может догадаться сам.
К сожалению, подобные гибриды и прочие эксперименты нельзя распространять как готовый продукт, чтобы случайно не нарушить какую-нибудь лицензию.
Шрифты в формате FON обычно являются пиксельными и достаточно высокого качества. Если у вас есть лицензионное право или законы вашей страны разрешают использование таких ресурсов, то распаковываем файлы *.fnt
:
mkdir fnt # создать директорию fnt
wrestool -x -R -t 8 -o fnt *fon # распаковать ресурсы из файлов *.fon в текущей директории
perl perl fnt_renamer.pl # переименовать файлы шрифтов во что-то вменяемое
perl fnt2png.pl 8514oem-oem-10x12.fnt # сконвертить один из шрифтов в картинку
Одним из лучших открытых пиксельных шрифтов является GNU Unifont, который среди прочего распространяется в формате BDF
. Увы, но мне лень писать конвертер этого формата. Он достаточно простой и читателю предлагается это упражнение для самостоятельного исполнения.
Под старыми системами были замечательные шрифты, которые рисовались годами, даже после официальной смерти этих платформ на рынке. Множество красивых шрифтов можно найти здесь:
- https://damieng.com/typography/zx-origins/ - куча шрифтов в самых разных форматах
- https://github.com/patrickmollohan/c64-fonts - штатные шрифты C64
- https://c64gfx.com/compo/2128 - конкурс шрифтов для C64
Самые лучшие шрифты конечно же лежат в демосцене, а как их оттуда выковыривать - зависит от самой демки. Увы, кириллица в таких шрифтах бывает не слишком часто. Если же она там есть, то это наверняка будет отличный шрифт.
Шрифт вытащенный из cracktro Vendetta:
Увы, не смотря на всю красоту шрифта, в нем нету кириллицы
Как такое вытащить? Берем GBS и долго крутим ползунки, пока не найдем что-то похожее на шрифты, записываем адреса. Потом как-то так:
# dd if=binary.exe bs=239487 skip=1 of=font.bin # отрезаем от бинарника нужный кусочек
# mkdir aaa # создаем временную директорию
# ffmpeg -f rawvideo -s 8x16 -pix_fmt monob -i font.bin aaa/tst%4d.png # нарезаем буковки
# montage -geometry 8x16+0+0 -tile 16x16 aaa/* font.png # склеиваем буковки
Для компьютера все буквы, которые он выводит на экран - на самом деле байты. К примеру, если в программе вывести байт со значением 48
, то скорее всего
компьютер отобразит символ 0
(ноль), а если значение будет 65
, то будет выведена латинская A
. Как компьютер узнает, какому коду какой символ соответствует?
Для первых 128 значений есть хороший стандарт: ASCII, который потом был дополнен и во времена msdos был известен как cp437.
А что с кириллицей? Как вывести букву Я
? Сегодня, когда мы почти повсеместно перешли на стандарт Unicode
, нам нужно просто отобразить символ с номером
1071 (он же 0x42F в 16-ричной системе). И тут нас встречает первая проблема: как нам засунуть такое большое число в 1 байт? Возможно ли это?
Чтобы сделать это возможным, можно воспользоваться одной из современных кодировок, таких как UTF-8
. Увы, но в этой кодировке все кириллические
символы занимают по 2 байта, что заставляет нас тратить байты с избытком, по сравнению, esli bi mi pisali tekst translitom. Латинские же символы в этой кодировке
занимают по 1 байту, что делает сложным подсчет длинный строки. В принципе, есть и другие кодировки, к примеру UTF-16
, где для большинства символов
нужно по 2 байта, что решает проблему с измерением длинны строки, но не решает проблему с удвоением объема текста. Еще появляется вопрос: а какие символы
нам вообще нужны в шрифте? Только базовая кириллица? Расширенная кириллица с поддержкой других языков?
Стандарт Unicode
версии 15.1, содержит 149878 символов, покрывая 161 язык мира. Мы точно хотим засунуть все это в наш маленький микроконтроллер?
Но можно воспользоваться старыми и проверенными кодовыми таблицами, или таблицами символов.
Для кириллицы было изобретено множество таких таблиц, но вот основные, которые я смог вспомнить:
cp866 cp1251 ISO/IEC_8859-5
☺☻♥♦♣♠•◘○◙♂♀♪♫☼ ☺☻♥♦♣♠•◘○◙♂♀♪♫☼ ☺☻♥♦♣♠•◘○◙♂♀♪♫☼
►◄↕‼¶§▬↨↑↓→←∟↔▲▼ ►◄↕‼¶§▬↨↑↓→←∟↔▲▼ ►◄↕‼¶§▬↨↑↓→←∟↔▲▼
!"#$%&'()*+,-./ !"#$%&'()*+,-./ !"#$%&'()*+,-./
0123456789:;<=>? 0123456789:;<=>? 0123456789:;<=>?
@ABCDEFGHIJKLMNO @ABCDEFGHIJKLMNO @ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_ PQRSTUVWXYZ[\]^_ PQRSTUVWXYZ[\]^_
`abcdefghijklmno `abcdefghijklmno `abcdefghijklmno
pqrstuvwxyz{|}~⌂ pqrstuvwxyz{|}~⌂ pqrstuvwxyz{|}~⌂
АБВГДЕЖЗИЙКЛМНОП ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏ ����������������
РСТУФХЦЧШЩЪЫЬЭЮЯ ђ‘’“”•–—�™љ›њќћџ ����������������
абвгдежзийклмноп ЎўЈ¤Ґ¦§Ё©Є«¬®Ї ЁЂЃЄЅІЇЈЉЊЋЌЎЏ
░▒▓│┤╡╢╖╕╣║╗╝╜╛┐ °±Ііґµ¶·ё№є»јЅѕї АБВГДЕЖЗИЙКЛМНОП
└┴┬├─┼╞╟╚╔╩╦╠═╬╧ АБВГДЕЖЗИЙКЛМНОП РСТУФХЦЧШЩЪЫЬЭЮЯ
╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀ РСТУФХЦЧШЩЪЫЬЭЮЯ абвгдежзийклмноп
рстуфхцчшщъыьэюя абвгдежзийклмноп рстуфхцчшщъыьэюя
ЁёЄєЇїЎў°∙·√№¤■ рстуфхцчшщъыьэюя №ёђѓєѕіїјљњћќ§ўџ
KOI8-R KOI8-U MacCyrillic
☺☻♥♦♣♠•◘○◙♂♀♪♫☼ ☺☻♥♦♣♠•◘○◙♂♀♪♫☼ ☺☻♥♦♣♠•◘○◙♂♀♪♫☼
►◄↕‼¶§▬↨↑↓→←∟↔▲▼ ►◄↕‼¶§▬↨↑↓→←∟↔▲▼ ►◄↕‼¶§▬↨↑↓→←∟↔▲▼
!"#$%&'()*+,-./ !"#$%&'()*+,-./ !"#$%&'()*+,-./
0123456789:;<=>? 0123456789:;<=>? 0123456789:;<=>?
@ABCDEFGHIJKLMNO @ABCDEFGHIJKLMNO @ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_ PQRSTUVWXYZ[\]^_ PQRSTUVWXYZ[\]^_
`abcdefghijklmno `abcdefghijklmno `abcdefghijklmno
pqrstuvwxyz{|}~⌂ pqrstuvwxyz{|}~⌂ pqrstuvwxyz{|}~⌂
─│┌┐└┘├┤┬┴┼▀▄█▌▐ ─│┌┐└┘├┤┬┴┼▀▄█▌▐ АБВГДЕЖЗИЙКЛМНОП
░▒▓⌠■∙√≈≤≥ ⌡°²·÷ ░▒▓⌠■∙√≈≤≥ ⌡°²·÷ РСТУФХЦЧШЩЪЫЬЭЮЯ
═║╒ё╓╔╕╖╗╘╙╚╛╜╝╞ ═║╒ёє╔ії╗╘╙╚╛ґ╝╞ †°Ґ£§•¶І®©™Ђђ≠Ѓѓ
╟╠╡Ё╢╣╤╥╦╧╨╩╪╫╬© ╟╠╡ЁЄ╣ІЇ╦╧╨╩╪Ґ╬© ∞±≤≥іµґЈЄєЇїЉљЊњ
юабцдефгхийклмно юабцдефгхийклмно јЅ¬√ƒ≈∆«»… ЋћЌќѕ
пярстужвьызшэщчъ пярстужвьызшэщчъ –—“”‘’÷„ЎўЏџ№Ёёя
ЮАБЦДЕФГХИЙКЛМНО ЮАБЦДЕФГХИЙКЛМНО абвгдежзийклмноп
ПЯРСТУЖВЬЫЗШЭЩЧЪ ПЯРСТУЖВЬЫЗШЭЩЧЪ рстуфхцчшщъыьэю€
Шрифт Fifaks может отобразить это так:
Если мы используем кодировку cp866
, то для вывода буквы Я
нужно вывести символ с номером 159 (или 0x9F), что отлично умещается в 1 байт и решает
множество проблем нашего крохотного микроконтроллера. В довесок у нас есть символы псевдографики, которыми мы можем рисовать рамочки и даже есть несколько
смайликов и иконок, которые тоже можно использовать. К недостаткам этой кодировки можно пожалуй отнести только отсутствие «ёлочек», которые есть
в кодировке cp1251
(кодировке кириллицы в Windows), но если «ёлочки» или какой-то другой символ сильно нужны, никто не мешает выбрать другую кодировку.
Лично я для себя сделал выбор в пользу cp866
.
Всем любопытным крайне рекомендую: http://aspell.net/charsets/cyrillic.html - краткая история кодировок с кириллицей
Так как у нас цель сконвертить шрифт под микроконтроллер, то такие фичи как кернинг нам не сильно нужны, равно как и полутона. Следовательно, чтобы уменьшить размер нашего битмапа, мы смело можем конвертить его в 1-битную картинку. К примеру, сконвертим шрифт от редактора BGE:
convert bge.png -colorspace gray -auto-level -negate -colors 2 bge-bw.png
Далее, нам надо как-то превратить нашу картинку в шрифт. Обычно шрифт представлен набором линий по 8 пикселей, которые кодируются 1 байтом.
Порядок байтов и битов разнится от библиотеки к библиотеке и может меняться. К примеру, в FNT-файлах байты идут по вертикали, как только самые левые 8 пикселей символа будут записаны в файл, можно переходить к следующему вертикальному столбцу и кодировать следующие 8 пикселей. Чаще же шрифты хранятся по строчкам, в когда вся строка кодирована в байты, то осуществляется переход на новую строчку. Иногда глифы могут быть повернуты на 90 или 180 градусов, в некоторых случаях эта оптимизация имеет смысл.
На прилагаемой картинке видно, что место справа и снизу от глифа зачастую расходуется напрасно. Поэтому я предлагаю хранить картинку более оптимальным образом: в виде глифов, разложенных по сетке 16х16 пикселей. Как вариант, разложить все глифы в одну длинную картинку по горизонтали, но лично мне такие картинки сложнее воспринимать. Как и вертикальные строчки, с повернутыми символами на 90 градусов. Но такие варианты хранения могут быть более оптимальными в вашем конкретном случае.
Итак, превратим нашу картинку... В такую же картинку, но только пригодную для добавления в наш код:
convert bge-bw.png -negate bge_bw.xbm
И после этого получим файл bge_bw.xbm
, пригодный для включения в наш код:
#define bge_bw_width 80
#define bge_bw_height 128
static char bge_bw_bits[] = {
0x00, 0x00, 0x00, 0xC6, 0x62, 0x8C, 0x63, 0xC8, 0x14, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFE, 0x03, 0x7A, 0x0F, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00,
(длинный файл)
0xCF, 0xD1, 0x8B, 0x1D, 0x96, 0x00, 0xF0, 0x00, 0x40, 0x00, 0x00, 0x60,
0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
И напишем простенький код, который будет как-то использовать наш шрифт. Пусть это будет аналог всем хорошо известной команды figlet, которая выводит текст на экран.
gcc -Wall figlet.c && ./a.out "`echo 'Привет, мир' | iconv -tcp866`"
****
* * **
* * *** * * * * *** **** * * * * ***
* * * * * * *** **** * **** * * * *
* * **** * ** * * * * * **** * ** ****
* * * * * **** ** * * * * * * *
* * *
В принципе, этот код можно переносить в наш микроконтроллер! Вместо выделения буфера, по всей видимости, за нас это сделает библиотека для работы с экраном. Эта же библиотека предоставит функцию setPixel, чтобы не обращаться к виртуальному холсту.
В случае arduino, возможно стоит переделать функции доступа к картинки через PGM, чтобы не тратить лишнюю память и без того слабого микроконтроллера.
Вот и все, у нас есть шрифты!
Например, возьмем библиотеку pico-ssd1306, она уже умеет управлять экраном, выделять память и в ней даже есть свои шрифты. Конечно, никто не мешает портировать нашу функцию, которая рисует 1 символ и пользоватьсянашими шрифтами в этой библиотеке, но... Как же богатое API и его консистентность? Как такие фичи, как поворот текста? Давайте добавим шрифты в таком виде, в каком библиотека будет считать их родными!
Открываем библиотеку и сразу открываем в ней первый попавшийся шрифт, пусть в нашем случае это будет файл pico-ssd1306-master/textRenderer/5x8_font.h
:
#ifndef SSD1306_5X8_FONT_H
#define SSD1306_5X8_FONT_H
#ifndef SSD1306_ASCII_FULL
const unsigned char font_5x8[] = {
0x5, 0x8, // font width, height
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
0x5c,
0x0,
0x0,
Так, что мы узнали отсюда?
- Ширину и высоту глифов эта библиотека хранит в самом файле шрифтов, без дефайнов в коде
- Ширина и высота является частью массива самого битмапа
- Есть какие-то предопределенные флаги, такие как
SSD1306_ASCII_FULL
- По всей видимости надо обращаться к переменной
font_5x8
для доступа к битмапу - Каждые 5 байт есть перевод строки - скорее всего так разделяют отдельные глифы
- Так как шрифт 5 пикселей в ширину и разделение по 5 пикселей, то скорее всего глифы повернуты на 90 градусов
- Первый глиф содержит нули, скорее всего это пробел, второй глиф почти все нули - это похоже на символ "!", значит адресация начинается с 32 символа стандартной таблицы ASCII, а не с нулевого, как у нас. Да, смайлики не всем нужны :(
Продолжаем изучать библиотеку:
# grep -r SSD1306_ASCII_FULL .
./examples/text_extended_ascii/main.cpp:// Define SSD1306_ASCII_FULL to use full ascii range (32 - 255)
Наше предположение о том, что адресация начинается с 32-го символа подвердилась. Более того, оказывается флагом SSD1306_ASCII_FULL
можно включать/выключать вторую половину символов (там как раз где кириллица), так как не всем это надо.
А нужно ли как-то регистрировать наш шрифт?
# grep -r font_5x8 .
./textRenderer/TextRenderer.h:#include "5x8_font.h"
Да, все шрифты должны быть перечислены в файле textRenderer/TextRenderer.h:
, когда мы будем добавлять свои шрифты, надо пропатчить этот файл, добавив свои шрифты где-то рядом:
#include "5x8_font.h"
#include "8x8_font.h"
#include "12x16_font.h"
#include "16x32_font.h"
Вроде бы все понятно. Вроде бы. Вроде. Чтобы быть уверенным, что мы поняли все правильно, давайте перепишем наш figlet, сделаем так, чтобы он использовал эти файлы библиотеки. В качестве отправной точки, давайте портируем функцию drawChar
из pico-ssd1306-master/textRenderer/TextRenderer.cpp
# gcc -Wall figlet2.c -o figlet_foreign && ./figlet_foreign "`echo 'Привет, мир' | iconv -tcp866`"
* * * *
* * * * *** * * *
** * * * * *
* * ** * * ** ** * * * * * ** *
**** * * * * * ** * * * * * * *
* * * * * * * * * ** * * *** * * *
* * ** * ** ** * * * * *** ** ** *
* *
Ой, библиотека же не имеет кириллицы! Ладно, я хотел сказать...
# gcc -Wall figlet2.c -o figlet_foreign && ./figlet_foreign "`echo 'Hello, world!' | iconv -tcp866`"
** ** **
* * * * * * *
* * ** * * ** * * ** * * * *** *
**** **** * * * * * * * * ** * * * *
* * * * * * * **** * * * * * *
* * *** *** *** ** * * * ** * *** *** *
*
Если сделать размер шрифта побольше, то видно:
Значит мы полностью познакомились с библиотекой и можем быть точно уверены в ее устройстве! И даже если мы сделаем что-то не так, у нас уже есть готовый тест! Можно начинать генерировать шрифты в формате этой библиотеки!
Пишем конвертор png2picolib.pl, где просто читаем пиксели из нашей картинки и упаковываем в виде сишного заголовка, стараясь максимально походить на оригинал. Я конечно же не устоял и добавил комментарии, чтобы видеть какой символ и где описывается, но это фактически украшательства.
# convert bge_bw.xbm -negate bge.png
# perl png2picolib.pl bge.png
Converting file "bge.png" as font "bge" size: 5x8
#
Все, шрифт скопирован в pico-ssd1306-master/textRenderer/
(путь должен существовать), теперь можно посмотреть что там, в этом свежем файле:
# cat pico-ssd1306-master/textRenderer/bge_5x8_font.h
#ifndef SSD1306_BGE_5X8_FONT_H
#define SSD1306_BGE_5X8_FONT_H
#ifndef SSD1306_ASCII_FULL
const unsigned char font_bge_5x8[] = {
5, 8, // font width, height
/* char 32 (0x20) */ 0,0,0,0,0,
/* char 33 (0x21) ! */ 0,94,0,0,0,
/* char 34 (0x22) " */ 8,4,8,4,0,
/* char 35 (0x23) # */ 40,124,40,124,0,
/* char 36 (0x24) $ */ 40,110,36,20,0,
/* char 37 (0x25) % */ 72,32,16,72,0,
/* char 38 (0x26) & */ 104,84,40,64,0,
/* char 39 (0x27) ' */ 0,8,4,0,0,
/* char 40 (0x28) ( */ 0,0,60,66,0,
/* char 41 (0x29) ) */ 0,66,60,0,0,
/* char 42 (0x2a) * */ 72,48,48,72,0,
/* char 43 (0x2b) + */ 0,16,56,16,0,
/* char 44 (0x2c) , */ 0,128,96,0,0,
/* char 45 (0x2d) - */ 0,16,16,16,0,
/* char 46 (0x2e) . */ 0,0,64,0,0,
/* char 47 (0x2f) / */ 0,96,24,6,0,
/* char 48 (0x30) 0 */ 56,82,74,60,0,
/* char 49 (0x31) 1 */ 0,68,126,64,0,
/* char 50 (0x32) 2 */ 100,82,82,76,0,
/* char 51 (0x33) 3 */ 36,66,74,52,0,
/* char 52 (0x34) 4 */ 30,16,0,126,0,
/* char 53 (0x35) 5 */ 46,74,74,50,0,
/* char 54 (0x36) 6 */ 60,74,74,48,0,
/* char 55 (0x37) 7 */ 2,98,26,6,0,
/* char 56 (0x38) 8 */ 52,74,74,52,0,
/* char 57 (0x39) 9 */ 12,82,66,60,0,
Все отлично, наш новый шрифт готов!
Не забываем добавить новые шрифты в TextRenderer.h
, чтобы система их увидела и все готово.