
В этой статье я постараюсь доступно рассказать о структуре 8-битного BMP-файла и о том, как его создать на языке C# байт за байтом.
Зачем?
Например, когда нужно вытащить изображения из файлов, формат которых неясен (если это — игра, скажем, со спрайтовой графикой). Графическим редактором их, ясное дело, не открыть. А с помощью хекс-редактора можно "разглядеть" структуру похожую на графику и написать свою программу для конвертирования. Знание популярных графических структур сильно облегчает поиски.
Введение
Некоторые тонкости описания формата я пропущу, поскольку они не влияют на правильность созданного битмапа. BMP-файлы, сохраненные разными программами, могут заметно отличаться внутри, но отображаться они будут одинаково. Не беру в расчёт любимый Paint, который нещадно портит цвета при сохранении в 8-битный BMP (вы убедитесь в этом, посмотрев пару примеров в конце). Если вам нужно более ёмкое описание формата, обратитесь к Wikipedia.
Основные характеристики 8-битного BMP:
- Количество цветов в палитре от 1 до 256;
- Цвет в палитре кодируется 24 битами (по байту на каждый компонент RGB + один неиспользуемый байт);
- Ширина изображения должна быть кратной четырём. Иначе к каждой строке добавляется от 1 до 3 пикселей любого цвета. Они остаются невидимыми, но влияют на физический размер файла;
- Построение изображения начинается с левого нижнего угла и идёт построчно вверх;
- Каждый пиксель кодируется номером его цвета из палитры от 0x00 до 0xFF (вариант битмапа без сжатия).
Палитра - это таблица всех цветов, которые присутствуют в изображении.
На рисунке ниже представлена общая структура файла 8-битного изображения, далее разберём её подробнее.
Файлы 8-битных изображений устроены немного сложнее 24-битных. У последних отсутствует секция COLOR_TABLE, а IMAGE_DATA содержит все данные о цвете каждого пикселя.
HEADER
Эта секция содержит данные о файле изображения.
Длина секции — 14 байт.
Смещение от начала файла — 0x00.
Описание полей:
- Signature — байты, идентифицирующие файл как битмап. Записываются значения 0x42 и 0x4D;
- Filesize — 32-разрядное число, содержащее размер файла в байтах. Это вычисляемое поле. Нужно знать количество цветов и размер изображения. В конце статьи находятся примеры кода на C#, в которых есть этот несложный алгоритм;
- Reserved — зарезервированные байты, некоторые программы используют их под свои нужды. Мы запишем нулевые значения;
- Image_Data_Offset — 32-разрядное число, содержащее адрес (смещение), с которого начинается секция IMAGE_DATA. Оно равно сумме длин секций HEADER, INFO_HEADER и COLOR_TABLE.
INFO_HEADER
Эта секция содержит информацию, необходимую для отображения битмапа: длину и ширину изображения, тип сжатия, количество цветов и прочее.
Длина секции — 40 байт.
Смещение от начала файла — 0x0E.
Описание полей:
- Size — 32-разрядное число, содержащее длину секции INFO_HEADER;
- Width — 32-разрядное число. Ширина изображения;
- Height — 32-разрядное число. Высота изображения;
- Planes — 16-разрядное число, содержащее количество цветовых плоскостей, всегда равно 0x0001;
- BitCount — 16-разрядное число, указывает используемое кол-во бит на кодирование одного пикселя, записываем 0x0008;
- Compression — 32-разрядное число, содержит тип сжатия. Мы не будем использовать компрессию, поэтому запишем нулевое значение;
- ImageSize — 32-разрядное число. Содержит размер изображения, но можно записать ноль;
- HorResolution — 32-разрядное число. Горизонтальное разрешение изображения (кол-во пикселей на метр). Можно записать нулевое значение;
- VerResolution — 32-разрядное число. Вертикальное разрешение. Также можно записать ноль;
- ColorsUsed — 32-разрядное число, содержащее кол-во цветов в палитре;
- ColorsImportant — 32-разрядное число, содержащее кол-во важных цветов. Если записывается нулевое значение, это означает — все цвета в палитре важны. Хотя многие программы туда пишут то же значение, что и в поле ColorsUsed.
COLOR_TABLE
Эта секция содержит палитру битмапа. Цвета, представленные в изображении, записаны группами по четыре байта и исчисляются от 0 до 255.
Максимальная длина секции — 1024 байта (представлены все 256 цветов).
Минимальная длина секции — 4 байта (только 1 цвет).
Смещение от начала файла — 0x36.
Описание полей:
- R — красный компонент цвета;
- G — зелёный компонент цвета;
- B — синий компонент цвета;
- Unused — не используется, записываем туда 0xFF.
Цветов в битмапе может быть гораздо меньше 256. Отсутствующие цвета многие программы часто забивают нулями до максимальной длины секции. В случае монохромного рисунка, размер файла бессмысленно увеличивается почти на килобайт.
IMAGE_DATA
Самая простая по структуре секция. Даже иллюстрации не надо — всё очень тривиально. В ней каждый байт представляет собой пиксель изображения начиная с левого нижнего угла, а затем построчно вверх. Значение этого байта — есть номер цвета пикселя из палитры COLOR_TABLE.
Длина секции — зависит от количества пикселей.
Смещение от начала файла — записано в поле Image_Data_Offset в секции HEADER.
На этом, пожалуй, всё. Перейдём к примерам.
Код на C#
Пример 1.
Напишем программу, которая создаст рисунок из шести пикселей и сохранит его в файл.

using System;
using System.Drawing;
using System.IO;
namespace HandMadeBitmap
{
class Program
{
const short HEADER_SIGN = 0x4D42;
const int HEADER_RESERVED = 0x00000000;
const int HEADER_LENGTH = 14;
const short INFO_HEADER_PLANES = 0x0001;
const short INFO_HEADER_BITCOUNT = 0x0008;
const int INFO_HEADER_COMPRESSION = 0x00000000;
const int INFO_HEADER_COLORSIMPORTANT = 0x00000000;
const int INFO_HEADER_IMAGESIZE = 0x00000000;
const int INFO_HEADER_HOR = 0x00000000;
const int INFO_HEADER_VER = 0x00000000;
const int INFO_HEADER_LENGTH = 40;
const string fileName = "NewBitmap.bmp";
static Color[] usedClrs = {
Color.FromArgb(0x00AECF00),
Color.FromArgb(0x009999FF),
Color.FromArgb(0x00FF9966)
};
static int width = 3;
static int height = 2;
/*
* Далее вычисляем количество пикселей, которые
* нужно добавить к каждой строке, чтобы получить
* значение, кратное 4 (В данном примере оно равно 1)
*/
static int extraPixels = width % 4 == 0 ? 0 : 4 - width % 4;
static MemoryStream ms = new MemoryStream();
static BinaryWriter bmp = new BinaryWriter(ms);
static void Main(string[] args)
{
// Записываем HEADER
bmp.Write(HEADER_SIGN);
bmp.Write(GetFileSize());
bmp.Write(HEADER_RESERVED);
bmp.Write(GetImageDataOffset());
// Записываем INFO_HEADER
bmp.Write(INFO_HEADER_LENGTH);
bmp.Write(width);
bmp.Write(height);
bmp.Write(INFO_HEADER_PLANES);
bmp.Write(INFO_HEADER_BITCOUNT);
bmp.Write(INFO_HEADER_COMPRESSION);
bmp.Write(INFO_HEADER_IMAGESIZE);
bmp.Write(INFO_HEADER_HOR);
bmp.Write(INFO_HEADER_VER);
bmp.Write(usedClrs.Length);
bmp.Write(INFO_HEADER_COLORSIMPORTANT);
// Записываем COLOR_TABLE
foreach (Color clr in usedClrs)
{
bmp.Write(clr.B);
bmp.Write(clr.G);
bmp.Write(clr.R);
bmp.Write(byte.MaxValue);
}
/*
* Записываем IMAGE_DATA. Байты представляют
* пиксели в строках изображения.
* Чтобы сократить код, я объединил 4 байта каждой строки в
* 32-разрядное число, учитывая little-endian порядок байт
*/
bmp.Write(0x00010200);
bmp.Write(0x00020001);
new Bitmap(ms).Save(fileName);
}
private static int GetImageDataOffset()
{
return HEADER_LENGTH + INFO_HEADER_LENGTH + usedClrs.Length * 4;
}
private static int GetFileSize()
{
return GetImageDataOffset() + height * (width + extraPixels);
}
}
}
Сравнение результатов
Ради интереса сравним созданный программой рисунок с картинкой, нарисованной и сохраненной в Paint в 256-цветном формате:

Видно невооруженным глазом, что Paint не сохранил ни одного цвета правильно, и размер файлов изображений заметно отличается:
- Наша программа — 74 байта;
- Paint (Windows 7 SP1) — 1086 байт.
Пример 2.
Создаём битмап размером 256x256 с глубиной цвета 8 бит, заполняем вертикальным градиентом и сохраняем в файл.

using System;
using System.IO;
using System.Drawing;
namespace GradientBitmap
{
class Program
{
const short HEADER_SIGN = 0x4D42;
const int HEADER_LENGTH = 14;
const short INFO_HEADER_PLANES = 0x0001;
const short INFO_HEADER_BITCOUNT = 0x0008;
const int INFO_HEADER_LENGTH = 40;
const string fileName = "GradientBitmap.bmp";
static int width = 256;
static int height = 256;
static int usedColors = 256;
static int extraPixels = width % 4 == 0 ? 0 : 4 - width % 4;
static MemoryStream ms = new MemoryStream();
static BinaryWriter bmp = new BinaryWriter(ms);
static void Main(string[] args)
{
bmp.Write(HEADER_SIGN);
bmp.Write(GetFileSize());
bmp.Write(0);
bmp.Write(GetImageDataOffset());
bmp.Write(INFO_HEADER_LENGTH);
bmp.Write(width);
bmp.Write(height);
bmp.Write(INFO_HEADER_PLANES);
bmp.Write(INFO_HEADER_BITCOUNT);
bmp.Write(new byte[16]);
bmp.Write(usedColors);
bmp.Write(0);
for (int i = 0; i < usedColors; i++)
{
bmp.Write(new byte[] { (byte)i, byte.MaxValue,
(byte)i, byte.MaxValue });
}
byte rowColor = 0;
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width + extraPixels; column++)
{
bmp.Write(rowColor);
}
rowColor++;
}
new Bitmap(ms).Save(fileName);
}
private static int GetImageDataOffset()
{
return HEADER_LENGTH + INFO_HEADER_LENGTH + usedColors * 4;
}
private static int GetFileSize()
{
return GetImageDataOffset() + height * (width + extraPixels);
}
}
}
Сравнение результатов
Обязательно сравним результат работы своей программы с достижениями Paint. Повторим свои действия как и в предыдущем примере: откроем и сохраним файл в 256-цветный BMP.

Что ж, и здесь Paint отжёг. Ну, хотя бы размер файлов одинаковый, мы ведь использовали всю доступную палитру: 256 оттенков зелёного. Правда с умом, в отличие от некоторых других программ.
xborifan © 2014


