JavaでZopfli(ツオップリ)を使う

Java Zopfli 圧縮する方法を紹介します。

Zopfli ツオップリ 2013 年に Google が発表した圧縮アルゴリズムおよびその実装です。zlib よりも 3~8%くらい圧縮率が高くなります。圧縮率だけで言えば Zopfli を上回るアルゴリズムはいくつもあります。Zopfli の最大の特徴は Deflate 互換であることです。つまり Zopfli で圧縮したデータは既存の Deflate デコーダーで展開することができます。

PNG gzip Deflate を使用しています。たとえば Zopfli に同梱されている zopflipng を使って画像を PNG 形式に圧縮すれば ファイルサイズを小さくすることができ そして それは既存の PNG デコーダーで展開することができます。すなわち 従来のブラウザーでそのまま表示できるということです。

PNG だけではありません。HTML/CSS/JavaScript といったテキストファイルの転送量も削減することができます。ほとんどのブラウザーは gzip 圧縮されたデータを展開する機能を持っています。これまで Web サーバーは zlib で圧縮した gzip データをブラウザーに返すことが多かったのですが これを Zopfli で圧縮した gzip データを返すように変更することができます。ブラウザーは Zopfli で圧縮されたデータを従来のデコーダーでそのまま展開することができるため 互換性が損なわれることはありません。

データ展開の互換性を維持したまま圧縮率を高くすることができる これが Zopfli の最大の特徴です。

Google 公式の Zopfli C 言語で実装されています。Go 言語のラッパーは用意されていますが 他の言語の実装は提供されていません。

肩を落とす必要はありません。Zopfli はオープンソースです。すでに 様々なプログラミング言語で実装された Zopfli ライブラリが続々と登場しています!

読み方を教えて?
ツオップリと読みます。zopf ツオップ パンの図鑑によるとドイツ語で三つ編みのような形をしたパンのことだそうです。日本でも ツオップ という名前のパン屋さんを見かけますね。li は英語の let みたいに小さな感じを表わします。Server + let Servlet サーブレット App + let Applet アプレット のような。

jzopfli

jzopfli Zopfli Java に移植したライブラリです。

ビルド済みの JAR ファイルを jzopfli の公式サイトからダウンロードすることはできません。サイトからソースコードをダウンロードして自分でビルドするか もしくは jcenter mavenCentral を利用することもできます。

Gradle Maven も使っていない人は mavenCentral から JAR ファイルをダウンロードすると良いでしょう。JAR ファイルへの直接リンクを載せておきます。

私は Java でプログラミングするときは Eclipse を使っています。Eclipse には Buildship という Gradle プラグインが標準で含まれていて 手軽に Gradle プロジェクトを手軽に扱えるようになっています。

今回も Gradle の場合の手順を説明していきます。ライブラリの導入手順が異なるだけで jzopfli の使い方は JAR ファイルを直接ダウンロードして導入した場合と同じです

build.gralde を以下のように書きます。

build.gradle
apply plugin: 'eclipse' apply plugin: 'java' repositories { jcenter() } dependencies { compile 'com.github.luccappellaro:jzopfli:0.0.4' }

これだけで jzopfli ライブラリが jcenter から自動的にダウンロードされて自分のプロジェクトから参照できる状態になります。

C 言語の匂いがする

jzopfli 元々 C 言語で実装されている Zopfli をそのまま Java に置き換えたような感じのコードになっています。そのせいか メソッド名が大文字で始まっていたり 結果を引数で受け取る 出力引数 といった Java らしくない API になっています。

まずは 簡単なラッパーを書いて Java っぽい API にしてみます。

Zopfli.java
package com.example; import java.util.Arrays; import lu.luz.jzopfli.Zopfli_lib; import lu.luz.jzopfli.ZopfliH.ZopfliFormat; import lu.luz.jzopfli.ZopfliH.ZopfliOptions; public class Zopfli { public static byte[] compress(byte[] data) { ZopfliOptions options = new ZopfliOptions(); ZopfliFormat output_type = ZopfliFormat.ZOPFLI_FORMAT_GZIP; byte[][] out = { { 0 } }; int[] outsize = { 0 }; Zopfli_lib.ZopfliCompress( options, output_type, data, data.length, out, outsize); return Arrays.copyOf(out[0], outsize[0]); } }

jzopfli の本体は ZopfliCompress メソッド 関数? です。このメソッドに 圧縮オプション 形式 入力バイト列 入力バイト数 出力バイト列 出力バイト数を指定します。

出力バイト列 out と出力バイト数 outsize の型が配列になっているのは結果を返すためですね。Java には参照渡しがなく値のコピーが引数として渡されるため メソッドで引数に値を代入しても 呼び出し元ではそれを受け取ることができません。ただし 渡されたインスタンスのフィールドを書き換えたり 渡された配列の要素を書き換えた場合 その影響はメソッド復帰後も残ります。結果を引数に乗せて返すために 引数の型を int ではなく int[] byte[] ではなく byte[][] にしているわけです。このような結果を返すことを目的とした引数を出力引数といいます。

それから 出力バイト列の長さ out.length と出力バイト数 outsize は一致しないことにも気を付けてください。outsize が出力データの正しい長さを表しています。出力バイト列は 64 バイト単位で確保されるようで たとえば outsize 40 でも out.length 64 になります。

そこで Arrays.copyOf メソッドを使って 正しい長さ outsize に切り詰めたバイト列を作り直しています。

gzip zlib deflate の違い

形式 output_type には ZopfliFormat.ZOPFLI_FORMAT_GZIP を指定しています。これで 出力形式が gzip になります。

Zopfli jzopfli では出力形式として gzip zlib deflate が選択できます。選択した形式によって出力バイト列に付加されるヘッダーやトレイラーが変わりますが 本体である圧縮データ部はどの形式を選択しても同じです。

deflate
deflate 形式は圧縮されたデータのバイト列そのものです。ヘッダーやトレイラーが付いていません。
zlib
zlib 形式は deflate にヘッダーとトレイラーが付いたものです。ヘッダーとトレイラーを除いた本体部分は deflate 形式そのものです。詳細は RFC1950 日本語訳で確認できます。
gzip
gzip 形式は deflate にヘッダーとトレイラーが付いたものです。ヘッダーとトレイラーを除いた本体部分は deflate 形式そのものです。詳細は RFC1952 日本語訳で確認できます。

つまり gzip zlib deflate で圧縮率に違いはありません。ヘッダーやトレイラーによってわずかなファイルサイズの違いはでますが ここで数バイト小さいことに執着するべきではありません。ヘッダーやトレイラーの付いてない deflate よりも CRC が付加されていてデータの破損を検出できる gzip のほうが安全です。

使ってみる

ラッパークラス Zopfli を使ってみましょう。使い方は簡単です。スタティックメソッド compress の引数に byte[] を渡すと圧縮されたデータが戻り値 byte[] で返されます。これだけです。

以下の例では Hello, World!! という文字列を圧縮して data 変数に格納しています。

String s = "Hello, World!!";
byte[] b = s.getBytes();

byte[] data = Zopfli.compress(b);

この圧縮したデータを正しく展開できるかも試しておきましょう。形式を gzip にしているので このデータは Java に標準で含まれている GZIPInputStream クラスを使って展開できます。これが Zopfli の強みですね!

data 変数を元に GZIPInputStream インスタンスを作成します。そして readAllBytes メソッドを呼び出せば展開された元のバイト列 b2 が得られます。これを String にして画面に表示すれば元の文字列 Hello, World!! が表示されます。

try(GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(data))) {
	byte[] b2 = in.readAllBytes();
	String s2 = new String(b2);
	System.out.println(s2);
}

ファイルを圧縮してサイズを比較する

ファイルも圧縮してみましょう。サンプルデータとして Yahoo!トップページの HTML yahoo-top-page.html というファイル名でローカルに保存しました。これを jzopfli で圧縮して yahoo-top-page.html.gz というファイル名で保存します。

File input  = new File("yahoo-top-page.html");
File output = new File("yahoo-top-page.html.gz");

try(InputStream in = new FileInputStream(input);
		OutputStream out = new FileOutputStream(output)) {
	out.write(Zopfli.compress(in.readAllBytes()));
}

短いコードで書けました。これだけでファイルを圧縮できてしまいます。

圧縮前の yahoo-top-page.html 277,181 バイト 圧縮後の yahoo-top-page.html.gz 80,556 バイトでした。29.06%のサイズになっています。

でも これって他の gzip エンコーダーより本当に優れているのでしょうか? Java 標準の GZIPOutputStream クラス gzip.exe 7-zip の圧縮結果とも比較してみましょう。

gzip.exe では 最高圧縮レベル -9 を指定しました。

コマンドプロンプト
C:¥>gzip.exe -9 yahoo-top-page.html

7-Zip では gzip 超圧縮 ワードサイズ=258 を指定しました。

結果は以下のようになりました。

ファイルサイズ圧縮率
未圧縮277,181100.00%
GZIPOutputStream85,28930.77%
gzip.exe -984,51730.49%
7-Zip gzip 書庫形式81,26629.30%
jzopfli80,55629.06%

jzopfli 7-Zip を上回る圧縮率を叩き出してくれました。Java 標準の GZIPOutputStream と比べて 5.54%も小さくなっています。Zopfli の看板に偽りなしですね。

サンプル プログラム

サンプル プログラムのソースコードは下記のリンクからダウンロードできます。

zopfli-sample.zip (137KB)

この記事を共有しませんか?