[Bitmap Project] Bitmap File Analysis

 

Wikipedia: BMP File Format (BITMAPV5HEADER의 DIB형식을 가진 Bitmap 파일의 구조 예시이다.)

BMP_File_Format

역시 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색을 가진다.
  • 이 경우 compressionBI_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으로 지정되어 있거나, 틀린 경우도 있다.
    • sizeImagecompression==0인 경우 0으로 저장되기도 한다. 이때는 어렵진 않지만 손수 계산해주어야 한다.
    • compression!=0인 경우에도 sizeImage가 맞지 않는 경우를 보았다. sizeImage는 그냥 무시하고 width, height로 계산하기로 했다. 근데 이게 왜 그런가 했더니… 앞서 언급한 16bit565의 특징을 살펴보아라. sizeImage에 Extra bit mask의 공간까지 포함되어서이다. 이러한 경우 때문에 sizeImage는 Pixel Array의 크기와 항상 같지 않고, Pixel Array크기는 결국 widthheight로 구해야 한다.
    • 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

Github: bitmap.h

This work is licensed under Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
(The excerpted works are exceptionally subject to a licence from its source.) Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)