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;
}