woshidan's blog

そんなことよりコードにダイブ。

iOSにてzlibのdeflate()コマンドでファイルをgzip形式で圧縮する方法について

zlibについてコピペで使うのはいやだなと思ったのでちょっと調べてメモ。

比較的小さいファイルの圧縮を行うなど、圧縮前後のデータを全てメモリ上に展開できる場合は、 compress() を使って一気に圧縮できますが、今回はストリームから少しずつデータを出し入れして圧縮していく deflate() コマンドを利用する場合についてメモします。

zlibとは

まず、zlibが何かをこちらのページから引用します。

zlibとは、圧縮アルゴリズムの一種である Deflate のライブラリであり,C#, Haskell, Java, Perl, Python, Ruby など,主要なプログラミング言語では,軒並み使えるように整備されています.圧縮・伸長が高速なこともあり,ディスク領域の有効利用や通信量の削減を目的として,zlib は気軽に利用できます.

XCodeでのiOSの開発環境への導入について

XCodeでのiOSの開発環境下では、iOS SDKに一部APIが利用できる状態でzlib.hが含まれているため umcompress() はリンクエラーで使えないようでしたが、compress() については zlib.h をimportすれば利用可能でした。

導入しようとしたプロジェクトによっては zlib.h のimportだけでは

Undefined symbols for architecture x86_64:
  "_deflate", referenced from:
      +[GzipSample compress:] in Sunaba(GzipSample.o)
  "_deflateInit2_", referenced from:
      +[GzipSample compress:] in Sunaba(GzipSample.o)
  "_deflateEnd", referenced from:
      +[GzipSample compress:] in Sunaba(GzipSample.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

のようなエラーメッセージでリンクに失敗することがあります。

その場合は、

f:id:woshidan:20170927212154p:plain

libzを利用するアプリのプロジェクトのターゲットの設定で、 Build Phases > Link Binary With Libraries から libz と入力して出てきたものの中から利用するバージョンの libz を追加してビルドしなおしてください *1

f:id:woshidan:20170927212226p:plain

今回は圧縮の方法についてだけひとまずメモしようと思っているのでこれで進みますが、iOS添付でない zlib.h の導入には以下が参考になりそうです。

https://gist.github.com/dulaccc/75f1f49f53e544cef549

zlibでファイルを圧縮する

z_stream構造体のオブジェクトを生成

zlibの基本的な使い方ですが、 z_stream 構造体のオブジェクトを生成して、 zalloc, zfree, opaque のメンバを設定します。

z_stream stream;
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.opaque = Z_NULL;

この時、設定する値はデフォルト値である Z_NULL で基本的に問題ありません。gzip形式で圧縮する場合は、 delateInit2() で stream の初期化処理を行います。

入力データの設定

z_stream 構造体の avail_in, next_in の設定をします。

stream.next_in = (Bytef *)[inputData bytes]; // 入力バッファへのポインタ
stream.avail_in = (uint)[inputData length];  // 入力データの量

出力データの初期設定

自分の環境のzlibではデフォルト値でもよかったんですが、ライブラリなどにする場合、どのzlibとリンクされるか固定ではないので、 z_stream 構造体の avail_out, total_out の初期値の設定をしておきます。

stream.total_out = 0; // これまでの出力されたデータの合計長
stream.avail_out = 0; // 出力データのバッファ上の残量

ストリームの初期化

gzip圧縮の場合、ストリームの初期化は下記の deflateInit2() コマンドで行います。

// int deflateInit2(z_stream *strm,
//                  int level,  .... 圧縮レベル(数値が小さいほど圧縮時間が小さく、大きいほど圧縮後のサイズが小さい)
//                                   デフォルト   Z_DEFAULT_COMPRESSION(=6)
//                                   圧縮速度最高 Z_BEST_SPEED(=1)
//                                   圧縮率最高   Z_BEST_COMPRESSION(=9)
//                                   圧縮無効     Z_NO_COMPRESSION(=0)
//                  int method, .... 圧縮方法     zlib-1.2.6 で指定できるのは Z_DEFLATED のみ
//                  int windowBits,  ウィンドウ・サイズ gzip 形式では  31 を指定する必要あり
//                  int memLevel, .. メモリの消費量を指定 大きくても特に嬉しいことはない. デフォルト8
//                  int strategy); . 圧縮の方式を指定。デフォルトは Z_DEFAULT_STRATEGY
deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY)

これによって、出力バッファの合計長 stream.total_out や、出力データのバッファ上の残量 stream.avail_out などの出力関連のパラメータが設定されます。

圧縮したデータをstreamに出力する

int result = deflate(&stream, Z_FINISH);
// 戻り値の意味
// Z_OK ... まだ出力できる範囲が残っているのでもう一回deflate()が呼び出せる
// Z_STREAM_END ... もう出力できる範囲がない
// Z_BUF_ERROR ... 入力データ、出力バッファの一時的な不足
// Z_STREAM_ERROR ... next_in, next_outがNULLの時や内部状態が破壊された

deflate を実行するたびに stream.next_out で指定されたポインタのバッファへ圧縮結果が書き込まれます。 この出力先のポインタは書き込むたびに書き込んだ分ずらす必要があります。

stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out;
// ... 出力先のバッファのポインタの位置をこれまで書き込んだ分 stream.total_out だけずらして設定
// [NSMutableData mutableBytes] はNSMutableDataで管理している生のバイト列へのポインタを返してくれる

また、出力を書き込むたびにバッファ残量の値が減少するので、不足するようであれば書き込み先のバイト列の長さを伸ばしたり、次に書き込む前に出力バッファ残量が不足しないよう設定し直してやる必要があります。

if (stream.total_out >= [data length])
{
    data.length += ChunkSize;
}               
stream.avail_out = (uInt)([data length] - stream.total_out);

圧縮処理に使ったメモリの解放

最後に後始末のため deflateEnd(&stream) を呼び出して、使ったメモリを開放します。

deflateEnd(&stream);

最終的に書いたみたコード

以上を踏まえつつ、

のコードを参考にして書くとこういうコードになりました。

#import <Foundation/Foundation.h>
#import <zlib.h>

static const NSUInteger CHUNK_SIZE = 1000; // バイト列を伸ばす処理を確認するために小さめにしている
static const int GZIP_WINDOW_BITS = 31;
static const int DEFAULT_MEMORY_USAGE = 8;

+ (NSData *)compress:(NSData *)sourceData
{
    if ([sourceData length])
    {
        z_stream stream;
        stream.zalloc = Z_NULL;
        stream.zfree = Z_NULL;
        stream.opaque = Z_NULL;
        stream.avail_in = (uint)[sourceData length];
        stream.next_in = (Bytef *)[sourceData bytes];
        stream.total_out = 0;
        stream.avail_out = 0;
        
        if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, GZIP_WINDOW_BITS, DEFAULT_MEMORY_USAGE, Z_DEFAULT_STRATEGY) == Z_OK)
        {
            NSMutableData *data = [NSMutableData dataWithLength:CHUNK_SIZE];
            int result = 0;
            while (result == Z_OK)
            {
                if (stream.total_out >= [data length])
                {
                    data.length += CHUNK_SIZE;
                }
                stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out;
                stream.avail_out = (uInt)([data length] - stream.total_out);
                result = deflate(&stream, Z_FINISH);
            }
            deflateEnd(&stream);
            if (result != Z_STREAM_END) {
                return nil;
            }
            data.length = stream.total_out;
            return data;
        }
    }
    return nil;
}

@end

参考

どー考えてもライブラリのリンク関連についてまた勉強する必要がありますが、それはおかわり案件にします。現場からは以上です。

*1:ライブラリ用のプロジェクトなので /path/to/libz.tbd is not an object file (not allowed in a library) とエラーメッセージが出てテスト用にビルドできなくて... という場合はテスト用のターゲットに追加する