
В предыдущей статье я совершенно проигнорировал тот факт, что формат BMP имеет множество версий (целых 7), что там присутствует компрессия, разное количество битов на пиксель и цветовые маски. Также, C++ там был только в названии статьи, код был написан на чистом C.
Здесь я постарался добавить поддержку большего количества форматов (конечно не все, но самые популярные) и использовать известные мне возможности C++.
Задача
Из bmp изображения записать в двумерный массив информацию о цвете каждого пикселя, с использованием только стандартных библиотек.
Немного о формате
Представленный ниже код несколько отличается от итогового. Сделано это для того, чтобы читателю было проще понять алгоритм работы программы.
Шапка
В первых 14 байтах располагается «шапка» файла. Перед любыми действиями, следует сверить формат открытого файла с необходимым нам, для этого нужно прочитать первые 2 байта и сравнить их с 0x4D42. Больше информация из «шапки» нам не понадобится.
Заголовок
После «шапки» идёт заголовок с информацией о bitmap файле. В первых 2 байтах находятся данные о длине этого заголовка, эту информацию также можно использовать для определения версии формата. Также нам понадобится информация о высоте и ширине изображения, количестве бит на один пиксель и цветовые маски.
Чтение файла
Читать файл нужно последовательно и побайтово. Для удобного осуществления этого подойдёт следующая шаблонная функция:
template <typename Type> void read(std::ifstream &fp, Type &result, std::size_t size) { fp.read(reinterpret_cast<char*>(&result), size); }
Отступ
Одной из особенностей bmp формата является то, что количество байт в строке обязательно должно быть кратно 4, недостающие пиксели добавляются в конец каждого пиксельного ряда. То есть, если наше изображение имеет 24 бита на пиксель и ширину 2 пикселя, то, так как 24 * 2 бита — это 6 байт (48 / 8 бит = 6 байт), полученный отступ будет равняться 2 байтам. Определить размер отступа можно следующим образом:
int padding = ((4 - (width * (bitCount / 8)) % 4) & 3;
Цветовая маска
Если открываемый файл имеет версию формата 2 или выше, то у него, возможно, установлена цветовая маска. Если это не так, то нужно установить маску цвета по умолчанию:
int colorsCount = bitCount >> 3; if (colorsCount < 3) { colorsCount = 3; } int bitsOnColor = bitCount / colorsCount; int maskValue = (1 << bitsOnColor) - 1; if (redMask == 0 || greenMask == 0 || blueMask == 0) { redMask = maskValue << (bitsOnColor * 2); greenMask = maskValue << bitsOnColor; blueMask = maskValue; }
Чтобы извлечь цвет по маске, нужно использовать логическое И (конъюнкция) и, после этого, сместить полученный результат вправо на количество бит равных количеству нулей справа от маски.
Функция извлечения битов будет выглядеть таким образом:
unsigned char bitextract(const unsigned int byte, const unsigned int mask) { if (mask == 0) { return 0; } // определение количества нулевых бит справа от маски int maskBufer = mask, maskPadding = 0; while (!(maskBufer & 1)) { maskBufer >>= 1; maskPadding++; } // применение маски и смещение return (byte & mask) >> maskPadding; }
Код записи будет выглядеть таким образом:
unsigned int bufer; read(fileStream, bufer, bitCount / 8); rgbRed = bitextract(bufer, biRedMask); rgbGreen = bitextract(bufer, biGreenMask); rgbBlue = bitextract(bufer, biBlueMask); rgbReserved = bitextract(bufer, biAlphaMask);
Код
Файл main.h
#ifndef MAIN_H_INCLUDED #define MAIN_H_INCLUDED // CIEXYZTRIPLE stuff typedef int FXPT2DOT30; typedef struct { FXPT2DOT30 ciexyzX; FXPT2DOT30 ciexyzY; FXPT2DOT30 ciexyzZ; } CIEXYZ; typedef struct { CIEXYZ ciexyzRed; CIEXYZ ciexyzGreen; CIEXYZ ciexyzBlue; } CIEXYZTRIPLE; // bitmap file header typedef struct { unsigned short bfType; unsigned int bfSize; unsigned short bfReserved1; unsigned short bfReserved2; unsigned int bfOffBits; } BITMAPFILEHEADER; // bitmap info header typedef struct { unsigned int biSize; unsigned int biWidth; unsigned int biHeight; unsigned short biPlanes; unsigned short biBitCount; unsigned int biCompression; unsigned int biSizeImage; unsigned int biXPelsPerMeter; unsigned int biYPelsPerMeter; unsigned int biClrUsed; unsigned int biClrImportant; unsigned int biRedMask; unsigned int biGreenMask; unsigned int biBlueMask; unsigned int biAlphaMask; unsigned int biCSType; CIEXYZTRIPLE biEndpoints; unsigned int biGammaRed; unsigned int biGammaGreen; unsigned int biGammaBlue; unsigned int biIntent; unsigned int biProfileData; unsigned int biProfileSize; unsigned int biReserved; } BITMAPINFOHEADER; // rgb quad typedef struct { unsigned char rgbBlue; unsigned char rgbGreen; unsigned char rgbRed; unsigned char rgbReserved; } RGBQUAD; // read bytes template <typename Type> void read(std::ifstream &fp, Type &result, std::size_t size) { fp.read(reinterpret_cast<char*>(&result), size); } // bit extract unsigned char bitextract(const unsigned int byte, const unsigned int mask); #endif // MAIN_H_INCLUDEDs
Файл main.cpp
#include <iostream> #include <fstream> #include "main.h" int main(int argc, char *argv[]) { if (argc < 2) { std::cout << "Usage: " << argv[0] << " file_name" << std::endl; return 0; } char *fileName = argv[1]; // открываем файл std::ifstream fileStream(fileName, std::ifstream::binary); if (!fileStream) { std::cout << "Error opening file '" << fileName << "'." << std::endl; return 0; } // заголовк изображения BITMAPFILEHEADER fileHeader; read(fileStream, fileHeader.bfType, sizeof(fileHeader.bfType)); read(fileStream, fileHeader.bfSize, sizeof(fileHeader.bfSize)); read(fileStream, fileHeader.bfReserved1, sizeof(fileHeader.bfReserved1)); read(fileStream, fileHeader.bfReserved2, sizeof(fileHeader.bfReserved2)); read(fileStream, fileHeader.bfOffBits, sizeof(fileHeader.bfOffBits)); if (fileHeader.bfType != 0x4D42) { std::cout << "Error: '" << fileName << "' is not BMP file." << std::endl; return 0; } // информация изображения BITMAPINFOHEADER fileInfoHeader; read(fileStream, fileInfoHeader.biSize, sizeof(fileInfoHeader.biSize)); // bmp core if (fileInfoHeader.biSize >= 12) { read(fileStream, fileInfoHeader.biWidth, sizeof(fileInfoHeader.biWidth)); read(fileStream, fileInfoHeader.biHeight, sizeof(fileInfoHeader.biHeight)); read(fileStream, fileInfoHeader.biPlanes, sizeof(fileInfoHeader.biPlanes)); read(fileStream, fileInfoHeader.biBitCount, sizeof(fileInfoHeader.biBitCount)); } // получаем информацию о битности int colorsCount = fileInfoHeader.biBitCount >> 3; if (colorsCount < 3) { colorsCount = 3; } int bitsOnColor = fileInfoHeader.biBitCount / colorsCount; int maskValue = (1 << bitsOnColor) - 1; // bmp v1 if (fileInfoHeader.biSize >= 40) { read(fileStream, fileInfoHeader.biCompression, sizeof(fileInfoHeader.biCompression)); read(fileStream, fileInfoHeader.biSizeImage, sizeof(fileInfoHeader.biSizeImage)); read(fileStream, fileInfoHeader.biXPelsPerMeter, sizeof(fileInfoHeader.biXPelsPerMeter)); read(fileStream, fileInfoHeader.biYPelsPerMeter, sizeof(fileInfoHeader.biYPelsPerMeter)); read(fileStream, fileInfoHeader.biClrUsed, sizeof(fileInfoHeader.biClrUsed)); read(fileStream, fileInfoHeader.biClrImportant, sizeof(fileInfoHeader.biClrImportant)); } // bmp v2 fileInfoHeader.biRedMask = 0; fileInfoHeader.biGreenMask = 0; fileInfoHeader.biBlueMask = 0; if (fileInfoHeader.biSize >= 52) { read(fileStream, fileInfoHeader.biRedMask, sizeof(fileInfoHeader.biRedMask)); read(fileStream, fileInfoHeader.biGreenMask, sizeof(fileInfoHeader.biGreenMask)); read(fileStream, fileInfoHeader.biBlueMask, sizeof(fileInfoHeader.biBlueMask)); } // если маска не задана, то ставим маску по умолчанию if (fileInfoHeader.biRedMask == 0 || fileInfoHeader.biGreenMask == 0 || fileInfoHeader.biBlueMask == 0) { fileInfoHeader.biRedMask = maskValue << (bitsOnColor * 2); fileInfoHeader.biGreenMask = maskValue << bitsOnColor; fileInfoHeader.biBlueMask = maskValue; } // bmp v3 if (fileInfoHeader.biSize >= 56) { read(fileStream, fileInfoHeader.biAlphaMask, sizeof(fileInfoHeader.biAlphaMask)); } else { fileInfoHeader.biAlphaMask = maskValue << (bitsOnColor * 3); } // bmp v4 if (fileInfoHeader.biSize >= 108) { read(fileStream, fileInfoHeader.biCSType, sizeof(fileInfoHeader.biCSType)); read(fileStream, fileInfoHeader.biEndpoints, sizeof(fileInfoHeader.biEndpoints)); read(fileStream, fileInfoHeader.biGammaRed, sizeof(fileInfoHeader.biGammaRed)); read(fileStream, fileInfoHeader.biGammaGreen, sizeof(fileInfoHeader.biGammaGreen)); read(fileStream, fileInfoHeader.biGammaBlue, sizeof(fileInfoHeader.biGammaBlue)); } // bmp v5 if (fileInfoHeader.biSize >= 124) { read(fileStream, fileInfoHeader.biIntent, sizeof(fileInfoHeader.biIntent)); read(fileStream, fileInfoHeader.biProfileData, sizeof(fileInfoHeader.biProfileData)); read(fileStream, fileInfoHeader.biProfileSize, sizeof(fileInfoHeader.biProfileSize)); read(fileStream, fileInfoHeader.biReserved, sizeof(fileInfoHeader.biReserved)); } // проверка на поддерку этой версии формата if (fileInfoHeader.biSize != 12 && fileInfoHeader.biSize != 40 && fileInfoHeader.biSize != 52 && fileInfoHeader.biSize != 56 && fileInfoHeader.biSize != 108 && fileInfoHeader.biSize != 124) { std::cout << "Error: Unsupported BMP format." << std::endl; return 0; } if (fileInfoHeader.biBitCount != 16 && fileInfoHeader.biBitCount != 24 && fileInfoHeader.biBitCount != 32) { std::cout << "Error: Unsupported BMP bit count." << std::endl; return 0; } if (fileInfoHeader.biCompression != 0 && fileInfoHeader.biCompression != 3) { std::cout << "Error: Unsupported BMP compression." << std::endl; return 0; } // rgb info RGBQUAD **rgbInfo = new RGBQUAD*[fileInfoHeader.biHeight]; for (unsigned int i = 0; i < fileInfoHeader.biHeight; i++) { rgbInfo[i] = new RGBQUAD[fileInfoHeader.biWidth]; } // определение размера отступа в конце каждой строки int linePadding = ((fileInfoHeader.biWidth * (fileInfoHeader.biBitCount / 8)) % 4) & 3; // чтение unsigned int bufer; for (unsigned int i = 0; i < fileInfoHeader.biHeight; i++) { for (unsigned int j = 0; j < fileInfoHeader.biWidth; j++) { read(fileStream, bufer, fileInfoHeader.biBitCount / 8); rgbInfo[i][j].rgbRed = bitextract(bufer, fileInfoHeader.biRedMask); rgbInfo[i][j].rgbGreen = bitextract(bufer, fileInfoHeader.biGreenMask); rgbInfo[i][j].rgbBlue = bitextract(bufer, fileInfoHeader.biBlueMask); rgbInfo[i][j].rgbReserved = bitextract(bufer, fileInfoHeader.biAlphaMask); } fileStream.seekg(linePadding, std::ios_base::cur); } // вывод for (unsigned int i = 0; i < fileInfoHeader.biHeight; i++) { for (unsigned int j = 0; j < fileInfoHeader.biWidth; j++) { std::cout << std::hex << +rgbInfo[i][j].rgbRed << " " << +rgbInfo[i][j].rgbGreen << " " << +rgbInfo[i][j].rgbBlue << " " << +rgbInfo[i][j].rgbReserved << std::endl; } std::cout << std::endl; } return 1; } unsigned char bitextract(const unsigned int byte, const unsigned int mask) { if (mask == 0) { return 0; } // определение количества нулевых бит справа от маски int maskBufer = mask, maskPadding = 0; while (!(maskBufer & 1)) { maskBufer >>= 1; maskPadding++; } // применение маски и смещение return (byte & mask) >> maskPadding; }
Полезные ссылки
- Подробное описание формата (именно английская версия): https://en.wikipedia.org/wiki/BMP_file_format
- Подробное и наглядное описание битмапов: http://paulbourke.net/dataformats/bitmaps/
- Посмотреть код на GitHub Gist: https://gist.github.com/ziggi/e15a95b9feac8f59c7c1