О библиотеке сжатия zlib

Сегодня я хочу рассказать о замечательной библиотеке zlib.

Мне, как программисту игрушек, раньше приходилось решать проблему быстрой распаковки ресурсов при загрузке игровых уровней. Самое неприятное, что может принести игра пользователю, кроме падения и ошибок, это заставить его долго ожидать загрузки уровня (хотя 10-15 лет назад загрузка одного уровня по 5-10 минут было совершенно нормальным явлением).

Впервые в жизни мне пришлось столкнуться с компрессией данных в игре «Петрович и все, все, все», которую мы делали в Trickster Games для Андрея Бильжо.

Все изображения в игре используют от двух до десяти цветов (а музыка восемь нот, ага). Вот пример:

Петрович и инопланетянин

На этом скриншоте присутствуют: Петрович (два цвета), Инопланетянин aka «логотип Trickster Games» (2 цвета), фон (4 цвета). Каждое изображение может занимать от 256 килобайт до 4-х мегабайт (потому что DDS). Загрузка одного уровня, состоявшего из 50-200 таких изображений, занимала от 10 до 45 секунд. Разумеется, это нас в корне не устраивало.

После мозгового штурма на эту тему было найдено решение — использовать алгоритм RLE. Этот алгоритм позволяет очень эффективно сжимать длинную цепочку одинаковых бит, что наглядно показано в википедии: блок данных «AAAAAAAAAAAAAAABBBBBC» мы записываем вот так «15A5BC». Экономия, как говорится, налицо.

Интерфейс для работы с этим алгоритмом был выбран очень простой. Как оказалось впоследствии, такой интерфейс с мелкими различиями предоставляют практически все библиотеки сжатия без потерь.

/* Сжимает данные из буфера src в dst.
 * src - входной буфер
 * srcSize - размер входного буфера в байтах
 * dst - выходной буфер
 * dstSize - перед вызовом должен быть установлен в размер буфера dst, 
 *     после вызова туда записывается реальное количество сжатых байт
 *
 * Функция возвращает true, если данные были сжаты успешно 
 * (это значит, что уровень сжатия меньше или равен единице)
 */
bool compressRLE( void *dst, int *dstSize, const void *src, int srcSize);
 
/* Расжимает данные из буфера src в dst.
 * src - входной буфер
 * srcSize - размер входного буфера в байтах
 * dst - выходной буфер
 * dstSize - перед вызовом должен быть установлен в размер буфера dst, 
 *     после вызова туда записывается реальное количество распакованных байт
 *
 * Функция возвращает true, если данные были расжаты успешно 
 */
bool uncompressRLE( void *dst, int *dstSize, const void *src, int dstSize);

Разумеется, я не могу показать нашу реализацию алгоритма без разрешения начальства, но, думаю, вы справитесь сами. Коэффициент сжатия на наших данных составлял где-то 0.01 процента (то есть тысяча байт сжимались в десять).

..И тут мы плавно переходим к zlib.

Библиотека zlib используется практически везде, где есть CPU: мобильные телефоны, компьютеры, микроволновки, современные автомобили, телевизоры — список можно продолжать бесконечно. Кто знает, может она сейчас трудится на Марсе внутри ровера Curiosity, сжимая, передаваемые на Землю, данные.

Библиотека может сжимать блок данных или поток. Рассмотрим вкратце сжатие блока данных. Описание метода вот такое:

// Сжимает данные методом deflate из буфера src в dst
// dest - выходной буфер
// destLen - размер выходного буфера (после удачного сжатия 
//     сюда сохраняется размер сжатых данных)
// source - входной буфер
// sourceLen - размер входного буфера
// Возвращает Z_OK в случае удачи, записывает размер сжатых данных в destLen
ZEXTERN int ZEXPORT compress OF((Bytef *dest,   uLongf *destLen,
                                 const Bytef *source, uLong sourceLen));

Пример вызова этой функции. Он ничего полезного не делает, просто для наглядности.

#include <zlib.h>
#include <memory.h>
#include <stdio.h>
 
#define SRC_SIZE 512
#define DST_SIZE 512
 
int main()
{
    // Входной буфер размером SRC_SIZE
    unsigned char source[SRC_SIZE];
    // Тут мы будем хранить размер входного буфера 
    // (переменная просто для наглядности)
    const unsigned long sourceLen = SRC_SIZE;
    // Выходной буфер
    unsigned char dest[DST_SIZE];
    // Размер выходного буфера
    unsigned long destLen = DST_SIZE;
 
    // Будем сжимать буфер забитый нулями
    memset(source, 0, SRC_SIZE * sizeof(source[0]));
 
    if (Z_OK == compress(dest, &destLen, source, sourceLen))
    {
        // Все успешно
        printf("Compress ratio: %.2f\n", destLen/(float) sourceLen);
    }
    else
    {
        // Тут можете разобрать код ошибки и вывести более подробную информацию
        printf("Compress failed\n");
    }
 
    return 0;
}

Пример сжатия потока данных вы можете посмотреть в маленькой утилитке zpipe.

Есть вопросы? Спрашивайте, не стесняйтесь.

P.S. Если вы дочитали досюда, рекомендую изучить libdeflate.

Продление водительских прав за один час в Петербурге

Приспичило мне на днях водительские права продлить в воскресенье с утра. Рассказываю об этой «сложнейшей» бюрократической процедуре, если кому понадобится..

Водительские права
Первым делом нужна медицинская справка. Да-да это та самая бумажка, которая кладется в пыльный угол сразу после получения прав. По слухам ее можно купить за 2000 рублей и в срок от двух дней до недели. А можно получить совершенно легально за 20 минут и за 1200 рублей. Поддерживаем коррупцию — ждем пока справку доставят домой знакомые подельники, хотим жить честно — тратим 20 минут. «Ну ты понел» (;

Для получения справки необходим российский паспорт, водительские права и военный билет (тут я рассказываю для мужчин). Процесс получения справки у меня выглядел так: пришел на медкомиссию, заплатил денежку (200 рублей — электрокардиограмма, 100 рублей — фотография, 800 — сама справка). Сделал тут же ЭКГ, затем кабинет «глазника» (офтальмолог), после кабинет психиатра-нарколога (стандартные вопросы на тему «бухаешь? колешься? как часто бухаешь?»), затем хирург и прочие врачи. Процедура занимает реально 20 минут.

Мушкетеры на мотоциклеПосле получения справки поехал в Единый Центр Документов (находится на Красного Текстильщика). Сдал паспорт, права и справку в одно окошко, заплатил там же 1500 рублей, прошел в другой кабинет — сделал фото для прав, затем обратно в то же окошко, откуда меня послали к терминалу (800 рублей права и 60 — комиссия терминала), после этого отправился к третьему окошку, где мне выдали новенькие права.

Итог: чистого времени 50 минут, потрачено 3560 рублей. Повторяю — все это было проделано в воскресенье утром, ни пробок, ни людей.

Кстати тем, кто получал права не в Петербурге, возможно, вам надо взять свидетельство об окончании автошколы.

Источник прав Путина, источник фотографии мушкетеров.

Формат файлов в папке .git/objects

- А не запилить ли мне свой .git с преферансом и блудницами, — подумал я. И запилил! По крайней мере, малюсенькую часть, а именно папку .git/objects. Формат хранения обычных файлов простой — берем sha1 от данных этого файла, создаем папку из первых двух символов этого хеша, а внутрь заливаем пакованные данные с именем оставшегося хеша.

Вот такой скрипт получился на питоне. Зачем делал — не спрашивайте, не знаю. :)

import os
import hashlib
import zlib
 
# Какие папки сканируем
directories = ( "D:\\resources", )
# Какие файлы учитываем
extensions = ( '.png', '.wav', '.jpg', '.dds' )
 
# Количество повторов
numDuplicates = 0
# Общее количество обработанных файлов
numFiles = 0
 
for directory in directories:
    for root, dirs, files in os.walk(directory):
        for filename in files:
            basename, ext = os.path.splitext(filename)
            full = os.path.join(root, filename)
            # Пропустим неизвестный файл
            if ext not in extensions:
                print 'Skip %s' % full
                continue
 
            # Понеслась!
            print 'Processing %s' % full,
 
            # Возьмем хеш от содержимого файлов
            data = open(full, 'rb').read()
            hash = hashlib.sha1(data).hexdigest()
            print '.. done'
 
            # Название папки - первые два символа от хеша
            prefix = hash[:2]
            # Название файла - остальные символы хеша
            postfix = hash[2:]
 
            # Создадим подпапку в папочке objects
            dirname = os.path.join('objects', prefix)
            if not os.path.isdir(dirname):
                os.makedirs(dirname)
 
            # Выходной файл с данными
            filename = os.path.join(dirname, postfix)
            if not os.path.isfile(filename):
                # Пишем данные, предварительно запаковав их с помощью zlib
                open(filename, 'wb').write(zlib.compress(data))
            else:
                # Такой файл уже есть, найден дубликат, куда смотрел дизайнер?!
                numDuplicates += 1
            numFiles += 1
 
# Краткий отчет
print 'Total: %d, duplicates: %d' % (numFiles, numDuplicates)
Блог Евгения Жирнова