Blog
개발중인 미완성 페이지로, 일부 기능이 동작하지 않을 수 있습니다.

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

아래와 같은 결과를 얻을 수 있다.

생성된 BMP 이미지

주의사항

uint8_t 대신 unsigned char를 쓰는 건 괜찮지만, char를 쓰면 운영체제에 따라 signed/unsigned가 다르기 때문에 운영체제에 따라 다른 결과가 나온다.

C
BMP
fopen
fwrite
exit
EXIT_FAILURE

Comments