まず、ここでは2つのパートに分けて説明します。 はじめは画像ファイルを特定する方法。 そして、次に画像ファイルを解析する方法について説明します。 画像ファイルを特定することは、画像を逆アセンブラを用いて解析する場合にも、 「眺める」ことで解析する場合にでも非常に重要な作業となります。 しかし、画像ファイルを特定することは、これから画像形式を解析しようとする 人だけでなく、グラフィックローダーを使用して目的の画像を閲覧しようとしている 人にも重要な作業です。 というのは、せっかく画像ローダーやコンバータがあっても、 画像ファイル名がわからなければ、これらを利用できないケースがあるからです。 というわけで、解析には興味がない人でも、画像ローダーを使うひとは一読してみて ください。
まず、画像形式を解析する際に必要となる最初の作業は、 どのファイルに画像のデータが保存されているかを特定することです。 多くの場合、これは画像ファイルの拡張子を特定することと同義です。 画像ファイルの拡張子が特定できれば、その拡張子をもつファイルを 重点的にいくつか「眺める」ことにより、画像形式の解析を行うことができます。
画像ファイルの拡張子を特定する方法として、以下の方法があるでしょう。
.BMP .TIM .GIF .JPG .PNG など、すでにある画像形式の代表的な拡張子と 同じ拡張子を画像ファイル名として利用している場合があります。 データ形式そのものは全く同じ場合もありますし、そうでない場合もあります。 もちろん、既知の画像形式の代表的な拡張子と同じ拡張子だけど、 画像ファイルではないファイルの場合もあります。 たとえば、.BMP はあくまでも BitMaP の略で、 マップデータファイルの拡張子かもしれません。
ファイルの拡張子には通常ファイル形式の略称をつけます。 画像形式の拡張子には画像形式ファイル(Graphic Format File) の略称として、 拡張子のどこかに G の文字が入るケースが多いようです。 .GF .GRP .GP? という拡張子の場合はほぼ画像ファイルとみて間違いないでしょう。 また、Computer Graphics の略として CG という文字が入っている場合も ありますし、無圧縮のベタデータという意味で B という文字が入っている 場合も考えられます。 しかし、PMS(AliceSoft)、IPF(F&C) など、拡張子からはファイル形式が 特定しにくいものを拡張子にもつファイル名が多いのも事実です。
ゲームが使用するファイルには、 音声、動画、静止画、シナリオ、BGMなどがあります。 ゲームの種類によるので一概には言えませんが、 これらをファイル中の多い順に並べると大体、 音声、静止画、シナリオ、BGM、動画 となります。 これを踏まえ、同じ拡張子を持つファイル名の量から画像形式の拡張子を 特定することも可能です。 しかし、拡張子別のファイルの数の統計的性質はゲームによって異なるので、 この方法を特定するための主力としてはあまり使えません。 この方法は例えば、少なくとも 100枚以上の CG があるゲームで、 ファイル数が 10未満である拡張子は画像ファイルではないとするなど、 拡張子を特定する範囲を狭める目的でのみ使えるでしょう。
最近のほとんどのゲームはファイルを一つ、または複数のファイルに アーカイブして保存しています。 ゲームによっては、データの種類によらず、全てのファイルをごちゃまぜに アーカイブしている場合もありますが、 データの種類別にファイルをアーカイブしている場合もあります。 その場合、アーカイブファイル名として、GRAPHIC や CG など、 画像ファイルをアーカイブしていることを示すファイル名をアーカイブファイル名と している場合が多いです。 そういったアーカイブファイル中のファイルは全て画像ファイルと 思っていいと思います。
ファイル名から画像ファイルがどれかを大まかに特定したところで、 実際にファイルの先頭を眺めてみて、それをより確信に近づけてみます。
画像ファイルの先頭には、通常、そのファイルに収められている画像の情報が 保存されています。 例えば、画像ファイルであることを示す ID、データサイズ、 画像の大きさ(幅、高さ)、色数、パレットなどです。 ゲーム用の画像データの場合は、さらに表示位置という情報が加わることもあります。 ゲーム用の画像データの場合は、色数が固定であることが多いので、 画像の大きさ、表示位置、パレットだけしかないことがほとんどです。 画像の大きさの情報すら保存されていない特殊な形式も極希に存在しますが 眺めて解析する場合はとにかく画像サイズが保存されている場所を 特定することから始まるでしょう。 たとえば、640x480 の CG が多用されているゲームでファイルの比較的先頭から 640 と 480 が見つかれば、それは画像ファイルである可能性が非常に高いです。
ほとんど無理でしょう。 とはいえ、「眺める」だけで解析できるケースもいくつかあるのも事実です。 一昔前、16色CGが全盛のころは、 各形式毎に画像を圧縮するために独特の思想で画像形式が作られていました。 したがって、どのような思想で作られた形式であるか予測することは難しく、 「眺めて」解析するのはきわめて困難でした。 しかし、256色、ハイカラー、フルカラーCGが当たり前となった現在では、 画像の圧縮に一般的なデータ圧縮、しかも最もシンプルな 1,2種類の アルゴリズムを用いている例が多くなっています。 そのため、どのような考えのもとで作成した画像形式なのか予想しやすく、 一昔前に比べれば「眺めて」みて、それを確認、解析しやすくなってはいます。 実際、私は SEGA Saturn 版「下級生」以降、 Win95/98版 WORDS WORTH までの elf のゲームの画像形式は すべて「眺める」だけで解析してきました。 とはいえ、ちらっと眺めただけで解析ができたわけではなく、 SS版下級生、SS版YU-NOでは 1日、 臭作ではとりあえず見える画像がある程度になるまで 1日、完全に解析できるまでに さらに 2日かかっています。 確かに「眺めて」解析することは無理ではないですが、 少ない紙面で説明出来るほど、そう簡単にできるわけではありません。 ただ、上記 3作品が解析できれば、それ以外の elf 作品の画像形式は 毎回異なっているにも関わらず本当の意味でちらっと眺めただけで解析できて しまうのですが。 たとえば、Win95/98版 WORDS WORTH の画像データを解析するのに 眺めはじめてから解析が完了するまで10秒かかりませんでした。
ちらっと眺めただけで解析できてしまうような画像形式は そんなに多くはないので「眺める」だけで解析ができるかと問われれば、 ほとんど無理ということになります。 ただ、私が「眺めてみた」画像形式の中で、 簡単に解析できてしまう事例がありましたので、 これを例にして説明していきたいと思います。 今回「眺めて」みる画像は、 SEGA Saturn版 機動戦艦ナデシコ The blank of 3 years(以下、ナデシコ) です。
実際に画像データを眺めてみる前に、 データ圧縮の基本的なことについて説明します。
データの圧縮はあるデータ列をそれと等価なより短いデータ列に 置き換えることによって実現します。 その方法として、 エントロピー符号化、ランレングス符号化、辞書ベース符号化などがあります。 また、これらは複合して用いられたりもします。
ランレングス符号化は 何文字も同じ文字が連続して出現しているデータ列があれば、 このデータ列を連続している文字(run)とその長さ(length)を 表すデータに置き換えるものです。 たとえば、A という文字が10文字続いているデータ列があれば、これを <Aが10個>というデータに置き換えます。この置き換えたデータが たとえば 2文字だったとしたら、10文字から 2文字にデータが圧縮されたことに なります。(1)
BCDEFAAAAAAAAAABCDEF --> BCDEF<Aが10個>BCDEF ....(1)辞書ベース符号化には色々ありますが、 ゲーム用画像形式としては LZ77アルゴリズムの考え方をもとにしているものがほとんどでしょう。 これはあるデータ列が過去にすでに出現していた場合にこのデータ列を 過去のデータ列の出現位置とその長さを表すデータに置き換えるというものです。 たとえば、 ある 5文字のデータ列が過去 15文字前に すでに登場していた場合、これを<15文字前から5個> というデータに 置き換えます。この置き換えたデータがたとえば 2文字だったとしたら、5文字から 2文字にデータが圧縮されたことになります。 また、通常 LZ77アルゴリズムは動作としてランレングス符号化を 内包します。 (2 参照)
BCDEFAAAAAAAAAABCDEF --> BCDEFA<1文字前から9個><15文字前から5個> ...(2)
上記のようにデータ列を別のより短いデータ列で置き換えるで データ圧縮が行えることがわかったと思います。 しかし、すでに勘の良い方ならお気づきかもしれませんが、 実際には置き換えたデータと置き換えていないデータとの 区別をどのようにするのかという問題があります。 これを解決する方法は三者三様。 したがって、同じ考え方に基づいて作られた画像形式だとしても さまざまな画像形式が存在することになります。
それでは、実際にナデシコの DISK1 に入っている CSP_455.PAC という ファイルについて眺めてみましょう。 このファイルの先頭数バイトのダンプリストを図1に示します。
0000 63 73 70 5F 34 35 35 5F-62 6D 70 00 00 00 05 01 | csp_455_bmp..... | 0010 00 00 00 20 00 06 04 36-00 00 00 00 00 00 00 00 | .......6........ | 0020 FB 42 4D 36 04 06 05 00-FB 36 04 00 00 28 04 00 | .BM6............ | 0030 FF 02 03 00 FA 03 00 00-01 00 08 07 00 F8 06 00 | ................ | 0040 13 0B 00 00 13 0B 0A 00-F0 F0 F8 F8 00 F0 F8 F8 | ................ | 0050 00 E0 F8 F8 00 D8 F8 F8-00 03 F8 EF 00 D0 F8 F8 | ................ | 0060 00 C8 F8 F8 00 C0 F8 F8-00 B8 F8 F8 00 03 F0 BF | ................ | 0070 00 B0 F8 F8 00 E0 F0 F8-00 E8 F0 F0 00 D0 F0 F8 | ................ | 0080 00 F0 F0 E8 00 F8 F0 E8-00 E8 F0 E8 00 C8 F0 F8 | ................ | 図1 CSP_455.PAC のダンプリスト
ファイルを眺めてみると、先頭にいきなり "csp_455_bmp" というファイル名らしきものが見えます。 これはこのファイル CSP_455.PAC が csp_455.bmp という BMPファイルから 生成されたことを示していると考えるのが普通でしょう。 そして、オフセット 21Hの 2バイトにはこれが BMPファイルである ことを示す BM という文字が見えます。 というわけで、この画像形式は先頭 21Hバイトのごみがある BMPファイルだということで解析終了。 といきたいところですが、さすがにそう簡単には終わりません。
BMP ファイルには BM という文字のあとにファイルサイズが格納されている わけですが、これを確認してみると、 60436H = 394294 バイトになります。 ところが、CSP_455.PAC の実際のファイルサイズは 303632バイトです。 どうやらなんらかの方法で圧縮がかかっているようです。 しかし、伸張してみると BMPファイルになるとわかっているのですから、 比較的解析は容易でしょう。
ところで、ファイルサイズが 60436H ということがわかったわけですが、 オフセット 14H からの 4バイトにも同じく 00060436 が保存されている ことがわかります。 ということから、この位置に元のファイルサイズが保存されていると考える ことができます。 オフセット 10H からの 4バイトはどのファイルをみても 20H と かかれています。ですから別に無視してもいいのですが、 BM という文字が 21H から始まっていることを考えると、 データ開始アドレスが保存されていると考えるのが妥当でしょう。 オフセット 18H からの 8バイトはどれファイルでも 0 なのでこれは 本当に無視して構わないでしょう。 オフセット 0EH からの 2バイトは色々なファイルを眺めてみると、 0501H や 0502H などといくつか種類があることに気がつきます。 その内、いままで説明してきた傾向にあるファイルは 0501H の場合だけの ようですので、ここにはファイルの形式が保存されていると考えていいでしょう。
以上、PAC 形式の画像のヘッダ形式は C言語で記述すると以下のようになる ことがわかります。
typedef struct { char name[14]; // 元ファイル名 char type[2]; // ファイルタイプ(0501H, 0502H など) char offset[4]; // データの開始位置(通常 20H) char orgSize[4]; // 元のファイルサイズ char reserved[8]; // 予約またはパディング } PACHDR;以下、ここでは 0501Hタイプの画像形式についてのみ説明します。
CSP_455.PAC は 256色BMPファイルを圧縮したファイルのように見えます。 なぜ、16色やフルカラーBMPファイルでないかといえば、 ダンプリスト中にパレット情報があるようにみえるからです。 フルカラーBMPファイルにはパレット情報はありませんし、 明らかに 16個以上のパレット情報があるように見えます。
画像サイズが 394294 である 256色BMPファイルの場合は、 ファイルの先頭 18 バイトは確実に次の様になります。
42 4D 36 04 06 00 00 00 00 00 36 04 00 00 28 00 00 00
今回の場合は、CSP_455.PCK の 20H からの数バイト、例えば、以下の16バイトが
FB 42 4D 36 04 06 05 00 FB 36 04 00 00 28 04 00が上記のBMPファイルの先頭18バイトのようになるような 圧縮手法を見つければ解析できたことになります。
この 2つのバイト列はぱっとみたところところどころ一致している 部分があるように見受けられます。 一致している部分をわかりやすく分解してみると次の様になるでしょう。
|42 4D 36 04 06|00 00 00 00 00|36 04 00 00 28|00 00 00 FB|42 4D 36 04 06|05 00 FB |36 04 00 00 28|04 00
ここで一致していない部分、たとえば 00 00 00 00 00 と 05 00 FB との 関係を考えてみます。 00 00 00 00 00 は 00 が 5個連続したデータであることに着目し、 05 00 FB の方と見比べてみると、こちらには 05 と 00 という文字があることに 気がつきます。これは 00 が 5個連続している ことを示しているデータであると考えるのが妥当でしょう。 つまり、ランレングス符号化されているのです。 では、次の FB とは一体なんなのでしょうか? FB のさらに次の 5文字を見ると、これはもとのデータと何も変化がありません。 よくみると圧縮されたデータの最初の 1文字にも FB があり、 次の 5文字もやはりもとのデータと何も変化がありません。 つまり、FB は次の 5文字はもとのデータと何も変化がないことを 示すデータであるということになります。 さて、前者では 05 という文字は、次の文字、この場合 00 が 5個連続で出現するという意味の 5であることが推測できますが、 後者の FB ではどうして 5文字なのでしょうか。 慣れている方ならすぐに気がつくでしょう。 これは 100H - FBH = 5 の 5なのです。 つまり、この画像形式は、 ある一定値以下のデータが出現した場合は、その次に出現する文字を その値個分だけ連続して出力させ、ある一定値以上のデータが出現した場合は、 それ以降 256-<その値> 個分のデータをそのまま出力するというものだ ということがわかります。 ある一定値というのはおそらく勘のよい慣れた方なら 128であると 予想するでしょう。 実際、この値はデータ列をずっと追いかけて眺めていくと 128である ことがわかります。 以上で解析は終了しました。 リスト1 はこの解析結果と元に PAK 0501 形式の画像を BMPにコンバートする最も単純なソースファイルです。 この文章で PAC 0501 の画像形式がわからなくても、 ソースの方を見れば画像形式が一目瞭然だと思いますので そちらを参考にしてみてください。
リスト1 #include#include typedef unsigned char LONGB[4]; typedef unsigned char WORDB[2]; #define GETWORDB(p) (((long)*(p)<<8)|(long)*(p+1)) #define GETLONGB(p) ((GETWORDB(p)<<16)|GETWORDB(p+2)) typedef struct { char name[14]; WORDB type; LONGB offset; LONGB orgSize; char reserved[8]; } PACHDR; void unpac(FILE *ifp, FILE *ofp, long size) { while ( size > 0 ) { int c2, ch = fgetc(ifp); if ( ch >= 128 ) { char buf[128]; ch = 256-ch; fread (buf, 1, ch, ifp); fwrite(buf, 1, ch, ofp); size -= ch; } else { size -= ch; for ( c2 = fgetc(ifp); ch > 0; ch-- ) fputc(c2, ofp); } } } int main(int argc, char **argv) { char *p, *infile = argv[1]; FILE *ifp, *ofp; if ( argc < 2 ) { fprintf(stderr, "usage: %s pac-file\n", argv[0]); return 1; } if ( ifp = fopen(infile,"rb") ) { PACHDR hdr; fread(&hdr, 1, sizeof(hdr), ifp); if ( GETWORDB(hdr.type) == 0x0501 && (p = strstr(hdr.name,"_bmp")) ) { *p = '.'; if ( ofp = fopen(hdr.name,"wb") ) { unpac(ifp, ofp, GETLONGB(hdr.orgSize)); fclose(ofp); } else { perror(hdr.name); } } else { fprintf(stderr, "sorry. can not convert file.\n"); } fclose(ifp); return 0; } perror(infile); return 1; }