Описание формата ZIP файла. Часть 2 (расширение ZIP64, поддержка больших файлов)

Давным-давно, когда создавался формат ZIP и всем хватало 640KB памяти, никто не задумывался, что файл может быть >4GB и содержать в себе больше 65535 элементов, поэтому поддержка таких объемов прикручена костылями и называется ZIP64. До сих пор этот формат плохо понимают Far Manager, 7-Zip и проводник седьмой винды (а говорят, что в Microsoft берут только гениальных программистов, возьмите меня, я умнее, чем выгляжу).

Расширение ZIP64 добавляет две основные фичи:

  • структура ZIP64 End Of Central Directory Record (EOCD64) с 64-битными полями и ZIP64 End Of Central Directory Locator для поиска структуры EOCD64
  • если значения некоторых полей Central Directory Header и Local File Header превышают максимальный, то эти значения записываются в Extra Field этих структур в специальном формате ZIP64 Extended Extra Field

А теперь подробнее про эти нововведения.

ZIP64 End Of Central Directory Locator

Используется для поиска структуры EOCD64 и находится сразу перед стандартным End Of Central Directory Record.

EOCD64Locator

struct EOCD64Locator
{
    // Обязательная сигнатура, равна 0x07064b50
    uint32_t signature;
    // Номер диска для поиска EOCD64
    uint32_t diskNumber;
    // Смещение от начала файла до EOCD64
    uint64_t eocd64Offset;
    // Количество дисков
    uint32_t totalDiskCount;
};

Определить, что наш ZIP файл содержит такую запись просто: проверьте поля стандартного End of central directory record. Если 16-битные поля равны 0xFFFF или 32-битные — 0xFFFFFFFF, значит такая запись должна быть.

ZIP64 End Of Central Directory Record

Эта структура представляет собой 64-битную версию стандартного End Of Central Directory Record. Находится по адресу EOCD64Locator.eocd64Offset.

EOCD64

struct EOCD64
{
    // Обязательная сигнатура, равна 0x06064b50
    uint32_t signature;
    // Размер записи EOCD64
    uint64_t eocd64Size;
    // Версия для создания
    uint16_t versionMadeBy;
    // Версия для распаковки
    uint16_t versionToExtract;
    // Номер текущего диска
    uint32_t diskNumber;
    // Номер диска для поиска Central Directory
    uint32_t startDiskNumber;
    // Количество записей в Central Directory
    uint64_t numberCentralDirectoryRecord;
    // Всего записей в Central Directory
    uint64_t totalCentralDirectoryRecord;
    // Размер Central Directory
    uint64_t sizeOfCentralDirectory;
    // Смещение Central Directory
    uint64_t centralDirectoryOffset;
    // zip64 extensible data sector (переменной длины)
    uint8_t *dataSector;
};

Extra Field

Если вы внимательно читали мои предыдущие заметки на тему ZIP формата (описание формата ZIP, чтение ZIP файла, запись ZIP файла), то могли заметить, что у записей типа Local File Header и Central directory file header есть поля под названием extraField.

В это поле записываются расширенные данные, о которых не было известно на момент создания первой версии формата ZIP. В этом поле могут содержаться разные типы данных и реализуется это последовательностью структур типа ExtraFieldRecord:

ExtraFieldRecord

struct ExtraFieldRecord
{
    // Заголовок (для ZIP64 extended information равен 0x0001)
    uint16_t headerId;
    // Размер данных
    uint16_t dataSize;
    // Какие-то данные размером dataSize
    uint8_t *data;
};

Данные ExtraFieldRecord.data для ZIP64 extended information выглядят так:

ZIP64ExtendInformation
struct ZIP64ExtendInformation
{
    // Размер несжатых данных
    uint64_t uncompressedSize;
    // Размер сжатых данных
    uint64_t compressedSize;
    // Смещение Local File Header от начала файла
    uint64_t localFileHeaderOffset;
    // Номер диска для поиска
    uint32_t diskNumber;
};

Обратите внимание — в это записи важен лишь порядок полей. Все поля необязательно будут присутствовать.

Например, структура Central directory file header будет содержать 0xFFFFFFFF только в поле localFileHeaderOffset, значит в Extra Field будет лежать только значение localFileHeaderOffset, а остальные поля будут отсутствовать.

Порядок действий

  • определить наличие EOCD64 проверкой полей EOCD на переполнение и считать сначала EOCD64Locator, затем EOCD64
  • для каждой структуры типа Central directory file header и Local File Header определить поля на переполнение и, если таковые присутствуют, то считать их значения из ExtraField

Вот, в принципе, и вся наука. Вы не подумайте, что я нашел эту информацию тщательным изучением ZIP файлов, просто читайте спецификацию ZIP формата (на английском).

Используем FreeType для вывода строки

Продолжаем наши посиделки с библиотекой растеризации шрифтов FreeType. Из предыдущих моих постов вы уже должны знать какими способами можно выводить текст в OpenGL и как получить изображение символа из шрифта с помощью FreeType.

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

По вертикали у строки есть три основных параметра:

  • базовая линия (baseline) — это ноль по вертикали в нашей системе координат. Символы позиционируются по вертикали относительно этой линии
  • верхняя граница (ascender) — это верхняя горизонтальная линия, которую (теоретически) не пересекает ни один символ в строке (в нашей системе координат ее вертикальная позиция имеет положительное значение)
  • нижняя граница (descender) — это нижняя горизонтальная линия, которую (теоретически) не пересекает ни один символ в строке (ее вертикальная позиция имеет отрицательное значение)

Для нашей задачи нам достаточно лишь базовой линии.

Sphinx
автор картинки: Max Naylor

По горизонтали строка состоит из символов. У каждого символа есть своё посадочное место, его размер и расстояние до следующего символа. Также надо учитывать кернинг (это специальное расстояние между двумя символами, чтобы текст смотрелся более гармонично).

Библиотека FreeType даёт нам возможность узнать о глифе практически все. Есть три источника информации:

  • FT_Glyph — содержит advance.x (расстояние до следующего символа)
  • FT_BitmapGlyph — содержит left (расстояние от текущей позиции posX до картинки), top (расстояние от базовой линии до верхней линии глифа), bitmap (содержит изображение глифа и его размер)
  • FT_Bitmap — содержит width (ширина изображения глифа), height (высота изображения глифа, поле называется rows), buffer (буфер с изображением)

Для наглядности нарисовал вот такую симпатичную схему:

ascender,descender,baseline,advance.x,width,height,top,left

Оранжевым цветом обозначено содержимое FT_Bitmap.buffer, серым — пространство, занимаемое символом в строке.

Алгоритм решения нашей задачи состоит из двух основных шагов:

  1. вычислить ширину и высоту строки и место для рисования каждого символа в строке
  2. выделить память для картинки и нарисовать каждый символ строки по ранее вычисленным позициям

Перед практической частью хотел бы ещё рассказать вкратце про систему счисления библиотеки 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;

Порадуемся за результат:

freetype

Вспомогательные функции:

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);
}

И весь код целиком под спойлером.

Исходный код C++ (с комментариями)

// Подключаем 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().

Тестовое задание для программиста в Trickster Games

Давным-давно, в далеком 2007 году, я работал в славной компании «Trickster Games». Сейчас ее уже нет, но она была известна как разработчик игр для детей и квеста «Петрович и все, все, все..».

Так вот, в те далекие времена мы придумали тестовое задание для программиста:

#include <SomeStream.h>
 
void main()
{
    SomeStream stream;
    stream.info() << "Привет, мир!";
}

Задача: реализовать класс SomeStream таким образом, чтобы после выполнения main() в std::cout было выведено «Привет, мир!\n» (без кавычек). То есть добавить перевод строки в конец фразы.

Тогда никто из кандидатов не смог его решить. Может вам повезет? (:

Вот вам подсказка: крутая система логирования.

Использование библиотеки FreeType для растеризации символов

Всем поклонникам OpenSource приложений известна библиотека растеризации шрифтов FreeType. Ее используют практически все графические приложения под GNU/Linux. Попробуем и мы освоить этот несложный навык.

Сперва про используемые типы:

  • FT_Library — представляет собой структуру для иницизации библиотеки
  • FT_Face — шрифт, который загружается при помощи FT_Library
  • FT_Glyph — глиф шрифта, который создается при помощи FT_Face

Таким образом, для отображения символа нам надо создать объект типа FT_Library, затем загрузить шрифт в объект типа FT_Face, после этого рисовать символы при помощи FT_Glyph.

Символ из шрифта можно извлечь двумя способами:

  1. Получить информацию для его рисования кривыми
  2. Получить готовое изображение

Первый способ нам неинтересен, будем использовать сразу второй. Поехали (проверка ошибок убрана для упрощения кода):

#include <ft2build.h>
#include FT_FREETYPE_H
 
int main()
{
    // Библиотека FreeType
    FT_Library library = 0;
 
    // Инициализация библиотеки
    FT_Init_FreeType(&library);
 
    // Шрифт
    FT_Face face = 0;
 
    // Загрузка шрифта
    FT_New_Face(library, "tahoma.ttf", 0, &face);
 
    // Установка размера пикселя
    FT_Set_Pixel_Sizes(face, 24, 12);
 
    // Код символа (юникод)
    const FT_ULong charCode = L'W';
 
    // Загрузка глифа из шрифта с его отрисовкой
    FT_Load_Char(face, charCode, FT_LOAD_RENDER);
 
    // Получение готового к использованию глифа
    FT_GlyphSlot glyph = face->glyph;
 
    // Получение размеров глифа
    const int width = glyph->bitmap.width;
    const int height = glyph->bitmap.rows;
    const int pitch = glyph->bitmap.pitch;
 
    // Вывод символа в консоли
    for (int y = 0; y < height; ++y)
    {
        for (int x = 0; x < width; ++x)
        {
            // Получение прозрачности точки (x, y)
            const int a = glyph->bitmap.buffer[y * pitch + x];
 
            if (a > 127)
            {
                printf("*");
            }
            else
            {
                printf(" ");
            }
        }
        printf("\n");
    }
 
    // Удаление шрифта
    FT_Done_Face(face);
    face = 0;
 
    // Удаление библиотеки
    FT_Done_FreeType(library);
    library = 0;
}

Консоль:

**        ***      ***
 **      ****      **
 **      ****     ***
  **    **  **    **
  **    *   **   ***
   **  **    **  **
   **  *      * ***
    ****      ****
    ***        ***

Отличная работа, но для вывода слова из нескольких букв простого изображения символа недостаточно. Необходимо иметь следующую информацию:

  • положение горизонтальной базовой линии (например, «W» — внизу, «y» — примерно середина высоты)
  • межстрочный интервал (если вы хотите вывести многострочный текст)
  • кернинг (расстояние между различными буквами)

Подробнее об этом в следующей части.

Запись ZIP файла

Запись ZIP файла практически операция чтения наоборот:

  • для каждого файла в архиве необходимо записать LocalFileHeader, затем содержимое файла, предварительно посчитав контрольную сумму
  • для каждой записи LocalFileHeader, записываем CentralDirectoryFileHeader
  • записываем EOCD

Теперь подробнее по пунктам

Запись Local File Header

Для каждого файла в архиве существуют две записи — это LocalFileHeader и CentralDirectoryFileHeader. При записи необходимо создавать эти структуры одновременно, только LocalFileHeader записывать сразу, а для CentralDirectoryFileHeader сохранять информацию во временный список для использования ее потом. Также для данных необходимо посчитать контрольную сумму с помощью функции crc32(), которую предоставляет библиотека zlib.

Пример кода

struct LocalFileHeader
{
    uint16_t versionToExtract;
    uint16_t generalPurposeBitFlag;
    uint16_t compressionMethod;
    uint16_t modificationTime;
    uint16_t modificationDate;
    uint32_t crc32;
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t filenameLength;
    uint16_t extraFieldLength;
 
} __attribute__((packed));
 
struct FileInfo
{
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t compressionMethod;
    uint32_t crc32;
    uint32_t offset;
};
 
// Буфер для чтения
std::vector<uint8_t> readBuffer;
// Буфер для сжатых данных
std::vector<uint8_t> dataBuffer;
// Информация для создания Central directory file header
std::vector<FileInfo> fileInfoList;
// Список файлов
std::vector<std::string> filenames;
 
for (size_t i = 0; i < filenames.size(); ++i)
{
    const std::string filename = filenames[i];
 
    // Открываем файл для чтения
    std::ifstream f(filename.c_str(), std::ios::binary | std::ios::in);
 
    // Структура типа Local File Header
    LocalFileHeader lfh;
    memset(&lfh, 0, sizeof(lfh));
 
    // Узнаем размер файла
    f.seekg(0, f.end);
    lfh.uncompressedSize = f.tellg();
    f.seekg(0, f.beg);
 
    // Считаем весь файл целиком в readBuffer
    readBuffer.resize(lfh.uncompressedSize);
    f.read((char *) readBuffer.data(), lfh.uncompressedSize);
 
    // Считаем контрольную сумму
    lfh.crc32 = crc32(0, readBuffer.data(), lfh.uncompressedSize);
 
    // Выделим память для сжатых данных
    dataBuffer.resize(lfh.uncompressedSize);
 
    // Структура для сжатия данных
    z_stream zStream;
    memset(&zStream, 0, sizeof(zStream));
    deflateInit2(
        &zStream, 
        Z_BEST_SPEED,
        Z_DEFLATED,
        -MAX_WBITS,
        8,
        Z_DEFAULT_STRATEGY);
 
    // Сжимаем данные
    zStream.avail_in = lfh.uncompressedSize;
    zStream.next_in = readBuffer.data();
    zStream.avail_out = lfh.uncompressedSize;
    zStream.next_out = dataBuffer.data();
    deflate(&zStream, Z_FINISH);
 
    // Размер сжатых данных
    lfh.compressedSize = zStream.total_out;
    lfh.compressionMethod = Z_DEFLATED;
 
    // Очистка
    deflateEnd(&zStream);
 
    // Длина имени файла
    lfh.filenameLength = filename.size();
 
    // Сохраним смещение к записи Local File Header внутри архива
    const uint32_t lfhOffset = os.tellp();
 
    // Запишем сигнатуру Local File Header
    const uint32_t signature = 0x04034b50;
    os.write((char *) &signature, sizeof(signature));
 
    // Запишем Local File Header
    os.write((char *) &lfh, sizeof(lfh));
    // Запишем имя файла
    os.write(filename.c_str(), filename.size());
    // Запишем данные
    os.write((char *) dataBuffer.data(), lfh.compressedSize);
 
    // Сохраним все данные для Central directory file header
    FileInfo fileInfo;
    fileInfo.compressedSize = lfh.compressedSize;
    fileInfo.uncompressedSize = lfh.uncompressedSize;
    fileInfo.compressionMethod = lfh.compressionMethod;
    fileInfo.crc32 = lfh.crc32;
    fileInfo.offset = lfhOffset;
    fileInfoList.push_back(fileInfo);
}

Запись Central directory file header

Используя данные, полученные на предыдущем шаге, записываем для каждого файла структуру типа CentralDirectoryFileHeader. Предварительно необходимо сохранить смещение в файле для финальной записи EOCD.

Пример кода

struct CentralDirectoryFileHeader
{
    uint16_t versionMadeBy;
    uint16_t versionToExtract;
    uint16_t generalPurposeBitFlag;
    uint16_t compressionMethod;
    uint16_t modificationTime;
    uint16_t modificationDate;
    uint32_t crc32;
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t filenameLength;
    uint16_t extraFieldLength;
    uint16_t fileCommentLength;
    uint16_t diskNumber;
    uint16_t internalFileAttributes;
    uint32_t externalFileAttributes;
    uint32_t localFileHeaderOffset;
 
} __attribute__((packed));
 
// Смещение первой записи для EOCD    
const uint32_t firstOffsetCDFH = os.tellp();
 
for (int i = 0; i < filenames.size(); ++i)
{
    const std::string &filename = filenames[i];
    const FileInfo &fileInfo = fileInfoList[i];
    CentralDirectoryFileHeader cdfh;
    memset(&cdfh, 0, sizeof(cdfh));
 
    cdfh.compressedSize = fileInfo.compressedSize;
    cdfh.uncompressedSize = fileInfo.uncompressedSize;
    cdfh.compressionMethod = fileInfo.compressionMethod;
    cdfh.crc32 = fileInfo.crc32;
    cdfh.localFileHeaderOffset = fileInfo.offset;
    cdfh.filenameLength = filename.size();
 
    // Запишем сигнатуру
    const uint32_t signature = 0x02014b50;
    os.write((char *) &signature, sizeof(signature));
 
    // Запишем структуру
    os.write((char *) &cdfh, sizeof(cdfh));
 
    // Имя файла
    os.write(filename.c_str(), cdfh.filenameLength);
}
 
// Посчитаем размер данных для следующего шага
const uint32_t lastOffsetCDFH = os.tellp();

Запись EOCD

Самый простой этап — сформировать и записать структуру типа EOCD.

Пример кода

EOCD eocd;
memset(&eocd, 0, sizeof(eocd));
eocd.centralDirectoryOffset = firstOffsetCDFH;
eocd.numberCentralDirectoryRecord = filenames.size();
eocd.totalCentralDirectoryRecord = filenames.size();
eocd.sizeOfCentralDirectory = lastOffsetCDFH - firstOffsetCDFH;
 
// Пишем сигнатуру
const uint32_t signature = 0x06054b50;
os.write((char *) &signature, sizeof(signature));
 
// Пишем EOCD
os.write((char *) &eocd, sizeof(eocd));

Замечания по коду:

  • ничего не раскрыто по поводу структуру DataDescriptor (ни разу не сталкивался)
  • иногда размер сжатых данных превышает размер несжатых, поэтому выделяйте память по науке (смотри документацию zlib)
  • если размер данных очень большой, то придется воспользоваться структурой DataDescriptor
Блог Евгения Жирнова