Pull to refresh

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

Reading time 5 min
Views 12K
Original author: Andrea Cognolato
Добро пожаловать в первую часть «Современного рендеринга текста в 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, 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 <stdio.h>

#include <freetype2/ft2build.h>
#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 интерпретирует это как «такая же, как другие», в данном случае 32px. Это можно использовать для отображения символа, например, с шириной 10px и высотой 16px.

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

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 по сравнению с оттенками серого, их плюсах и минусах.

До скорой встречи.
Tags:
Hubs:
+22
Comments 15
Comments Comments 15

Articles