BMP 파일 렌더링
이미지를 화면에 띄우려면 보통 라이브러리에 파일 경로를 넘기면 된다. 2023년 게임프로그래밍입문 수업에서 작은 게임 엔진을 만드는 과제를 받았을 때는 그 한 줄 안에서 무슨 일이 일어나는지 궁금했다. 그래서 이미지 파일의 바이트를 직접 읽어 Pygame 화면에 그려 보기로 했다.
처음 고른 형식은 JPG였다. 곧 엔트로피 부호화, 양자화와 색 공간 변환까지 알아야 한다는 사실을 알았다. 한 학기 과제로 다루기에는 범위가 너무 컸다. 압축되지 않은 24비트 BMP 하나를 읽는 것으로 목표를 줄였다. 지금 돌아보면 이 결정이 과제에서 가장 중요했다. 이미지 디코더 전체를 어설프게 흉내 내기보다 파일 형식과 화면 출력 사이의 연결을 직접 확인할 수 있었기 때문이다.
헤더 탐색
사용한 BMP 파일은 파일 헤더 14바이트와 40바이트짜리 DIB 헤더 뒤에 픽셀 데이터가 놓이는 형식이었다. 코드에서는 너비와 높이, 색 깊이와 압축 방식을 정해진 위치에서 읽었다. Python의 struct.unpack()으로 2바이트와 4바이트 정수를 변환했고, 색 깊이가 24비트가 아니거나 압축 방식이 0이 아니면 처리하지 않았다.
file.read(18)
width = struct.unpack("I", file.read(4))[0]
height = struct.unpack("I", file.read(4))[0]
file.read(2)
bit_depth = struct.unpack("H", file.read(2))[0]
compression = struct.unpack("I", file.read(4))[0]
if bit_depth != 24 or compression != 0:
raise NotImplementedError
읽을 범위를 제한하니 픽셀 하나는 단순해졌다. 24비트 BMP에서는 파랑, 초록, 빨강 값이 각각 1바이트씩 BGR 순서로 저장된다. 세 바이트를 읽은 다음 Pygame에서 사용할 RGB 순서로 바꿔 2차원 리스트에 담았다.
b, g, r = struct.unpack("BBB", file.read(3))
row.append((r, g, b))
파일에 들어 있는 첫 번째 행은 화면의 아래쪽 행이었다. 그대로 그리면 이미지가 위아래로 뒤집히므로 출력할 때 pixels[height - y - 1][x]처럼 세로 인덱스를 반전했다. 예제로 사용한 의자 사진을 Pygame과 Windows 사진 앱에서 각각 열어 같은 방향과 색으로 보이는 것도 확인했다.

직접 읽은 픽셀을 Pygame 창에 그린 결과.

같은 파일을 Windows 사진 앱에서 열어 방향과 색을 비교했다.
예제 파일 밖의 입력 문제
당시에는 사진이 제대로 나타난 것만 보고 로더가 완성됐다고 생각했다. 코드를 다시 읽어 보니 예제 파일이 구현의 빈틈을 우연히 피하고 있었다.
BMP의 각 행은 크기가 4바이트의 배수가 되도록 뒤에 패딩이 붙을 수 있다. 내가 사용한 이미지는 가로 1280픽셀이고 한 픽셀은 3바이트라서 한 행이 3840바이트였다. 이미 4의 배수였기 때문에 패딩이 없었다. 원래 코드는 행이 끝날 때 패딩을 건너뛰지 않았지만 이 파일에서는 문제가 나타나지 않았다. 가로 길이가 다른 사진을 넣었다면 첫 행 이후부터 색과 위치가 어긋났을 것이다.
픽셀 데이터가 시작되는 위치도 파일 헤더의 오프셋을 읽지 않고 항상 54바이트라고 가정했다. 너비와 높이는 부호 없는 정수로 해석했고, 파일이 BM으로 시작하는지도 확인하지 않았다. 이 코드는 BMP 전반을 읽는 로더라기보다 40바이트 DIB 헤더를 쓰는 압축되지 않은 24비트 BMP 중 일부를 읽는 코드에 가까웠다.
다시 구현한다면 먼저 파일 서명과 픽셀 오프셋을 헤더에서 읽고, 너비와 높이는 리틀 엔디언 부호 있는 정수로 해석하겠다. 한 행을 읽은 뒤에는 다음 식으로 구한 패딩만큼 이동해야 한다.
row_size = width * 3
padding = (4 - row_size % 4) % 4
음수 높이로 표시되는 위에서 아래 방향의 BMP와 잘린 파일도 따로 처리해야 한다. 이런 조건을 몇 가지 작은 테스트 이미지로 확인해야 비로소 지원 범위를 말할 수 있다.
직접 구현에서 얻은 교훈
이 코드를 실제 프로그램에 다시 쓸 일은 거의 없다. Pillow나 Pygame의 이미지 로더가 더 많은 형식과 예외를 이미 처리한다. 그래도 파일의 바이트가 헤더를 거쳐 너비와 높이가 되고, BGR 값이 화면의 RGB 픽셀로 이어지는 과정을 한 번 직접 따라가 본 것은 의미가 있었다.
예제 하나의 성공이 구현의 정확성을 보장하지 않는다는 점도 뒤늦게 보였다. 당시에는 화면에 사진이 뜨는 순간을 결과로 삼았다. 지금 같은 작업을 한다면 먼저 지원할 형식의 범위를 정하고 그 경계에 있는 입력부터 시험하겠다.