C BMP 직렬화, 출력
2024. 6. 15.|2024. 10. 6.
C로 bmp 파일 출력을 직렬화를 구현해보자
목차
목표
bmp.h
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
typedef struct bmp_pixel {
uint8_t r;
uint8_t g;
uint8_t b;
} bmp_pixel_t;
typedef struct bmp {
size_t width;
size_t height;
bmp_pixel_t extra[];
} bmp_t;
/**
* @brief Serialize bmp
*
* @param self target image
* @param out serialized result
* @param out_length result length
* @return true on failure
* @return false on success
*/
bool serialize_bmp(bmp_t *self, char **out, size_t *out_length);
이런 serialize_bmp 함수를 구현하는 것을 목표로 하자
출력할 BMP 파일 구조
자세한 파일 구조는 생략한다. 그럴 것 같지 않지만, 정 궁금하다면 위키백과를 참고하자.
일단 여기서 출력할 BMP 파일의 구조는 대충 아래와 같다.
--- 공통 (14바이트) ---
0~1: 글자 'B'
1~2: 글자 'M'
2~6: 이 파일의 크기 (리틀 엔디안) = 54 + 이미지 크기
6~10: 0
10~14: 이미지 데이터가 시작하는 오프셋 (리틀 엔디안)
--- BITMAPINFOHEADER 헤더 (40바이트) ---
14~18: 이 헤더의 크기 (40)
18~22: 이미지 너비 (리틀 엔디안)
22~26: 이미지 높이 (리틀 엔디안)
26~28: 1 (리틀 엔디안)
28~30: 24 (픽셀 당 비트 수)
30~34: 0 (압축 타입)
34~38: 이미지 데이터 크기
38~54: 0 (쓰이지 않음. 아무 값이나)
--- 이미지 데이터 ---
줄 단위 패딩이 들어간 이미지 데이터
픽셀당 3바이트씩 출력하되, 한 줄의 크기가 4의 배수가 되도록 0~3바이트의 패딩을 추가함
소스 코드
bmp.c
#include "bmp.h"
#include <stdlib.h>
static uint32_t u32_to_le(uint32_t u32) {
const uint32_t test = 42;
const char *const source = (const char *)&u32;
uint32_t result;
char *const dest = (char *)&result;
if (*((char *)&test))
return (u32);
dest[0] = source[3];
dest[1] = source[2];
dest[2] = source[1];
dest[3] = source[0];
return (result);
}
static uint16_t u16_to_le(uint16_t u16) {
const uint16_t test = 42;
const char *const source = (const char *)&u16;
uint16_t result;
char *const dest = (char *)&result;
if (*((char *)&test))
return (u16);
dest[0] = source[1];
dest[1] = source[0];
return (result);
}
bool serialize_bmp(bmp_t *self, char **out, size_t *out_length) {
const size_t row_padding = (4 - (self->width * 3) % 4) % 4;
const size_t row_size = self->width * 3 + row_padding;
const size_t whole_size = row_size * self->height;
const size_t length = 54 + whole_size;
char *const result = malloc(length);
if (!result)
return (true);
// header
result[0] = 'B';
result[1] = 'M';
*((uint32_t *)(result + 2)) = u32_to_le(54 + (uint32_t)whole_size);
*((uint32_t *)(result + 6)) = u32_to_le(0);
*((uint32_t *)(result + 10)) = u32_to_le(54);
*((uint32_t *)(result + 14)) = u32_to_le(40);
*((uint32_t *)(result + 18)) = u32_to_le((uint32_t)self->width);
*((uint32_t *)(result + 22)) = u32_to_le((uint32_t)self->height);
*((uint16_t *)(result + 26)) = u16_to_le(1);
*((uint16_t *)(result + 28)) = u16_to_le(24);
*((uint32_t *)(result + 30)) = u32_to_le(0);
*((uint32_t *)(result + 34)) = u32_to_le((uint32_t)whole_size);
*((uint32_t *)(result + 38)) = u32_to_le(0);
*((uint32_t *)(result + 42)) = u32_to_le(0);
*((uint32_t *)(result + 46)) = u32_to_le(0);
*((uint32_t *)(result + 50)) = u32_to_le(0);
// image data
size_t offset = 54;
for (size_t y = self->height - 1; y != (size_t)-1; y--) { // per line
for (size_t x = 0; x < self->width; x++) {
*((uint8_t *)&result[offset++]) = self->extra[self->width * y + x].b;
*((uint8_t *)&result[offset++]) = self->extra[self->width * y + x].g;
*((uint8_t *)&result[offset++]) = self->extra[self->width * y + x].r;
}
// padding
for (size_t i = 0; i < row_padding; i++)
result[offset++] = 0;
}
*out_length = length;
*out = result;
return (false);
}
특별할 건 없다. 그냥 할당받아서 내용 채우고 끝.
결과
main.c
#include <stdio.h>
#include <stdlib.h>
#include "bmp.h"
static char buf[sizeof(bmp_t) + sizeof(bmp_pixel_t) * 256 * 256];
int main(int argc, char *argv[]) {
// arguments validation
if (argc != 2)
exit(EXIT_FAILURE);
// dummy image
bmp_t *bmp = (void *)&buf[0];
bmp->width = 256;
bmp->height = 256;
for (size_t i = 0; i < 256; i++) {
for (size_t j = 0; j < 256; j++) {
size_t index = i * 256 + j;
bmp->extra[index].r = i;
bmp->extra[index].g = j;
bmp->extra[index].b = 255;
}
}
// serialize
char *result;
size_t length;
if (serialize_bmp(bmp, &result, &length)) {
exit(EXIT_FAILURE);
}
// print
FILE *fp = fopen(argv[1], "wb");
if (!fp)
exit(EXIT_FAILURE);
if (fwrite(result, 1, length, fp) != length)
exit(EXIT_FAILURE);
}
이 파일과 함께 아래의 CMakeLists.txt로 빌드해 아래 스크립트로 실행하면
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(bmp)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR})
add_executable(dummy_bmp main.c bmp.c)
build.sh
#!/bin/sh
set -e
cmake -DCMAKE_BUILD_TYPE=Release -B builddir
cmake --build builddir --config Release
builddir/dummy_bmp ./result.bmp
아래와 같은 결과를 얻을 수 있다.

주의사항
uint8_t 대신 unsigned char를 쓰는 건 괜찮지만, char를 쓰면 운영체제에 따라 signed/unsigned가 다르기 때문에 운영체제에 따라 다른 결과가 나온다.
C
BMP
fopen
fwrite
exit
EXIT_FAILURE