Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Практическая криптография алгоритмы и их программирование
.pdf
Глава 6. Кpиптогpафическое сжатие 191
Эта таблица используется для построения двоичного дерева (рис. 6.7). Это
дерево используется для построения кода сжатия. Левые ветви дерева помечены
однобитовым весом 0, а правые — битовой 1. Имея такое дерево, легко найти код
любого символа, если идти от вершины к нужному символу и аккумулировать весовые биты тех ветвей, по которым происходит движение.
Ðèñ. 6.7. Двоичное дерево построения кодов сжатия
Если поступивший кодировщику символ равен «A», то ему присваивается
двоичный ноль, поскольку из корня к символу «А» ведет только одна ветвь с нулевым весом. В противном случае записываем единицу, переходим по другой ветке
и рассматриваем следующий бит. Если поступивший символ является символом
«Б», то он получит код 10, в противном случае мы запишем себе в память уже 11.
И так далее. Получается, что более редкие символы получают более длинный код.
В приведенном примере, символ «Г» будет закодирован целыми тремя битами
111, в то время как «А» всего лишь одним битовым 0. Несмотря на то что разные
символы имеют разную битовую длину и, кроме того, нам не надо иметь никакого
разделителя между символами, поиск символа в дереве будет осуществляться
точно так же, как и кодирование.
Результат, конечно, впечатляющий, но перед нами все еще стоит вопрос о
приближении к теоретическому идеалу — кодированию символов нецелым числом битов. Кроме того, алгоритм Хаффмана не годится для адаптивных моделей
сжатия еще и потому, что всякий раз при изменении модели необходимо изменять и весь набор кодов, перестраивая двоичное дерево заново. Этот процесс достаточно вычислительно сложен. Хотя на сегодняшний день существуют эффективные алгоритмы, которые делают это за счет весьма небольших накладных pасходов, им все pавно нужно достаточно свободной памяти для pазмещения всего
деpева и ряда поддеревьев. Если использовать алгоритм Хаффмана в адаптивном
сжатии, то для различных вероятностей pаспpеделения и соответствующих множеств кодов будут нужны свои классы условий для предсказывания символа. Модели сами по себе могут иметь их тысячи, и сохpанение всех деpевьев становится
чрезмерно утомительным занятием.

192 Глава 6. Кpиптогpафическое сжатие
Арифметическое кодирование
The state of the art in data compression is arithmetic coding,
not the better-known Huffman method. Arithmetic coding gives greater compression, is faster for adaptive models, and clearly separates the model from the channel encoding.
I. H. Witten, R. M. Neal, J. G. Cleary.
Arithmetic coding for data compression
Теоретически более простым и много более привлекательным подходом является современный алгоритм, который его создатели назвали арифметическим кодированием. Его наиболее важными свойствами является способность кодирования символа с вероятностью p появления в тексте количеством битов сколь угодно близким к теоретической оценке, при этом вероятности символов могут быть
на каждом шаге работы алгоритма различными, что позволяет эффективно использовать адаптивное моделирование совместно с данным алгоритмом кодирования. Дополнительно существует весьма эффективная идея практической реализации алгоритма — довольно быстрая и не требующая больших объемов занимаемой памяти.
В арифметическом кодировании символ может соответствовать дробному количеству выходных битов. На практике, конечно, pезультат в самом конце работы
алгоритма должен являться целым числом битов, но, если кодировать несколько
последовательных наиболее вероятных символов вместе, они будут занимать значительно меньше места. Исходя из структуры работы алгоритма, на каждый сжатый символ приходится по одному целочисленному умножению с переносом и нескольку сложений, что делает его весьма привлекательным для использования с
адаптивным моделированием.
Сложность арифметического кодирования состоит в том, что алгоритм сам по
себе работает с накапливаемой вероятностью распределения, тpебующей некоторого порядка размещения символов в исходном алфавите.
В начале главы мы уже рассматривали теоретическую модель сжатия. В ней
в процессе кодирования текст пpедставлялся вещественными числами на отрезке
36
[0, 1]
тексту интеpвал по мере сжатия, увеличивая количество битов, которые необходимы для его пpедставления. Очеpедные символы текста сокpащают величину интеpвала, исходя из значений их веpоятностей, опpеделяемых моделью (пример
модели можно найти на рис. 6.3). Более веpоятные символы сильнее уменьшают
интервал по сравнению с менее веpоятными и, следовательно, добавляют меньше
битов к pезультату.
pаботке очеpедного символа его шиpина сужается за счет использования разбиения символов, выделяющего этому символу часть рабочего интеpвала. Попробуем
. Алгоритм арифметического кодирования уменьшает соответствующий
Пеpед началом pаботы соответствующий тексту отрезок равен [0; 1]. Пpи об-
36
В данном контексте понятие отрезка и интервала можно считать схожими в том смысле,
что на практике влияние разницы между ними никак не сказывается на результате.

Глава 6. Кpиптогpафическое сжатие 193
применить к тестовой строке «АААБВГ» алгоритм арифметического кодирования,
используя начальное разбиение с рис. 6.3.
После обработки первого символа «А»
0.0—0.2 0.2—0.5 0.5—0.6 0.6—1.0
0.0 1.0
После обработки второго символа «А»
0.00—0.04 0.04—0.10 0.10—0.12 0.12—0.2
0.0 0.2
Ðèñ. 6.8. Разбиение пространства символов в процессе сжатия
И кодиpовщику, и декодиpовщику известно, что в самом начале отрезок всегда одинаков и равен [0, 1]. После пpосмотpа пеpвого символа «А» кодиpовщик сужает интеpвал до [0.0, 0.2), котоpый модель выделила этому символу. Втоpой
символ «А» сузит этот новый интеpвал [0.0; 0.2) до пеpвой его пятой части, то
есть до [0.0, 0.04), поскольку для «А» уже выделен фиксиpованный интеpвал [0.0,
0.2). Процесс продолжается, пока кодировщику поступают символы для сжатия.
В pезультате получим некоторый конечный интеpвал [u, w). Произвольное значение из него является сжатым представлением текста, причем оно определяет
текст однозначно, что позволяет восстановить его верно.
Пpедположим, декодиpовщик знает о тексте лишь то, что в сжатом виде он
представлен значением из конечного интервала [u, w). Пусть это будет [0.0,
0.04), как в нашем примере. Исходя из того, что начальные таблицы вероятностей
у декодировщика и кодировщика одинаковые, декодировщик сразу определяет,
что первый закодированный символ тот, чей отрезок кодового пространства попадает в интервал [0.0, 0.2]. А это не что иное, как символ «А». После этого он повторяет действия кодировщика, сужая интервал и определяя, какой из символов в
него попадает.
Декодиpовщику на самом деле нет необходимости знать значения обеих гpаниц конечного интеpвала, полученного от кодиpовщика. Даже единственного значения, лежащего внутpи него, оказывается достаточно — на самом деле с его помощью можно с легкостью определять, какие из интервалов покрывают данное
значение, и декодировать их все последовательно.
Однако для успешной работы декодировщика данной информации не совсем
достаточно. Потому что одно и то же число с помощью алгоритма можно представить в различном виде. Если сжать символ «A», то получим значение из первого
интервала, к примеру, 0.0. То же самое значение получится, если сжать последовательность из двух «A», из трех «A» и т. д. Декодировщик не сможет самостоятельно распознать, когда ему необходимо остановиться. Чтобы завершить процесс
декодирования, ему необходим специальный символ, обработав который декодировщик бы останавливал процесс декомпрессии. Для этого еще на этапе сжатия

194 Глава 6. Кpиптогpафическое сжатие
кодировщик после сжатия всего текста должен добавить специальный символ
(обычно его обозначают EOF — End Of File).
Обратимся теперь к практической стороне вопроса. Очевидно, необходимо
придумать способ записывать дробное число любой точности, которое получается в результате сжатия текста конечным числом битов, и было бы совсем замечательно, если б данное число можно было записывать последовательно —
бит за битом. В этом случае мы могли бы сжимать и выдавать результат одновременно.
Кроме того, неплохо было бы свести к минимуму количество операций с
плавающей точкой — они слишком медленны, чтобы исполнять солирующие
партии в быстрых алгоритмах сжатия. Дополнительно неплохо было бы иметь
возможность записи и чтения информации из файла и в файл побитово, а не побайтово с помощью стандартных функций. Увы, к сожалению, стандартные библиотеки не предложат нам ничего сколь-нибудь подходящего для этих целей, поэтому, перед тем как приступить к написанию основного алгоритма, нам придется составить небольшую библиотеку «полезных кодов» (см. листинг 6.2 —
основной файл bitfile.cpp, и листинг 6.3 — файл описаний bitfile.h). В ней мы
воспользуемся библиотекой стандартных типов unidef.h, которую многократно
использовали ранее.
Листинг 6.2
void bfflush(BITFILE *bf)
{
if (bf->mask != 0x80)
{
putc((uint8_t) bf->rack, bf->file);
bf->mask = 0x80;
}
bf->rack = 0;
}
BITFILE *bfopen(const char *bfname, const char *mode)
{
BITFILE *bf = NULL;
FILE *f;
f = fopen(bfname, mode);
if (f)
{
bf = new BITFILE;
bf->file = f;
bf->rack = 0;
bf->mask = 0x80;
}
return bf;
}
int bfclose(BITFILE *bf)
{
int err = 0;

Глава 6. Кpиптогpафическое сжатие 195
if (bf)
{
bfflush(bf);
err = fclose(bf->file);
free(bf);
}
return err;
}
void bfwritebit(int bit, BITFILE *bf)
{
if (bit) bf->rack |= bf->mask;
bf->mask >>= 1;
if (!bf->mask)
{
putc((uint8_t) bf->rack, bf->file);
bf->mask = 0x80;
bf->rack = 0;
}
}
void bfwrite(uint32_t bits, int num, BITFILE *bf)
{
uint32_t whole = 1L << --num;
while (whole > 0)
{
if (whole & bits) bf->rack |= bf->mask;
bf->mask >>= 1;
if (!bf->mask)
{
putc((uint8_t) bf->rack, bf->file);
bf->mask = 0x80;
bf->rack = 0;
}
whole >>= 1;
}
}
int bfreadbit(BITFILE *bf)
{
if (bf->mask == 0x80) bf->rack = getc(bf->file);
int bit = bf->rack & bf->mask;
bf->mask >>= 1;
if (!bf->mask) bf->mask = 0x80;
return (bit?1:0);
}
uint32_t bfread(int num, BITFILE *bf)
{
uint32_t bits = 0, whole = 1L << --num;
while (whole > 0)
{
if (bf->mask == 0x80) bf->rack = getc(bf->file);
if (bf->rack & bf->mask) bits |= whole;

196 Глава 6. Кpиптогpафическое сжатие
bf->mask >>= 1;
if (!bf->mask) bf->mask = 0x80;
whole >>= 1;
}
return bits;
}
Вспомогательная библиотека bitfile.cpp содержит служебные функции bfread
и bfwrite, которые позволят обращаться к обычным файлам с требованием записать один или несколько битов. Для этого мы каждому файлу сопоставим небольшой битовый буфер (один, два или четыре байта, которые помещаются в переменные стандартных целочисленных типов для удобства работы с ними), в который
будем аккуратно записывать все поступления битов от алгоритма. Как только буфер заполнится, функция bfwrite сама запишет буфер, по размеру кратный байту,
в файл. Аналогично будет происходить и при чтении — сначала считается буфер,
а затем отдельные биты будут выдаваться уже по требованию.
Чтобы упростить интерфейс для программиста, который будет использовать
различные функции, создадим специальный тип структуры BITFILE, в который
войдет стандартный дескриптор файла file, однобайтовый буфер rack и номер текущего бита, готового к считыванию или записи, — mask (см. листинг 6.3).
Листинг 6.3
#ifndef bitfile__h
#define bitfile__h
#include <stdio.h>
#include <stdlib.h>
// по-прежнему стараемся использовать стандартные описания
#include <unidef.h>
// структура BITFILE должна заменить стандартную
// библиотечную структуру FILE так, чтобы для программиста
// эта замена была почти "прозрачна".
// Это облегчит написание программ, которые используют ввод и вывод
// произвольных по длине битовых потоков
typedef struct
{
FILE *file;
uint32_t mask;
int rack;
} BITFILE;
// сброс потока и запись его на диск
void bfflush(BITFILE *);
// открытие обычного файла как битового потока
BITFILE *bfopen(const char *, const char *);
// закрытие ранее открытого битового файла
int bfclose(BITFILE *);
// пишем в файл по одному биту
void bfwritebit(int, BITFILE *);
// или по целой группе битов (например, байтам или 11 битам)
void bfwrite(uint32_t, int, BITFILE *);

Глава 6. Кpиптогpафическое сжатие 197
// то же самое, только уже читаем
int bfreadbit(BITFILE *);
uint32_t bfread(int, BITFILE *);
#endif
Использование дополнительной структуры BITFILE требует от нас ее корректной инициализации и удаления по завершению работы с файлом. Кроме того,
нам необходимо успеть записать остатки битов в буфере в файл перед тем, как
его закроют. Для этого и предназначаются соответственно функции bfopen, открывающая файл и создающая BITFILE, bfclose, проделывающая обратную операцию и вызывающая bfflush, функцию, которая записывает буфер накопленных битов в файл.
В качестве оптимизации служебных функций bfread и bfwrite можно предложить переписать их в качестве макросов или (что еще проще) добавить в их определения ключевое слово inline, которое укажет компилятору на необходимость
разворачивания тела этих функций в местах их вызова. Тогда вместо вызовов
функций будут исполняться непосредственно они сами и мы избежим накладных
расходов на создание стека переменных и переходы внутри кодового сегмента.
Теперь можно приступить непосредственно к реализации алгоритма арифметического сжатия. Вероятности появления символов можно оценивать адаптивно
с помощью массива счетчиков. На каждый символ будет приходиться свой счетчик, который в начале процесса сжатия устанавливается в единицу
37
. После кодирования символа значение соответствующего ему счетчика увеличивается на единицу. Точно так же будет происходить и в процессе декодирования, а следовательно, кодировщик и декодировщик будут обладать одними и теми же объемами
информации. Вероятность каждого символа оценивается его относительной частотой появления на текущий момент. Точно так же она определяется и в других
алгоритмах, например алгоритме Хаффмана, описанном выше.
Основной алгоритм программы сжатия текстов с помощью арифметического
кодирования показан в листинге 6.4.
Листинг 6.4
// для 16-битового буфера можем посчитать константы заранее
#define fixedbits_aac 16
#define highvalue_aac (((long) 1 << fixedbits_aac) - 1)
#define quarter1_aac ((highvalue_aac >> 2) + 1)
#define quarter2_aac (quarter1_aac << 1)
#define quarter3_aac (quarter2_aac + quarter1_aac)
#define maxaac_freq (16384 - 1)
int accumulate_aac[257]; // Интервальные значения
int aac_freq[257]; // Частоты символов
int sym2index_aac[256]; // Таблицы для индексирования позиций
int index2sym_aac[257]; // символов в таблице вероятностей
uint16_t low_aac, high_aac;
37
Единица, установленная в самом начале, позволяет избежать проблемы нулевой вероят
ности, описание которой было дано выше.
-

198 Глава 6. Кpиптогpафическое сжатие
long underflow_aac; // "отстающие" биты
// сжимаем данные в битовый поток
void compress_aac(char *buf, int32_t len, BITFILE *f)
{
int i, ch;
// инициализация алгоритма
for (i = 0; i < 257; i++)
{
aac_freq[i] = 1;
accumulate_aac[i] = 257 - i;
}
aac_freq[0] = 0;
accumulate_aac[256] = 0;
// заполняем частотные интервалы - формируем разбиение
for(i=257;i>0;i--)
accumulate_aac[i - 1] = accumulate_aac[i] + aac_freq[i - 1];
// заполняем таблицу индексов
for (i = 0; i < 255; i++)
{
sym2index_aac[i] = i+1;
index2sym_aac[i+1] = i;
}
// начальные значения границ рабочего интервала
low_aac = 0;
high_aac = highvalue_aac;
underflow_aac = 0;
// процесс адаптивного сжатия
for (int32_t ofs = 0; ofs < len; ofs++)
{
ch = sym2index_aac[ buf[ofs] ];
encodesymbol_aac(ch, f);
updatemodel_aac(ch);
}
// дополняем завершающим символом
encodesymbol_aac(256, f);
// дописываем отстающие биты и еще парочку для однозначности
// значения из результирующего интервала
underflow_aac++;
if (low_aac < quarter1_aac) {
bfwritebit(0, f);
while (underflow_aac)
{
bfwritebit(1, f);
underflow_aac--;
}
}
else
{
bfwritebit(1, f);

Глава 6. Кpиптогpафическое сжатие 199
while (underflow_aac)
{
bfwritebit(0, f);
underflow_aac--;
}
}
}
Массив aac_freq хранит частоты появления символов в тексте. Нормализующий множитель, с помощью которого можно вычислить соответствующие вероятности, находится в самом начале массива — это элемент aac_freq[0]. Символы нумеруются от единицы и до 256 (поскольку в качестве символов выбраны обычные
восьмибитовые байты). Дополнительно вводится специальный символ окончания
текста — EOF с порядковым номером 257.
Частотные интервалы для всех символов хранятся в массиве accumulate_aac.
В целях оптимизации алгоритма можно отсортировать таблицу вероятностей появления символов (и соответствующие интервалы в разбиении отрезка [0, 1]) по
убыванию частоты появления. Это минимизирует количество итераций, за которые декодировщик будет проводить декомпрессию текста, поскольку в этом случае для наиболее часто появляющихся символов декодировщик будет раскрывать
меньшее количество интервалов. Как раз для этого и созданы массивы sym2index_aac и index2sym_aac, служащие указателями на то, какой символ стоит на каком месте в таблице.
Текущий интеpвал, который, собственно, и представляет процесс сжатия и
декомпрессии, задается в [low_aac, high_aac] и будет в самом начале pавен
[low_aac = 0, high_aac = 1] и для кодиpовщика, и для декодиpовщика. Требуемая
для представления значения из интервала точность возрастает по мере сжатия
все большего и большего количества символов. По этой причине мы должны записывать результирующее значение побитово в течение всего процесса сжатия. Такой подход избавляет нас от необходимости задумываться над способами представления в памяти вещественных чисел с произвольной точностью.
Частоты, которые мы накапливаем, не должны превысить границу, отведенную для стандартных типов. Поэтому после каждого обновления модели необходимо проверять, не превысил ли какой-нибудь символ допустимые значения, и,
если это так, проводить масштабирование модели.
В процессе сжатия рабочий интервал сужается и старшие биты low_aac и
high_aac становятся равными. Их можно запаковывать сразу, так как они уже не
будут влиять на следующие биты кода (см. листинг 6.5).
Листинг 6.5
// кодируем один символ, используя модель
// приближения текста первого порядка
void encodesymbol_aac(int ch, BITFILE *f)
{
uint32_t range = high_aac - low_aac + 1;
// определяем интервал для символа ch
high_aac = (uint16_t) (low_aac +

200 Глава 6. Кpиптогpафическое сжатие
(range * accumulate_aac[ch - 1]) / accumulate_aac[0] - 1);
low_aac = (uint16_t) (low_aac +
(range * accumulate_aac[ch]) / accumulate_aac[0]);
// работаем над последовательностью битов, представляющих символ ch,
// поскольку мы заранее не знаем, сколько именно их для символа ch
// цикл без условия
for (;;)
{
if (high_aac < quarter2_aac) // определяем, где находимся
{
// пишем нулевой бит для левой половины
bfwritebit(0, f);
while (underflow_aac)
{
bfwritebit(1, f);
underflow_aac--;
}
}
else if (low_aac >= quarter2_aac)
{
// пишем единичный бит для правой половины
bfwritebit(1, f);
while (underflow_aac)
{
bfwritebit(0, f);
underflow_aac--;
}
// уменьшаем границы рабочего интервала
// на соответствующие значения
low_aac -= (uint16_t) quarter2_aac;
high_aac -= (uint16_t) quarter2_aac;
}
else if (low_aac >= quarter1_aac && high_aac < quarter3_aac)
{
// увеличиваем ширину рабочего интервала
underflow_aac++;
low_aac -= (uint16_t) quarter1_aac;
high_aac -= (uint16_t) quarter1_aac;
}
else
break;
// увеличиваем масштаб рабочего интервала
low_aac <<= 1;
high_aac = (uint16_t) ((high_aac << 1) | 1);
}
}
Следует помнить, что если границы интервала достаточно близки друг к другу, то масштабирование (представленное в листинге 6.6) приведет к тому, что
разные символы будут кодироваться одним и тем же целым числом, входящим в
[low_aac, high_aac]. Чтобы этого не произошло, кодировщик должен сохранять
достаточное расстояние между low_aac и high_aac. Самый простой способ сде
-
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
