CGIで既存のGIF画像ファイルを転送することは簡単ですが、
CGI自体でGIF画像を計算して描くことは難しい上に特許が絡んでいるので
近寄り難いプログラムです。
世の中にはGDライブラリと呼ばれるGIF画像生成ライブラリもありますが、
筆者はそれに詳しくないので、ppmtogif という名の画像形式変換コマンドを
利用することにします。つまり単純な画像形式であるppm形式でメモリ内に
絵を描いて、そのデータを ppmtogif コマンドにパイプで送り込んで
ppmtogifコマンドの標準出力をCGIの出力とするのです。
恐らく GDライブラリを普通に使う方法より遥かに面倒なプログラミングに
なるでしょうが、色々大事な技術を取得できますし、プログラミング自体が
楽しめます。
筆者は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形式をデータを取り出します。
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 を作成しましょう。
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つのメンバ関数をお手本として、他の描画関数も作成できるでしょう。 興味のある人は頑張ってください。
この絵のように太陽系の惑星の位置を示す絵を描いてみましょう。 各惑星の絵柄は描画ソフトで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; }