document

HTML5 & JavaScript side
twitter: @y_imaya

2012年12月

ファイルサイズを考慮した Canvas の保存

こんにちは、18 日以降の Graphical Web Advent Calendar が空いているので、場をつなぐ意味も込めて簡単な記事を投稿させていただきます。
先日の記事では PNG の仕様について書きましたが、その知識をさっそく生かす事ができます。

また、この記事では HTMLCanvasElement を省略して Canvas と表記させていただきます。

Canvas#toDataURL()

さて、一般的に Canvas の描画状況を保存しようと思うと、Canvas#toDataURL メソッドを使用すると思います。
ですが、このメソッドで保存された画像がどのようになっているかご存知の方はあまりいないと思います。

まずは、以下のコードで簡単な Canvas 描画を行ってみます。

function draw1(targetId) {
    var target = document.getElementById(targetId);
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');

    ctx.fillStyle = 'rgb(0, 0, 0)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    target.src = canvas.toDataURL();
}

これを行うと、以下のような画像が表示されます。

Chrome でこの画像を保存し、IHDR を見てみると以下のようになっています。

Width300
Height150
BitDepth8
ColourTypeTruecolor_with_alpha (6)
CompressionMethodDeflate (0)
FilterMethodBasic (0)
InterlaceMethodNone (0)

使用している色が限られているのに、Truecolour with alpha で保存されています。
Canvas#toDataURL メソッドでは、全ての画像が Truecolour with alpha になってしまうようです。
これは大きな無駄となります。

たとえば WebStorage に保存するときなど、少しでもサイズを小さくするために Indexed Colour やグレイスケールで保存したい場合もあると思います。
ここから、Indexed Colour などで Canvas を保存する方法について説明します。

CanvasTool.PngEncoder

また自作ツールの宣伝で恐縮なんですが、だいぶ前に PNG エンコーダを JavaScript で自作したのでそれを利用します。
まだパフォーマンスの面などで問題はあるんですが、そこそこ使えると思います。

PNG byte-string の作成

このライブラリは以下のように使用します。

var encoder = new CanvasTool.PngEncoder(
    canvas,
    {
        bitDepth: 1,
        colourType: CanvasTool.PngEncoder.ColourType.GRAYSCALE
    }
);
var png = encoder.convert();

上記の描画例では、黒で塗りつぶしてるかそうでないかだけなので、ビット深度 1 のグレイスケール画像で保存します。
また、Indexed Colour で保存する場合は以下のようにします。

var encoder = new CanvasTool.PngEncoder(
    canvas,
    {
        bitDepth: 1,
        colourType: CanvasTool.PngEncoder.ColourType.INDEXED_COLOR
    }
);
var png = encoder.convert();

(注意: Indexed Colour では使用している色数が 2^bitDeph を超えていたら例外を投げます。色数がパレットの最大数を超えないように使用してください。)

単純に RGBA ではなく RGB で保存したい場合は以下のようにします。

var encoder = new CanvasTool.PngEncoder(
    canvas,
    {
        bitDepth: 8,
        colourType: CanvasTool.PngEncoder.ColourType.TRUECOLOR
    }
);
var png = encoder.convert();

ちなみにこのライブラリでは Truecolour などで bitDepth: 16 も指定できますが、元の色が bitDepth:8 のため意味はないでしょう。

Data URL への変換

変数 png は ByteString なので、これを DataURL で使用するには Base64 形式にエンコードします。(そのままで使える場合もあるのですが、ここでは省略します)
ByteString から Base64 へのエンコードは window.btoa が使える環境ではそれを使うのが最も簡単で高速です。
window.btoa が使えない環境では polyfill を使うのが良いと思います。

window.btoa を使うと、以下のようになります。

var dataUrl = 'data:image/png;base64,' + window.btoa(png);

動作例

実際に前述のコードをそれぞれ動かしてみます。

ビット深度 1 のグレイスケール画像

ビット深度 1 の Indexed Colour 画像

ビット深度 8 の Truecolour 画像

比較

生成した画像をダウンロードしてファイルサイズを比較してみます。( Google Chrome 23 で確認)

Canvas#toDataURL()GrayscaleIndexed ColourTruecolour
1,869106136543

一般的な画像での比較 (16:00追加)

例として使用している Canvas が適当すぎるので、一応きちんとした画像でも比較してみたいと思います。以下の画像を Canvas に drawImage して、それを保存します。

ubunchu01_01_small
  • 架空線 – AERIAL LINE - : 第1話 「うぶんちゅがやって来た!」:
    http://www.aerialline.com/comics/ubunchu/episode01
  • © 瀬尾浩史
  • 元の画像から 1/4 サイズにして 256 色に減色しています
  • いつもお世話になっております

Canvas#toDataURL()

CanvasTool.PngEncoder

Chrome 23 で試したところ、以下のような結果になりました。

Canvas#toDataURL()CanvasTool.PngEncoder
処理時間32 ms1,614 ms
サイズ161,78761,006

処理時間がかかりすぎていますが、CanvasTool.PngEncoder では WebWorkers でも(少し手を加えるだけで)利用可能なので、タイムアウトする可能性がある場合は WebWorkers の利用を考えてみるのも良いかもしれません。

また、今回ためした結果は PC なので Canvas#toDataURL メソッドは割と速く終わっていますが、モバイルなどでは毎フレーム実行するなどの激しい操作はまだ難しいと思います。

おわりに

いかがでしたでしょうか。
Canvas#toDataURL メソッドでは指定できない、PNG の細かい設定を行う事によりかなりの容量の節約ができるようになります。
今回はライブラリを使いましたが、簡単なPNGであれば自作も楽ですのでぜひ挑戦してみてください。

また、自力でエンコードは圧縮処理の都合上どうしても遅くなってしまうので、速さとサイズ、どちらを優先するか見極めて調整することが必要となります。

以上、何かの参考になれば幸いです。

PNG 画像の解析と最適化ツール

はじめに

この記事は Graphical Web Advent Calendar の 16 日目の記事として書かれました。
Graphical Web ということで、PNG フォーマットの簡単な説明と Web ブラウザ上で動作する PNG 解析ツールを作ったので使い方と解析結果の見方について書いていこうと思います。
また、人気のある PNG 画像最適化ツールがどのような最適化を行っているのか調べていきます。

PNG の仕様に入る前に

ここから、PNG の仕様について最低限の説明を書いていきます。
PNG の最適化や検証するときに必要になるので、退屈かもしれませんが軽く目を通してください。
以下の項目について、なんとなく分かれば良いです。

  • シグネチャ
  • 必須チャンクの役割
    • IHDR
    • PLTE
    • IDAT
    • IEND

PNG 仕様概要

PNG フォーマットは、先頭にシグネチャがあり、そこからチャンクと呼ばれるデータが連続する形で構成されています。

structure

シグネチャ

シグネチャは PNG ファイルであることを確認するために使用されます。
シグネチャは必ず以下の8バイトとなっています。

0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a

PNG ファイルをテキストエディタなどで開いたときに先頭のあたりに「PNG」という文字を見かけた事はないでしょうか?
それはこのシグネチャを ASCII にしたときの表示の一部です。

00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
00000010  00 00 01 00 00 00 01 00  08 06 00 00 00 5c 72 a8  |.............\r.|
00000020  66 00 00 00 19 74 45 58  74 53 6f 66 74 77 61 72  |f....tEXtSoftwar|
00000030  65 00 41 64 6f 62 65 20  49 6d 61 67 65 52 65 61  |e.Adobe ImageRea|
...

チャンク

チャンクはデータを格納する形式で、PNG ではシグネチャをのぞいて全てのデータがこの形式で記述されています。
チャンクは以下のような形になっています。

データの大きさチャンクタイプデータ破損チェック
4 Byte4 Byte可変4 Byte

チャンクの大きさは計算可能なので、対応していないチャンクタイプでも読み飛ばすことで表示に影響を与えないようにする事ができます。

  • データの大きさ
    • 文字通り、データの大きさが整数で入っている
    • つまり、チャンクの大きさは 12 + この数値となる
  • チャンクタイプ
    • ASCII で 4 文字はいっている。先頭が大文字なら必須チャンク
  • データ
    • チャンクタイプごとに決められた形式ではいっているデータ
  • 破損チェック
    • CRC-32 という形式でチャンクタイプ+データが破損していないか確認するためのデータ

必須チャンク

PNG では画像として最低限必要な情報を必須チャンクと呼び、以下のものが必須チャンクとされています。

  • IHDR
  • PLTE (ただし、画像タイプが Indexed Colour のときのみ必須)
  • IDAT
  • IEND

PNG はこの4つのチャンクさえサポートしていれば大体なんとかなるので、今回は必須チャンクに絞って説明します。

IHDR チャンク

IHDR チャンクは画像の形式に関する情報を扱います。

横幅縦幅ビット深度カラータイプ圧縮方法フィルタ方法インタレース方法
4 Byte4 Byte1 Byte1 Byte1 Byte1 Byte1 Byte
  • 横幅
    • 画像の横幅
  • 縦幅
    • 画像の縦幅
  • ビット深度
    • 色情報につかうビット数を指定します
    • RGB の時、ビット深度が 8 だったら R8G8B8 で 24 bit になります
    • グレイスケールの時、ビット深度が 8 だったら 8 bit になります
    • 1, 2, 4, 8, 16 が有効ですが、カラータイプによって選択できない組み合わせもあります
  • カラータイプ
    • 0: Grayscale (グレイスケール)
    • 2: True Colour (RGB形式)
    • 3: Indexed Colour (パレット形式、カラーマップ形式)
    • 4: Grayscale with Alpha (アルファチャンネル付きグレイスケール)
    • 6: True Colour with Alpha (RGBA形式)
  • 圧縮方法
    • 0: Deflate
  • フィルタ方法
    • 0: Basic
  • インタレース方法
    • 0: None
    • 1: Adam7

PLTE チャンク

カラータイプが Indexed Colour のときだけ必須となり、パレットの情報を扱います。
それ以外のカラータイプでは優先して表示する色を表すチャンクとなります。
データ形式は以下のように RGB でそれぞれ 1 Byte ずつのものが最大 256 個まで連続しています。
いくつ連続しているかはチャンクのデータサイズを 3 で割って算出します。

1 Byte1 Byte1 Byte

また、 Indexed Colour 形式でアルファチャンネルが必要なときは、この後にアルファチャンネルのみの tRNS チャンクが追加されることがあります。

IDAT チャンク

画像本体です。IDAT チャンクでは ZLIB という形式で圧縮された画像の色情報がはいっています。
画像の色情報は、行ごとに分かれていて以下のようになっています。

フィルタ色情報色情報色情報...
1 Byte可変可変可変...

色情報はビット深度と Colour Type によって変わります。

  • Grayscale: (ビット深度) bit
  • Indexed Colour: (ビット深度) bit
  • Truecolor: (ビット深度 * 3) bit
  • Truecolor with alpha: (ビット深度 * 4) bit
  • Grayscale with alpha: (ビット深度 * 2) bit

フィルタについては以下のようになっています。

  • 0: None (そのまま)
  • 1: Sub (左の Byte との差)
  • 2: Up (上の Byte との差)
  • 3: Average (左と上のバイトの平均との差)
  • 4: Paeth (Paethアルゴリズムによる値(詳細は仕様を見てください))

フィルタというとぼかしたりなどのエフェクトを思い浮かべる方もいるかもしれませんが、PNG でいうフィルタとはデータを偏った形で表現するための仕組みです。
グラデーションが多用される画像などでは、隣接する色との差分にした方が圧縮しやすくなるためです。

フィルタに関して注意する点が一つあり、一部の解説では「隣の色」とされていることがありますが「隣の色を含むバイト」との比較です。例えば、RGBA それぞれ 8bit ずつだとすると、隣の対応するバイトとの距離は 4 バイトになります。以前は「隣のバイト」と書いてしまっていたので修正しました。(2013/06/28追記)

また、IDAT チャンクはデータ部分を分割して複数の IDAT チャンクにすることができます。その際、IDAT は必ず連続していなければいけません。

IEND チャンク

画像の終わりを示す空チャンク。データ部分にはなにも入りません。

PNG ファイルの解析

ここまで、PNG の仕様について説明してきました。
いきなり、延々と仕様の話しをされてうんざりした人もいるかもしれません。
しかし、多くの Web 開発関係者は画像サイズを小さくしようとしていますが、そのために PNG とはどういうものか把握しておくのはとても重要な事です。
残念な事に Web 上の情報を見ていると、PNG がどういうものか理解せずにサイズを小さくすると謳って、結果誤った事を書いたものが多く見られます。

ここからは、最近人気のある最適化ツールが一体どのような最適化を行っているのか検証していきます。
最適化については、別のブログで簡単に書いたのでそちらも参考になるかもしれません。

png Identify

では、解析をどのようにするかですが、ちょうど良いツールが見当たらなかったので JavaScript で自作しました。
これは他の PNG の情報ツールでは見られないような圧縮関連の情報も表示するツールとなっていて、圧縮効率の調査や最適化アルゴリズムの調査に活用する事が出来ます。

調査するツール

以下の最近人気のある最適化ツールなどについて調べていきたいと思います。

最適化対象の画像

ライセンスがはっきりしていて、PNG に向いているイラスト調の画像なので「うぶんちゅ!」という漫画の表紙絵を例に使わせていただきます。

ubunchu01_01

サイズ比較

まずは、一番気になるサイズ比較から行っていきましょう。

元画像PNGGauntletImageOptimAzConvPNG
サイズ1,698,5061,629,6681,631,5061,702,386
差分--68,838-67,000+3,880

Indexed Colour 形式での挙動も調べるため、ImageAlpha で 256 色に減色した画像も比較します。

元画像PNGGauntletImageOptimAzConvPNG
サイズ520,806494,704493,989519,658
差分--26,102-26,817-1,148

Truecolor 形式では PNGGauntlet, Indexed Colour では ImageOptim が優れた結果となりました。
では、ここから各ツールでどのような最適化が行われたか見ていきます。

チャンク構成

Truecolor

元の画像では以下のチャンクが付与されていますが、各ツールでは除去されています。

  • pHYs (Physical pixel dimensions)
  • iCCP (Embedded ICC profile)
  • cHRM (Primary chromaticities and white point)
元画像のチャンク構成
chunk_source
ImageOptim で最適化した後のチャンク構成
chunk_imageoptim

これらは、表示する際に色や大きさを決定する際に使われることもありますが、ブラウザによっては無視される事もあるため削除しても特に問題にならない事が多いです。
(これらのチャンクに対応したブラウザで、きっちりと表示色などを制御したい場合は削除してはいけません。)

Indexed Colour

元画像を ImageAlpha で減色した後のチャンク構成
chunk_imagealpha
上記のものを PNGGauntlet で最適化した後のチャンク構成
chunk_imagealpha_pnggauntlet

ImageAlpha で減色した画像では gAMA (Image gamma) という表示に関するチャンクが付与されていますが、これも無視されることがあるので削除してもあまり影響しません。

それよりも特徴的なのは、IDAT が細かく分割されている点です。これはファイルサイズが増えるだけなので最適化ツールにかけると一つにまとめられています。

フィルタ最適化

PNG の特徴であるデータを偏らせる行毎のフィルタですが、どういう状況でどのフィルタを選択するのがベストかというのは実際にやってみるしかありません。
また、仕様にも「Indexed Colour の時はフィルタを使わない方が良い」と書いてあるように、最適化ツールでも Indexed Colour の時はすべて None になっています。(ので省略します)

元画像

filter_source

PNGGauntlet

filter_pnggauntlet

ImageOptim

filter_imageoptim

AzConvPNG

filter_azconvpng

PNGGauntlet と ImageOptim のフィルタの並びが同じになりましたが、これは共に PNGOUT, OptiPNG を使用しているので、同じ結果になったのだと思います。AzConvPNG はフィルタの最適化は行っていないようです。

パレット最適化

ImageAlpha で減色した画像は (Indexed Colour なので当然ですが) パレットをもっており、このパレットの並び順によっても ZLIB の圧縮率に影響する事があります。

記事が縦長になってきたので画像は省略して結論だけ以下にまとめます。

  • AzConvPNG: 独自の並び替え(元のパレットと全然違う)
  • PNGGauntlet: 元のパレットと似ているけど多少異なる
  • ImageOptim: 並び替えを行っていない

ZLIB の圧縮率

ここからが本題です。

フィルタやパレットの最適化を行っても、それだけではファイルサイズは変わりません。
しかし、フィルタやパレット最適化によって「圧縮しやすい状態」になった画像は最終的なファイルサイズで大きな差がでます。
実際に見てみます。

Truecolor

元画像
zlib_source
PNGGauntlet
zlib_pnggauntlet
ImageOptim
zlib_imageoptim
AzConvPNG
zlib_azconvpng

Truecolor では PNGGauntlet と ImageOptim が優秀な結果を出しています。ファイルによって優劣が入れ替わる事も多々あるので、この二つはどちらもほぼ互角といって良い性能ではないでしょうか。
それぞれの各ブロックの圧縮をみると、LZSSの平均長(LZSS-Avg) が似た傾向にあり、どちらもかなりの高圧縮となっています。

Indexed Colour

元画像
zlib_azconvpng
PNGGauntlet (すいません、ブロック数が多すぎたので省略してます)
zlib_pnggauntlet_p
ImageOptim
zlib_imageoptim_p
AzConvPNG
zlib_azconvpng_p

こちらも PNGGauntlet と ImageOptim がほぼ互角です。しかし、パレットの入れ替えを行っていない ImageOptim の方が最終的に小さくなっているため、ImageOptim はこの結果を見越して、あえて入れ替えなかったのかもしれません。フィルタとパレットの選択によっても結果が変わるため、このあたりのアルゴリズムが当たるかどうかで結果が変わるのかもしれません。

また、全体的に各ツールでの圧縮の特徴ですが、元画像や AzConvPNG ではブロックごとの圧縮後のサイズが大体同じサイズになっているのに対し、ImageOptim と PNGGauntlet は大きくばらついています。 ImageOptim と PNGGauntlet の特徴が似ている理由は推測になりますが後述します。

まとめ

最終的な ZLIB の再圧縮では ImageOptim と PNGGauntlet が優秀な結果となりました。

AzConvPNG がこの二つと比べてあまり結果が出せなかったのは、ZLIB 最適化のアルゴリズムにあるとおもいます。
これは推測ですが、ImageOptim と PNGGauntlet はどちらも PNGOUT というツールを使っているのですが、そこで採用されている ZLIB 部分の最適化を担う Kflate (と一部で呼ばれている最適化アルゴリズム)の影響があると思います。
(AzConvPNG は zlib ver 1.2.5 の Deflate パラメータの中で最もサイズの小さくなるものを選択している模様。 zlib のパラメータはバッファのサイズなどを一律で扱うために、圧縮後のブロックサイズがほぼ平坦になるものだと思われます。)

今回は一つのファイルのみを対象としましたが、対象となるファイルによって優劣が変わる事もあるので、どのツールで最適化するか悩んだ場合は自分で解析してみて比べるのも一つの手段です。

また、PNGGauntlet と ImageOptim どちらを選択したほうが良いかについては、わずかでも小さくしたい場合は両方使用して優れている方を採用、そうでない場合は使用しているOSで選択すれば良いのではないでしょうか。

以上、長くなりましたが PNG の仕様と最適化の挙動について書いてきました。
何かの参考になれば幸いです。

記事検索
最新コメント
最新トラックバック
カテゴリ別アーカイブ
タグクラウド
QRコード
QRコード
  • ライブドアブログ