圧縮zipを作ってみる

 それではzlibによるDeflateアルゴリズムで圧縮されたzipアーカイブファイルのプログラミングを行ってみましょう。以前紹介した無圧縮zipにおけるヘッダとの主な違いは以下の2つです。

 zlibライブラリによる圧縮ルーチンも前回紹介した圧縮ルーチンと異なる点があります。ひとつめは、圧縮が終了したら「deflateEnd」を指示してメモリを解放していましたが、対象のファイルを扱うたびに「deflateEnd」「deflateInit」を繰り返すとやや非効率なので、z_streamデータの再初期化は行うものの、メモリの解放は行わない「deflateReset」関数をプログラムに挿入します。

 特に何もしない状態でdeflateを実行すると、deflateに関する情報がヘッダとして自動的に出力バッファに追加されます。しかし、zipファイルはヘッダ内で圧縮に関するデータが含まれているため、zlibのヘッダは不要などころか、アーカイバはこのzlib自体が出力したヘッダを不正なデータと判断してしまうため、deflateInitの拡張関数である「deflateInit2」でヘッダ出力をしないオプションを指定しなくてはいけません。

deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY);

 引数の1番目にはz_stream構造体を、2番目には圧縮度(0~9)、3番目には圧縮方法(Z_DEFLATED固定)、4番目には圧縮のために確保するメモリ領域(1で最小、9で最大。8が標準。確保量が少ないと計算に時間がかかり、圧縮効率が低下する)、5番目には圧縮アルゴリズムの調整(Z_HUFFMAN_ONLYは単語などおなじバイナリ集合が多いデータ向き、Z_RLEはベタ画像のような連続するバイナリが続くデータ向き、Z_DEFAULT_STRATEGYは両者の中間)を指定します。

 最も重要なのが3番目の履歴バッファのサイズ(8~15の範囲内。数値が大きいとメモリ消費量は増えるが、圧縮効率はよくなる)で、この値をマイナスにすると、ヘッダの付加されない圧縮データが生成されるようになります。

 また、zlibライブラリにはcrc32を算出するための関数が用意されており、既存のcrc32関数と入れ替えることもできるのですが、次回紹介する暗号化におけるCRC32計算においては、zlibの関数はやや扱いづらいので、独自のCRC32関数を引き続き利用します。

#include "ziputil.h"

#define ARCHIVECOUNT 3

void _tmain()
{
	char *teststrs[ARCHIVECOUNT] = {
		"真の友情は、前と後ろ、どちらから見ても同じもの。\r\n"
		"前から見ればバラ、後ろから見ればトゲなどというものではない。",
		"近頃の若い者云々という中年以上の発言は、"
		"おおむね青春に対する嫉妬の裏返しの表現である。",
		"Whatever you do will be insignificant, "
		"but it is very important that you do it."
	};
	

	// ヘッダ情報の作成
	ZipHeader zipheader[ARCHIVECOUNT];
	CentralDirHeader archeader[ARCHIVECOUNT];
	memset(zipheader, 0, sizeof(ZipHeader) * ARCHIVECOUNT);
	memset(archeader, 0, sizeof(CentralDirHeader) * ARCHIVECOUNT);

	char filename[ARCHIVECOUNT][20];
	time_t now; tm *nowtm;
	for(int i = 0; i < ARCHIVECOUNT; i++){
		zipheader[i].signature = ZIPSIG_CENTDIR;	// PK0304
		zipheader[i].needver = 20;					// Deflate圧縮を利用できるVer
		zipheader[i].comptype = 8;					// Deflate圧縮

		// 現在時刻を取得
		now = time(NULL);
		nowtm = localtime(&now);
		zipheader[i].filedate = GetDosDate(nowtm->tm_year + 1900, nowtm->tm_mon, nowtm->tm_mday);
		zipheader[i].filetime = GetDosTime(nowtm->tm_hour, nowtm->tm_min, nowtm->tm_sec);

		// 展開時に用いるファイル名の設定
		sprintf(filename[i], "sample%d.txt", i + 1);
		zipheader[i].fnamelen = strlen(filename[i]);
		zipheader[i].filename = filename[i];

		// ファイルサイズの設定
		zipheader[i].uncompsize = strlen(teststrs[i]);


		archeader[i].signature = ZIPSIG_ARCHIVEFILE;	// PK0102
		archeader[i].madever = 20;
	}


	FILE *fp = _tfopen(_T("test.zip"), _T("wb"));
	if(fp == NULL){
		printf("ファイルが開けませんでした。");
		return;
	}


	// 圧縮関連変数の初期化
	const uInt tempsize = 16384;
	Byte outbuf[tempsize];
	uInt compsize;

	z_stream zs;
	memset(&zs, 0, sizeof(z_stream));

	deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY);

	// ファイルの位置を一時的に格納する変数
	unsigned int current;


	// アーカイブデータの書き込み
	InitCRC32();
	for(int i = 0; i < ARCHIVECOUNT; i++){
		// データの位置を格納
		archeader[i].headerpos = ftell(fp);

		// zipヘッダの書き込み
		WriteZipHeader(fp, &zipheader[i]);

		// 現在のファイル位置を一時的に取得
		current = ftell(fp);
	
		// zlib構造体をメモリの開放を行わず再初期化する
		deflateReset(&zs);

		// crc32の初期値は0xFFFFFFFFにする
		zipheader[i].crc32 = CRC32_DEFAULT;

		// データの圧縮と書き込み
		compsize = 0;
		zs.avail_in = zipheader[i].uncompsize;
		zs.next_in = (Bytef*)teststrs[i];
		do{
			zs.avail_out = tempsize;
			zs.next_out = outbuf;

			// CRC32値の更新
			zipheader[i].crc32 = GetCRC32(zs.next_in, zs.avail_in, zipheader[i].crc32);

			deflate(&zs, Z_FINISH);

			fwrite(outbuf, 1, tempsize - zs.avail_out, fp);
		}while(zs.avail_out == 0);


		// 圧縮サイズデータの更新
		zipheader[i].compsize = ftell(fp) - current;

		current = ftell(fp);

		// ファイルデータの書き換え
		fseek(fp, archeader[i].headerpos + 14, SEEK_SET);
		fwrite(&zipheader[i].crc32, 1, sizeof(unsigned int), fp);
		fwrite(&zipheader[i].compsize, 1, sizeof(unsigned int), fp);
		fseek(fp, 0, SEEK_END);


		// ヘッダ内容のコピー
		CopyToCentralDirHeader(&archeader[i], &zipheader[i]);
	}

	// zlibメモリの開放
	deflateEnd(&zs);


	// 中央ディレクトリ情報開始位置の格納
	current = ftell(fp);

	// 中央ディレクトリの書き込み
	for(int i = 0; i < ARCHIVECOUNT; i++){
		WriteCentralDirHeader(fp, &archeader[i]);
	}

	// 終端ヘッダの書き込み
	EndCentDirHeader endheader;
	memset(&endheader, 0, sizeof(EndCentDirHeader));
	endheader.signature = ZIPSIG_ENDCENTDIR;	// PK0506
	endheader.direntry = ARCHIVECOUNT;
	endheader.diskdirentry = endheader.direntry;
	endheader.startpos = current;
	endheader.dirsize = ftell(fp) - current;
	WriteEndCentDirHeader(fp, &endheader);

	fclose(fp);
}

展開例
(7-zipによるサンプルファイル展開例)

 2番目の文字列には、共通する単語がほとんどないため、逆にファイルサイズが増える現象が発生しています。元のデータが小さいのであれば、圧縮後データの結果次第ではあえて無圧縮のオリジナルデータをそのままzipファイルに格納するのもよいかもしれません。

 今回ご紹介した圧縮zip作成サンプルに関するプロジェクトファイル(Visual Studio 2008用)は、こちらのリンクよりダウンロードできます。なお、「zlib.h」「zconf.h」「zlib.lib」については各自で入手・コンパイルしてください。