[Bitmap Project] Low Bit Supports & Restructuring

 

Low Bit Supports & Restructuring


그만 하기로 했었는데… 결국 또 일을 저질러버렸다. 전날 써 놓고 만족한 코드는 꼭 다음날 보면 마음에 들지 않는다. 그럼 다시 뒤엎으면서 저질스러운 실력과 근시안적 사고를 후회한다.

어쩌다 또 코딩도장에서 구조체 내에서 비트단위 접근이 가능한 변수를 만들어 비트필드 사용을 용이하게 한다는 내용을 읽었다.

삽질

그동안 16bit 비트맵을 읽기위해 떡칠한 매크로 함수는 뭘까. 삽질

역시 한번 공부할때는 제대로 공부해야한다는 생각을 다시금 한다.

부끄럽게도 C언어를 오랫동안 보아왔으나, 접할때마다 이러는 듯하다. 처음 C를 배울때는 비트연산 저건 어디다 써먹냐 그랬었고, 포인터는 왜 쓸데없이 있을까 라는 생각을 했었다. 그 후로도 그 필요성을 깨닫지 못한 문법들은 무시하거나 잊어버리기 일쑤였다. 사실 당연하기도 하다. 어디다 써먹나. 그렇다고 기초를 배울때 실사용해보기도 만만치 않다. 처음 C를 배울때 지금과 같이 비트맵 프로젝트를 해보자하는 건 포기하게 만드는 것과 다름이 없다.

어쨌건, 그 사이를 못 참고 어제오늘 작업한 결과물들을 정리해보자.

1. Structure BitField

16bit 접근을 위한 매크로 함수들을 떡칠을 했다만, 그래도 나름 예쁘게 정리해놨었는데, 더 이상 필요가 없어졌다. 구조체를 통한 비트필드 접근이 퍼포먼스 측면에서 좋은지 나쁜지는 모르겠다만, 바꾸는 게 낫겠다는 생각을 했다.

typedef struct{
    unsigned short blue : 5;
    unsigned short green : 5;
    unsigned short red : 5;
    unsigned short alpha : 1;
} RGB16_555;

typedef struct{
    unsigned short blue : 5;
    unsigned short green : 6;
    unsigned short red : 5;
} RGB16_565;

비록 저 비트필드 변수들은 메모리 주소를 가지지 않기에 포인터 연산이 불가능하지만 (이게 꽤 불편하다.) 이게 어딘가. 이제 16비트도 24비트 32비트 접근하듯이 할 수 있다!

RGB16_555* p = (RGB16_555*) IDX(img,x,y);
unsigned short gray = ( p->red + p->green + p->blue ) /3;
p->red=p->green=p->blue=gray;

16비트 비트맵을 grayscale로 바꾸는 gray16함수의 일부이다. 매크로 함수로 떡칠한 것보다 훨씬 직관적으로 변했다. 눈물이 난다.

어쨌든, 다른 부분도 모두 이렇게 바꿔줬다.

하지만 인간의 욕심은 끝이 없고…

typedef struct{
    unsigned char _7 : 1;
    unsigned char _6 : 1;
    unsigned char _5 : 1;
    unsigned char _4 : 1;
    unsigned char _3 : 1;
    unsigned char _2 : 1;
    unsigned char _1 : 1;
    unsigned char _0 : 1;
} RGB1;

typedef struct{
    unsigned char _1 : 4;
    unsigned char _0 : 4;
} RGB4;

전에 비트필드 접근하기 불편하다고 포기했던 것도 없잖아 있던 1비트와 4비트도 이참에 만들어줬다.

판도라의 상자를 열고 말았다.

2. sizData 산정 방식 수정

2-1. sizData 산정 방식의 문제 발견

이제 1비트, 4비트 비트맵에서 행, 열에 따른 Pixel Array의 인덱스와 그 위치를 찾아내고, 거기서 약간의 계산을 통해 어느 비트에 해당하는지 찾아 꺼내오면 된다.

하지만 이전에 잘 되던 것도 다른 걸 수정하고 만지다보면 항상 문제가 생긴다. bugs.gif

그래서 1비트, 4비트 비트맵 파일이 잘 읽혀서 IMAGE img에 담기는지 확인해보고자 readImage다음에 바로 writeImage를 써서 파일복사를 해보았다. 역시나 무슨 이유에선지 모르지만 제대로 복사가 되지 않았는지 이미지뷰어가 읽어오지 못했다.

한참을 요리조리 만져보고 뒤져보다 결국 HxD로 바이너리 Hex를 까보았다. 역시 파일이 온전하게 복사되어 있지 않은게 한 눈에 보였다. File Header, Info Header 그리고 color table까지는 잘 복사가 되었고, Pixel Array영역도 처음부분은 잘 복사가 되었으나 한참 잘 가다가 뚝 끊겨버렸다.

왜지…? 읽기나 쓰기에 실패했다는 에러 문구도 출력이 되지 않았는데… 아무래도 sizData 산정이 잘못되었다는 생각이 들었다. 물론 이때까지도 8비트 이상은 아무런 문제가 없었다. 다만 처음 sizData구현 당시 애초에 1비트 4비트는 포기하고 8비트 이상에 대해 잘 작동함을 확인하고 그냥 넘어갔던 기억이 스쳐지나갔다.


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);

1비트와 4비트의 경우 sizPxl이 0이지 않나. 따라서 pixel array 자체를 읽을 수 없었다. 이부분은 그래서

img->padding = ((img->bi.width)*(img->bi.bitCount)/8)%PIXEL_ALIGN;
if(img->padding) img->padding = PIXEL_ALIGN-(img->padding);
    
img->sizData = ((img->bi.width)*(img->bi.bitCount)/8+img->padding)*(img->bi.height);

이렇게 bitCount를 8로 나누는 걸로 대치하였다. sizPxl도 똑같이 bitCount를 8로 나눈 값이지만, 정수형이다보니 그냥 0만 저장이 되지만 저기서 저렇게 곱하고 나누어주면, 든든한width가 있어서 0이되지 않고 필요한 바이트수를 반영한다. (더불어 sizPxl을 쓰던 모든 코드를 bi.bitCount/8로 대치하였다.)

하지만 이래도 해결이 되지 않았다는 것이다. 여기서 한참 삽질을 했었다. 1비트 비트맵을 확대해서, 해당 pixel array영역으로가서 직접 비트단위로 대조해보고… 1비트, 4비트 비트맵 역시 4바이트 정렬로 인한 padding이 있음도 확인하고 진귀한 경험이었다.

2-2. 예시, 직접 해보자.

어쨌든 예시를 들어보자. 우선 64×64 크기의 1비트 비트맵 사이즈를 계산해보자. 한 행에는 총 64개의 픽셀이 있다. 한 행의 모든 픽셀이 차지하는 픽셀데이터는 얼마나 될까? 8개의 픽셀이 1바이트가 됨에 유의하자. 64를 8로 나누면 정확히 8로 나누어 떨어진다. 즉 64개의 픽셀은 각 픽셀이 1비트씩, 그러니까 8개 픽셀이 1바이트를 이루어 총 8바이트를 차지한다. 한 행이 8바이트이기 때문에 4바이트 정렬도 만족해서 padding 따위는 필요 없다. 역시 깔끔한 숫자가 좋다.

그럼 이제 65×65 크기의 1비트 비트맵의 사이즈를 계산해보겠다.

한 행에는 총 65개의 픽셀이 있다. 한 행의 모든 픽셀이 차지하는 픽셀데이터는 얼마나 될까? 아까처럼 64개의 픽셀은 8바이트를 이루며 잘 있고 아무런 문제가 없다. 근데 그러고도 1비트가 남는다. 이 친구 어떻게 하나. 직접 HxD로 바이너리를 까본 결과 4바이트 정렬이 되고 있었다. 그러니까 8바이트 뒤에 4바이트가 이 1비트를 쓴 이후에 그냥 낭비되고 있는 것이다. 그 공간 모아서 방 하나는 만들겠더라

어쨌든, 65를 8로 나누면 몫은 8이지만 12바이트가 필요하다. 총 데이터는 9바이트에 들어가있고 3바이트는 padding으로 계산되어야 했다.

  • 64의 경우
    • \(64×1÷8=8\) (나누어 떨어짐)
    • padding = \(8 mod 4 = 0\)
    • sizData = \(8×64=512\)
  • 65의 경우 (앞선 계산식에 따른 잘못된 계산)
    • \(65×1÷8=8\) (정수형 버림)
    • padding = \(8 mod 4 = 0\)
    • sizData = \(8×64=512\)
  • 65의 경우 (바람직 한 계산)
    • \(65×1÷8=8\) (정수형 버림)
    • \(8+1=9\) ?? (이걸 어떻게 구현할지가 관건이다.)
    • padding = \(4 - ( 9 mod 4 )) mod 4 = (4-1) mod 4 = 3\)
    • sizData = \((9+3)×64=768\)

위에서 보인 방법대로 계산하면 width가 4의배수가 아닌 경우는 1바이트를 온전히 채우지 못하는 마지막 부분이 모조리 잘려나가고 있었다. 그러니 sizData가 그거밖에 안 되고 그거밖에 복사가 안되지…

저런_식으로_하니까_망하지

???: 저런 식으로 했으니 망했지

2-3. 계산방식 수정, sizRow의 도입

쨌든, 음… 그러니까 정수로 떨어질때는 상관이 없는데 나머지가 있으면 버리지 말고 올려버려야 한다? 정수형 변수를 쓰면 자동으로 버림이 되어 편할때가 많았는데 이를 거스르니 썩 기분이 좋지 않다. 물론 그렇다고 소중한 데이터를 버려버릴 순 없지.

정수가 아닌 실수를 버림해주는 걸로는 최대정수함수, 우리가 고등학교때 질리도록 본 그 가우스 기호가 있다. 필자는 보통 이게 필요한 경우는 정수형으로 강제형변환을 하여 자동으로 버려지도록 했었다만 <math.h>에서 floor라는 함수를 제공한다.

반대로 <math.h>에서 ceil이라는 함수를 제공한다. 정수가 아닌 실수는 올려버린다. (정수이면 그대로 둔다.) 이 상황에 딱 맞는다.

!! 주의: gcc, clang을 사용한다면 <math.h>를 사용하는 경우 링크옵션 LDLIB에 -lm을 추가해야함에 유의하여라. !!

#include <math.h>

...(omitted)

img->sizData = (img->bi.width)*(img->bi.bitCount);

if(img->bi.bitCount < 8)
    img->sizData=(int)ceil(((double)(img->sizData))/8);
else img->sizData/=8;

img->padding = (PIXEL_ALIGN-((img->sizData)%PIXEL_ALIGN))%PIXEL_ALIGN;
img->sizData += img->padding;
img->sizData *= img->bi.height;

음… 좀 지저분해졌지만 우선 급한 불은 껐다. 하지만 아직 문제가 남아있는게,

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

이 친구. 앞서 우리가 sizData 계산한 것과 매우 유사해 보인다. 당연히 그래야지. 인덱스 계산도 pixel array에 pixel data가 순서대로 나열되어있음을 가정하고 계산하는 거니까 한 행에 얼마인지 계산하는 작업이 있어야한다.

결국 똑같은 문제가 또 발생한다. 젠장 이것도 ceil함수를 써야할텐데 이제 매크로 함수를 못 쓰고 매번 함수로 불러야 하나…

그건 아닌 것 같았다. 그래서 이리저리 짱구를 굴려봤다. sizData 산정방식과 IDX 계산 방식이 되게 복잡하게 되어있다. 아니 뭐 저게 맞긴한데, 저게 메모리 할당할때, 접근할때마다 필요한데 저 과정을 계속 거쳐야 하나? 라고 생각을 하던 찰나, paddingbi.width는 거의 저기서밖에 쓰이지 않는다는 사실을 깨달았다. 매번 저렇게 저 형태 그대로 가져다 쓰는데 굳이 저런 애매한 변수들을 가져다 써야 해? 그냥 통째로 저장하고 말지.

고심끝에 padding을 없애기로 했다. 더불어 이름도 없는 그것, (img->bi.width)*(img->bi.bitCount)/8을 매번 계산하는 건 낭비라는 사실을 이제야 깨닫고는, 이것도 해결하자고 마음 먹었다.

앞서 내가 계산을 해볼때도 그렇고, 지금 IDXsizData에서도 그렇고 결국 필요한건 한 행이 점유하는 크기(byte)이다. 그래서 sizPxl도 없애고, padding도 없애고 bi.bitCountsizRow(한 행의 크기를 저장할 새로운 변수)로 대체하기로 했다.

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, sizRow;
    // (size of Image Data only (Pixel Array)), (size(byte) of one row including padding)
} IMAGE;

#define IDX(IMG,X,Y) (((IMG)->data)+((Y)*((IMG)->sizRow)) + ((X)*((IMG)->bi.bitCount)/8))

IMAGE 구조체와 IDX도 이렇게 수정해줬다. 그리고,

img->sizRow = (img->bi.width)*(img->bi.bitCount);

if(img->bi.bitCount < 8)
    img->sizRow=(int)ceil(((double)(img->sizRow))/8.0);
else img->sizRow/=8;

img->sizRow += (PIXEL_ALIGN-((img->sizRow)%PIXEL_ALIGN))%PIXEL_ALIGN;
img->sizData = (img->sizRow) * (img->bi.height);

sizRow 그리고 최종적으로 sizData를 구하는 과정을 이렇게 수정했다. 여전히 지저분하긴 하지만 한결 간결해진 것 같다. 이거지 보다시피 어차피 padding은 저렇게 더해지면 끝인데 그동안 왜 계속 달고 다니면서 매번 일일히 더해줬나 모르겠다.

번외 : 왜 이게 IDX야…?

IDX를 수정하다보니 이상하단 생각이 들었다. 아니 왜 이걸 IDX라고 이름지어놨지? 꼴에 IMG 변수까지 받아서 img->data에 접근하고 있다. 인덱스 계산후 이걸 더해서 실제 pixel data의 주소를 반환하는데 이게 어떻게 인덱스지…?

그래서 이참에 수정하기로 했다.

#define IDX(X,Y,SIZROW,BITCOUNT) (((Y)*(SIZROW)) + ((X)*(BITCOUNT)/8))
#define PIXEL(IMG,X,Y) (((IMG).data)+IDX(X,Y,((IMG).sizRow),((IMG).bi.bitCount)))
//#define _PIXEL(IMG,X,Y) (((IMG)->data)+IDX(X,Y,((IMG)->sizRow),((IMG)->bi.bitCount)))

IDXsizRowbi.bitCount만 받아서 인덱스 계산만 하도록. 그래도 확실히 pixel data 주소를 직접 반환받는게 코드 본문에서 보기에 더 깔끔하고 편리해보여 기존 IDX가 하던 역할을PIXEL이란 매크로 함수로 대신하기로 했다. IMG를 받아 IDX로부터 구한 인덱스를 더해 pixel data 주소를 받도록. (IMG가 포인터인 경우를 위해 _PIXEL 함수도 만들었었으나 이후 코드를 수정하면서 사용하지 않게되어 없앴다. )

뭔가 이제 비정상이 정상으로 바뀌어가고, 깔끔해지는 것 같다.

3. 다시, low bit supports

어쨌든, 잠재되어있던 문제는 해결이 되었고, 다시 본래의 이야기로 돌아오자. 1bit, 4bit 파일이 잘 읽히고 온전히 복사된다. 이제 읽기, 쓰기를 구현하고, 각 프로세싱을 구현해주어야겠다. (사실 이 시점에서 contrast, gray, invert는 사용가능하다. 어차피 1bit, 4bit 비트맵은 색상테이블을 사용하므로 색상조작을 위해선 색상테이블만 조작하면 된다. 1bit 색상조작해서 뭐할건데)

다행히도,

typedef struct{
    unsigned char _7 : 1;
    unsigned char _6 : 1;
    unsigned char _5 : 1;
    unsigned char _4 : 1;
    unsigned char _3 : 1;
    unsigned char _2 : 1;
    unsigned char _1 : 1;
    unsigned char _0 : 1;
} RGB1;

typedef struct{
    unsigned char _1 : 4;
    unsigned char _0 : 4;
} RGB4;

와 같이 구조체 멤버변수로 비트필드 접근이 가능해서 이를 이용해보고자 한다. 근데 멤버변수는 왜 저따구로 선언했을까.

변수이름은 숫자로 시작할 수 없기 때문이다. 그건 그렇다치고 왜 그럼 배열로 안하나. 배열로 하면 인덱스로 접근하기 용이한데.

비트단위로 접근하는 구조체 비트필드 멤버변수는 배열로 선언할 수 없다.

비록 저 비트필드 변수들은 메모리 주소를 가지지 않기에 포인터 연산이 불가능하지만 (이게 꽤 불편하다.) 이게 어딘가. 이제 16비트도 24비트 32비트 접근하듯이 할 수 있다!

앞서 이렇게 이야기한 바 있다. 메모리주소도 없는 친구들로 어떻게 배열을 만드나. 그래서 들떠있다가 잠시 좌절했었다.

3-1. 비트 읽기

그러나, 뭐 어쩌겠나. 비트연산으로 떡칠을 해야지.

#define GET1(VAL,X) (((VAL)&(0x80>>((X)%8)))>>(7-((X)%8)))
#define GET4(VAL,X) (((VAL)&(((X)&1)?0x0F:0xF0))>>(((X)&1)?0:4))

가져오는 건 이렇게 깔끔하게 해결된다. 알다시피 Little Endian 방식으로 저장되기 때문에 저렇게 비트접근 멤버변수들을 내림차순으로 선언하면, 최상위비트부터 오름차순으로 쭉 할당된다. (당연히, 픽셀데이터 저장도 이 순서이다.)

픽셀의 열을 8 또는 1로 나눈 나머지를 이용했다. (X&1하면 홀수는 1 짝수는 0이 나온다.) 1바이트는 8비트이고, 당연히 나머지가 한 바이트내에서 몇번째 비트인지를 결정하기 때문이다. 그래서 최상위비트에서 시작해서 나머지만큼 하위비트로 이동하여 비트를 읽어내었고, 이 후 우리가 제대로 읽을 수 있게 값을 반환하도록 7에서 나머지를 뺀만큼 이동하여 자릿수를 맞추었다.

(글쓰면서 생각한건데 어차피 1비트는 결국 true or false이므로 굳이 최하위비트로 이동시켜줄 필요 없이 논리 판별로 해결해도 된다.)

4비트 읽기도 방식은 같으나 두 영역으로만 갈라졌다보니 식이 간단하게 나온다. 이제 1bit, 4bit를 위한 toASCII를 구현할 수 있게되었다.

char index = _PIXEL(img,x,y);
index = GET1(index,x);
RGBQUAD* p = ((RGBQUAD*)(img->extra))+index;

// 해당 픽셀에 저장된 인덱스를 참조해 색상을 읽어내는 과정
// 밝기 추출 및 ascii 문자 반환

이렇게 1bit 비트맵 파일을 읽어 각 픽셀을 ASCII문자로 대응시킬 수 있게되었다. 4bit도 같은 방법이다.

3-2. 비트 쓰기

그러나 좌우반전, mirror의 경우는 아직 해결할 수 없다. img에 담긴 정보를 다시 dst파일에 써야하기 때문에 IMAGE img에 비트쓰기를 해야한다. 비트쓰기도 구현하자.

void write1Bit(RGB1* byte, char bit, unsigned char value){
    switch(bit){
        case 0: byte->_0=value; break;
        case 1: byte->_1=value; break;
        case 2: byte->_2=value; break;
        case 3: byte->_3=value; break;
        case 4: byte->_4=value; break;
        case 5: byte->_5=value; break;
        case 6: byte->_6=value; break;
        case 7: byte->_7=value; break;
        default: break;
    }
}

void write4Bit(RGB4* byte, char part, unsigned char value){
    if(part&1) byte->_1=value;
    else byte->_0=value;
}

충격과 공포

이게 최선일까… 자괴감이 든다. 배열처럼 사용할 수 없다보니, 인덱스를 구해도 인덱스와 멤버변수를 연결지을 방법이 switch-case가 유일하다.

그래도 4비트의 경우 경우의 수가 두가지뿐이어서 꽤 깔끔하다. 간단해서 이거 매크로로도 가능하겠다.

#define SET4(BYTE,PART,VAL) if((PART)&1){((BYTE)->_1)=(VAL);} else{((BYTE)->_0)=(VAL);}

깔끔하다. 다만 여태까지 썼던 다른 매크로 상수와는 다르게 세미콜론’;’를 붙이면 안 된다는 사실에 주의하자. if문이지 않나.

뭐 4비트는 평화롭게 끝났는데, 1비트는 정말 저게 최선일까. 비트연산도 답이지만 회피하고 싶다. 값이 0이면 and 연산을 통해서 써야하고, 1이면 or 연산으로 써야한다. 게다가 4비트는 자릿수도 여럿이어서 4번해야한다. 반복문을 써야하나. 결국 함수로…

근데 1비트는 자릿수가 결국 한 자리라는 소리니 의외로 짧게 끝낼 수 있을 것 같다. 겁이나지만 한번 해보자.

  • 해당 비트가 1인 경우 해당 비트가 현재 0이든 1이든, 무조건 1로 만들어주어야 한다. 해당 비트를 1과 OR 연산해주면 무조건 해당비트는 1이 되겠다. 나머지 비트는 영향을 받으면 안되니 0과 OR연산하도록 나머지 비트는 0으로 설정해야겠다. 그럼, 아래와 같이 연산하면 ‘bit’번째 바이트를 1로 확정시킬 수 있겠다.
byte|=(0x80>>(bit))
  • 해당 비트가 0인 경우 해당 비트가 현재 0이든 1이든, 무조건 0으로 만들어주어야 한다. 해당 비트를 0과 AND 연산해주면 무조건 해당비트는 0이 되겠다. 나머지 비트는 영향을 받으면 안되니 1과 AND연산하도록 나머지 비트는 1으로 설정해야겠다. 아까랑 정반대네. 그럼 아까처럼 만들어준후 비트반전을 할까. 아래와 같이 연산하면 ‘bit’번째 바이트를 0으로 확정시킬 수 있겠다.
byte&=~(0x80>>(bit))

된 거 같다! 이거 매크로 함수로 해결 되겠다!

#define SET1(BYTE,BIT,VAL) if(VAL){*(BYTE)|=((0x80)>>(BIT%8));} else{*(BYTE)&=~((0x80)>>(BIT%8));}

깔끔하게 해결되었다!

결국 RGB1은 만들어놓고 쓰지 않았다. 인덱스에 맞춰 각 멤버함수를 연결시키느니 이게 더 빠르긴하겠다…

unsigned char *a = (unsigned char*)(_PIXEL(img,x,y));
unsigned char *b = (unsigned char*)(_PIXEL(img,(img->bi.width-x-1),y));
unsigned char a_, b_;
if(img->bi.bitCount==1){
    b_ = GET1(*a,x);
    a_ = GET1(*b,img->bi.width-x-1);
    SET1((unsigned char*)a,x,a_)
    SET1((unsigned char*)b,(img->bi.width-x-1),b_)
}
else // 4bit

이렇게 하면 중앙선을 기준으로 대칭인 pixel끼리 pixel data swap이 가능하겠다. 4비트도 같은 방법으로 구현하면 된다.

4. Restructuring

이건 못참지

또 고질병이 돋았다. 급하게 구현한다고 구현해놓은 구조가 마음에 들지 않아 좀 뜯어 고쳤다. 아래는 새로 고친 invert.c이다.

#include "bitmap.h"

void invRGB16(void* p){ *((unsigned short*)p)=~*((unsigned short*)p); }
void invTriple(void* p){
    ((RGBTRIPLE*)p)->rgbtRed=~(((RGBTRIPLE*)p)->rgbtRed);
    ((RGBTRIPLE*)p)->rgbtGreen=~(((RGBTRIPLE*)p)->rgbtGreen);
    ((RGBTRIPLE*)p)->rgbtBlue=~(((RGBTRIPLE*)p)->rgbtBlue);
}
void invQuad(void* p){
    ((RGBQUAD*)p)->rgbAlpha=~(((RGBQUAD*)p)->rgbAlpha);
    ((RGBQUAD*)p)->rgbRed=~(((RGBQUAD*)p)->rgbRed);
    ((RGBQUAD*)p)->rgbGreen=~(((RGBQUAD*)p)->rgbGreen);
    ((RGBQUAD*)p)->rgbBlue=~(((RGBQUAD*)p)->rgbBlue);
}

int invert(char* src, char* dst){
    IMAGE img;
    void (*inv)(void*);
    
    if(!readImage(&img,src)) return 0;
    switch(img.bi.bitCount){
        case 1: case 4: case 8: 
            for(RGBQUAD* p=((RGBQUAD*)(img.extra))+(img.bi.clrUsed-1);p>=(RGBQUAD*)(img.extra);--p)
                invQuad(p);
        goto TERMINATE; break;
        case 16: inv=invRGB16; break;
        case 24: inv=invTriple; break;
        case 32: inv=invQuad; break;
        default: goto ERROR; break;
    }

    for (int y=img.bi.height-1; y>=0; --y)
        for (int x=img.bi.width-1; x>=0; --x)
            inv(PIXEL(img,x,y));
    
TERMINATE:
    if(!writeImage(&img,dst)) goto ERROR;
    freeImage(&img);
    return 1;

ERROR:
    freeImage(&img);
    return 0;
}

다시보니 굳이 왜 바꿨나 싶기도 하다. 그래도 재사용 되는 코드를 줄였고, 함수 안에 있던 반복문을 바깥으로 빼 좀더 깔끔해보인다. 물론 덕분에 함수의 호출횟수는 1회에서 픽셀의 갯수만큼으로 증가했지만, 함수 자체는 매우 가벼워서 inline으로 컴파일러 최적화도 기대할수 있을듯 싶어 딱히 오버헤드가 걱정되지는 않는다.

보다시피 인수의 갯수를 최소화하고 픽셀데이터에 직접 접근하기 위해, 해당 인덱스의 픽셀데이터의 주소값을 직접 받는다. 또, switch-case문에서 색 깊이를 먼저 판별하여 어떤 함수를 실행할지 함수포인터를 통해 저장하여, 반복문에 switch case가 들어가서 매번 판별하는 불상사를 막았다. 함수인자의 자료형을 통일해야하기 때문에 부득이 void*으로 통일하였다.

색 깊이 1,4,8의 경우 모든 픽셀 데이터가 아니라 색상 테이블만 처리해주면 되기때문에 반복문이 달라진다. 색상테이블만 후딱 훑은 뒤 바로 마무리짓도록 goto문으로 따로 빼주었다. 아직, 여기까진 goto문이 남용되고 있는 것 같진않다. 이정도가 건강한 goto문의 사용이지 않을까 싶다.

contrastgray 역시 색상변조이기에 똑같은 구조로 바꿔줄수 있었다. toASCIImirror의 경우 픽셀 데이터 접근이 필요한데, 잘 알다시피 1bit와 4bit에서의 픽셀 데이터 접근이 똥망이기 때문에 조금 지저분해졌다.

int mirror(char* src, char* dst){
    IMAGE img; 
    
    if(!readImage(&img,src)) return 0;

    void (*swap)(void*,void*);
    void (*swap_low)(void*,void*,int,int);
    switch(img.bi.bitCount){
        case 1: swap_low=swap1; goto PALETTE; break;
        case 4: swap_low=swap4; goto PALETTE; break;
        case 8: swap=swap8; break;
        case 16: swap=swap16; break;
        case 24: swap=swap24; break;
        case 32: swap=swap32; break;
        default: goto ERROR; break;
    }

    for (int y=img.bi.height-1; y>=0; --y)
        for (int x=((img.bi.width>>1)-1); x>=0; --x)
            swap(PIXEL(img,x,y),PIXEL(img,(img.bi.width-x-1),y));
    goto TERMINATE;

PALETTE:
    for (int y=img.bi.height-1; y>=0; --y)
        for (int x=((img.bi.width>>1)-1); x>=0; --x)
            swap_low(PIXEL(img,x,y),PIXEL(img,(img.bi.width-x-1),y),x,(img.bi.width-x-1));

TERMINATE:
    if(!writeImage(&img,dst)) goto ERROR;
    freeImage(&img);
    return 1;

ERROR:
    freeImage(&img);
    return 0;
}

예시로 mirror를 보자. swap1이나 swap4같은 함수들은 열 정보 x가 무조건 필요하다. GET1, SET1, GET4, SET4 때문이다. 이런 함수들을 저장할 함수포인터를 따로 만들었고, PALETTE라는 새로운 레이블을 만들어 분기하여 따로 처리하도록 하였다. goto문이 좀 많아지긴 했지만 아직 나쁘진 않다. 여기서 더 추가하지만 않는다면…


이쯤에서 마무리하고자 한다. 제발

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)