BMP画像の書き出し(ヘッダ)

作成:

BMPファイルの読み込みについて説明が終わったところで、次は書き出し処理について解説する。 ソースコードは GitHub にて公開している。 BMPの入出力を記述しているのは bmp.c である。

入力については様々な形式に対応してきた。 出力関数についても指定次第で任意の形式での出力ができたほうがいいだろうが、 ここでは基本的に減色などの画像の加工処理を行わずに、 画像データ構造体が表現しているものをそのまま出力するにとどめている。 特に16bitカラーについては内部的に表現できる形式を定義していないため、 ここで用意した方法では出力することはできない。 また、色空間や解像度情報も保持するようになっていないため、 出力上それらパラメータが必要な場合はデフォルト値を埋め込むのみとしている。

ただし、インデックスカラーの非圧縮とランレングス圧縮については表現内容自体に違いは無いため、 インデックスカラーの色数が 8bit もしくは 4bit の場合にのみ 圧縮の有無を引数で指定することで出し分けることができるようにした。 それ以外の場合は圧縮フラグは無視される。

バイトストリーム

出力においても入力と同様に処理の中でエンディアンを意識しなくてすむようにバイトストリームを利用する。 入力とは逆に予め用意されたバッファへ書き込んでいく形の構成になる。

具体的な処理としては特に特別なことはしていないが、以下に32bitの出力関数を示す。 32bit のデータを引数として、 1Byte ずつ抽出し内部バッファに順次書き込む処理となっている。

static void bs_write32(bs_t *bs, uint32_t data) {
  uint8_t *dst = &bs->buffer[bs->offset];
  if (bs->offset + 4 > bs->size) {
    bs->error = ERROR;
    return;
  }
  dst[0] = 0xff & (data >> 0 );
  dst[1] = 0xff & (data >> 8 );
  dst[2] = 0xff & (data >> 16);
  dst[3] = 0xff & (data >> 24);
  bs->offset += 4;
}

この処理もビットシフトを利用した処理にしているため、エンディアン依存性はない。 キャストを利用したほうが演算負荷は小さいだろうが、その場合はエンディアン依存の処理となり、 エンディアンごとに実装を差し替える必要がある。 これは開発方針次第でどちらでも良いだろう。

BMPファイルの書き出し

これまでと同様、書き出し関数はファイル名を指定する方法と、 オープン済みのファイルストリームを利用するもの両方を用意している。 以下がオープン済みのファイルストリームに出力する関数である。

result_t write_bmp_stream(FILE *fp, image_t *img, int compress) {
  result_t result = FAILURE;
  image_t *work = NULL;
  int bc;
  int size;
  if (img == NULL) {
    return FAILURE;
  }
  if (img->color_type == COLOR_TYPE_GRAY) {
    work = clone_image(img);
    if (work == NULL) {
      return FAILURE;
    }
    img = image_gray_to_index(work);
  }
  if (img->color_type == COLOR_TYPE_INDEX) {
    if (img->palette_num <= 2) {
      bc = 1;
    } else if (img->palette_num <= 16) {
      bc = 4;
    } else {
      bc = 8;
    }
  } else if (img->color_type == COLOR_TYPE_RGB) {
    bc = 24;
  } else if (img->color_type == COLOR_TYPE_RGBA) {
    bc = 32;
  } else {
    goto error;
  }
  size = (img->width * bc + 31) / 32 * 4 * img->height;
  if (write_header(fp, img, bc, size, compress) != SUCCESS) {
    goto error;
  }
  if (bc <= 8) {
    if (write_palette(fp, img, bc) != SUCCESS) {
      goto error;
    }
  }
  result = write_bitmap(fp, img, bc, compress);
  error:
  free_image(work);
  return result;
}

まず、直接出力とあまり関係ない処理として、 出力の方法としてグレースケールを直接表現する方法はBMP形式には無いため、 グレースケールの場合はインデックスカラー方式に変換してから出力するようにしている。 ただし、引数として受け取った画像構造体自体を変更するのではなく、 コピーを行ってからコンバートし、外部に影響を与えないようにしている。 このコピーは処理が終わったら開放する。

その後、各形式から1色あたりのビット数を計算している。 このパラメータに従い以降の出力方法を分岐させている。 あとはヘッダの出力、カラーパレットの出力、画像データの出力と続く。

ヘッダの出力では、イメージサイズを計算して引数として渡している。 この計算式はまとめて書いてしまっているが、 圧縮を行わない場合は1行のデータサイズと高さを掛けあわせれば求めることができる。 1行のデータサイズは、1色のデータサイズ×幅+4Byte境界にするためのパディングである。

圧縮を行う場合は、実際に圧縮処理を行ってみないことにはサイズは不明のため、この時点ではサイズはわからない。 こ非圧縮の場合のサイズをまずは出力しておき、 圧縮処理を行った後、サイズが判明してからヘッダ部分を再度上書きして修正する方針とした。 この修正の出力でも同じヘッダ出力関数を使うため、サイズを引数で渡せるようにしている。

処理全体の流れとしては以上となる。 以降、個々の処理を具体的に解説する。

ヘッダの出力

以下がヘッダの出力関数である。

static result_t write_header(
    FILE *fp, image_t *img, int bc, int image_size, int compress) {
  result_t result = FAILURE;
  bs_t bs;
  uint8_t *header = NULL;
  int info_header_size = (bc == 32 ? V5_HEADER_SIZE : INFO_HEADER_SIZE);
  int header_size = FILE_HEADER_SIZE + info_header_size;
  int palette_size = 0;
  if (bc <= 8) {
    palette_size = (1 << bc) * 4;
  }
  if ((header = calloc(header_size, 1)) == NULL) {
    return FAILURE;
  }
  // ヘッダ情報書き出し
  bs_init(header, header_size, &bs);
  bs_write16(&bs, FILE_TYPE);  // bfType
  bs_write32(&bs, header_size + palette_size + image_size);  // bfSize
  bs_write16(&bs, 0);  // bfReserved1
  bs_write16(&bs, 0);  // bfReserved2
  bs_write32(&bs, header_size + palette_size);  // bfOffBits
  bs_write32(&bs, info_header_size);  // biSize
  bs_write32(&bs, img->width);  // biWidth
  bs_write32(&bs, img->height);  // biHeight
  bs_write16(&bs, 1);  // biPlanes
  bs_write16(&bs, bc);  // biBitCount
  if (bc == 32) {  // biCompression
    bs_write32(&bs, BI_BITFIELDS);
  } else if (bc == 8 && compress) {
    bs_write32(&bs, BI_RLE8);
  } else if (bc == 4 && compress) {
    bs_write32(&bs, BI_RLE4);
  } else {
    bs_write32(&bs, BI_RGB);
  }
  bs_write32(&bs, image_size);  // biSizeImage
  bs_write32(&bs, 0);  // biXPelsPerMeter
  bs_write32(&bs, 0);  // biYPelsPerMeter
  bs_write32(&bs, img->palette_num);  // biClrUsed
  bs_write32(&bs, 0);  // biClrImportant
  if (bc == 32) {
    bs_write32(&bs, 0xff000000);  // bV5RedMask
    bs_write32(&bs, 0x00ff0000);  // bV5GreenMask
    bs_write32(&bs, 0x0000ff00);  // bV5BlueMask
    bs_write32(&bs, 0x000000ff);  // bV5AlphaMask
    bs_write32(&bs, LCS_sRGB);  // bV5CSType
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzRed.ciexyzX
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzRed.ciexyzY
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzRed.ciexyzZ
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzGreen.ciexyzX
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzGreen.ciexyzY
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzGreen.ciexyzZ
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzBlue.ciexyzX
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzBlue.ciexyzY
    bs_write32(&bs, 0);  // bV5Endpoints.ciexyzBlue.ciexyzZ
    bs_write32(&bs, 0);  // bV5GammaRed
    bs_write32(&bs, 0);  // bV5GammaGreen
    bs_write32(&bs, 0);  // bV5GammaBlue
    bs_write32(&bs, LCS_GM_GRAPHICS);  // bV5Intent
    bs_write32(&bs, 0);  // bV5ProfileData
    bs_write32(&bs, 0);  // bV5ProfileSize
    bs_write32(&bs, 0);  // bV5Reserved
  }
  if (fwrite(header, header_size, 1, fp) != 1) {
    goto error;
  }
  result = SUCCESS;
  error:
  free(header);
  return result;
}

処理としては非常に単純なのだが、ヘッダの構造をベタ書きにせざるを得ないため、行数としては多くなってしまっている。

出力の方法としては、基本的には BITMAPINFOHEADER で出力を行うが、 アルファチャンネルを扱う 32bit 出力については、この方式では表現できないため、 BITMAPV5HEADER での出力を行う。 ヘッダの構造が変わると、サイズに関する情報も変える必要が有るため、予め計算しておく。 また、 8bit 以下の場合はパレットの出力も必要となり、 bfOffBits の値に影響する。この値についても予め計算しておく。 あとは、ヘッダの定義にしたがって予め確保したバッファに出力してゆき、データが揃ったらまとめてファイルに出力する。

biCompresson についてはビット数に応じて分岐して出力している。 基本は BI_RGB だが、アルファチャンネルの出力の際は BI_BITFIELD とする必要がある。 また、圧縮が有効な場合は、 BI_RLE8 / BI_RLE4 とする必要がある。

32bit出力の時に利用するカラーマスクについては、どのような順序でも良いのだが、 ABGRの順で格納する前提でマスクを設定している。 これに伴い、画像データの出力の際には ABGR で出力するように記述する。

カラーパレットの出力

次にインデックスカラー方式の場合に必要なカラーパレットの出力だ。

static result_t write_palette(FILE *fp, image_t *img, int bc) {
  result_t result = FAILURE;
  bs_t bs;
  int i;
  uint8_t *buffer = NULL;
  int palette_size = (1 << bc) * 4;
  if ((buffer = calloc(palette_size, 1)) == NULL) {
    return FAILURE;
  }
  bs_init(buffer, palette_size, &bs);
  for (i = 0; i < img->palette_num; i++) {
    bs_write8(&bs, img->palette[i].b);
    bs_write8(&bs, img->palette[i].g);
    bs_write8(&bs, img->palette[i].r);
    bs_write8(&bs, 0);
  }
  if (fwrite(buffer, palette_size, 1, fp) != 1) {
    goto error;
  }
  result = SUCCESS;
  error:
  free(buffer);
  return result;
}

これについては非常にシンプルな処理なのであまり説明が必要なところもない。 基本的にやっていることはヘッダと同じで、パレットサイズ分のバッファを予め確保し、 その上にデータ構造を作成、最後にまとめてファイルへ出力という構成にしている。 各情報が1Byteに収まるため、エンディアンを気にする必要もなく、バイトストリームをわざわざ使う必然性もあまりないのだが、 バイトストリームを利用し、バッファ上のデータを構築してから出力という形式をとっている。 RGBQUADの構造になるようにBGRの順に出力を行う。 読み込みと違って、ここではOS/2ヘッダの出力は行わないため、RGBTRIPLEのことは考えず、Reserveの値として0を出力する。 これをカラーパレットの色数分行う。

以上で、BMP出力のうちヘッダ情報に関する部分の説明となる。 入力に比べて自前のデータを整形して出力するだけなので、特に難しいところも無いと思う。 次回は、画像データの出力について説明する。