Продолжаем наши посиделки с библиотекой растеризации шрифтов FreeType. Из предыдущих моих постов вы уже должны знать какими способами можно выводить текст в OpenGL и как получить изображение символа из шрифта с помощью FreeType.
Наша задача вывести строку каким-нибудь шрифтом в прямоугольную картинку. Для этого у нас есть библиотека FreeType и мы умеем получать с помощью неё изображение символа. Изображения символа для составления строки недостаточно, потому что строка состоит из символов разделенных разным расстоянием, при этом каждый символ имеет разную высоту и позицию по вертикали.
По вертикали у строки есть три основных параметра:
- базовая линия (baseline) — это ноль по вертикали в нашей системе координат. Символы позиционируются по вертикали относительно этой линии
- верхняя граница (ascender) — это верхняя горизонтальная линия, которую (теоретически) не пересекает ни один символ в строке (в нашей системе координат ее вертикальная позиция имеет положительное значение)
- нижняя граница (descender) — это нижняя горизонтальная линия, которую (теоретически) не пересекает ни один символ в строке (ее вертикальная позиция имеет отрицательное значение)
Для нашей задачи нам достаточно лишь базовой линии.
автор картинки: Max Naylor
По горизонтали строка состоит из символов. У каждого символа есть своё посадочное место, его размер и расстояние до следующего символа. Также надо учитывать кернинг (это специальное расстояние между двумя символами, чтобы текст смотрелся более гармонично).
Библиотека FreeType даёт нам возможность узнать о глифе практически все. Есть три источника информации:
FT_Glyph
— содержитadvance.x
(расстояние до следующего символа)FT_BitmapGlyph
— содержитleft
(расстояние от текущей позицииposX
до картинки),top
(расстояние от базовой линии до верхней линии глифа),bitmap
(содержит изображение глифа и его размер)FT_Bitmap
— содержитwidth
(ширина изображения глифа),height
(высота изображения глифа, поле называетсяrows
),buffer
(буфер с изображением)
Для наглядности нарисовал вот такую симпатичную схему:
Оранжевым цветом обозначено содержимое FT_Bitmap.buffer, серым — пространство, занимаемое символом в строке.
Алгоритм решения нашей задачи состоит из двух основных шагов:
- вычислить ширину и высоту строки и место для рисования каждого символа в строке
- выделить память для картинки и нарисовать каждый символ строки по ранее вычисленным позициям
Перед практической частью хотел бы ещё рассказать вкратце про систему счисления библиотеки FreeType.
Для нецелых чисел в ней используется формат 26.6, где 26 бит хранят целую часть числа, а 6 бит — все остальное. Для получения целой части числа его необходимо сдвинуть вправо на шесть бит. Для того, чтобы не терять дробную часть числа, сложение и вычитание надо производить как есть и лишь непосредственно перед использованием целой части сдвигать на шесть бит. Еще есть формат 16.16 (целая часть 16 бит и дробная часть 16 бит). Чего только не придумают, лишь бы float не использовать.
Используемые заголовки, вспомогательные функции и структуры (тело функций ниже по тексту):
// Подключаем FreeType #include <freetype2/ft2build.h> #include FT_FREETYPE_H #include FT_GLYPH_H // Подключаем libpng (http://libpng.sourceforge.net/index.html) #include <png.h> // Поддержка uint8_t, int32_t и т.д. #include <stdint.h> // Поддержка std::string #include <string> // Поддержка std::vector #include <vector> // Получить изображение символа FT_Glyph getGlyph(FT_Face face, uint32_t charcode); // Получить кернинг между двумя символа FT_Pos getKerning(FT_Face face, uint32_t leftCharcode, uint32_t rightCharcode); // Сохранить картинку в PNG void savePNG(uint8_t *image, int32_t width, int32_t height); // Позиция и размер глифа в строке struct Symbol { // Позиция по горизонтали int32_t posX; // Позиция по вертикали (от базовой линии) int32_t posY; // Ширина глифа int32_t width; // Высота глифа int32_t height; FT_Glyph glyph; }; |
Для начала загрузим шрифт из файла arial.ttf:
// Инициализация библиотеки FT_Library ftLibrary = 0; FT_Init_FreeType(&ftLibrary); // Загрузка шрифта arial.ttf из текущей папки FT_Face ftFace = 0; FT_New_Face(ftLibrary, "arial.ttf", 0, &ftFace); // Установим размер символа для рендеринга FT_Set_Pixel_Sizes(ftFace, 100, 0); |
Затем посчитаем позицию каждого символа и размеры строки:
// Выводимая строка const std::string exampleString("FreeType it's amazing!"); // Набор готовых символов std::vector<Symbol> symbols; int32_t left = INT_MAX; int32_t top = INT_MAX; int32_t bottom = INT_MIN; uint32_t prevCharcode = 0; // Позиция текущего символа в формате 26.6 int32_t posX = 0; for (std::size_t i = 0; i < exampleString.size(); ++i) { // Получаем код символа const uint32_t charcode = exampleString[i]; // Получаем глиф для этого символа FT_Glyph glyph = getGlyph(ftFace, charcode); if (!glyph) { // Глифы в шрифте есть не для всех символов continue; } if (prevCharcode) { // Используем кернинг posX += getKerning(ftFace, prevCharcode, charcode); } prevCharcode = charcode; symbols.push_back(Symbol()); Symbol &symb = symbols.back(); FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph) glyph; // Вычисляем горизонтальную позицию символа symb.posX = (posX >> 6) + bitmapGlyph->left; // Вычисляем вертикальную позицию символа относительно базовой // линии. Отрицательные значения - сверху, положительные - снизу. symb.posY = -bitmapGlyph->top; // Ширина символа symb.width = bitmapGlyph->bitmap.width; // Высота символа symb.height = bitmapGlyph->bitmap.rows; // Ссылка на глиф symb.glyph = glyph; // Смещаем позицию текущего символа // (glyph->advance имеет формат 16.16, поэтому для приведения // его к формату 26.6 необходимо сдвинуть число на 10 бит враво) posX += glyph->advance.x >> 10; // Вычисляем самую левую позицию left = std::min(left, symb.posX); // Вычисляем самую верхнюю позицию top = std::min(top, symb.posY); // Вычисляем самую нижнюю позицию bottom = std::max(bottom, symb.posY + symb.height); } for (std::size_t i = 0; i < symbols.size(); ++i) { // Смещаем все символы влево, чтобы строка примыкала к левой части symbols[i].posX -= left; } const Symbol &lastSymbol = symbols.back(); // Ширина строки (изображения) - это крайняя правая // точка последнего символа в строке const int32_t imageW = lastSymbol.posX + lastSymbol.width; // Высота строки (изображения) const int32_t imageH = bottom - top; |
По полученным размерам заполним нашу картинку:
// Выделяем память для картинки std::vector<uint8_t> image(imageW * imageH); for (std::size_t i = 0; i < symbols.size(); ++i) { const Symbol &symb = symbols[i]; FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph) symb.glyph; FT_Bitmap bitmap = bitmapGlyph->bitmap; for (int32_t srcY = 0; srcY < symb.height; ++srcY) { // Координата Y в итоговой картинке const int32_t dstY = symb.posY + srcY - top; for (int32_t srcX = 0; srcX < symb.width; ++srcX) { // Получаем пиксель из изображения символа, // (обязательно используйте pitch вместо width) const uint8_t c = bitmap.buffer[srcX + srcY * bitmap.pitch]; // Если пиксель полностью прозрачный, то пропускаем его if (0 == c) { continue; } // Приводим множество [0..255] к [0..1] для удобства блендинга const float a = c / 255.0f; // Координата X в итоговой картинке const int32_t dstX = symb.posX + srcX; // Вычислим смещение в итоговой картинке uint8_t *dst = image.data() + dstX + dstY * imageW; // Рисуем этот пиксель в итоговую картинку с блендингом dst[0] = uint8_t(a * 255 + (1 - a) * dst[0]); } } } |
Выведем картинку в какой-нибудь простой формат. Например, PNG:
// Сохраняем изображение в PNG формате с прозрачностью savePNG(image.data(), imageW, imageH); |
В конце программы не забудем освободить всю используемую память:
// Освобождаем памяти для глифов for (std::size_t i = 0; i < symbols.size(); ++i) { FT_Done_Glyph(symbols[i].glyph); } // Освобождаем шрифт FT_Done_Face(ftFace); ftFace = 0; // Заканчиваем работу с библиотекой FT_Done_FreeType(ftLibrary); ftLibrary = 0; |
Порадуемся за результат:
Вспомогательные функции:
FT_Glyph getGlyph(FT_Face face, uint32_t charcode) { // Загрузка глифа в face->glyph с отрисовкой FT_Load_Char(face, charcode, FT_LOAD_RENDER); FT_Glyph glyph = 0; // Получаем глиф FT_Get_Glyph(face->glyph, &glyph); return glyph; } FT_Pos getKerning(FT_Face face, uint32_t leftCharcode, uint32_t rightCharcode) { // Получаем индекс левого символа FT_UInt leftIndex = FT_Get_Char_Index(face, leftCharcode); // Получаем индекс правого символа FT_UInt rightIndex = FT_Get_Char_Index(face, rightCharcode); // Здесь будет хранится кернинг в формате 26.6 FT_Vector delta; // Получаем кернинг для двух символов FT_Get_Kerning(face, leftIndex, rightIndex, FT_KERNING_DEFAULT, &delta); return delta.x; } void savePNG(uint8_t *image, int32_t width, int32_t height) { // Файл для сохранения картинки FILE *f = fopen("output.png", "wb"); png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); png_infop info_ptr = png_create_info_struct(png_ptr); png_init_io(png_ptr, f); // Изображение в формате RGBA по 8 бит на // канал и по четыре канала на пиксель png_set_IHDR( png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ptr, info_ptr); // Одна строка в формате RGBA, 4 канала std::vector<uint8_t> row(width * 4); // Сохраняем PNG построчно for (int32_t y = 0; y < height; ++y) { // Преобразуем нашу строку из одноканальной в формат RGBA for (int32_t x = 0; x < width; ++x) { // Цвет одинаковый для всех пикселей 0x202020 row[x * 4 + 0] = 0x20; row[x * 4 + 1] = 0x20; row[x * 4 + 2] = 0x20; // Прозрачность берём из исходных данных row[x * 4 + 3] = image[y * width + x]; } // Сохраняем строку в PNG png_write_row(png_ptr, row.data()); } png_write_end(png_ptr, 0); // Закончили работу, освобождаем ресурсы fclose(f); png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); png_destroy_write_struct(&png_ptr, 0); } |
И весь код целиком под спойлером.
// Подключаем FreeType #include <freetype2/ft2build.h> #include FT_FREETYPE_H #include FT_GLYPH_H // Подключаем libpng (http://libpng.sourceforge.net/index.html) #include <png.h> // Поддержка uint8_t, int32_t и т.д. #include <stdint.h> // Поддержка std::string #include <string> // Поддержка std::vector #include <vector> // Получить изображение символа FT_Glyph getGlyph(FT_Face face, uint32_t charcode); // Получить кернинг между двумя символа FT_Pos getKerning(FT_Face face, uint32_t leftCharcode, uint32_t rightCharcode); // Сохранить картинку в PNG void savePNG(uint8_t *image, int32_t width, int32_t height); // Позиция и размер глифа в строке struct Symbol { // Позиция по горизонтали int32_t posX; // Позиция по вертикали (от базовой линии) int32_t posY; // Ширина глифа int32_t width; // Высота глифа int32_t height; FT_Glyph glyph; }; int main() { // Инициализация библиотеки FT_Library ftLibrary = 0; FT_Init_FreeType(&ftLibrary); // Загрузка шрифта arial.ttf из текущей папки FT_Face ftFace = 0; FT_New_Face(ftLibrary, "arial.ttf", 0, &ftFace); // Установим размер символа для рендеринга FT_Set_Pixel_Sizes(ftFace, 100, 0); // Выводимая строка const std::string exampleString("FreeType it's amazing!"); // Набор готовых символов std::vector<Symbol> symbols; int32_t left = INT_MAX; int32_t top = INT_MAX; int32_t bottom = INT_MIN; uint32_t prevCharcode = 0; // Позиция текущего символа в формате 26.6 int32_t posX = 0; for (std::size_t i = 0; i < exampleString.size(); ++i) { // Получаем код символа const uint32_t charcode = exampleString[i]; // Получаем глиф для этого символа FT_Glyph glyph = getGlyph(ftFace, charcode); if (!glyph) { // Глифы в шрифте есть не для всех символов continue; } if (prevCharcode) { // Используем кернинг posX += getKerning(ftFace, prevCharcode, charcode); } prevCharcode = charcode; symbols.push_back(Symbol()); Symbol &symb = symbols.back(); FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph) glyph; // Вычисляем горизонтальную позицию символа symb.posX = (posX >> 6) + bitmapGlyph->left; // Вычисляем вертикальную позицию символа относительно базовой // линии. Отрицательные значения - сверху, положительные - снизу. symb.posY = -bitmapGlyph->top; // Ширина символа symb.width = bitmapGlyph->bitmap.width; // Высота символа symb.height = bitmapGlyph->bitmap.rows; // Ссылка на глиф symb.glyph = glyph; // Смещаем позицию текущего символа // (glyph->advance имеет формат 16.16, поэтому для приведения // его к формату 26.6 необходимо сдвинуть число на 10 бит враво) posX += glyph->advance.x >> 10; // Вычисляем самую левую позицию left = std::min(left, symb.posX); // Вычисляем самую верхнюю позицию top = std::min(top, symb.posY); // Вычисляем самую нижнюю позицию bottom = std::max(bottom, symb.posY + symb.height); } for (std::size_t i = 0; i < symbols.size(); ++i) { // Смещаем все символы влево, чтобы строка примыкала к левой части symbols[i].posX -= left; } const Symbol &lastSymbol = symbols.back(); // Ширина строки (изображения) - это крайняя правая // точка последнего символа в строке const int32_t imageW = lastSymbol.posX + lastSymbol.width; // Высота строки (изображения) const int32_t imageH = bottom - top; // Выделяем память для картинки std::vector<uint8_t> image(imageW * imageH); for (std::size_t i = 0; i < symbols.size(); ++i) { const Symbol &symb = symbols[i]; FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph) symb.glyph; FT_Bitmap bitmap = bitmapGlyph->bitmap; for (int32_t srcY = 0; srcY < symb.height; ++srcY) { // Координата Y в итоговой картинке const int32_t dstY = symb.posY + srcY - top; for (int32_t srcX = 0; srcX < symb.width; ++srcX) { // Получаем пиксель из изображения символа, // (обязательно используйте pitch вместо width) const uint8_t c = bitmap.buffer[srcX + srcY * bitmap.pitch]; // Если пиксель полностью прозрачный, то пропускаем его if (0 == c) { continue; } // Приводим множество [0..255] к [0..1] для удобства блендинга const float a = c / 255.0f; // Координата X в итоговой картинке const int32_t dstX = symb.posX + srcX; // Вычислим смещение в итоговой картинке uint8_t *dst = image.data() + dstX + dstY * imageW; // Рисуем этот пиксель в итоговую картинку с блендингом dst[0] = uint8_t(a * 255 + (1 - a) * dst[0]); } } } // Сохраняем изображение в PNG формате с прозрачностью savePNG(image.data(), imageW, imageH); // Освобождаем памяти для глифов for (std::size_t i = 0; i < symbols.size(); ++i) { FT_Done_Glyph(symbols[i].glyph); } // Освобождаем шрифт FT_Done_Face(ftFace); ftFace = 0; // Заканчиваем работу с библиотекой FT_Done_FreeType(ftLibrary); ftLibrary = 0; return 0; } FT_Glyph getGlyph(FT_Face face, uint32_t charcode) { // Загрузка глифа в face->glyph с отрисовкой FT_Load_Char(face, charcode, FT_LOAD_RENDER); FT_Glyph glyph = 0; // Получаем глиф FT_Get_Glyph(face->glyph, &glyph); return glyph; } FT_Pos getKerning(FT_Face face, uint32_t leftCharcode, uint32_t rightCharcode) { // Получаем индекс левого символа FT_UInt leftIndex = FT_Get_Char_Index(face, leftCharcode); // Получаем индекс правого символа FT_UInt rightIndex = FT_Get_Char_Index(face, rightCharcode); // Здесь будет хранится кернинг в формате 26.6 FT_Vector delta; // Получаем кернинг для двух символов FT_Get_Kerning(face, leftIndex, rightIndex, FT_KERNING_DEFAULT, &delta); return delta.x; } void savePNG(uint8_t *image, int32_t width, int32_t height) { // Файл для сохранения картинки FILE *f = fopen("output.png", "wb"); png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); png_infop info_ptr = png_create_info_struct(png_ptr); png_init_io(png_ptr, f); // Изображение в формате RGBA по 8 бит на // канал и по четыре канала на пиксель png_set_IHDR( png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ptr, info_ptr); // Одна строка в формате RGBA, 4 канала std::vector<uint8_t> row(width * 4); // Сохраняем PNG построчно for (int32_t y = 0; y < height; ++y) { // Преобразуем нашу строку из одноканальной в формат RGBA for (int32_t x = 0; x < width; ++x) { // Цвет одинаковый для всех пикселей 0x202020 row[x * 4 + 0] = 0x20; row[x * 4 + 1] = 0x20; row[x * 4 + 2] = 0x20; // Прозрачность берём из исходных данных row[x * 4 + 3] = image[y * width + x]; } // Сохраняем строку в PNG png_write_row(png_ptr, row.data()); } png_write_end(png_ptr, 0); // Закончили работу, освобождаем ресурсы fclose(f); png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); png_destroy_write_struct(&png_ptr, 0); } |
#include <ft2build.h> #include FT_FREETYPE_H #include FT_GLYPH_H #include <png.h> #include <stdint.h> FT_Glyph getGlyph(FT_Face face, uint32_t charcode); FT_Pos getKerning(FT_Face face, uint32_t leftCharcode, uint32_t rightCharcode); void savePNG(uint8_t *image, int32_t width, int32_t height); struct Symbol { int32_t posX; int32_t posY; int32_t width; int32_t height; FT_Glyph glyph; }; const size_t MAX_SYMBOLS_COUNT = 128; #define MIN(x, y) ((x) > (y) ? (y) : (x)) #define MAX(x, y) ((x) > (y) ? (x) : (y)) int main() { FT_Library ftLibrary = 0; FT_Init_FreeType(&ftLibrary); FT_Face ftFace = 0; FT_New_Face(ftLibrary, "arial.ttf", 0, &ftFace); FT_Set_Pixel_Sizes(ftFace, 100, 0); const char *exampleString = "FreeType it's amazing!"; const size_t exampleStringLen = strlen(exampleString); struct Symbol symbols[MAX_SYMBOLS_COUNT]; size_t numSymbols = 0; int32_t left = INT_MAX; int32_t top = INT_MAX; int32_t bottom = INT_MIN; uint32_t prevCharcode = 0; size_t i = 0; int32_t posX = 0; for (i = 0; i < exampleStringLen; ++i) { const uint32_t charcode = exampleString[i]; FT_Glyph glyph = getGlyph(ftFace, charcode); if (!glyph) { continue; } if (prevCharcode) { posX += getKerning(ftFace, prevCharcode, charcode); } prevCharcode = charcode; struct Symbol *symb = &(symbols[numSymbols++]); FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph) glyph; symb->posX = (posX >> 6) + bitmapGlyph->left; symb->posY = -bitmapGlyph->top; symb->width = bitmapGlyph->bitmap.width; symb->height = bitmapGlyph->bitmap.rows; symb->glyph = glyph; posX += glyph->advance.x >> 10; left = MIN(left, symb->posX); top = MIN(top, symb->posY); bottom = MAX(bottom, symb->posY + symb->height); } for (i = 0; i < numSymbols; ++i) { symbols[i].posX -= left; } const struct Symbol *lastSymbol = &(symbols[numSymbols - 1]); const int32_t imageW = lastSymbol->posX + lastSymbol->width; const int32_t imageH = bottom - top; uint8_t *image = malloc(imageW * imageH); for (i = 0; i < numSymbols; ++i) { const struct Symbol *symb = symbols + i; FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph) symb->glyph; FT_Bitmap bitmap = bitmapGlyph->bitmap; for (int32_t srcY = 0; srcY < symb->height; ++srcY) { const int32_t dstY = symb->posY + srcY - top; for (int32_t srcX = 0; srcX < symb->width; ++srcX) { const uint8_t c = bitmap.buffer[srcX + srcY * bitmap.pitch]; if (0 == c) { continue; } const float a = c / 255.0f; const int32_t dstX = symb->posX + srcX; uint8_t *dst = image + dstX + dstY * imageW; dst[0] = (uint8_t)(a * 255 + (1 - a) * dst[0]); } } } savePNG(image, imageW, imageH); free(image); for (i = 0; i < numSymbols; ++i) { FT_Done_Glyph(symbols[i].glyph); } FT_Done_Face(ftFace); ftFace = 0; FT_Done_FreeType(ftLibrary); ftLibrary = 0; return 0; } FT_Glyph getGlyph(FT_Face face, uint32_t charcode) { FT_Load_Char(face, charcode, FT_LOAD_RENDER); FT_Glyph glyph = 0; FT_Get_Glyph(face->glyph, &glyph); return glyph; } FT_Pos getKerning(FT_Face face, uint32_t leftCharcode, uint32_t rightCharcode) { FT_UInt leftIndex = FT_Get_Char_Index(face, leftCharcode); FT_UInt rightIndex = FT_Get_Char_Index(face, rightCharcode); FT_Vector delta; FT_Get_Kerning(face, leftIndex, rightIndex, FT_KERNING_DEFAULT, &delta); return delta.x; } void savePNG(uint8_t *image, int32_t width, int32_t height) { FILE *f = fopen("output.png", "wb"); png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); png_infop info_ptr = png_create_info_struct(png_ptr); png_init_io(png_ptr, f); png_set_IHDR( png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ptr, info_ptr); uint8_t *row = malloc(width * 4); for (int32_t y = 0; y < height; ++y) { for (int32_t x = 0; x < width; ++x) { row[x * 4 + 0] = 0xc0; row[x * 4 + 1] = 0xc0; row[x * 4 + 2] = 0xc0; row[x * 4 + 3] = image[y * width + x]; } png_write_row(png_ptr, row); } free(row); png_write_end(png_ptr, 0); png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); png_destroy_write_struct(&png_ptr, 0); fclose(f); } |
UPD: Если вам необходимо получить данные о символах в векторном виде, рекомендую ознамиться с этим примером, особенно со структурой FT_Outline_Funcs
и функцией FT_Outline_Decompose()
.