Добро пожаловать в первую часть «Современного рендеринга текста в 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 по сравнению с оттенками серого, их плюсах и минусах.

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

Комментарии (13)


  1. COKPOWEHEU
    26.07.2019 17:09

    Этот пример может (и будет) расширен для рендеринга символов в текстуру OpenGL
    Планируется ли дальнейшее расширение примера для вывода полностью трехмерного текста, то есть построение трехмерной модели глифа?


    1. KroTozeR
      28.07.2019 18:29

      А разве FreeType на подобное рассчитан?


      1. COKPOWEHEU
        29.07.2019 09:34

        Не знаю. Разве формат шрифта не векторный?


        1. KroTozeR
          29.07.2019 20:24

          Он векторный, однако, плоский. Третье измерение не появилось, хотя 3D-эффекты надписей «рисовались» уже давно.


          1. COKPOWEHEU
            30.07.2019 14:25

            Если возможно построить векторное плоское изображение, то и «выдавить» способ найдется.


  1. kommie2050
    26.07.2019 17:17

    ну и зачем оно надо, чем бы люди не тешились. лучше б внесли вклад в нормальную граф. библиотеку вместо этого


    1. KroTozeR
      28.07.2019 18:28

      Разрабатывал рендер надписей с эффектами, компоновкой и смешением с подложкой контекста на базе этой библиотеки. Лично не хватало только одного: расчёта габаритов глифов без их рендеринга и, соответственно, генерации массива графического контекста. ИМХО, это — единственный бич данной либы. 100 пудов решаемый, но для этого её надо либо изменять, либо форкать. Ну и огромные глифы долго рендерит из-за закраски.


  1. iig
    27.07.2019 11:46

    Этому современному рендерингу (через freetype) уже больше 20 лет. И, эта, подключать библиотеки явным указанием флагов линковщика, некрасиво как-то. 21 век давно наступил.


    1. KroTozeR
      28.07.2019 18:22

      Правильно, надо подсаживать программеров инструкциями на конкретные IDE, напрочь игнорируя суть действий. А то начнёшь указывать на флаги компиляторов — они научатся сравнивать сами компиляторы, изучать их. Глядишь, составят конкуренцию на рынке труда… Оно надо? Вот-вот! Надо нарисовать ещё одну «мартышкинскую» инструкцию и сопроводить это восхвалением 21-го века, мол, нынче модно быть не создателем, а потребителем.


    1. and_marsh
      29.07.2019 06:32

      А как стоит подключать библиотеки?


      1. iig
        29.07.2019 11:59

        1. Так, как делает автор в своих проектах. Он же использует какую-то систему конфигурации сборки?
        2. Так, как сделано в этом самом freetype (autoconf).


        1. and_marsh
          29.07.2019 12:11
          +1

          Да, autotools это конечно хорошо, как и cmake и тд. Но в конечном итоге все это превращается в длинный список флагов компилятора. И на мой взгляд понимать как это все работает на таком уровне крайне полезно.
          Понятно, что для больших проектов никто не станет писать это все руками, но если смотреть на эту статью, как на обучающую, то использование такого подхода вполне обоснованно. Плюс, чтобы добавить здесь сборку через autotools, автору пришлось бы дописать чёртову кучу объяснений, что да как, и зачем тут эти 5 файлов(точно не помню, сколько нужно для минимальной конфигурации). На мой взгляд статья не об этом.


          1. MooNDeaR
            29.07.2019 16:40

            autotools просто боль и отчаяние) Если бы автор упомянул тут autotools, статья была бы на 95% процентов забита текстом о том, как это всё завести)