Wikipedia: BMP File Format (BITMAPV5HEADER의 DIB형식을 가진 Bitmap 파일의 구조 예시이다.)
역시 wikipedia랑 나무위키만한게 없다.
1. Bitmap: DDB와 DIB
Bitmap file은 크게 DDB와 DIB로 나누어진다.
DDB(Device Dependent Bitmap)
- 윈도우즈 3.0이전에 주로 쓰임.
- 이미지의 크기, 색상 등 기본적인 정보와 이미지 데이터로 구성
- 출력장치에 의존적이다.
DIB(Device Independent Bitmap)
- 윈도우즈 3.0부터 지원.
- 이미지의 크기, 색상 등 기본적인 정보와 이미지 데이터, 뿐 아니라 색상테이블, 해상도 등의 추가 정보를 포함.
- 출력장치로부터 독립적이다.
때문에 우리가 생각하는 현재 저장되고 사용되는 bmp파일은 DIB이다. 하지만 DIB형식은 그만큼 복잡하기 때문에 출력장치에 직접 출력하거나, 프로그램에서 사용하는 임시 파일로 적절하지 않아 이러한 용도로 DDB 형식이 여전히 쓰인다고 볼 수 있다.
어쨌거나, 우리가 지금 관심을 가지는 부분은 그림판으로 작업하고 나면 남는 흔히 .bmp으로 끝나는 그 비트맵 파일이고, 어느 장치로 복사해도 읽을 수 있는 이 파일은 DIB 형식이다.
2. DIB Bitmap File Overall Structure
그러니 DIB Bitamp file을 좀 더 살펴보자. DIB Bitmap file의 구조는 일반적으로
- Bitmap File Header
- Bitmap Info Header (DIB Header)
- Color Table (Palette, Semi-Optional)
- Pixel Array
- ICC Color Profile (Optional)
의 순서로 구성되어 있다. 파일의 속성에 따라 일부 요소가 다르거나 없기도 하다.
File Header(BITMAPFILEHEADER)는 고정되어 있으나, DIB header(Info Header)는 7가지나 된다. 물론 이 중에 3가지는 현재 잘 쓰이지 않고, 나머지 중
가 가장 많이 쓰인다고 봐야 할 것이다.
3. Bitmap Header Structure
우선 BITMAPINFOHEADER
가 가장 대중적이니 여기에 집중하고자 한다. MSDN문서를 링크해놨는데, 해당 구조체를 거의 그대로 가져와봤다. (bf, bi 접두어는 각각 BITMAPFILE, BITMAPINFO의 약자여서 생략했다. 어차피 구조체멤버로 접근할때 뭐가뭔지 다 아는데 쓸데없이 길다.)
//file: bitmap.h
#ifndef __BITMAP_H__
#define __BITMAP_H__
#pragma pack(push, 1) // Align structure by 1-byte
typedef struct _BITMAPFILEHEADER{ // BMP file header Structure
unsigned short type; // The File Type(aka Magic Number), must be BM
unsigned int size; // This BMP File Size, in bytes
unsigned short reserved1; // Reserved
unsigned short reserved2; // Reserved
unsigned int offBits; // The offset to Bitmap bits(Pixel Array)
} BITMAPFILEHEADER;
typedef struct _BITMAPINFOHEADER{ // BMP info header Structure(DIB header)
unsigned int size; // The size(byte) of this Structure
int width; // The Width of Bitmap
int height; // The Height of Bitmap
unsigned short planes; // The # of planes (must be set to 1)
unsigned short bitCount; // The # of bits per pixel (24 for 24bit bitmap)
unsigned int compression; // The type of compression for bottom-up bitmap
unsigned int sizeImage; // The size(byte) of image data only (Pixel Array)
int xPelsPerMeter; // The horizonatal resolution (pixel per meter)
int yPelsPerMeter; // The vertical resolution (pixel per meter)
unsigned int clrUsed; // The # of color indexed in color table.
unsigned int clrImportant; // The # of color required for displaying.
} BITMAPINFOHEADER;
#pragma pack(pop) // Disable alignment option
#endif
주석으로 대충 설명해놓긴 했지만, 속성이나 임의로 정해놓은 값들 등 자세한 설명이 필요한 부분들이 보인다.
먼저 비트맵 파일을 HxD, HEX Editor와 같은 Hex Editor로 살펴보면, Bitmap File Header부터 나온다.
Offset | Size | Var. Name | Description |
---|---|---|---|
0x00 | 2 byte | type |
BMP 파일 식별에 쓰이는 ‘매직넘버’: 0x4D42(little endian) |
0x02 | 4 byte | size |
BMP 파일의 실제 크기 (Byte 단위) |
0x06 | 2 byte | reserved1 |
예약공간1 |
0x08 | 2 byte | reserved2 |
예약공간2 |
0x0A | 4 byte | offBits |
BMP data (Pixel Array)가 시작되는 Offset |
이 다음에 Bitmap Info Header가 나온다. 앞서 BITMAPINFOHEADER
를 사용하기로 했으니 이 기준으로 보자.
Offset | Size | Var. Name | Description |
---|---|---|---|
0x0E | 4 byte | size |
이 헤더의 크기 (BITMAPINFOHEADER는 40byte) |
0x12 | 4 byte | width |
비트맵의 가로 pixel 수 (signed integer) |
0x16 | 4 byte | hight |
비트맵의 세로 pixel 수 (signed integer) |
0x1A | 2 byte | planes |
사용하는 색상 판의 수 (1개) |
0x1C | 2 byte | bitCount |
pixel 당 bit 수 (1, 4, 8, 16, 24, 32) |
0x1E | 4 byte | compression |
압축 방식 (0~5에 해당하는 압축방식) |
0x22 | 4 byte | sizeImage |
Bitmap data(Pixel Array)의 크기(byte) |
0x26 | 4 byte | xPelsPerMeter |
그림의 가로 해상도 (미터당 픽셀, signed integer) |
0x2A | 4 byte | yPelsPerMeter |
그림의 세로 해상도 (미터당 픽셀, signed integer) |
0x2E | 4 byte | clrUsed |
팔레트(색상 테이블)의 색 수 (테이블이 없는 경우 0) |
0x32 | 4 byte | clrImportant |
출력시 반드시 필요한 색 수(?) |
위와 같이 binary bitmap file은 2 byte 단위로 변수들을 촘촘히 빼곡히 배정해 놓는다.
size_t fread (void * DstBuf, size_t ElementSize, size_t Count, FILE * FileStream)
fread
함수로 해당 구조체에 바로 집어 넣기 위해서 헤더 구조체들도 오프셋 순서대로, 같은 크기로 선언하였다. 또한 기본설정으로 4 byte 정렬 후 남는 공간에 padding
을 넣으면 변수에 제대로 데이터가 들어가지 않으므로,
#pragma pack(push, 1)
를 통해 1 byte씩 정렬하도록 했다. [Wikipedia: Data Structure Alignment], [코딩도장: 구조체 멤버 정렬 사용하기]를 참고하였다. 구조체 정렬이 있다는 것을 배울 때 한 귀로 듣고 한 귀로 흘렸던 기억이 난다. 임의로 정렬하는 법과 그 필요성에 대해서 한 번도 생각해본적이 없었는데 이럴 때 필요한 거 구나 또 잘 알아간다.
4. Color Data & Pixel Data
자, 그 다음 무얼 살펴볼까. 앞서 살펴본 바에 따르면 Color Table과 Pixel Array가 있을 것이다. 근데 이 구성이 bitCount
, pixel당 bit수에 따라 다르게 구성되어 있다. (사실 생각해보면 당연한 일이다. pixel당 bit수가 다르니 저장할 수 있는 정보도 다르고, 너무 작거나 크면 저장할 수 있는 방식도 달라질 것이다.)
앞서 하이퍼 링크를 달았지만, BITINFOHEADER MSDN 문서의 bitCount
설명부분에 자세히 나와있다. 영어로 쓰여있기도 하고, 필요한 정보를 집약적으로 보여주지 않아서 여기저기서 찾은 정보들을 정리해봤다.
또 이해를 돕기위해, 앞서 본 bitmap.h
파일에 다음과 같이 구조체를 pragma pack
push
pop
사이에 미리 추가하였다.
#define BM 0x4D42 //Read "BM" by 2-byte of LittleEndian (B: 0x42, M: 0x4D)
#define BI_SIZE 40 // size of BITMAPINFOHEADER is 40
#define BI_RGB 0 // biCompression default value: Not Compressed
#define BI_BITFIELDS 3 // biCompression value: Manipulated by bitfield
#define PIXEL_ALIGN 4 // Pixel Array will be aligned by 4
typedef unsigned short RGB16; // 2byte of 16bit bitmap color data carrier
#define R555(P) (((P)&0x7C00)>>10) // read Red value from RGB16 of 16bit 555
#define G555(P) (((P)&0x3E0)>>5) // read Green value from RGB16 of 16bit 555
#define B555(P) ((P)&0x1F) // read Blue value from RGB16 of 16bit 555
#define RGB555(R,G,B) (((R)<<10)|((G)<<5)|(B)) // write RGB16 from each channel value of 16bit 555
#define R565(P) (((P)&0xF800)>>11) // read Red value from RGB16 of 16bit 565
#define G565(P) (((P)&0x7E0)>>5) // read Green value from RGB16 of 16bit 565
#define B565(P) ((P)&0x1F) // read Blue value from RGB16 of 16bit 565
#define RGB565(R,G,B) (((R)<<11)|((G)<<5)|(B)) // write RGB16 from each channel value of 16bit 565
typedef struct _RGBTRIPLE{
unsigned char rgbtBlue;
unsigned char rgbtGreen;
unsigned char rgbtRed;
} RGBTRIPLE;
typedef struct _RGBQUAD{
unsigned char rgbBlue;
unsigned char rgbGreen;
unsigned char rgbRed;
unsigned char rgbAlpha;
} RGBQUAD;
bitCount
기준 (색심도: Color Depth 라고한다.)
4-0.공통 사항
- Pixel Array의 원소 1개의 크기는
bitCount
크기이다. - Pixel Array는 4바이트로 정렬된다.
예를들어
width==75
,bitCount==24
이면, (75*(24/8))=225byte가 한 행을 위해 필요한 저장공간이다. 이때, 4바이트로 정렬하기 위해 225byte뒤의 3byte는 예약되어 있게된다. 즉, 이 경우 모든 행마다 마지막에 3byte의padding
을 가진다.
4-1. 8bit 이하
- Info Header 다음에 Color Table이 있다.
- Color Table의 색상 수는 \(2^{bitCount}\) 이다.
- Color Table 배열은
RGBQUAD
구조체를 원소로 가지며, 이 구조체에 색상 정보가 담겨있다. offBits
부터 나오는 Pixel Array에는 Color Table의 인덱스가 저장 되어 있으며, 이 인덱스가 가리키는 색상이 해당 pixel의 색상이다.
4-1-1. 8bit 미만의 경우 (1, 4bit)
- 1byte가 안되는 크기이기 때문에 한 바이트를 8개, 2개로 나누어 점유한다. (Pixel Data를 읽어오는데부터 Bit Operation이 필요하다.)
- Pixel Array의 원소를
unsigned char
로 읽은 뒤, 비트연산으로 해당 비트만 읽어와 인덱스를 구한 뒤, Color Table의 해당 인덱스를 읽는 방법으로 비트맵을 읽어야 한다.골때린다.이걸 굳이 해야할까
4-1-2. 8bit
- Pixel Array의 원소를
unsigned char
로 읽어 색상 인덱스를 얻어낸 뒤, Color Table의 해당 인덱스를 읽는 방법으로 비트맵을 읽어야 한다.
4-2. 16bit 이상
- Info Header 다음엔 Pixel Array가 있다. (Color Table 없음)
- Pixel Array의 각 원소가 가지고 있는 값 그 자체가 색상 정보이다.
bitCount
에 따라 Pixel Array의 원소의 형태, 저장 방식이 상이하다.
4-2-1. 16bit
- 16bit에 R,G,B 세 정보를 저장하기는 상당히 구리다.
- 결국에 16비트를 비트별로 5:5:5, 5:5:5:1, 5:6:5로 나누는 방식이 있다.
RGB16
으로 재정의 된unsigned short
로 한 픽셀을 읽어온 후 비트 연산으로 각 색상 정보를 읽어와야 한다.- 때문에 편의를 위해
RGB16
을 인자로하여 각 채널 값을 읽거나 채널 값을 넣어RGB16
을 구하는 매크로함수를 추가하였다.
4-2-1-1. 16bit 555
-
최상위 1비트는 무시하고 그 다음 5비트를
R
, 그 다음 5비트를G
, 그 다음 5비트를B
에 할당한다. 때문에 다음과 같이 정보를 읽어야 한다.r=(pixel&0x7C00)»10; g=(pixel&0x3E0)»5; b=(pixel&0x1F);
-
때문에 각 채널당 32색씩 가지니, 32×32×32=32768색을 가진다.
4-2-1-2. 16bit 5551
- R:G:B=5:5:5로 부여하고, 잔여 1비트를 A(알파채널, 투명)에 할당하였다.
- 비트맵 이미지의 투명도는
BITMAPV3INFOHEADER
이후에 추가 되었기때문에 고려하지 않겠다.
4-2-1-3. 16bit 565
-
R:G:B=5:6:5로 비트를 부여하였다. 따라서 다음과 같이 정보를 읽는다.
r=(pixel&0xF800)»11; g=(pixel&0x7E0)»5; b=(pixel&0x1F);
- 때문에
R
,B
채널은 32색씩, G채널은 64색을 가지니, 32×64×32=65536색을 가진다. - 이 경우
compression
이BI_BITFIELDS
로 3 값을 가진다. 이게 꽤나 골때리는게, 여기 문서의 파일구조 표에서 DIB헤더 다음 Extra bit mask에 주목하라. 어차피 Pixel Array를 찾는건 FileHeader의offBits
를 통해 offset을 참조하면 되므로 크게 문제가 되지 않는다. 문제는 InfoHeader의sizeImage
에 이 Extra bit mask가 해당된다는 점이다. 즉,sizeImage
는 Pixel Array의 사이즈 뿐 아니라 Extra bit mask의 사이즈도 포함한다.(Extra bit mask가 존재하는 경우) (이러한 점을 간과하여 16bit 565 조작에 애를 먹었다.)
4-2-2. 24bit
- Pixel Array의 원소는
RGBTRIPLE
구조체 이다. - Pixel Array에
RGBTRIPLE
이 여백없이 붙어서 들어있다. (탐색시 3byte씩 이동해야한다.) RGBTRIPLE
은 3byte이므로 4byte align시padding
을 고려해야 한다.
4-2-3. 32bit
- Pixel Array의 원소는
RGBQUAD
구조체 이다. RGBTRIPLE
은 4byte이므로 4byte align시padding
이 고려될 필요가 없다.
5. 알아두어야 할 bitmap file 속성
여러 비트맵 파일을 가지고 장난치면서 꽤 신경 써주어야 할 부분이 많다고 느꼈는데 막상 쓰려니 뭘 써야할지 기억이 나지 않는다. 생각나는 것부터 생각날때마다 하나씩 정리해보고자 한다.
- Pixel Array에 접근할 때 평소 프로그래밍 시 다른 좌표접근하듯 접근해서 프로세싱하면 뭔가 상하 반전된 결과물을 얻을 수도 있다. bitmap 파일은 bottom-up 방식으로 저장되어 있다. 관련 kldp 글 때문에 순서대로 프린트 한다면 아래와 같이 접근해야 한다.
for (int y=height-1; y>=0; --y){
for (int x=0; x<width; ++x){
//processing
}
}
BITMAPINFOHEADER
에 의외로 있어야 할 정보가 0으로 지정되어 있거나, 틀린 경우도 있다.sizeImage
는compression==0
인 경우 0으로 저장되기도 한다. 이때는 어렵진 않지만 손수 계산해주어야 한다.compression!=0
인 경우에도sizeImage
가 맞지 않는 경우를 보았다.sizeImage
는 그냥 무시하고width
,height
로 계산하기로 했다.근데 이게 왜 그런가 했더니…앞서 언급한 16bit565의 특징을 살펴보아라.sizeImage
에 Extra bit mask의 공간까지 포함되어서이다. 이러한 경우 때문에sizeImage
는 Pixel Array의 크기와 항상 같지 않고, Pixel Array크기는 결국width
와height
로 구해야 한다.clrUsed
도 비어있는 경우가 있었다. 앞서 언급했듯, \(2^{bitCount}\)임을 이용해 계산할 수밖에 없다.clrUsed=(1<<bitCount);
이것도 뭔가 그럴듯한 이유가 있으려나
- 위에 것 하나 쓰는동안 하나를 잊어버렸다.
6. bitmap.h
bitmap.h
라 이름지은 사용자 커스텀 헤더가 완성되었다.
//file: bitmap.h
#ifndef __BITMAP_H__
#define __BITMAP_H__
#pragma pack(push, 1) // Align structure by 1-byte
typedef struct _BITMAPFILEHEADER{ // BMP file header Structure
unsigned short type; // The File Type(aka Magic Number), must be BM
unsigned int size; // This BMP File Size, in bytes
unsigned short reserved1; // Reserved
unsigned short reserved2; // Reserved
unsigned int offBits; // The offset to Bitmap bits(Pixel Array)
} BITMAPFILEHEADER;
typedef struct _BITMAPINFOHEADER{ // BMP info header Structure(DIB header)
unsigned int size; // The size(byte) of this Structure
int width; // The Width of Bitmap
int height; // The Height of Bitmap
unsigned short planes; // The # of planes (must be set to 1)
unsigned short bitCount; // The # of bits per pixel (24 for 24bit bitmap)
unsigned int compression; // The type of compression for bottom-up bitmap
unsigned int sizeImage; // The size(byte) of image data only (Pixel Array)
int xPelsPerMeter; // The horizonatal resolution (pixel per meter)
int yPelsPerMeter; // The vertical resolution (pixel per meter)
unsigned int clrUsed; // The # of color indexed in color table.
unsigned int clrImportant; // The # of color required for displaying.
} BITMAPINFOHEADER;
#define BM 0x4D42 //Read "BM" by 2-byte of LittleEndian (B: 0x42, M: 0x4D)
#define BI_SIZE 40 // size of BITMAPINFOHEADER is 40
#define BI_RGB 0 // biCompression default value: Not Compressed
#define BI_BITFIELDS 3 // biCompression value: Manipulated by bitfield
#define PIXEL_ALIGN 4 // Pixel Array will be aligned by 4
typedef unsigned short RGB16; // 2byte of 16bit bitmap color data carrier
#define R555(P) (((P)&0x7C00)>>10) // read Red value from RGB16 of 16bit 555
#define G555(P) (((P)&0x3E0)>>5) // read Green value from RGB16 of 16bit 555
#define B555(P) ((P)&0x1F) // read Blue value from RGB16 of 16bit 555
#define RGB555(R,G,B) (((R)<<10)|((G)<<5)|(B)) // write RGB16 from each channel value of 16bit 555
#define R565(P) (((P)&0xF800)>>11) // read Red value from RGB16 of 16bit 565
#define G565(P) (((P)&0x7E0)>>5) // read Green value from RGB16 of 16bit 565
#define B565(P) ((P)&0x1F) // read Blue value from RGB16 of 16bit 565
#define RGB565(R,G,B) (((R)<<11)|((G)<<5)|(B)) // write RGB16 from each channel value of 16bit 565
typedef struct _RGBTRIPLE{
unsigned char rgbtBlue;
unsigned char rgbtGreen;
unsigned char rgbtRed;
} RGBTRIPLE;
typedef struct _RGBQUAD{
unsigned char rgbBlue;
unsigned char rgbGreen;
unsigned char rgbRed;
unsigned char rgbAlpha;
} RGBQUAD;
#pragma pack(pop)
#endif