73. 비트맵 파일
1. 키워드
- 비트맵 파일(
.bmp
)
2. 비트맵 파일을 아스키 아트로 변환하기
- 아스키 아트는 보통 프로그램을 사용하여 원본 그림 파일을 아스키 아트로 변환해서 만든다.
- 비트맵 그림 파일의 픽셀 정보를 읽어서 아스키 아트로 출력하는 프로그램을 만들어보자.
3. 비트맵 파일 포맷 알아보기
- 비트맵 파일은 바이너리 형식이므로 메모장 등 텍스트 편집기로 열어도 내용을 알아볼 수 없다.
- 따라서 비트맵 파일에서 픽셀 정보를 읽으려면 먼저 비트맵 파일의 구조를 알아야 한다.
- 다음은 비트맵 파일의 구조이며 비트맵 파일은 픽셀 하나를 몇 비트로 저장하느냐에 따라 구조가 달라진다.
- 비트맵 파일 헤더는 비트맵 파일 식별 정보, 파일 크기, 데이터 위치 등의 정보를 담고 있으며 DIB(Device Independent Bitmap) 헤더는 가로, 세로 크기, 해상도, 픽셀의 비트 수 등 그림의 자세한 정보를 담고 있다.
- 픽셀 데이터에는 그림 파일의 실제 색상 정보가 들어간다.
- 단, 픽셀당 색상 수가 16비트 미만일 때는 색상 테이블에 따로 색상 정보를 저장하고, 픽셀 데이터에서는 색상 테이블의 인덱스를 저장한다.
- 다음은 비트맵 파일 헤더(
BITMAPFILEHEADER
)의 구조이다.
- 다음은 비트맵 정보 헤더(
BITMAPINFOHEADER
)의 구조이다.
- 24비트 비트맵은 픽셀(
RGBTRIPLE
)을 파랑(B
), 초록(G
), 빨강(R
) 순서로 저장하며 각 색상의 크기는1
바이트이다. - 따라서 픽셀당
3
바이트를 사용한다.
- 즉, 우리가 화면에서 보는 24비트 비트맵 파일의 픽셀은
3
바이트로 되어 있다.
- 여기서 색상이 파랑, 초록, 빨강 3색이라서
RGBTRIPLE
구조체의 이름에TRIPLE
이 들어간다. - 그리고 구조체 멤버에서
rgb
뒤에 붙는t
도triple
을 의미한다.
4. 비트맵 구조체 작성하기
- 비트맵 파일의 구조를 알아봤으니 이제 비트맵 구조체를 작성한다.
- 이때 주의할 점은 반드시 구조체를
1
바이트 크기로 정렬해야 한다는 점이다. - 즉, 비트맵 파일에서 각 정보는 위치와 크기가 정확하게 정해져 있으므로 반드시 구조체의 크기와 형태 그대로 읽어야 한다.
#pragma pack(push, 1) // 구조체를 1바이트 크기로 정렬
typedef struct _BITMAPFILEHEADER // BMP 비트맵 파일 헤더 구조체
{
unsigned short bfType; // BMP 파일 매직 넘버
unsigned int bfSize; // 파일 크기
unsigned short bfReserved1; // 예약
unsigned short bfReserved2; // 예약
unsigned int bfOffBits; // 비트맵 데이터의 시작 위치
} BITMAPFILEHEADER;
typedef struct _BITMAPINFOHEADER // BMP 비트맵 정보 헤더 구조체(DIB 헤더)
{
unsigned int biSize; // 현재 구조체의 크기
int biWidth; // 비트맵 이미지의 가로 크기
int biHeight; // 비트맵 이미지의 세로 크기
unsigned short biPlanes; // 사용하는 색상판의 수
unsigned short biBitCount; // 픽셀 하나를 표현하는 비트 수
unsigned int biCompression; // 압축 방식
unsigned int biSizeImage; // 비트맵 이미지의 픽셀 데이터 크기
int biXPelsPerMeter; // 그림의 가로 해상도(미터당 픽셀)
int biYPelsPerMeter; // 그림의 세로 해상도(미터당 픽셀)
unsigned int biClrUsed; // 색상 테이블에서 실제 사용되는 색상 수
unsigned int biClrImportant; // 비트맵을 표현하기 위해 필요한 색상 인덱스 수
} BITMAPINFOHEADER;
typedef struct _RGBTRIPLE // 24비트 비트맵 이미지의 픽셀 구조체
{
unsigned char rgbtBlue; // 파랑
unsigned char rgbtGreen; // 초록
unsigned char rgbtRed; // 빨강
} RGBTRIPLE;
#pragma pack(pop)
- 24비트 비트맵을 처리할 것이므로 비트맵 파일 헤더, 비트맵 정보 헤더, 픽셀 구조체만 정의했다.
- 왜냐하면 24비트 비트맵은 색상 테이블을 사용하지 않으므로 색상 테이블 구조체는 정의하지 않아도 된다.
5. 픽셀을 아스키 문자로 저장하기
- 이제 앞에서 정의한 각 구조체를 사용하여 이미지의 픽셀에 접근해 보고, 각 픽셀을 ASCII 문자로 저장해 보자.
- 다음은 비트맵 파일을 ASCII 문자로 저장하는 과정이다.
- 먼저 다음과 같이 자주 사용하는 고정값은 매크로로 정의한다.
- 코드에서
3
과4
를 그대로 사용하면3
과4
가 계속 나올 때마다 이 숫자는 무엇을 뜻하는지 한 번 더 생각해야 한다. - 하지만
PIXEL_SIZE
,PIXEL_ALIGN
과 같이 이름을 정해 주면 의미가 명확해지므로 코드가 읽기 쉬워진다.
1] PIXEL_SIZE
- 픽셀 한 개의 크기이다.
- 24비트 비트맵은 파랑(
B
), 초록(G
), 빨강(R
)을1
바이트씩 저장하므로3
바이트이다.
2] PIXEL_ALIGN
- 픽셀 데이터의 가로 한 줄에서 남는 공간을 구하기 위한 정렬값이다.
- 비트맵 포맷은 픽셀 데이터의 가로 한 줄을 저장할 때
4
의 배수 크기로 저장한다.
- 이제
main
함수에서 비트맵 파일 포인터, 출력 결과를 저장할 텍스트 파일 포인터, 비트맵 파일 헤더 구조체 변수, 비트맵 정보 헤더 구조체 변수를 선언한다. - 그리고 픽셀 데이터를 읽기 위한 포인터, 픽셀 데이터 크기, 비트맵 이미지의 가로와 세로 크기, 픽셀 데이터의 가로 크기가
4
의 배수가 아닐 때 남는 공간을 저장할 변수를 선언한다.
int main()
{
FILE *fpBmp; // 비트맵 파일 포인터
FILE *fpTxt; // 텍스트 파일 포인터
BITMAPFILEHEADER fileHeader; // 비트맵 파일 헤더 구조체 변수
BITMAPINFOHEADER infoHeader; // 비트맵 정보 헤더 구조체 변수
unsigned char *image; // 픽셀 데이터 포인터
int size; // 픽셀 데이터 크기
int width, height; // 비트맵 이미지의 가로, 세로 크기
int padding; // 픽셀 데이터의 가로 크기가 4의 배수가 아닐 때 남는 공간의 크기
- 각 픽셀을 표현할 ASCII 문자를 배열로 만든다.
- 비트맵 이미지에서 픽셀의 RGB 색상값이 모두
0
이면 검정색이고, 모두255
이면 흰색이다. - 즉, 값이 작을수록 어두워지고 값이 클수록 밝아진다.
- 따라서 배열의 낮은 인덱스에는 획수가 많은 문자를 배치하고 높은 인덱스에는 획수가 적은 문자를 배치한다.
- 그리고 가장 큰 인덱스에는
' '
(공백 문자)를 넣는다.
// 각 픽셀을 표현할 ASCII 문자. 인덱스가 높을 수록 밝아지는 것을 표현
char ascii[] = {'#', '#', '@', '%', '=', '+', '*', ':', '-', '.', ' '}; // 11개
fopen
함수로 비트맵 파일을 바이너리 모드(rb
)로 연 뒤fread
함수로 비트맵 파일 헤더를 읽는다.- 그다음에 비트맵 파일이 맞는지 확인하기 위해
bfType
의 값이'MB'
가 맞는지 확인한다. - 이때
2
바이트 크기의'BM'
을 리틀 엔디언 방식으로 읽었으므로'B'
와'M'
이 뒤집혀서'MB'
가 된다. - 만약 이 값이 맞지 않으면 비트맵 파일이 아니다.
fpBmp = fopen("Peppers80x80.bmp", "rb"); // 비트맵 파일을 바이너리 모드로 열기
if (fpBmp == NULL) // 파일 열기에 실패하면
return 0; // 프로그램 종료
// 비트맵 파일 헤더 읽기. 읽기에 실패하면 파일 포인터를 닫고 프로그램 종료
if (fread(&fileHeader, sizeof(BITMAPFILEHEADER), 1, fpBmp) < 1)
{
fclose(fpBmp);
return 0;
}
// 매직 넘버가 MB가 맞는지 확인(2바이트 크기의 BM을 리틀 엔디언으로 읽었으므로 MB가 됨)
// 매직 넘버가 맞지 않으면 프로그램 종료
if (fileHeader.bfType != 'MB')
{
fclose(fpBmp);
return 0;
}
fread
함수로sizeof(BITMAPFILEHEADER)
만큼 한 번 읽었을 때 읽기에 성공하면 읽은 횟수1
을 반환하고, 실패하면1
보다 작은 수를 반환한다.- 따라서 파일 읽기에 실패했을 때는 파일 포인터를 닫고 프로그램을 종료한다.
- 이후에도 각종 실패 상황이나 조건에 맞지 않으면 앞에서 열었던 파일 포인터를 닫고 프로그램을 종료해 준다.
- 이번에는 비트맵 정보 헤더를 읽는다.
- 비트맵 정보 헤더는 비트맵 파일 헤더 바로 뒤에 있으므로 파일 포인터를 이동시키지 않고 바로
fread
함수로 읽으면 된다. - 비트맵 정보 헤더를 읽었으면
biBitCount
의 값이24
인지 확인한다. - 만약
24
가 아니면 파일 포인터를 닫고 프로그램을 종료한다.
// 비트맵 정보 헤더 읽기. 읽기에 실패하면 파일 포인터를 닫고 프로그램 종료
if (fread(&infoHeader, sizeof(BITMAPINFOHEADER), 1, fpBmp) < 1)
{
fclose(fpBmp);
return 0;
}
// 24비트 비트맵이 아니면 프로그램 종료
if (infoHeader.biBitCount != 24)
{
fclose(fpBmp);
return 0;
}
- 픽셀 데이터의 크기, 이미지의 가로, 세로 크기와 같이 자주 사용하는 정보는 변수에 따로 저장해놓는다.
- 매번 구조체에서 끌어다 써도 상관없지만 변수에 저장해놓고 사용하면 코드가 좀 더 간단해진다.
size = infoHeader.biSizeImage; // 픽셀 데이터 크기
width = infoHeader.biWidth; // 비트맵 이미지의 가로 크기
height = infoHeader.biHeight; // 비트맵 이미지의 세로 크기
- 이번에는 픽셀 데이터의 가로 공간이 저장될 때 남는 공간을 구해야 한다.
- 비트맵 포맷은 픽셀의 가로 한 줄을 저장할 때
4
의 배수 크기로 저장하는데, 만약 가로 한 줄의 크기가4
의 배수가 아니라면 남는 공간은0
으로 채워서 저장한다. - 따라서 픽셀 데이터를 읽기 위해서는 남는 공간을 알고 있어야 한다.
- 이런 방식을 사용하는 이유는 CPU가 데이터를 처리할 때
4
바이트 크기가 효율적이기 때문이다. - 예를 들어
3
,7
,11
처럼4
로 나누어 떨어지지 않는 크기보다는4
,8
,12
처럼4
로 나누어 떨어지는 크기가 효율적이다. - 데이터 크기를
4
의 배수로 맞추고0
을 채워서 저장 공간이 좀 늘더라도 성능을 위해 이렇게 만들어져 있다.
- 남는 공간을 구하기 전에 먼저 나머지를 구해야 하는데, 계산식은
(가로 크기 * PIXEL_SIZE) % PIXEL_ALIGN
이 된다. - 예를 들어 가로 크기가
27
일 때 나머지를 구하면 다음과 같다.
- 나머지는
1
이 나왔다. - 여기서 남는 공간을 구하려면 배수
4
에서1
을 빼면 된다.
- 따라서 남는 공간은
3
이 된다. - 만약 가로 크기가
20
이라면 어떻게 되는지 확인해 보자.
- 나머지가
0
이 나왔다. - 하지만 이렇게 되면
4
에서0
을 뺐을 때4
가 나오므로 남는 공간이0
이 아닌4
가 되어버린다.
- 따라서 나머지가
0
일 때를 대비해서PIXEL_ALIGN - 나머지
값을 다시 한 번PIXEL_ALIGN
으로 나머지 연산을 해준다.
- 이제 최종으로 남는 공간이
0
이 나왔다. - 만약
PIXEL_ALIGN - 나머지
가PIXEL_ALIGN - 1
이라 하더라도3 % 4
가 되므로3
을 그대로 구할 수 있다.
- 지금까지 알아본 계산식을 코드로 만들면 다음과 같이 된다.
// 이미지의 가로 크기에 픽셀 크기를 곱하여 가로 한 줄의 크기를 구하고 4로 나머지를 구함
// 그리고 4에서 나머지를 빼주면 남는 공간을 구할 수 있음.
// 만약 남는 공간이 0이라면 최종 결과가 4가 되므로 여기서 다시 4로 나머지를 구함
padding = (PIXEL_ALIGN - ((width * PIXEL_SIZE) % PIXEL_ALIGN)) % PIXEL_ALIGN;
- 비트맵 파일 중에 픽셀 데이터의 크기
size
가0
인 경우가 있다. - 이때는 이미지의 가로 크기에 픽셀 크기를 곱한 뒤 남는 공간을 더해주면 완전한 가로 한 줄의 크기를 구할 수 있다.
- 그리고 여기에 이미지의 세로 크기를 곱해주면 픽셀 데이터의 크기를 구할 수 있다.
if (size == 0) // 픽셀 데이터 크기가 0이라면
{
// 이미지의 가로 크기 * 픽셀 크기에 남는 공간을 더해주면 완전한 가로 한 줄 크기가 나옴
// 여기에 이미지의 세로 크기를 곱해주면 픽셀 데이터의 크기를 구할 수 있음
size = (width * PIXEL_SIZE + padding) * height;
}
- 픽셀 데이터의 크기
size
가0
일 때는size
를 다시 구했고,0
이 아니라면 원래 값을 쓰면 된다. size
를 이용하여image
포인터에 픽셀 데이터의 크기만큼 동적 메모리를 할당한다.
- 이제 비트맵 이미지의 픽셀 데이터를 읽는다.
- 24비트 비트맵 파일에서는 비트맵 정보 헤더 바로 다음에 픽셀 데이터가 있어서 바로
fread
함수로 읽어도 된다. - 하지만 여기서는
bfOffBits
값을 활용하여 파일 포인터를 픽셀 데이터의 시작 위치로 이동시켰다.
// 파일 포인터를 픽셀 데이터의 시작 위치로 이동
fseek(fpBmp, fileHeader.bfOffBits, SEEK_SET);
// 파일에서 픽셀 데이터 크기만큼 읽음. 읽기에 실패하면 파일 포인터를 닫고 프로그램 종료
if (fread(image, size, 1, fpBmp) < 1)
{
fclose(fpBmp);
return 0;
}
fclose(fpBmp); // 비트맵 파일 닫기
- 결과 출력용 테스트 파일을 쓰기 모드(
w
)로 연다. - 변환된 ASCII 문자를 콘솔(명령 프롬프트, 터미널)에 바로 출력해도 되지만, 여기서는 파일에 저장한다.
- 만약 파일 열기에 실패하면 픽셀 데이터를 저장한 동적 메모리를 해제하고 프로그램을 종료한다.
fpTxt = fopen("ascii.txt", "w"); // 결과 출력용 텍스트 파일 열기
if (fpTxt == NULL) // 파일 열기에 실패하면
{
free(image); // 픽셀 데이터를 저장한 동적 메모리 해제
return 0; // 프로그램 종료
}
- 이제 픽셀 데이터를 ASCII 문자로 변환하는 부분이다.
// 픽셀 데이터는 아래 위가 뒤집혀서 저장되므로 아래쪽부터 반복
// 세로 크기만큼 반복
for (int y = height - 1; y >= 0; y--)
{
// 가로 크기만큼 반복
for (int x = 0; x < width; x++)
{
// 일렬로 된 배열에 접근하기 위해 인덱스를 계산
// (x * 픽셀 크기)는 픽셀의 가로 위치
// (y * (세로 크기 * 픽셀 크기))는 픽셀이 몇 번째 줄인지 계산
// 남는 공간 * y는 줄별로 누적된 남는 공간
int index = (x * PIXEL_SIZE) + (y * (width * PIXEL_SIZE)) + (padding * y);
// 현재 픽셀의 주소를 RGBTRIPLE 포인터로 변환하여 RGBTRIPLE 포인터에 저장
RGBTRIPLE *pixel = (RGBTRIPLE *)&image[index];
// RGBTRIPLE 구조체로 파랑, 초록, 빨강값을 가져옴
unsigned char blue = pixel->rgbtBlue;
unsigned char green = pixel->rgbtGreen;
unsigned char red = pixel->rgbtRed;
// 파랑, 초록, 빨강값의 평균을 구하면 흑백 이미지를 얻을 수 있음
unsigned char gray = (red + green + blue) / PIXEL_SIZE;
// 흑백값에 ASCII 문자의 개수를 곱한 뒤 256으로 나누면 흑백값에 따라
// ASCII 문자의 인덱스를 얻을 수 있음
char c = ascii[gray * sizeof(ascii) / 256];
// 비트맵 이미지에서는 픽셀의 가로, 세로 크기가 똑같지만
// 보통 ASCII 문자는 세로로 길쭉한 형태이므로 정사각형 모양과 비슷하게 보여주기 위해
// 같은 문자를 두 번 저장해줌
fprintf(fpTxt, "%c%c", c, c); // 텍스트 파일에 문자 출력
}
fprintf(fpTxt, "\n"); // 가로 픽셀 저장이 끝났으면 줄바꿈을 해줌
}
fclose(fpTxt); // 텍스트 파일 닫기
free(image); // 픽셀 데이터를 저장한 동적 메모리 해제
- 보통 비트맵 파일의 픽셀 데이터는 아래 위가 뒤집혀서 저장된다.
- 따라서 반복문에서는 그림의 아래쪽부터 처리해야 한다.
- 먼저 세로를 처리해야 하는데 배열의 인덱스는
0
부터 시작하므로 세로의 시작 값은height
가 아닌height - 1
이 된다. - 즉,
height - 1
부터0
까지 감소한다. - 단, 가로는
0
부터 증가해서width
보다 작을 때까지만 반복하므로 따로-1
을 하지 않아도 된다.
// 픽셀 데이터는 아래 위가 뒤집혀서 저장되므로 아래쪽부터 반복
// 세로 크기만큼 반복
for (int y = height - 1; y >= 0; y--)
{
// 가로 크기만큼 반복
for (int x = 0; x < width; x++)
{
- 가로와 세로를 반복하면서 픽셀 정보를 가져온다.
- 이때
image
포인터는 일렬로 된 배열이므로x
와y
,padding
을 이용하여 인덱스를 계산해야 한다. - 먼저
(x * 픽셀 크기)
는 픽셀의 가로 위치이다. - 그리고
(y * (세로 크기 * 픽셀 크기))
는 픽셀이 몇 번째 줄인지 계산한다. - 마지막으로
padding * y
는 누적된 남는 공간이며, 반복할 때마다y
가 줄어서 누적된 남는 공간도 줄어들게 된다. - 이렇게 구한 모든 값을 더해주면 픽셀에 접근할 수 있는 인덱스가 된다.
// 일렬로 된 배열에 접근하기 위해 인덱스를 계산
// (x * 픽셀 크기)는 픽셀의 가로 위치
// (y * (세로 크기 * 픽셀 크기))는 픽셀이 몇 번째 줄인지 계산
// 남는 공간 * y는 줄별로 누적된 남는 공간
int index = (x * PIXEL_SIZE) + (y * (width * PIXEL_SIZE)) + (padding * y);
- 인덱스를 계산했으면
&image[index]
와 같이 배열(포인터)에 인덱스를 지정하여 현재 픽셀에 접근한 뒤 앞에&
를 붙여서 현재 픽셀의 메모리 주소를 구한다. - 그리고
RGBTRIPLE
구조체로 선언한 포인터에 저장해 준다. - 이렇게 하면
RGBTRIPLE
구조체를 이용하여 각 픽셀의 빨강, 초록, 파랑(RGB) 값을 손쉽게 가져올 수 있다.
- 이제 픽셀을 ASCII 문자로 변환해야 하는데 RGB 중 어느 한 값을 사용하는 것보다는 픽셀 값을 흑백으로 바꾼 뒤 변환하는 것이 좀 더 보기가 좋다.
- 흑백 값은 RGB 값의 평균을 구하면 된다.
// RGBTRIPLE 구조체로 파랑, 초록, 빨강값을 가져옴
unsigned char blue = pixel->rgbtBlue;
unsigned char green = pixel->rgbtGreen;
unsigned char red = pixel->rgbtRed;
// 파랑, 초록, 빨강 값의 평균을 구하면 흑백 이미지를 얻을 수 있음
unsigned char gray = (red + green + blue) / PIXEL_SIZE;
- 흑백 값을 얻었으니 이 값을 ASCII 문자로 변환해야 한다.
- 여기서
ascii
배열에 들어있는 ASCII 문자를 사용하려면 흑백 값gray
를 배열의 인덱스로 만들면 된다. - 즉,
unsigned char
의 범위인0~255
를0~10
으로 변환하는 것이다. - 흑백 값
gray
를sizeof(ascii)
로 곱한 뒤 다시256
으로 나누면 된다. gray * sizeof(ascii) / 256
식에 따라gray
가0
,100
,200
,255
일 때 예를 확인해 보자.
gray
값에 따라 적절한 인덱스가 나왔다.- 만약
256
대신255
를 나눠버리면gray
가255
일 때ascii
배열 범위를 벗어나므로 주의해야 한다.
gray
를ascii
배열의 인덱스로 변환한 뒤 ASCII 문자를 가져오는 방법을 코드로 나타내면 다음과 같이 된다.
// 흑백값에 ASCII 문자의 개수를 곱한 뒤 256으로 나누면 흑백값에 따라
// ASCII 문자의 인덱스를 얻을 수 있음
char c = ascii[gray * sizeof(ascii) / 256];
- 이제
fprintf
함수에 서식을 지정하여 ASCII 문자를 파일에 저장한다.
// 비트맵 이미지에서는 픽셀의 가로, 세로 크기가 똑같지만
// 보통 ASCII 문자는 세로로 길쭉한 형태이므로 정사각형 모양과 비슷하게 보여주기 위해
// 같은 문자를 두 번 저장해줌
fprintf(fpTxt, "%c%c", c, c); // 텍스트 파일에 문자 출력
}
- 픽셀을 ASCII 문자로 저장할 때 문자 하나로만 저장하면 정사각형 비트맵 이미지라도 막상 출력해 보면 세로로 길쭉한 직사각형으로 보인다.
- 왜냐하면 보통 ASCII 문자는 세로로 길쭉한 형태이기 때문에 가로, 세로를 같은 개수로 출력하더라도 세로로 길쭉한 형태가 된다.
- 따라서 정사각형 모양과 비슷하게 보여주기 위해 같은 문자를 두 번 저장해 준다.
- 가로 픽셀 저장이 끝났으면
\n
을 저장해서 줄바꿈을 해준다.
- 마지막으로 텍스트 파일(
fpTxt
)을 닫고, 픽셀 데이터를 저장한 동적 메모리(image
)를 해제해 준다.
- 전체 소스 코드는 다음과 같다.
#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h> // fopen, fread, fseek, fprintf, fclose 함수가 선언된 헤더 파일
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
#pragma pack(push, 1) // 구조체를 1바이트 크기로 정렬
typedef struct _BITMAPFILEHEADER // BMP 비트맵 파일 헤더 구조체
{
unsigned short bfType; // BMP 파일 매직 넘버
unsigned int bfSize; // 파일 크기
unsigned short bfReserved1; // 예약
unsigned short bfReserved2; // 예약
unsigned int bfOffBits; // 비트맵 데이터의 시작 위치
} BITMAPFILEHEADER;
typedef struct _BITMAPINFOHEADER // BMP 비트맵 정보 헤더 구조체(DIB 헤더)
{
unsigned int biSize; // 현재 구조체의 크기
int biWidth; // 비트맵 이미지의 가로 크기
int biHeight; // 비트맵 이미지의 세로 크기
unsigned short biPlanes; // 사용하는 색상판의 수
unsigned short biBitCount; // 픽셀 하나를 표현하는 비트 수
unsigned int biCompression; // 압축 방식
unsigned int biSizeImage; // 비트맵 이미지의 픽셀 데이터 크기
int biXPelsPerMeter; // 그림의 가로 해상도(미터당 픽셀)
int biYPelsPerMeter; // 그림의 세로 해상도(미터당 픽셀)
unsigned int biClrUsed; // 색상 테이블에서 실제 사용되는 색상 수
unsigned int biClrImportant; // 비트맵을 표현하기 위해 필요한 색상 인덱스 수
} BITMAPINFOHEADER;
typedef struct _RGBTRIPLE // 24비트 비트맵 이미지의 픽셀 구조체
{
unsigned char rgbtBlue; // 파랑
unsigned char rgbtGreen; // 초록
unsigned char rgbtRed; // 빨강
} RGBTRIPLE;
#pragma pack(pop)
#define PIXEL_SIZE 3 // 픽셀 한 개의 크기 3바이트(24비트)
#define PIXEL_ALIGN 4 // 픽셀 데이터 가로 한 줄은 4의 배수 크기로 저장됨
int main()
{
FILE *fpBmp; // 비트맵 파일 포인터
FILE *fpTxt; // 텍스트 파일 포인터
BITMAPFILEHEADER fileHeader; // 비트맵 파일 헤더 구조체 변수
BITMAPINFOHEADER infoHeader; // 비트맵 정보 헤더 구조체 변수
unsigned char *image; // 픽셀 데이터 포인터
int size; // 픽셀 데이터 크기
int width, height; // 비트맵 이미지의 가로, 세로 크기
int padding; // 픽셀 데이터의 가로 크기가 4의 배수가 아닐 때 남는 공간의 크기
// 각 픽셀을 표현할 ASCII 문자. 인덱스가 높을 수록 밝아지는 것을 표현
char ascii[] = {'#', '#', '@', '%', '=', '+', '*', ':', '-', '.', ' '}; // 11개
fpBmp = fopen("Peppers80x80.bmp", "rb"); // 비트맵 파일을 바이너리 모드로 열기
if (fpBmp == NULL) // 파일 열기에 실패하면
return 0; // 프로그램 종료
// 비트맵 파일 헤더 읽기. 읽기에 실패하면 파일 포인터를 닫고 프로그램 종료
if (fread(&fileHeader, sizeof(BITMAPFILEHEADER), 1, fpBmp) < 1)
{
fclose(fpBmp);
return 0;
}
// 매직 넘버가 MB가 맞는지 확인(2바이트 크기의 BM을 리틀 엔디언으로 읽었으므로 MB가 됨)
// 매직 넘버가 맞지 않으면 프로그램 종료
if (fileHeader.bfType != 'MB')
{
fclose(fpBmp);
return 0;
}
// 비트맵 정보 헤더 읽기. 읽기에 실패하면 파일 포인터를 닫고 프로그램 종료
if (fread(&infoHeader, sizeof(BITMAPINFOHEADER), 1, fpBmp) < 1)
{
fclose(fpBmp);
return 0;
}
// 24비트 비트맵이 아니면 프로그램 종료
if (infoHeader.biBitCount != 24)
{
fclose(fpBmp);
return 0;
}
size = infoHeader.biSizeImage; // 픽셀 데이터 크기
width = infoHeader.biWidth; // 비트맵 이미지의 가로 크기
height = infoHeader.biHeight; // 비트맵 이미지의 세로 크기
// 이미지의 가로 크기에 픽셀 크기를 곱하여 가로 한 줄의 크기를 구하고 4로 나머지를 구함
// 그리고 4에서 나머지를 빼주면 남는 공간을 구할 수 있음.
// 만약 남는 공간이 0이라면 최종 결과가 4가 되므로 여기서 다시 4로 나머지를 구함
padding = (PIXEL_ALIGN - ((width * PIXEL_SIZE) % PIXEL_ALIGN)) % PIXEL_ALIGN;
if (size == 0) // 픽셀 데이터 크기가 0이라면
{
// 이미지의 가로 크기 * 픽셀 크기에 남는 공간을 더해주면 완전한 가로 한 줄 크기가 나옴
// 여기에 이미지의 세로 크기를 곱해주면 픽셀 데이터의 크기를 구할 수 있음
size = (width * PIXEL_SIZE + padding) * height;
}
image = malloc(size); // 픽셀 데이터의 크기만큼 동적 메모리 할당
// 파일 포인터를 픽셀 데이터의 시작 위치로 이동
fseek(fpBmp, fileHeader.bfOffBits, SEEK_SET);
// 파일에서 픽셀 데이터 크기만큼 읽음. 읽기에 실패하면 파일 포인터를 닫고 프로그램 종료
if (fread(image, size, 1, fpBmp) < 1)
{
fclose(fpBmp);
return 0;
}
fclose(fpBmp); // 비트맵 파일 닫기
fpTxt = fopen("ascii.txt", "w"); // 결과 출력용 텍스트 파일 열기
if (fpTxt == NULL) // 파일 열기에 실패하면
{
free(image); // 픽셀 데이터를 저장한 동적 메모리 해제
return 0; // 프로그램 종료
}
// 픽셀 데이터는 아래 위가 뒤집혀서 저장되므로 아래쪽부터 반복
// 세로 크기만큼 반복
for (int y = height - 1; y >= 0; y--)
{
// 가로 크기만큼 반복
for (int x = 0; x < width; x++)
{
// 일렬로 된 배열에 접근하기 위해 인덱스를 계산
// (x * 픽셀 크기)는 픽셀의 가로 위치
// (y * (세로 크기 * 픽셀 크기))는 픽셀이 몇 번째 줄인지 계산
// 남는 공간 * y는 줄별로 누적된 남는 공간
int index = (x * PIXEL_SIZE) + (y * (width * PIXEL_SIZE)) + (padding * y);
// 현재 픽셀의 주소를 RGBTRIPLE 포인터로 변환하여 RGBTRIPLE 포인터에 저장
RGBTRIPLE *pixel = (RGBTRIPLE *)&image[index];
// RGBTRIPLE 구조체로 파랑, 초록, 빨강값을 가져옴
unsigned char blue = pixel->rgbtBlue;
unsigned char green = pixel->rgbtGreen;
unsigned char red = pixel->rgbtRed;
// 파랑, 초록, 빨강값의 평균을 구하면 흑백 이미지를 얻을 수 있음
unsigned char gray = (red + green + blue) / PIXEL_SIZE;
// 흑백값에 ASCII 문자의 개수를 곱한 뒤 256으로 나누면 흑백값에 따라
// ASCII 문자의 인덱스를 얻을 수 있음
char c = ascii[gray * sizeof(ascii) / 256];
// 비트맵 이미지에서는 픽셀의 가로, 세로 크기가 똑같지만
// 보통 ASCII 문자는 세로로 길쭉한 형태이므로 정사각형 모양과 비슷하게 보여주기 위해
// 같은 문자를 두 번 저장해줌
fprintf(fpTxt, "%c%c", c, c); // 텍스트 파일에 문자 출력
}
fprintf(fpTxt, "\n"); // 가로 픽셀 저장이 끝났으면 줄바꿈을 해줌
}
fclose(fpTxt); // 텍스트 파일 닫기
free(image); // 픽셀 데이터를 저장한 동적 메모리 해제
return 0;
}