Добро пожаловать в первую часть «Современного рендеринга текста в Linux». В каждой статье из этой серии мы разработаем самодостаточную программу на C для визуализации символа или последовательности символов. Каждая из этих программ будет реализовывать функцию, которую я считаю необходимой для современного рендеринга текста.
В первой части настроим FreeType и напишем простой рендерер символов в консоли.
Вот что мы будем писать. А вот и код.
Настройка системы
- Моя операционная система:
Ubuntu 18.04.2 LTS (bionic)
- Компилятор C:
clang version 6.0.0-1ubuntu2
Установка FreeType
На Ubuntu нужно установить FreeType и libpng.
$ sudo apt install libfreetype6 libfreetype6-dev
$ sudo apt install libpng16-16 libpng-dev
- У меня FreeType версии
2.8.1-2ubuntu2
, хотя на момент написания статьи последняя версияFreeType-2.10.1
, она тоже подходит. - libpng версии
(1.6.34-1ubuntu0.18.04.2)
Консольный рендерер
Создаём файл C (main.c
в моём случае)
#include <stdio.h>
int main() {
printf("Hello, worldn");
return 0;
}
$ clang -Wall -Werror -o main main.c
$ ./main
Hello, world
Подключаем библиотеки FreeType
Для поиска пути include (т. е. каталогов, которые компилятор проходит при поиске файлов в #include
) для FreeType запускаем:
$ pkg-config --cflags freetype2
-I/usr/include/freetype2 -I/usr/include/libpng16
Строка -I/usr/include/freetype2 -I/usr/include/libpng16
содержит флаги компиляции, необходимые для подключения FreeType в программу C.
#include <stdio.h>
#include <freetype2/ft2build.h>
#include FT_FREETYPE_H
int main() {
printf("Hello, worldn");
return 0;
}
$ clang -I/usr/include/freetype2
-I/usr/include/libpng16
-Wall -Werror
-o main
main.c
$ ./main
Hello, world
Печатаем версию FreeType
Внутри main()
инициализируем FreeType с помощью FT_Init_FreeType(&ft)
и проверяем наличие ошибок (функции FreeType возвращают 0 при успешном выполнении).
(С этого момента все функции, которые я буду использовать, взяты из справки по FreeType API).
FT_Library ft;
FT_Error err = FT_Init_FreeType(&ft);
if (err != 0) {
printf("Failed to initialize FreeTypen");
exit(EXIT_FAILURE);
}
Затем с помощью FT_Library_Version получаем номер версии.
FT_Int major, minor, patch;
FT_Library_Version(ft, &major, &minor, &patch);
printf("FreeType's version is %d.%d.%dn", major, minor, patch);
Если скомпилировать с помощью последней команды, то выскочит ошибка компоновщика:
/tmp/main-d41304.o: In function `main':
main.c:(.text+0x14): undefined reference to `FT_Init_FreeType'
main.c:(.text+0x54): undefined reference to `FT_Library_Version'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Для исправление добавляем -lfreetype
.
$ clang -I/usr/include/freetype2
-I/usr/include/libpng16
-Wall -Werror
-o main
-lfreetype
main.c
$ ./main
FreeType's version is 2.8.1
Загрузка шрифта
Первый шаг для рендеринга символа — загрузка файла шрифта. Я использую ubuntu mono.
Чтобы понять точную разницу между конструкцией font face, семейством шрифтов (font family) и отдельными шрифтами, см. документацию FreeType.
Третий аргумент называется face index. Он создан, чтобы позволить создателям шрифтов вставлять несколько face в один размер шрифта. Поскольку у каждого шрифта есть по крайней мере один face, то значение 0 будет работать всегда, выбирая первый вариант.
FT_Face face;
err = FT_New_Face(ft, "./UbuntuMono.ttf", 0, &face);
if (err != 0) {
printf("Failed to load facen");
exit(EXIT_FAILURE);
}
Установка пиксельного размера для face
С помощью этой инструкции мы сообщаем FreeType желаемую ширину и высоту для отображаемых символов.
Если для ширины передать нуль, FreeType интерпретирует это как «такая же, как другие», в данном случае 32px. Это можно использовать для отображения символа, например, с шириной 10px и высотой 16px.
Эта операция может потерпеть неудачу на шрифте фиксированного размера, как в случае эмодзи.
err = FT_Set_Pixel_Sizes(face, 0, 32);
if (err != 0) {
printf("Failed to set pixel sizen");
exit(EXIT_FAILURE);
}
Получение индекса для символа
Прежде всего, вернёмся к документации FreeType и установим соглашение об именах. Символ — это не то же самое, что глиф. Символ — это то, что указано в char
, а глиф — это образ, который каким-то образом связан с этим символом. Это отношение довольно сложное, потому что char может соответствовать нескольким глифам: т. е. акцентам. А глиф может соответствовать многим символам: т. е. лигатурам, где -> представляется как одно изображение.
Для получения индекса глифа, соответствующего символу, мы используем FT_Get_Char_Index
. Как вы можете понять, это предусматривает сопоставление символов и глифов только один к одному. В будущей статье из этой серии мы решим проблему с помощью библиотеки HarfBuzz.
FT_UInt glyph_index = FT_Get_Char_Index(face, 'a');
Загрузка глифа из face
Получив glyph_index, мы можем загрузить соответствующий глиф из нашего face.
В будущей части мы подробно обсудим различные флаги загрузки и то, как они позволяют использовать такие функции, как хинтинг и растровые шрифты.
FT_Int32 load_flags = FT_LOAD_DEFAULT;
err = FT_Load_Glyph(face, glyph_index, load_flags);
if (err != 0) {
printf("Failed to load glyphn");
exit(EXIT_FAILURE);
}
Отображение глифа в его контейнере (glyph slot)
Теперь мы можем, наконец, отобразить наш глиф в его контейнере (слоте), указанном в face->glyph
.
Флаги рендеринга мы тоже обсудим в будущем, потому что они позволяют использовать LCD- (или cубпиксельный) рендеринг и сглаживание оттенков серого (grayscale antialiasing).
FT_Int32 render_flags = FT_RENDER_MODE_NORMAL;
err = FT_Render_Glyph(face->glyph, render_flags);
if (err != 0) {
printf("Failed to render the glyphn");
exit(EXIT_FAILURE);
}
Вывод символа в консоль
Растровое изображение отрисованного глифа можно получить из face->glyph->bitmap.buffer
, где оно представлено в виде массива беззнаковых значений char, поэтому его значения находятся в диапазоне от 0 до 255.
Буфер возвращается в виде одномерного массива, но представляет собой 2D-изображение. Чтобы получить доступ к i-ой строки j-го столбца, рассчитываем column * row_width + row
, как в bitmap.buffer[i * face->glyph->bitmap.pitch + j]
.
Вы можете видеть, что при доступе к массиву мы использовали bitmap.width
в цикле и bitmap.pitch
, потому что длина каждой строки пикселей равна bitmap.width
, но «ширина» буфера составляет bitmap.pitch
.
В следующем коде перебираются все строки и столбцы, а в зависимости от яркости пикселя рисуются разные символы.
for (size_t i = 0; i < face->glyph->bitmap.rows; i++) {
for (size_t j = 0; j < face->glyph->bitmap.width; j++) {
unsigned char pixel_brightness =
face->glyph->bitmap.buffer[i * face->glyph->bitmap.pitch + j];
if (pixel_brightness > 169) {
printf("*");
} else if (pixel_brightness > 84) {
printf(".");
} else {
printf(" ");
}
}
printf("n");
}
Вывод консоли.
$ clang -I/usr/include/freetype2
-I/usr/include/libpng16
-Wall -Werror
-o main
-lfreetype
main.c && ./main
FreeType's version is 2.8.1
.*****.
.********.
.*********
. ***.
***
***
.********
***********
.**. ***
*** ***
*** ***
***. ***
.***********
***********
.*******..
Полный код можно посмотреть здесь.
Заключение
Мы создали базовый рендерер символов в консоли. Этот пример может (и будет) расширен для рендеринга символов в текстуру OpenGL для поддержки эмодзи, субпиксельной рендеринга, лигатур и многого другого. В следующей части поговорим о субпиксельном сглаживании LCD по сравнению с оттенками серого, их плюсах и минусах.
До скорой встречи.
Автор: m1rko