Вчера я писал, что цветные шрифты — не самое важное расширение стандарта FIGlet. От своих слов я не отказываюсь, но это расширение точно одно из самых весёлых.
Напомню, что это шрифты, которые составлены из текстовых символов. Они могут использовать там, где, кроме текста, ничего нет — например, в текстовых файлах, в письмах без оформления, в консоли и так далее.
Идея цветных шрифтов простая: в консоли можно использовать цвета, которые задаются специальными управляющими последовательностями. Терминал не выводит их на экран как текст, а воспринимает как команды. Например: «с этого места рисуй всё зелёным».
Таких последовательностей довольно много, но для управления цветом обычно используют несколько основных режимов.
Немного истории. Сначала появились базовые цвета — классический набор из восьми цветов. Потом к нему добавили яркие варианты, и в результате получилась 16-цветная палитра. Позже этого стало мало, поэтому появился режим, позволяющий выбирать цвет из палитры в 256 цветов. А затем добавили и true color — возможность задавать цвет напрямую в формате RGB, то есть практически из всей 24-битной палитры.
Терминалы работают с разными палитрами, что они, как правило, умеют сообщать через переменные окружения. Например, у меня на «Маке» есть переменная COLORTERM со значением truecolor, что означает, что моему терминал можно отсылать 24-битные цвета, а на рабочих «Линуксах» такой переменной нет. Но там есть переменная TERM со значением xterm-256color, которое указывает на то, что эти терминалы умеют обрабатывать 256 цветов.
Когда я начал разбираться с форматом шрифтов TOIlet и библиотекой libcaca, которая их отображает (автор большой оригинал, да), оказалось, что внутри реализована идея цветных шрифтов, но ни одного примера такого шрифта нет. Судя по коду, он слегка не дописан, но тем не менее, вполне работоспособен, жаль только автор остановился на палитре из 16 цветов.
Так что, первое, что я сделал — создал из набора стандартных эмодзи цветной шрифт в формате TOIlet, где смайлики составлены из раскрашенных юникодных блочных символов. Результат мне не особо понравился. Например, местами некоторые оттенки жёлтого у меня съехали в белый (из-за ограниченной палитры) и я потом ещё улучшал алгоритм, но всё равно — в шестнадцати цветах особо не разгуляешься.
Я подумал, что было бы здорово расширить формат, но так, чтобы утилита toilet по-прежнему выводила тот же шрифт в своих знакомых шестнадцати цветах.
Если посмотреть как libcaca парсит управляющие коды, то мы заметим, что парсер достаточно вольно обращается с грамматикой, не реагируя на некоторые ошибки, например, он пропускает все управляющие числа большие 107.
for(j = 0; j < argc; j++)
{
/* Defined in ECMA-48 8.3.117: SGR - SELECT GRAPHIC RENDITION */
if(argv[j] >= 30 && argv[j] <= 37)
im->fg = ansi2caca[argv[j] - 30];
else if(argv[j] >= 40 && argv[j] <= 47)
im->bg = ansi2caca[argv[j] - 40];
else if(argv[j] >= 90 && argv[j] <= 97)
im->fg = ansi2caca[argv[j] - 90] + 8;
else if(argv[j] >= 100 && argv[j] <= 107)
im->bg = ansi2caca[argv[j] - 100] + 8;
else switch(argv[j])
{
// парсятся 1—9, 21-29, 38, 39, 48—50
default:
debug("ansi import: unknown sgr %i", argv[j]);
break;
}
}
}Поэтому идея возникла такая. При кодировании цвета TOIlet использует цвет фона и шрифта, чтобы точнее имитировать пиксели в текстовом режиме. Иногда используется только один из них, чтобы реализовывать условную прозрачность. Это я пишу, чтобы рассказать, что у нас есть три варианта — указан цвет фона, указан цвет шрифта или указаны оба.
Если сразу за цветом в палитре из 16 цветов писать значение цвета, к которому я прибавлю такое число, чтобы оно гарантировано пропускалось парсером libcaca, то смогу спрятать от него там более точные значения цвета.
Например: \e[31;287;41;297m. Тут \e[ — начало специальной последовательности управляющих кодов (\e в реальности — символ с кодом 27, он никак не отображается, а только говорит, что дальше идёт команда).
Потом 31 — установка фона в красный, 287 — код цвета из 256-цветной палитры с прибавленным числом 256, чтобы оно пропускалось парсером. Так как оно идёт сразу после установки фона, то значит и тут мы ставим фон.
Следующим идёт, как возможно вы уже догадались, установка цвета шрифта, 41 — это тоже красный цвет, но для шрифта, коды разные, надо же отличать что именно мы устаналиваем. За ним — 297, принцип тот же, но так как тут значение идёт сразу после кода установки цвета шрифта, то мы понимаем, что и тут ставится цвет шрифта.
Символ m закрывает управляющую последовательность.
Таким образом утилита toilet и её библиотека libcaca увидят последовательность \e[31;41m, а мой парсер выкинет числа для 16-цветной палитры и оставит себе только 287 и 297, вычтет 256 и поставит нужный цвет.
При этом, если мой парсер таких числе не видит, то он читает цвет 16-цветной палитры. Что, во-первых, даёт обратную совместимость, во-вторых, в редких случаях, когда цвет из 256-цветной палитры совпадает в точности с 16-цветной, то можно указать «короткий» цвет и немного сэкономить.
С цветом RGB работает всё так же, только прибавляем 512. Правда цвет у меня кодируется в обратном порядке — BGR, потому что синяя компонента у́же всего используется в эмоджи, а красная — шире, поэтому если развернуть значения, числа получаются меньше и шрифт становится компактнее. Я думаю это оттого, что синий цвет хуже распознаётся глазом, недаром все градиенты во времена эпохи 256 цветов на мониторах старались делать синими.
Шрифты, конечно, получаются просто огромными — по несколько мегабайт, но здесь помогает сжатие: и FIGlet, и TOIlet поддерживают сжатые шрифты.
Для конвертации эмодзи я написал конвертер на «Пайтоне», который преобразует их в шрифт отдельно в 256 цветах и в полноцветной палитре: хранить все варианты цветов было бы слишком накладно. А библиотека при отображении уже сама приближает цвет, если это требуется для конкретного терминала.
Вот, кстати, где ИИ сильно у меня начал буксовать — на задаче как компактнее спрятать новые цвета в старом формате. Сначала пришлось подумать самому, а потом уже пошло веселее — «Опус 4.6», который я не устаю нахваливать, подхватил идею и мы с ним быстро проверили различные гипотезы.
Добавлено позднее: реализацию всего вышесказанного можно увидеть в моём репозитории на «Гитхабе».