22 янв. 2014 г.

Как создать 8-битный BMP-файл с нуля. Примеры на C#





В этой статье я постараюсь доступно рассказать о структуре 8-битного BMP-файла и о том, как его создать на языке C# байт за байтом.


Зачем?


Например, когда нужно вытащить изображения из файлов, формат которых неясен (если это — игра, скажем, со спрайтовой графикой). Графическим редактором их, ясное дело, не открыть. А с помощью хекс-редактора можно "разглядеть" структуру похожую на графику и написать свою программу для конвертирования. Знание популярных графических структур сильно облегчает поиски.


Введение


Некоторые тонкости описания формата я пропущу, поскольку они не влияют на правильность созданного битмапа. BMP-файлы, сохраненные разными программами, могут заметно отличаться внутри, но отображаться они будут одинаково. Не беру в расчёт любимый Paint, который нещадно портит цвета при сохранении в 8-битный BMP (вы убедитесь в этом, посмотрев пару примеров в конце). Если вам нужно более ёмкое описание формата, обратитесь к Wikipedia.

Основные характеристики 8-битного BMP:
  1. Количество цветов в палитре от 1 до 256;
  2. Цвет в палитре кодируется 24 битами (по байту на каждый компонент RGB + один неиспользуемый байт);
  3. Ширина изображения должна быть кратной четырём. Иначе к каждой строке добавляется от 1 до 3 пикселей любого цвета. Они остаются невидимыми, но влияют на физический размер файла;
  4. Построение изображения начинается с левого нижнего угла и идёт построчно вверх;
  5. Каждый пиксель кодируется номером его цвета из палитры от 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

Общее·количество·просмотров·страницы