画像の作成

CGIで既存のGIF画像ファイルを転送することは簡単ですが、 CGI自体でGIF画像を計算して描くことは難しい上に特許が絡んでいるので 近寄り難いプログラムです。
世の中にはGDライブラリと呼ばれるGIF画像生成ライブラリもありますが、 筆者はそれに詳しくないので、ppmtogif という名の画像形式変換コマンドを 利用することにします。つまり単純な画像形式であるppm形式でメモリ内に 絵を描いて、そのデータを ppmtogif コマンドにパイプで送り込んで ppmtogifコマンドの標準出力をCGIの出力とするのです。 恐らく GDライブラリを普通に使う方法より遥かに面倒なプログラミングに なるでしょうが、色々大事な技術を取得できますし、プログラミング自体が 楽しめます。


● 簡単なPPM画像の作成

■ PPM(P6)形式について

筆者はPPM形式には詳しくはありませんが、とりあえず次のように 書き始めればPPM形式の一種であるP6形式になるようです。

P6
# Comment
256 256
255

まず1行目にP6と書きます。これは以後のデータが カラー画像であり、画像のデータが生(raw)であることを示しています。
2行目以降で#で始める行はコメントです。
次の行に画像の横縦の長さをドット単位で示します。この例の場合は 256×256の画像になります。
次の行に画像データの最大輝度の値を示します。この例の場合は 輝度0が最小輝度であり、輝度255が最大輝度となります。

この行以降に画像データの本体を置きます。 1つのドット辺りにRGBの順に3バイトで輝度を指定します。

まず簡単な例としてCGIではなく、コマンドラインで動かして 標準出力に真っ黒なPPM形式の画像を出力するプログラムを紹介します。

// ppmtest.cc
#include <stdio.h>

const int IMG_HEIGHT = 256;
const int IMG_WIDTH  = 256;

struct Color
{
  u_char R, G, B;
  Color( void ){
    R = G = B = 0;
  }
  Color( u_char _r, u_char _g, u_char _b ){
    R = _r;
    G = _g;
    B = _b;
  }
};

int main( void )
{
  Color vram[IMG_HEIGHT][IMG_WIDTH];

  fprintf( stdout, "P6\n%d %d\n255\n", IMG_HEIGHT, IMG_WIDTH );
  fwrite( vram, sizeof(Color), IMG_HEIGHT*IMG_WIDTH, stdout );

  return 0;
}

ここで定義した Color型構造体は各点のRGB輝度を格納するための 構造体です。コンストラクタを装備してRGBの各輝度を0に初期化するように しています。
Color型構造体による2次元配列変数 vram は文字通りVRAMでして、画像を そのメモリに蓄えます。
fprintf()関数でPPMのヘッダーを出力して、続いて fwrite()関数で 画像の本体を出力します。

このプログラム(ppmtest)をコマンドラインで次のように動かします。

ppmtest | ppmtogif | xv -
これで xv により真っ黒な画像が表示されれば成功です。

なお、ppmtogifコマンドは netpbm という名前で配布されている 画像変換プログラム群のうちのひとつです。使い方は上記からも わかるように標準入力にPPM形式のデータを与えて、 標準出力からGIF形式をデータを取り出します。


■ ppmtogifコマンドをCGIから実行する方法

CGIからこのppmtogifコマンドを実行して、PPMデータを 与えて、GIFデータをCGIの出力とするには、UNIXのプロセス間通信の 技術が必要です。先のプログラム ppmtest を改造して popen()を使った簡単な例を示します。ppmtogifコマンドのpathを 定数 PATH_OF_PPMTOGIF に指定してください。

#define PATH_OF_PPMTOGIF "/usr/X11R6/bin/ppmtogif"

int main(void)
{
  Color vram[IMG_HEIGHT][IMG_WIDTH];

  close(2);

  FILE* pipe;
  if( (pipe=popen( PATH_OF_PPMTOGIF, "w" )) == NULL ) exit(1);

  fputs( stdout, "Content-Type: image/gif\n\n");
  fprintf( pipe, "P6\n%d %d\n255\n", IMG_HEIGHT, IMG_WIDTH );
  fwrite( vram, sizeof(Color), IMG_HEIGHT*IMG_WIDTH, pipe );

  fclose(pipe);

  return 0;
}

main()関数の冒頭でclose(2)として標準エラー出力を閉じています。 ppmtogif コマンドは stderr に色々なメッセージを出力するので それがhttpdの error_log ファイルに書かれてしまうのを防ぐためです。

popen()システムコールでppmtogifコマンドを実行し、そのコマンドの 標準入力への書き込み用の pipe を入手します。

CGIの標準出力に httpd header を出力します。

入手したpipeに対して、PPMデータを送り込みます。 ppmtogifの標準出力がCGIの標準出力に合流して出力されます。

これで browser に真っ黒な絵が表示されれば成功です。


● VRAMに点、線、円、描く関数の作成

先の例での vram配列変数に色々な図形を描く関数を作ることを考えます。 そうすると vram配列変数とそれらに操作を行う関数をクラスにまとめると よさそうな気になります。そこで以下のようなメンバ変数とメンバ関数を持つ クラス Vram を作成しましょう。

class Vram
{
  u_int  width, height;
  Color* vram;
  Color  color;

  int    pid, pipefds[2];
public:
       Vram( u_int _width, u_int _height );
      ~Vram();
  void SetColor( u_char R, u_char G, u_char B );
  void DrawPoint( int x, int y );
  void DrawLine( int x1, int y1, int x2, int y2 );
  void DrawCircle( int xc, int yc, u_int r );
  int  PastePPM( int xo, int yo, char* fname );
};

Constructorで画像の大きさを指定することにします。 constructorでは画像用のメモリを確保して、 さらに ppmtogif コマンドを子プロセスとして実行し、 パイプラインも設置します。以下のような作りになるでしょう。

Vram::Vram( u_int _width, u_int _height )
: width(_width), height(_height), color(0xff,0xff,0xff)
{
  pipe(pipefds);

  if( (pid=fork()) == 0 ){
    dup2( pipefds[0], 0 );
    close(pipefds[1]);
    close(2);

    write( 1, "Content-Type: image/gif\n\n", 25 );
    execl( PATH_OF_PPMTOGIF, "ppmtogif", 0 );
    exit(1);
  }else{
    close(pipefds[0]);
  }

  vram = new Color [width*height];
}

Destructorでメモリの内容をこのパイプラインに送り込みます。 最後にメモリを解放したり、ppmtogifのプロセスの終了を待機します。

Vram::~Vram()
{
  char head[32];
  int size = sprintf( head, "P6\n%3d %3d\n255\n", width, height );

  write( pipefds[1], head, size );
  write( pipefds[1], vram, sizeof(Color)*width*height );

  close( pipefds[1] );

  delete [] vram;

  waitpid( pid, 0, 0 );
}

メンバ変数colorは描く色を指定することにします。 その色の変更に SetColor()メンバ関数を使うことにします。

void Vram::SetColor( u_char R, u_char G, u_char B )
{
  color = Color(R,G,B);
}

DrawPoint()メンバ関数はVRAMの指定の位置に点を描く関数です。 VRAMの領域外をアクセスしないように注意します。

void Vram::DrawPoint( int x, int y )
{
  if( x<0 || (int)width<=x || y<0 || (int)height<=y ) return;
  vram[y*width+x] = color;
}

DrawLine()メンバ関数はVRAMの指定の2点間を結ぶ直線を描きます。 この線分上の各点の位置を割り出すアルゴリズムは昔の偉い人が 考案したものです。

void Vram::DrawLine( int x1, int y1, int x2, int y2 )
{
  int dx=x2-x1, dy=y2-y1, sx=1, sy=1;

  if( dx<0 ){
    dx *= -1;    sx *= -1;
  }
  if( dy<0 ){
    dy *= -1;    sy *= -1;
  }

  DrawPoint( x1, y1 );

  if( dx>dy ){
    for( int i=dx, de=i/2; i; i-- ){
      x1 += sx;
      de += dy;
      if( de>dx ){
	de -= dx;
	y1 += sy;
      }
      DrawPoint( x1, y1 );
    }
  }else{
    for( int i=dy, de=i/2; i; i-- ){
      y1 += sy;
      de += dx;
      if( de>dy ){
	de -= dy;
	x1 += sx;
      }
      DrawPoint( x1, y1 );
    }
  }
}

DrawCircle()メンバ関数はVRAMの指定の位置に真円を描きます。 この円上の各点の位置を割り出すアルゴリズムは昔の偉い人が 考案したものです。

void Vram::DrawCircle( int xc, int yc, u_int r )
{
  int i=0, j=r, de=1-2*r;

  while( i<=j ){
    DrawPoint( xc+i, yc+j );    DrawPoint( xc-i, yc+j );
    DrawPoint( xc+j, yc+i );    DrawPoint( xc-j, yc+i );
    DrawPoint( xc+i, yc-j );    DrawPoint( xc-i, yc-j );
    DrawPoint( xc+j, yc-i );    DrawPoint( xc-j, yc-i );

    if( de<0 ){
      de += 4*i+6;
      i++;
    }else{
      de += 4*(i-j)+10;
      i++; j--;
    }
  }
}

PastePPM()メンバ関数はVRAMの指定の位置に指定のファイル名のPPM 画像ファイルを張り付けます。

int Vram::PastePPM( int xo, int yo, char* fname )
{
  FILE* fptr;
  char  buf[64];
  int dx, dy;

  if( (fptr = fopen( fname, "r" ))==NULL ) return 1;

  fgets( buf, 64, fptr ); // read P6

  do{ fgets( buf, 64, fptr ); }while( buf[0] =='#' ); // Skip comments
  sscanf( buf, "%d %d", &dx, &dy ); // read width and height

  fgets( buf, 64, fptr ); // read maximam bright

  for( int j=0; j<dy; j++ ){
    fread( &vram[(yo+j)*width+xo], 1, sizeof(Color)*dx, fptr );
  }

  fclose(fptr);

  return 0;
}

上記のプログラムを ppmgraph.h として 置いておきます。 以上7つのメンバ関数をお手本として、他の描画関数も作成できるでしょう。 興味のある人は頑張ってください。


● 画像の作成例: 現在の太陽系の惑星の位置を描くCGI


今日の太陽系 内惑星の位置

この絵のように太陽系の惑星の位置を示す絵を描いてみましょう。 各惑星の絵柄は描画ソフトでPPM形式で用意しておきます。 惑星の運動は簡単のため等速円運動として、適当な天文データから その位置を計算します。

planet.ccのソースファイル
太陽の絵柄
水星の絵柄
金星の絵柄
地球の絵柄
火星の絵柄

// planet.cc
#include <math.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include "ppmgraph.h"

const int    IMG_WIDTH  = 256;
const int    IMG_HEIGHT = 256;
const int    XC         = IMG_WIDTH/2;
const int    YC         = IMG_HEIGHT/2;
const double MAG        = 72.0;

struct Planet
{
  double r, omg, tho;  // radius, anglur velocity, initial angle.
  char   fname[32]; // file name of the PPM.

  //---- Calculate the position at the given time.
  void Position( time_t t, double& x, double& y ){
    x = r*cos(omg*t+tho);
    y = r*sin(omg*t+tho);
  }
};

// Radius is given in Astronomic unit (mean span between Sun and Earth).
// Anglur velocity is given in rad/sec.
// Initial angle is given in rad, which is the angle
// at 00:00:00 on Jan 1, 2000.

// Set data for Sun and four planets.
Planet planets[5]= {
  { 0.000, 0.000e+00, 0.000, "planet0.ppm" }, // Sun
  { 0.387, 8.265e-07, 4.429, "planet1.ppm" }, // Mercury
  { 0.723, 3.236e-07, 3.145, "planet2.ppm" }, // Venus
  { 1.000, 1.991e-07, 1.737, "planet3.ppm" }, // Earth
  { 1.523, 1.058e-07, 0.021, "planet4.ppm" }  // Mars
};

//---- Calculate seconds since the intial time 00:00:00 on Jan 1, 2000.
time_t GetTime( void )
{
  struct tm t2000;
  t2000.tm_sec   = 0;
  t2000.tm_min   = 0;
  t2000.tm_hour  = 0;
  t2000.tm_mday  = 1;
  t2000.tm_mon   = 0;
  t2000.tm_year  = 100;
  t2000.tm_wday  = 0;
  t2000.tm_yday  = 0;
  t2000.tm_isdst = 0;

  return time(NULL) - mktime(&t2000);
}


int main( void )
{
  double x, y;
  time_t t;
  Vram V(IMG_WIDTH, IMG_HEIGHT);

  t = GetTime();

  for( int i=0; i<5; i++ ){
    planets[i].Position( t, x, y );

    // draw an orbit.
    V.DrawCircle( XC, YC, int(planets[i].r*MAG) );

    // draw an image of the planet.
    V.PastePPM( XC-10+int(x*MAG), YC-10-int(y*MAG), planets[i].fname );
  }

  return 0;
}

目次

Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp