[Bitmap Project] Bitmap Library

 

이전 포스트에서 비트맵 파일의 구조를 분석하면서 얼떨결에 비트맵 파일의 헤더와 바이너리 데이터를 읽어올때 필요할 구조체나 매크로 상수, 함수들을 만들었다. 비트맵 파일은 여러 바이너리 파일 포맷 중 단순한 포맷에 속하는 편이지만, 1바이트라도 어긋나면 정보를 읽을 수 없는 바이너리 파일의 특성상 여전히 복잡하여 읽고 쓰는 일만 하더라도 큰 작업으로 느껴진다. 적어도 나한테는 말이다.

그래서 고심끝에 비트맵 파일을 읽어오고, 쓰는 작업을 하는 모듈을 구현하기로 했다. 비트맵파일을 읽어 ASCII 코드로 바꿔주든 흑백필터를 씌우든, 바이너리 파일로부터 읽은 정보를 꺼내서 처리하기 적당한 형태로 저장해놓고, 다시 파일을 쓸 수 있어야 했다. 여러번의 시행 착오 끝에

typedef struct{
    BITMAPFILEHEADER bf;        // Bitmap File Header
    BITMAPINFOHEADER bi;        // Bitmap Info Header
    unsigned char *extra, *data;  // (intermediate extra area), (Pointer to Pixel Array Data)
    int sizData, sizPxl, padding;
    // (size of Image Data only (Pixel Array)), (Bytes per Pixel), (Padding size when width is not multiple of 4)
} IMAGE;

#define IDX(IMG,X,Y) (((IMG)->data)+((Y)*((((IMG)->bi.width)*((IMG)->sizPxl)+((IMG)->padding)))) + ((X)*((IMG)->sizPxl)))

이와 같은 구조체를 도입하여 정보를 저장하기로 하였다. bfbi는 파일을 쓸때도 필요하지만, 프로세싱시에 참고해야할 정보들도 있어 최대한 활용하기로 하였다. data에는 동적메모리 할당 후 Pixel Array의 데이터를 우선 쭉 읽어올 것이고, 8비트 이하면 (Color Table은 RGBQUAD형태임이 보장되어 있다.) color table도 읽어온다.

sizPxl(픽셀당 바이트)나 padding(4로 정렬시 남는 공간)의 경우 bitCount를 포함한 bi의 여러 정보들로 구할 수 있으나, 사용빈도가 잦아 계산을 줄이기 위해 저장하기로 하였다.

sizDataextra는 뒤늦게 추가했는데, 이전 포스트의 16bit 565항목에서 언급했던 문제 때문에 저장을 해야만 했다. 16bit 565 방식은 BI_BITFIELD에 해당하는 compression 방식이며, 비트필드가 어떻게 배분됐는지 (R:G:B=5:6:5로 표현했다고) BITMAPINFOHEADER와 Pixel Array 사이의 영역 extra에 저장한다. 이 영역이 제대로 저장되지 않으면 응용프로그램이 해당 bitmap파일의 compression을 해독할 수 없기 때문에 extra 영역도 읽어서 저장해두기로 하였다. (사실 원래같으면 bi.compression==BI_BITFIELD이니 gap영역을 읽고, 해독하여 5:6:5로 배분되어 있음을 알아내야 한다.)

이전에는 color table과 extra를 따로 구현했다가, 합칠 수 있을 것 같아 같이 사용하기로 했다.

매크로 함수로 IDX도 구현하였다. 사용하다보니 Pixel Array 원소의 실제 형태와 무관하게 data포인터의 형태를 unsigned char*로 해결하여 ‘y행의 x열 원소’와 같은 방식으로 접근하려면 매번 계산이 필요하여서 자주 가져다 쓸수 있도록 구현했다.

1. 이미지 읽어오기 구현

어쨌건, 파일구조체 포인터로 파일을 읽기 권한을 얻은 후, 순서대로 읽어와야 한다.

int readImage(IMAGE* img, char* fileName);

와 같은, fileName의 비트맵파일을 읽어서 그 정보를 img에 저장후 성공하면 1, 오류로 실패하면 0을 반환하는 함수를 구현해보자.

1-1. 파일 포인터로 바이너리 파일 읽기

int readImage(IMAGE* img, char* fileName){
    FILE *fpBMP;
    
    if(img==NULL){ puts("IMAGE container is NULL!"); goto ERROR; }
    if(!(fpBMP=fopen(fileName, "rb")))
    	{ puts("Failed to get read access to BMP file!"); goto ERROR; }
    
    if(fread(&(img->bf),sizeof(BITMAPFILEHEADER),1,fpBMP)<1)
        { puts("FileHeader read Error!"); goto ERROR; }
    if(fread(&(img->bi),sizeof(BITMAPINFOHEADER),1,fpBMP)<1)
        { puts("InfoHeader read Error!"); goto ERROR; }
    
    ... // 추가적인 필수정보를 읽어오는 과정 추가
    
    fclose(fpBMP);
    return 1;
    ERROR:
        if(fpBMP!=NULL)
            fclose(fpBMP);
        return 0;
}

뭐 평범하게 바이너리 파일 읽기를 구현하였다. freadfpBMP로부터 각각 헤더 사이즈만큼 한개씩 받아 각각 img의 멤버에 저장하도록 잘 구현이 되어있다. fread는 읽어낸 element의 개수를 반환하므로 1보다 작으면 제대로 읽히지 않은 것으로 간주하였다. 그리고 문제가 생긴 경우 goto 문으로 ERROR에서 fpBMP의 할당해제까지 확인후 0 값을 반환하도록 확실히 챙겼다.

1-2. 파일 포맷 속성 판별 및 분기

파일 데이터를 본격적으로 읽기 전에, 우리가 처리할 수 없는(또는 처리하지 않기로 한) 파일인지 확인하고 걸러주는 작업을 추가하자.

if(fread(&(img->bf),sizeof(BITMAPFILEHEADER),1,fpBMP)<1)
    { puts("FileHeader read Error!"); goto ERROR; }
if(img->bf.type != BM)
    { puts("Unacceptable Bitmap format file!"); goto ERROR; }

if(fread(&(img->bi),sizeof(BITMAPINFOHEADER),1,fpBMP)<1)
    { puts("InfoHeader read Error!"); goto ERROR; }
if(img->bi.size != BI_SIZE)
    { puts("Unsupported BMP info header!"); goto ERROR; }
if(img->bi.planes != 1)
    { puts("BMP has more than one of plane!"); goto ERROR; }
if(!(img->bi.compression == BI_RGB || img->bi.compression == BI_BITFIELDS))
    { puts("Cannot interpret compressed BMP file!"); goto ERROR; } 

bf.typeBM이 아니라면 OS/2 당시 사용되던, 굉장히 오래된 비트맵파일 형식이므로 걸러주자. 참고로 앞서 bitmap.h에서 #define BM 0x4D42 로 정의한 바 있다.

또 현재 Info Header (DIB헤더)가 어떤형식인지 식별할 가장 좋은 방법인 헤더의 사이즈 bi.size로 이 파일의 DIB헤더가 BITMAPINFOHEADER인지 식별한다. 우리가 알고있는 비트맵파일은 bi.plane==1이므로 역시 확인하고, 앞서 압축없는 BI_RGBBI_BITFIELDS(16bit 565에 해당)만 다루었으므로, 이 둘이 아닌 경우는 걸러주었다.

이제 데이터들을 각자 파일포맷형식에 맞게 읽어주자.

  1. bf.offBits0x36보다 크다면, 헤더와 Pixel Array 사이에 Extra Bit Mask가 존재한다는 뜻이므로, 읽어서 저장해주는 작업 또한 선행 되어야 한다. (Color Table 포함)

  2. 그후 사이즈를 적절히 계산해 Pixel Array를 읽어주자.

여기서 Extra Area에, 그리고 Pixel Array에 어떤 자료형이 들어오는지 구분하지 않고, 그 size만 계산해서 해당 공간에 입력받는다. (케이스별로 자료형을 따로 구현해 따로 처리하는 것은 비효율적이기 때문이다.) 때문에 나중에 해당 영역 접근시에도 강제 형변환이 필요할 것이다.

1-3. Extra Area (including Color Table)

if(img->bf.offBits>0x36){
    img->extra=malloc(img->bf.offBits - 0x36);
    if(img->bi.bitCount<=8)
        img->bi.clrUsed = (1<<img->bi.bitCount);
    if(fread(img->extra,img->bf.offBits-0x36,1,fpBMP)<1)
        { puts("Failed to read extra area!"); goto ERROR; }
}
else img->extra=NULL;

bfbi의 사이즈가 총 0x36이기 때문에 Pixel Array의 Offset이 0x36이 아니라면 사이에 color table이 있든, bit mask에 대한 정보가 있든 둘 중 하나이고, 어쨌든 복사해야한다. 더불어 색상 수도 그때그때 계산 안하고 쓸수 있도록 계산해 둔다.

1-4. Pixel Array

pixel array를 받기 전에 해야할 전처리가 조금 남아있다. 지난 포스트의 끝부분에서 다루었지만 bi.sizeImage는 color table은 포함하지 않지만 Pixel Array뿐 아니라 extra bit mask를 포함하기 때문에 이런 경우 bi.sizeImage를 가져다 Pixel Array의 크기로 활용할 수 없다. 게다가 bi.sizeImage가 0인 경우도 종종 있으니 어차피 size를 구해야 한다.

sizData를 구하려면 픽셀당 점유하는 바이트 크기, padding 값도 알아야 하니 먼저 구해줘야 한다. padding의 경우 4로 정렬하니 남는공간은 4의 나머지와 연관 있다고 생각하면 된다.

((img->bi.width)*(img->sizPxl))%PIXEL_ALIGN;

앞서 #define PIXEL_ALIGN 4로 정의하였으니 이대로 쓰면 될까? width가 4의 배수이면 0이니 문제 없지만 1일때는 3이, 2일때는 2가, 3일때는 1이 추가로 필요한거니 4에서 빼주는 작업을 더해주자.

if(img->padding) img->padding = PIXEL_ALIGN-(img->padding);

padding은 이렇게 구하면 되겠다. 그럼 한번 직접 구현해보자.

img->sizPxl = img->bi.bitCount/8;        // Bytes per Pixels

img->padding = ((img->bi.width)*(img->sizPxl))%PIXEL_ALIGN;
if(img->padding) img->padding = PIXEL_ALIGN-(img->padding);
    
img->sizData = ((img->bi.width)*(img->sizPxl)+img->padding)*(img->bi.height);
img->data=(char*)malloc(img->sizData);
fseek(fpBMP, img->bf.offBits, SEEK_SET);
if(fread(img->data,img->sizData,1,fpBMP)<1)
    { puts("Failed to read BMP data!"); goto ERROR; }

대충 완성된 듯 하다. 합쳐보자.

int readImage(IMAGE* img, char* fileName){
    FILE *fpBMP;

    if(img==NULL){ puts("IMAGE container is NULL!"); goto ERROR; }
    if(!(fpBMP=fopen(fileName, "rb")))
        { puts("Failed to get read access to BMP file!"); goto ERROR; }

    if(fread(&(img->bf),sizeof(BITMAPFILEHEADER),1,fpBMP)<1)
        { puts("FileHeader read Error!"); goto ERROR; }
    if(img->bf.type != BM)
        { puts("Unacceptable Bitmap format file!"); goto ERROR; }

    if(fread(&(img->bi),sizeof(BITMAPINFOHEADER),1,fpBMP)<1)
        { puts("InfoHeader read Error!"); goto ERROR; }
    if(img->bi.size != BI_SIZE)
        { puts("Unsupported BMP info header!"); goto ERROR; }
    if(img->bi.planes != 1)
        { puts("BMP has more than one of plane!"); goto ERROR; }
    if(!(img->bi.compression == BI_RGB || img->bi.compression == BI_BITFIELDS))
        { puts("Cannot interpret compressed BMP file!"); goto ERROR; } 
    
    if(img->bf.offBits>0x36){
        img->extra=malloc(img->bf.offBits - 0x36);
        if(img->bi.bitCount<=8)
            img->bi.clrUsed = (1<<img->bi.bitCount);
        if(fread(img->extra,img->bf.offBits-0x36,1,fpBMP)<1)
            { puts("Failed to read extra area!"); goto ERROR; }
    }
    else { img->extra=NULL; }

    img->sizPxl = img->bi.bitCount/8;        // Bytes per Pixels

    img->padding = ((img->bi.width)*(img->sizPxl))%PIXEL_ALIGN;
	if(img->padding) img->padding = PIXEL_ALIGN-(img->padding);
    
    img->sizData = ((img->bi.width)*(img->sizPxl)+img->padding)*(img->bi.height);
    img->data=(char*)malloc(img->sizData);
    fseek(fpBMP, img->bf.offBits, SEEK_SET);
    if(fread(img->data,img->sizData,1,fpBMP)<1)
        { puts("Failed to read BMP data!"); goto ERROR; }
    
    fclose(fpBMP);
	return 1;

    ERROR:
        if(fpBMP!=NULL)
            fclose(fpBMP);
        return 0;
}

2. void freeImage(IMAGE* img);

항상 동적메모리 할당을 시작하면 그 어떤 경우에도 할당 해제후 종료 되는지 확인해야한다. 우리의 메모리는 소중하니까! 하지만 우리는 지금 다른곳에서 가져다 쓸수 있는 라이브러리, 또는 그와 비슷한 모듈? 제작을 목표로 하고 있으며, readImage에서 읽어온 Image* img는 다른 코드에서 읽힐 예정이기 때문에 함부로 할당해제 할 수도 없고 언제 할당 해제할지도 알 수 없다.

다만, 우리가 malloc으로 동적메모리할당 후 free함수로 해제하듯, freeImage라는 함수를 만들어 IMAGE *img에 할당된 메모리들을 해제시키도록 사용자에게 편의를 제공할 수는 있다.

void freeImage(IMAGE *img){
    if(img->extra) free(img->extra);
    if(img->data) free(img->data);
}

깔끔하다.

3. 이미지 쓰기 작업 구현

흑백필터이든, 좌우반전이든 처리작업을 하면, 작업 결과물도 저장을 해야하니 이미지를 쓰는 함수도 구현해보자. readImage와 마찬가지로 IMAGE* img와 (새로 쓸)fileName을 받아 해당 file경로에 img에 있는 데이터를 쓰는 함수 writeImage를 만들자.

int writeImage(IMAGE* img, char* fileName){
    FILE *fpBMP;
    
    if(!(fpBMP=fopen(fileName, "wb")))
        { puts("Failed to get write access to BMP file!"); goto ERROR; }
    fseek(fpBMP, 0, SEEK_SET);
    if(fwrite(&(img->bf),sizeof(BITMAPFILEHEADER),1,fpBMP)<1)
        { puts("FileHeader write Error!"); goto ERROR; }
    if(fwrite(&(img->bi),sizeof(BITMAPINFOHEADER),1,fpBMP)<1)
        { puts("InfoHeader write Error!"); goto ERROR; }

    if(img->bf.offBits>0x36)
        if(fwrite(img->extra,img->bf.offBits-0x36,1,fpBMP)<1)
            { puts("Failed to write extra area!"); goto ERROR; }

    fseek(fpBMP, img->bf.offBits, SEEK_SET);
    if(fwrite(img->data,img->sizData,1,fpBMP)<1)
        { puts("Failed to write BMP data!"); goto ERROR; }

    fclose(fpBMP);
	return 1;

    ERROR:
    	if(fpBMP!=NULL)
            fclose(fpBMP);
        return 0;
}

readImage에 비해 간단하다. r이 w로, read가 write로 바뀌었을뿐 대부분 동일하다. 다만 우리가 readImage에서 걸렀던 처리 불가능한 조건들(맞지 않는 파일헤더 등등…) 이 있을 염려가 없을 뿐더러 있더라도 알 바가 아니다. 어차피 쓰고 끝낼건데 뭐. 그래서 그러한 조건들 확인작업을 뺐기 때문에 좀더 깔끔하게 보인다.

더 설명할 것도 딱히 없어보인다.

4. 최종 코드

#include <stdio.h>
#include <malloc.h>
#include "bitmap.h"

int readImage(IMAGE* img, char* fileName){
    FILE *fpBMP;

    if(img==NULL){ puts("IMAGE container is NULL!"); goto ERROR; }
    if(!(fpBMP=fopen(fileName, "rb")))
        { puts("Failed to get read access to BMP file!"); goto ERROR; }

    if(fread(&(img->bf),sizeof(BITMAPFILEHEADER),1,fpBMP)<1)
        { puts("FileHeader read Error!"); goto ERROR; }
    if(img->bf.type != BM)
        { puts("Unacceptable Bitmap format file!"); goto ERROR; }

    if(fread(&(img->bi),sizeof(BITMAPINFOHEADER),1,fpBMP)<1)
        { puts("InfoHeader read Error!"); goto ERROR; }
    if(img->bi.size != BI_SIZE)
        { puts("Unsupported BMP info header!"); goto ERROR; }
    if(img->bi.planes != 1)
        { puts("BMP has more than one of plane!"); goto ERROR; }
    if(!(img->bi.compression == BI_RGB || img->bi.compression == BI_BITFIELDS))
        { puts("Cannot interpret compressed BMP file!"); goto ERROR; } 
    
    if(img->bf.offBits>0x36){
        img->extra=malloc(img->bf.offBits - 0x36);
        if(img->bi.bitCount<=8)
            img->bi.clrUsed = (1<<img->bi.bitCount);
        if(fread(img->extra,img->bf.offBits-0x36,1,fpBMP)<1)
            { puts("Failed to read extra area!"); goto ERROR; }
    }
    else { img->extra=NULL; }

    img->sizPxl = img->bi.bitCount/8;        // Bytes per Pixels

    img->padding = ((img->bi.width)*(img->sizPxl))%PIXEL_ALIGN;
	if(img->padding) img->padding = PIXEL_ALIGN-(img->padding);
    
    img->sizData = ((img->bi.width)*(img->sizPxl)+img->padding)*(img->bi.height);
    img->data=(char*)malloc(img->sizData);
    fseek(fpBMP, img->bf.offBits, SEEK_SET);
    if(fread(img->data,img->sizData,1,fpBMP)<1)
        { puts("Failed to read BMP data!"); goto ERROR; }
    
    fclose(fpBMP);
	return 1;

    ERROR:
        if(fpBMP!=NULL)
            fclose(fpBMP);
        return 0;
}

int writeImage(IMAGE* img, char* fileName){
    FILE *fpBMP;
    
    if(!(fpBMP=fopen(fileName, "wb")))
        { puts("Failed to get write access to BMP file!"); goto ERROR; }
    fseek(fpBMP, 0, SEEK_SET);
    if(fwrite(&(img->bf),sizeof(BITMAPFILEHEADER),1,fpBMP)<1)
        { puts("FileHeader write Error!"); goto ERROR; }
    if(fwrite(&(img->bi),sizeof(BITMAPINFOHEADER),1,fpBMP)<1)
        { puts("InfoHeader write Error!"); goto ERROR; }

    if(img->bf.offBits>0x36)
        if(fwrite(img->extra,img->bf.offBits-0x36,1,fpBMP)<1)
            { puts("Failed to write extra area!"); goto ERROR; }

    fseek(fpBMP, img->bf.offBits, SEEK_SET);
    if(fwrite(img->data,img->sizData,1,fpBMP)<1)
        { puts("Failed to write BMP data!"); goto ERROR; }

    fclose(fpBMP);
	return 1;

    ERROR:
    	if(fpBMP!=NULL)
            fclose(fpBMP);
        return 0;
}

void freeImage(IMAGE *img){
    if(img->extra) free(img->extra);
    if(img->data) free(img->data);
}

지난 포스트에서 만든 bitmap.h를 함께 include 하였다. Github: bitmap.c

5. 사용

```c
int readImage(IMAGE* img,char* fileName);
int writeImage(IMAGE* img, char* fileName);
void freeImage(IMAGE *img);

와 같은 내용을 추가한 후,

#include "bitmap.h"
int main(char* src, char* dst){
    IMAGE img;
    char src[]="src.bmp";
    char dst[]="dst.bmp";
    
    if(!readImage(&img,src)) return -1;

    //Processing

    if(!writeImage(&img,dst)) return -1;
    freeImage(&img);
    return 0;
}

와 같이 사용할 수 있을 것이다. 저 processing 부분을 비워놓은채로 컴파일하여 실행하면, src.bmp 파일이 그대로 dst.bmp로 복사되는 것을 확인 해 볼수도 있다.

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)