Upd 07.04.2011 Статья обновлена, выложены новые версии программ
Статья получилась очень "заумная". Но тем не менее - публикую, надеясь на то, что кому то она пригодится.
Многие в курсе, что в прошивке есть картинки, которые нельзя просмотреть и отредактировать обычными средствами. Кроме того, если эти картинки скопировать в другое место, плеер откажется их показывать. Порывшись в интернете, и почитав информацию по этому вопросу, я совершил небольшой промах. Поверив на слово заявлениям достаточно уважаемых спецов в этой области, дал обещание написать конвертер для этих файлов. Но не тут то было... Потратил больше недели. А неделя возни с ерундовым алгоритмом - для меня это много ;)
Начну по порядку.
Итак, в прошивке для любого плеера есть файлы с расширением bmp, которые вовсе такими не являются. Позволю себе высказать свои предположения, откуда они появились и для чего нужны.
Ежу понятно, что появление этих файлов растет из здоровой конкуренции между производителями. Прогу для кодирования файлов дает сам Realtek с составе СДК, и ее можно найти в инете (bmp2felics). Кстати, под моей Fedora 11 она не запустилась. А вот проги для декодирования я нигде не нашел.
Тут предвижу вполне законный вопрос: "А зачем нужны эти кодированные файлы, если плеер прекрасно жрет обычные bmp в формате 16bit R5G6B5?". Мой ответ такой: эти кодированные файлы гораздо быстрее прорисовываются плеером (видимо, за счет аппаратной поддержки).
Исходя из вышесказанного, я счел нужным заняться изучением этого формата.
Ну-с, приступим-с...
Подопытным кроликом я выбрал файл FILE_MANAGER_1.bmp, но не посмотрел, от какой прошивки и от какого плеера. В принципе, это неважно, т.к. формат этих файлов одинаковый, но если кому то будет интересно покопать вместе со мной, я его
залил
Вполне естественно, что этот файл не открылся ни в одном графическом редакторе, и даже специальные программы не смогли определить его тип. Впрочем, я этого и ожидал. Поэтому открыл его в программе, которая может открыть абсолютно все - в HEX-редакторе. И увидел следующее:
Тем, кто когда нибудь открывал bmp подобным образом, сразу станет понятно, что битмапом тут и не пахнет. Хотя бы потому, что сигнатура bmp-файла другая. Обычно это BM, хотя встречаются и другие (в OS/2 - BA, CI, CP, IC, PT). Тут же мы видим символы RT, что очень смахивает на RealTek.
Немного пошевелив мозгами и покопавшись в исходниках (от самого первого СДК), а так же в коде функции DG_GetBmpHeader, предлагаю Вашему вниманию свои выводы:
Первые два байта являются сигнатурой, следующие два нуля либо входят в состав сигнатуры, либо не используются. Во всяком случае, во всех имеющихся у меня таких файлах эта комбинация абсолютно одинаковая.
Байты 4..7 представляют собой некоторую константу, из которой вычисляется количество бит и байт на пиксель. В данном случае это поле имеет значение 4. Следует читать байты в обратном направлении (т.е. 00 00 00 04). В коде предусмотрена возможность и отличного от 4 значения. На языке С это можно написать так:
Code
BitsPerPixel = x[4..7] == 4 ? 0x10 : 0x20 // 16 или 32 бита на пиксель
BytesPerPixel = x[4..7] == 4 ? 2 : 4 // 2 или 4 байта на пиксель
Pitch = Width * ( x[4..7] == 4 ? 2 : 4 ) // кол-во байт на одну строку
Следующие 2 байта (8..9) - это ширина (Width) изображения. 0x0087 = 135 пикселей. Аналогично байты 0xA..B - высота (Height). 0x7C = 124 пикселя.
В байтах 0xC..D и 0xE..F предыдущие значения повторяются. Для них в коде прошивки предусмотрены два лишних поля, названные как ori_width и ori_height. ИМХО, они особого значения не имеют, так как при разборе обычного bmp-заголовка в них копируются значения полей Width и Height.
Над смыслом следующих 3-х групп по 4 байта я думал довольно долго, пока не сложил их вместе. В результате получилось:
0xC70 (3184) + 0xFB0 (4016) + 0xDC0 (3520) = 0x29E0 (10720)
Если учесть, что длина файла составляет 0x2A00 (10752), разница между этими цифрами составляет 32 байта. Иными словами, 0x20 (32) - это длина заголовка, за которым следуют 3 блока с данными.
Последние 4 байта заголовка зарезервированы для Альфа-канала и в данном случае не используются.
Все вышесказанное нарисую в виде структуры:
Code
C
typedef struct {
BYTE magic[4];
DWORD bpp;
WORD Width;
WORD Height;
WORD ori_width;
WORD ori_height;
DWORD R_len;
DWORD G_len;
DWORD B_len;
DWORD Reserved;
} RTHEADER;
Pascal
RTHeader = packed record
magic: array [0..3] of Byte;
bpp: Cardinal;
Width: Word;
Height: Word;
ori_width: Word;
ori_height: Word;
R_len: Cardinal;
G_len: Cardinal;
B_len: Cardinal;
Reserved: Cardinal;
end;
Почему я дал название полям с длиной блоков с индексами цветов? Три блока - три цвета. А еще, если учесть, что в структуре R5G6B5 зеленый цвет занимает места больше остальных, становится понятным, почему размер второго блока больше других.
Для наглядности нарисую пару картинок.
Формат хранения R5G6B5 выглядит так:
1-й байт | 2-байт
RRRRRGGG | GGGBBBBB | и т.д.
В нашем случае, одинаковые цвета сгруппировали, примерно так:
1-й байт | 2-байт | 3-байт ..
000RRRRR | 000RRRRR | 000RRRRR ....
-------- -------- --------
| | цвет 3-го пикселя
| цвет 2-го пикселя
цвет 1-го пикселя
То же самое для синего цвета. А зеленый сгруппирован по 6 бит.
Однако, если поле bpp равно 7, формат становится другим. В этих файлах для каждого цвета используется 8 бит на пиксель + дополнительные 8 бит для канала яркости (т.н. Альфа-канал). Вот графическое представление формата A8R8G8B8:
1-й байт | 2-й байт | 3-й байт | 4-й байт
AAAAAAAA | RRRRRRRR | GGGGGGGG | BBBBBBBB | и т.д.
И еще очень важно: картинка считывается, начиная с нижней строки. Это вполне закономерно, т.к. плеер имеет точку начала координат в нижнем левом углу.
Но вот засада... Чтобы сохранить красный цвет в такой картинке, нужно:
135(ширина) * 124(высота) * 8(бит/пиксель) = 133920 бит или 16740 байт. А длина блока всего 3184 байта. Налицо сжатие более чем в 5 раз. Для сохранения синего цвета нужен такой же объем, но размер пакета другой...
Чем запакованы потоки? Как видно из названия проги от реалтека - это FELICS (Fast Efficient & Lossless Image Compression System). Правда, и тут была очередная засада - авторы "слегка" изменили алгоритм.
И есть еще одна подсказка. В блоке данных тоже есть заголовок (8 байт). Но в нем всего одна цифра (выделено на рис. зеленым цветом). Это - длина закодированного потока в битах, причем порядок чтения байт обратный. Это значение чуть меньше длины данных в блоке за счет округления размера блока в большую сторону (кратно 4). Иными словами, кодированный поток битов начинается с 9-го байта в блоке.
Попробую объяснить работу этого алгоритма. Основной принцип: каждый следующий пиксель кодируется относительно значений двух ближайших соседей, которые уже побывали в обработке. Это очень хорошо продемонстрировано на рисунке ниже.
Исключением в данном методе являются 2 первых пикселя, для которых еще не существует 2-х обработанных соседей. Поэтому значение первого пикселя записывается в поток целиком, а для значений обоих соседей второго используется значение первого.
После этого значения соседей сравниваются между собой и записываются в переменные H и L (соответственно, большее и меньшее). Несложно догадаться, что в некоторых случаях они будут равны. Затем проверяется, попадает ли значение текущего пикселя P в диапазон между H и L включительно. Если да, в поток записывается бит IN_RANGE, в противном случае OUT_RANGE. Для кодирования внутридиапазонного значения используется модифицированный Adjusted binary код, а внедиапазонного - код Голомба-Рэйза.
Код Голомба-Рэйза
Первым делом в поток записывается бит, определяющий, где находится значение P относительно диапазона, выше или ниже. Соответственно ABOVE_RANGE или UNDER_RANGE. Следующим шагом вычисляется delta по одной из формул:
delta = P - H - 1 или delta = L - P - 1.
После этого в поток записываются несколько единиц, количество которых является результатом целочисленного деления delta на 4, 0 в качестве разделителя, и 2 бита остатка от деления.
Adjusted binary код
Здесь немного посложнее. Базовыми понятиями здесь являются смещение значения текущего пикселя относительно минимального значения диапазона P - L, и сам диапазон delta = H - L. Кодирование значения производится методом определения положения P - L в диапазоне от 0 до delta по определенным правилам. Для примера приведу значения кодов для первых десяти значений delta + 1 (от 2 до 10), при P - L от 0 до 9.
|
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
0 |
1 |
11 |
10 |
111 |
110 |
101 |
100 |
1111 |
1110 |
1 |
0 |
0 |
11 |
00 |
111 |
110 |
101 |
000 |
1111 |
2 |
|
10 |
00 |
01 |
00 |
111 |
110 |
001 |
000 |
3 |
|
|
01 |
10 |
01 |
00 |
111 |
010 |
001 |
4 |
|
|
|
110 |
100 |
010 |
000 |
011 |
010 |
5 |
|
|
|
|
101 |
011 |
001 |
100 |
011 |
6 |
|
|
|
|
|
100 |
010 |
101 |
100 |
7 |
|
|
|
|
|
|
011 |
110 |
101 |
8 |
|
|
|
|
|
|
|
1110 |
1100 |
9 |
|
|
|
|
|
|
|
|
1101 |
Как видно из таблицы, количество бит, необходимое для кодирования, впрямую зависит от значения delta + 1, а точнее не может превышать степень, в которую надо возвести число 2, чтобы полученное значение было больше или равно delta + 1. Назовем этот параметр range. Соответственно, если delta + 1 = 2^range, все значения кодов будут иметь длину range, а в противном случае - от range - 1 до range.
А как же распределяются коды по диапазону? В случае delta + 1 = 2^range все достаточно просто: числа идут по порядку от середины диапазона и затем продолжаюся с его начала. Для второго варианта придется вычислить еще пару значений. Количество кодов с длиной range - 1 можно вычислить по формуле:
CountShortWord = 2^range - (delta + 1)
, а количество кодов с максимальной длиной:
CountLongWord = (delta + 1) - CountShortWord. Короткие коды всегда располагаются в середине диапазона, и их значения начинаются с нуля и возрастают с каждым шагом на 1. Длинные слова располагаются симметрично по краям диапазона, начинаясь после последнего короткого и продолжаясь сверху. Минимальное значение длинного слова равно (значение последнего короткого + 1) * 2. А максимальное значение длинного слова всегда равно 2^range - 1
Для варианта IN_RANGE при delta + 1 = 1, значение текущего байта P не кодируется вообще, так оно равно минимальному значению.И для тех, кто будет править графику. Нужно обязательно сохранять файл в формате BMP 16bit R5G6B5 или, если присутствует Альфа-канал - в формате BMP 32bit A8R8G8B8. Эти форматы поддерживают: фотошоп от 7-й версии и абсолютно бесплатный GIMP. Прозрачным (transparent) цветом обычно является абсолютно белый, но надо учитывать, что значение этого цвета может быть перепрограммировано в прошивке на другой.
На этом все. Осталось только сообщить значения констант:
IN_RANGE = 0
OUT_RANGE = 1
UNDER_RANGE = 0
ABOVE_RANGE = 1
Ну и для тех, кто дочитал до конца, маленький подарок. Программы для
декомпрессии и компрессии под винды.
Обновлены 07.04.2011 до версии 0.2 Пользоваться ими очень просто: бросить программу в папку с картинками и запустить. Компрессор найдет все некодированные файлы, немного поколдует над ними и сложит в папку
rfc. В свою очередь, декомпрессор ищет только закодированные файлы и складывает результат работы в папку
rfd
PS. Для тех, кто боится вирусов, гарантирую - вирей и прочей нечисти там нет (я этим не занимаюсь). Для желающих собрать проги самостоятельно - обращайтесь в приват, дам ссылку на исходники.