PNM (PPM / PGM / PBM) 画像の書き出し

作成:

前回に引き続き、今回はPNM画像の書き出し処理について説明する。 コード自体は GitHub にあり、PNM画像の入出力を記述しているのは pnm.c である。

読み込みは、いろいろなケースに対処するため、少し複雑になってしまったが、 書き出しは、仕様で決められた範囲の自分の都合の良い方法を一つ実装すればよいだけなので、よりシンプルになる。

出力する画像のデータ構造については、画像処理についてのページ 1 2 3 を参照して欲しい。

画像の書き出し

読み込みと同様に、書き出し関数は、ファイル名を指定するものと、オープン済みのファイルストリームを渡すものを作成する。 ファイル名を指定するものはオープン後、ファイルストリームを使うものを呼び出すだけなので、ほぼテンプレートだ。 ただし、PNMの場合P1~P6のどの形式かを引数で指定できるようにtype引数を用意している。

注意点としては出力に失敗した場合でも出力ファイルが作成されてしまう。 これが嫌な場合は、失敗時unlinkを呼び出してファイルを削除するようにすればよい。

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

次に、ファイルストリームへ書き出すもの

result_t write_pnm_stream(FILE *fp, image_t *img, int type) {
  image_t *work = NULL;
  if (img == NULL) {
    return FAILURE;
  }
  if (type < 1 || type > 6) {
    return FAILURE;
  }
  // 出力形式を指定するので、所望の形式でない場合は自動的に変換する
  switch (type) {
    case 1:
    case 4:
      if (img->palette_num != 2) {
        work = clone_image(img);
        img = image_to_gray(work);
        img = image_gray_to_binary(img);
      }
      break;
    case 2:
    case 5:
      if (img->color_type != COLOR_TYPE_GRAY) {
        work = clone_image(img);
        img = image_to_gray(work);
      }
      break;
    case 3:
    case 6:
      if (img->color_type != COLOR_TYPE_RGB) {
        work = clone_image(img);
        img = image_to_rgb(work);
      }
      break;
  }
  // ヘッダ出力、コメントなし
  fprintf(fp, "P%d\n", type);
  fprintf(fp, "%u %u\n", img->width, img->height);
  if (type != 1 && type != 4) {
    fprintf(fp, "255\n");
  }
  switch (type) {
    case 1:  // ASCII 2値
      write_p1(fp, img);
      break;
    case 2:  // ASCII グレースケール
      write_p2(fp, img);
      break;
    case 3:  // ASCII RGB
      write_p3(fp, img);
      break;
    case 4:  // バイナリ 2値
      write_p4(fp, img);
      break;
    case 5:  // バイナリ グレースケール
      write_p5(fp, img);
      break;
    case 6:  // バイナリ RGB
      write_p6(fp, img);
      break;
  }
  free_image(work);
  return SUCCESS;
}

前半は出力タイプ別にフォーマットを変換する処理なので割愛する。

以下がヘッダ出力の処理

  fprintf(fp, "P%d\n", type);
  fprintf(fp, "%u %u\n", img->width, img->height);
  if (type != 1 && type != 4) {
    fprintf(fp, "255\n");
  }

たったこれだけだ。 標準的なフォーマットで3~4つのパラメータをfprintfで出力するだけ。 読み込みの苦労に比べれば拍子抜けするほど簡単だ。 必要ならばコメントを埋め込んでもいいかもしれない。 最大値も余計な変換の必要のない255で出力する。

次に、画像データそのものの出力だが、読み込みと同様にタイプ別に関数を分けた。

P1(PBM形式)

static result_t write_p1(FILE *fp, image_t *img) {
  int x, y;
  for (y = 0; y < img->height; y++) {
    int line = 0;
    for (x = 0; x < img->width; x++) {
      if(++line > 69) {
        putc('\n', fp);
        line = 1;
      }
      putc('0' + img->map[y][x].i, fp);
    }
    putc('\n', fp);
  }
  return SUCCESS;
}

テキスト形式のモノクロ画像。 01をテキストに変換して出力する。 ただし、行の終わりか、70文字に達する前に改行するようにした。 間にスペースを開けるなどしてもいいし、全部改行でも問題ないだろう。

P2(PGM形式)

static result_t write_p2(FILE *fp, image_t *img) {
  int x, y;
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      fprintf(fp, "%u\n", img->map[y][x].g);
    }
  }
  return SUCCESS;
}

テキスト形式のグレースケール画像。 0~255の値をそのまま出力すれば良い。 デリミタは全部改行にした。 スペースにすると70文字で改行の判定が難しいためだ。 実際 GIMPPaint Shop Pro も改行で出力する仕様になっているようだ。

P3(PPM形式)

static result_t write_p3(FILE *fp, image_t *img) {
  int x, y;
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      fprintf(fp, "%u %u %u\n",
          img->map[y][x].c.r,
          img->map[y][x].c.g,
          img->map[y][x].c.b);
    }
  }
  return SUCCESS;
}

テキスト形式のフルカラー画像。 これもRGBの順で0~255の値をそのまま出力すれば良い。 デリミタはRGB間を空白、ピクセル間を改行にした。

P4(PBM形式)

static result_t write_p4(FILE *fp, image_t *img) {
  int x, y;
  uint8_t p;
  for (y = 0; y < img->height; y++) {
    int shift = 8;
    p = 0;
    // 上位ビットから詰め込み、1byte分たまったら出力
    for (x = 0; x < img->width; x++) {
      shift--;
      p |= img->map[y][x].i << shift;
      if (shift == 0) {
        putc(p, fp);
        shift = 8;
        p = 0;
      }
    }
    // 端があればここで出力
    if (shift != 8) {
      putc(p, fp);
    }
  }
  return SUCCESS;
}

バイナリ形式のモノクロ画像。 書き出しでも、これが一番複雑だ。 1bitずつ上位ビットから詰め込み、8bit揃えば出力する。 行の末尾は1bitでもあれば1byte出力になっている。

P5(PGM形式)

static result_t write_p5(FILE *fp, image_t *img) {
  int x, y;
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      putc(img->map[y][x].g, fp);
    }
  }
  return SUCCESS;
}

バイナリ形式のグレースケール画像。 8bit深度なので特に何も考える必要ない。1byteずつ出力する。

P6(PPM形式)

static result_t write_p6(FILE *fp, image_t *img) {
  int x, y;
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      putc(img->map[y][x].c.r, fp);
      putc(img->map[y][x].c.g, fp);
      putc(img->map[y][x].c.b, fp);
    }
  }
  return SUCCESS;
}

バイナリ形式のフルカラー画像。 これも8bit深度なので、RGBの順で1byteずつ出力すれば良い。

以上で、ひと通りのPNM形式出力ができるようになるはずだ。 読み込みと比べて、ずいぶんシンプルだったと思う。