[Перевод] Современный рендеринг текста в Linux: часть 1

Добро пожаловать в первую часть «Современного рендеринга текста в Linux». В каждой статье из этой серии мы разработаем самодостаточную программу на C для визуализации символа или последовательности символов. Каждая из этих программ будет реализовывать функцию, которую я считаю необходимой для современного рендеринга текста.

В первой части настроим FreeType и напишем простой рендерер символов в консоли.

f1df9124b08622215c74018b9dafe1d7.png

Вот что мы будем писать. А вот и код.


  • Моя операционная система: 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 

int main() {
  printf("Hello, world\n");
  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 

#include 
#include FT_FREETYPE_H

int main() {
  printf("Hello, world\n");
  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 FreeType\n");
  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.%d\n", 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 face\n");
  exit(EXIT_FAILURE);
} 


Установка пиксельного размера для face


С помощью этой инструкции мы сообщаем FreeType желаемую ширину и высоту для отображаемых символов.

Если для ширины передать нуль, FreeType интерпретирует это как «такая же, как другие», в данном случае 32 px. Это можно использовать для отображения символа, например, с шириной 10 px и высотой 16 px.

Эта операция может потерпеть неудачу на шрифте фиксированного размера, как в случае эмодзи.

err = FT_Set_Pixel_Sizes(face, 0, 32);
if (err != 0) {
  printf("Failed to set pixel size\n");
  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 glyph\n");
  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 glyph\n");
  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 по сравнению с оттенками серого, их плюсах и минусах.

До скорой встречи.

© Habrahabr.ru