BMP画像の書き出し(簡易版)

作成:

今回は、前回に引き続き、Windowsビットマップ24bitフルカラーに限定した、 簡易的なBMP画像の書き出し方法について説明する。

ソースコードは GitHub にて公開している。 今回説明する、BMP画像の簡易入出力を記述しているのは bmp_simple.c である。 簡易版でない正式版は bmp.c である。 説明についても長くなっているが、簡易版で説明済みの内容も含めて説明しており、 そこから読んでも問題ないようにしているので、 中途半端な簡易版の説明は不要という場合はこのページはスキップしてもらって問題ない。

定義

構造体等の定義については読み込みのところで説明しているので割愛する。 書き出しでも同じものを使う。

画像の書き出し

ファイル名を指定し、書き出す処理。 オープンしたファイルストリームへ書き出す処理へ処理を渡すだけなのでほぼテンプレート。

result_t write_bmp_simple_file(const char *filename, image_t *img) {
  result_t result = FAILURE;
  if (img == NULL) {
    return result;
  }
  FILE *fp = fopen(filename, "wb");
  if (fp == NULL) {
    perror(filename);
    return result;
  }
  result = write_bmp_simple_stream(fp, img);
  fclose(fp);
  return result;
}

次が、オープンしたファイルストリームへBMP画像を書き出す処理だ。 書き出し処理もフォーマットを限定しているとはいえ、かなりシンプルな処理になっている。

result_t write_bmp_simple_stream(FILE *fp, image_t *img) {
  uint8_t header_buffer[DEFAULT_HEADER_SIZE];
  BITMAPFILEHEADER *file = (BITMAPFILEHEADER*)header_buffer;
  BITMAPINFOHEADER *info = (BITMAPINFOHEADER*)(header_buffer + FILE_HEADER_SIZE);
  int x, y;
  int stride;
  uint8_t *row, *buffer;
  if (img->color_type != COLOR_TYPE_RGB) {
    return FAILURE;
  }
  stride = (img->width * 3 + 3) / 4 * 4;
  if ((buffer = malloc(stride)) == NULL) {
    return FAILURE;
  }
  file->bfType = FILE_TYPE;
  file->bfSize = DEFAULT_HEADER_SIZE + stride * img->height;
  file->bfReserved1 = 0;
  file->bfReserved2 = 0;
  file->bfOffBits = DEFAULT_HEADER_SIZE;
  info->biSize = INFO_HEADER_SIZE;
  info->biWidth = img->width;
  info->biHeight = img->height;
  info->biPlanes = 1;
  info->biBitCount = 24;
  info->biCompression = 0;
  info->biSizeImage = stride * img->height;
  info->biXPelsPerMeter = 0;
  info->biYPelsPerMeter = 0;
  info->biClrUsed = 0;
  info->biClrImportant = 0;
  if (fwrite(header_buffer, DEFAULT_HEADER_SIZE, 1, fp) != 1) {
    goto error;
  }
  memset(buffer, 0, stride);
  for (y = img->height - 1; y >= 0; y--) {
    row = buffer;
    for (x = 0; x < img->width; x++) {
      *row++ = img->map[y][x].c.b;
      *row++ = img->map[y][x].c.g;
      *row++ = img->map[y][x].c.r;
    }
    if (fwrite(buffer, stride, 1, fp) != 1) {
      goto error;
    }
  }
  free(buffer);
  return SUCCESS;
  error:
  free(buffer);
  return FAILURE;
}

ヘッダの書き出し処理について、実際に構造体に値を格納しているところは、 定義に従って適切な値を代入しているだけなので詳細については説明しないが、 読み込み時にはほとんどのパラメータを無視したが、書き出しについては本来あるべき数値を設定する必要が有るため、 端折らずに適切な値を設定する必要がある。

ヘッダの書き出し処理の肝になる部分が以下だ。

  uint8_t header_buffer[DEFAULT_HEADER_SIZE];
  BITMAPFILEHEADER *file = (BITMAPFILEHEADER*)header_buffer;
  BITMAPINFOHEADER *info = (BITMAPINFOHEADER*)(header_buffer + FILE_HEADER_SIZE);
  ...
  if (fwrite(header_buffer, DEFAULT_HEADER_SIZE, 1, fp) != 1) {
    goto error;
  }

この部分は読み込み処理とほぼ同じで、byte配列を各ヘッダ構造体にキャストしておく。 そして各構造体を経由して、値を設定すると、その構造体の構造を持ったbyte配列が出来上がるので あとはこれを書き出すだけである。 通常の構造体変数に値を代入し、それをuint8_tの配列にキャストして書き出すという方法でももちろん問題ないが、 その場合ヘッダ構造体が2個あるので書き出し処理が2つ必要になるためこのような方法をとっている。 後になって眺めてみると余計面倒になっただけな気もする。

次に画像データ部分の書き出し処理である。

  stride = (img->width * 3 + 3) / 4 * 4;
  if ((buffer = malloc(stride)) == NULL) {
    return FAILURE;
  }
  ...
  memset(buffer, 0, stride);
  for (y = img->height - 1; y >= 0; y--) {
    row = buffer;
    for (x = 0; x < img->width; x++) {
      *row++ = img->map[y][x].c.b;
      *row++ = img->map[y][x].c.g;
      *row++ = img->map[y][x].c.r;
    }
    if (fwrite(buffer, stride, 1, fp) != 1) {
      goto error;
    }
  }

これについても読み込み処理と同様で、 先に1行分のバッファを確保してしまい、行単位で処理することで、 4byte単位にするためのパディング処理を余計な分岐などなしに実現できている。 処理の前にmemsetで0フィルしているのは、パディング部分にゴミを出力しないためである。

以上でBMP画像の簡易的な書き出し処理が実現できる。 前回と合わせて、よく使われるデータ構造に限定したとはいえ、 書き込み読み出し合わせて200行程度のコードで実現できるので、 初級者向けの課題としてもちょうどいいボリュームではないだろうか。 パソコン上で表示できる本物(?)の画像データを出力できるので、モチベーションの上がる課題だろう。 より高度なプログラミングに取り掛かるための足がかりとしてチャレンジしてみてはどうだろう。

次回以降は簡易版ではなく、もう少し厳密にBMPファイルに対応させる方法について説明していく。