ランディングページに 6 秒のヒーローアニメーションを置くことになりました。風景写真の上をゆっくりパンする映像です。ffmpeg の定番である 2 パスのパレット生成(palettegen のあとに paletteuse)で、480 × 270、10 fps の GIF に書き出しました。結果は 60 フレーム、6.76 MB です。
ここまで大きくなる理由は 2 つあります。1 つはカメラの動きです。すべてのフレームで全ピクセルが変わるため、各フレームが 480 × 270 の画像としてまるごと保存されます。もう 1 つは、paletteuse がデフォルトで誤差拡散ディザリングをかけることです。細かなノイズが加わり、そのノイズは GIF の圧縮では短くできません。GIF はピクセルを LZW で圧縮しますが、LZW は可逆圧縮なので、ノイズの 1 ドット 1 ドットをすべて書き込むしかありません。
同じファイルを GIF 圧縮ツールのデフォルト設定(ロッシー 40、256 色)にかけると、元の 34% まで縮みました。幅を半分にすれば 545 KB、元の 8% です。とはいえ、これらの設定は魔法ではなく、どの GIF にも同じように効くわけでもありません。画面録画と写真のパンでは反応がまるで違います。この記事では、3 種類の GIF で各設定の効果を実測し、その原因となる GIF フォーマットの仕組みを説明したうえで、つまずきやすい点をまとめます。GIF の容量を減らしたい、GIF を軽量化したいときの判断材料にしてください。
設定ごとのファイルサイズへの効果
以下の数値は、3 つのテスト GIF を GIF 圧縮ツール のエンジンにかけた結果です。
- エディタ録画:640 × 360、92 フレーム、20.1 KB。コードエディタに文字を入力し、カーソルが点滅する様子を録画したもの。単色の背景とアンチエイリアスのかかった文字でできています。書き出しは ffmpeg で、各フレームの変化した矩形だけがすでに保存されています。
- グラデーション:480 × 270、80 フレーム、480.0 KB。ffmpeg のテストソース
gradientsで、なめらかな色の階調がゆっくり流れます。 - 写真パン:480 × 270、60 フレーム、6.76 MB。冒頭のファイルです。1920 × 1080 の写真の上を、切り抜き範囲がパンしていきます。
各セルは、元のサイズに対する出力サイズの割合です(ツールの表示と同じく 1 KB = 1,024 bytes)。
| 設定 | エディタ録画 | グラデーション | 写真パン |
|---|---|---|---|
| ロッシー 0、256 色(可逆での再エンコード) | 99% | 100.2% | 99% |
| ロッシー 40(デフォルト) | 88% | 52% | 34% |
| ロッシー 100 | 82% | 49% | 11% |
| ロッシー 40、64 色 | 72% | 25% | 29% |
| ロッシー 40、64 色、ディザリングあり | 73% | 26% | 32% |
| ロッシー 0、16 色 | 67% | 43% | 37% |
| ロッシー 0、16 色、ディザリングあり | 274% | 71% | 39% |
| ロッシー 40、幅を半分 | 49% | 24% | 8% |
| ロッシー 40、2 フレームごとに 1 枚 | 76% | 26% | 18% |
| ロッシー 80、128 色、幅を半分、2 フレームごとに 1 枚 | 33% | 5% | 2% |
ここから 3 つの傾向が読み取れます。エディタ録画では、フレームの大部分がもともとスキップされているため、ロッシー圧縮はあまり効きません。一方、毎フレーム全ピクセルが新しくなる写真パンでは大きく効きます。幅はどの GIF でも強い設定です。ピクセル数は縮小率の 2 乗で減るからです。そしてディザリングは、ファイルを元より大きくすることがあります。エディタ録画を 16 色にしたとき、ディザリングなしでは 67% でしたが、ありでは 274% に膨らみました。
どこから試すか
| GIF の種類 | 最初に試す設定 | 理由 |
|---|---|---|
| 画面録画(UI の大部分が静止) | 幅、次に色数 128 か 64 | 変化のない領域はすでにスキップされている。削れる余地はピクセル数と色数に残っている |
| 動画クリップやカメラのパン | ロッシー 60–100、次に幅 | 毎フレーム全ピクセルが変わる。ロッシー LZW は、ピクセル・フレーム・色を減らさずにそのデータを短くする |
| なめらかなグラデーションや色のフェード | 色数 64 とロッシー 40、ディザリングなし | 色が少ないほど同じ値が長く続く。ディザリングはバンディング(色の段差)が目立つときだけ有効にする |
| 高フレームレートの長い GIF | フレーム:2 フレームごとに 1 枚 | 保存するフレームが半分になる。残したフレームは置き換えたフレームの分だけ長く表示されるため、全体の長さは変わらない |
gifsicle -O3 を通した GIF | 色数と幅 | 可逆の再エンコードでは 100% 前後にとどまる。情報を削らない限り小さくならない |
| 透過のあるスタンプやロゴ | ロッシーと色数、ディザリングなし | ロッシー圧縮は透明ピクセルを動かさない。ディザリングは不透明ピクセルにノイズを加える |
| どこまで削っても大きく、形式は自由に選べる | MP4 かアニメーション WebP(Bash の節を参照) | 写真的な動きなら、動画コーデックはサイズの桁が違う |
Qiita・Zenn・GitHub に操作 GIF を貼る前に
操作手順を録画した GIF は、Qiita や Zenn の技術記事、GitHub の Issue や PR、社内ドキュメントでよく使われます。貼る前の GIF 圧縮で押さえておきたい点は 3 つです。
- アップロードの上限はサービスごとに違う。 GitHub Docs によると、Issue や PR に添付できる画像と GIF は 10 MB までです。冒頭の写真パン(6.76 MB)は収まりますが、読み手はその 6.76 MB をダウンロードすることになります。Qiita や Zenn にも画像アップロードの容量制限があるので、各サービスのヘルプで確認してください。
- 表示幅を指定してもファイルは軽くならない。 Zenn の
記法や HTML のwidth属性で表示を縮めても、読者がダウンロードするのは元の GIF です。バイト数を減らすには、GIF 自体の幅を縮めます。上の表のとおり、写真パンはロッシー 40 のまま幅を半分にするだけで元の 8% になりました。 - 社内画面の録画は外部にアップロードしたくない。 未公開の管理画面や顧客データが映った GIF を、オンラインの圧縮サービスに送るのは避けたいところです。GIF 圧縮ツールはファイルをブラウザ内だけで処理するので、社内向けの記事に貼る前の GIF 軽量化にも使えます。
GIF がアニメーションを保存する仕組み
GIF ファイルはブロックの並びで、その構造は GIF89a 仕様書 で定義されています。CompuServe が公開した文書で、日付は 1990 年 7 月 31 日です。仕様書には 2 つのバージョンが載っています。1987 年 5 月の「87a」と、1989 年 7 月の「89a」です。アニメーションは、89a のブロックである Graphic Control Extension に依存しています。
ファイルサイズを左右するのは次の部分です。
- カラーテーブル:各ピクセルは、最大 256 色の RGB テーブルへのインデックスです。テーブルのエントリ数は 2^(N+1) で、N は 3 ビットのフィールドなので、2、4、8 … 256 色を持てます。グローバルテーブルはファイル全体で使われます。フレームはローカルテーブルを持つこともでき、そのフレームに限ってグローバルテーブルの代わりになります。
- Image Descriptor:各フレームは、論理スクリーン(キャンバス)内での left・top・width・height を持ちます。フレームがキャンバス全体を覆う必要はありません。
- Graphic Control Extension:フレームごとに 1 つあり、100 分の 1 秒単位のディレイ、破棄方法(disposal method)、任意の透明色インデックスを持ちます。透明インデックスのピクセルは、キャンバスをそのまま残します。
- 画像データ:フレームの色インデックスを LZW で圧縮し、最大 255 バイトのサブブロックに分けて格納したものです。
GIF が保存するのは、インデックスの矩形と、それをどう扱うかのルールです。「フレーム 2 からフレーム 1 を引いた差分」という形のデータはありません。サイズ削減はすべてこのモデルの中で行います。インデックスを減らすか安くする、矩形を小さくする、LZW が短くできる繰り返しを作る、のいずれかです。
LZW と、GIF の圧縮がデフォルトで可逆な理由
LZW は、ピクセルを読みながらインデックス列の辞書を作ります。最初の辞書には、色ごとに 1 つのエントリと 2 つの制御コードが入っています。Clear(2^(最小コードサイズ))と End of Information(Clear + 1)です。エンコーダーは、辞書にある限り、処理中の列を 1 ピクセルずつ伸ばします。次のピクセルを足すと辞書にない列になったら、既知の列のコードを書き出し、伸ばした列を新しいエントリとして登録して、そのピクセルから列を始め直します。
コードの幅は最小コードサイズより 1 ビット広い状態から始まり、辞書が育つにつれて広がって、最大 12 ビット(4,096 エントリ)になります。テーブルが満杯になったら、エンコーダーは Clear を送ってやり直せます。仕様書の cover sheet では、満杯のテーブルを使い続ける「deferred clear」も認められています。ただし、それを扱えない「a large base of decoders」(多数の既存デコーダー)があるため、エンコーダーには使用を控えるよう求めています。GIF 圧縮ツールは、テーブルが満杯になると必ず Clear を送ります。
繰り返しのパターンは辞書の長い列になり、1 つのコードで多くのピクセルを表せます。ノイズはその逆です。インデックスの新しい組み合わせは、再利用できるようになる前にまず辞書に登録しなければならないため、ノイズの多いフレームからは短い列が大量に生まれます。
GIF の特許問題の発端も LZW でした。Terry Welch が 1983 年 6 月に出願し、1985 年 12 月に 米国特許 4,558,302 として成立しています。当初の譲受人は Sperry Corporation です。同社は 1986 年に Burroughs と合併し、Unisys になりました。米国特許は 2003 年 6 月 20 日に失効しています。英国、フランス、ドイツ、イタリア、日本、カナダの対応特許は 2004 年 6 月から 7 月にかけて失効し、それ以降 GIF のエンコードに LZW のライセンスは要りません。
パレットサイズとコード幅
パレットのサイズがコードの幅を決めます。GIF 圧縮ツールはアニメーション全体に 1 つのグローバルカラーテーブルを書き、ローカルテーブルは書きません。色数を 64 にすると、見える色 63 色と透明用の 1 枠を確保します。エントリは 64 なので最小コードサイズは 6、コードは 7 ビットから始まります。256 色なら最小コードサイズは 8、コードは 9 ビットからです。パレットが小さいと近い色が同じインデックスにまとまり、LZW が見つける繰り返しも長くなります。
パレットは、残すすべてのフレームから 1 回だけ作ります。フレーム全体で使われている色が、透明用の枠と合わせてテーブルに収まる数なら、すべての色をそのまま保持し、何も失われません。収まらない場合は、不透明ピクセルのヒストグラムをチャンネルあたり 5 ビット(32,768 バケット)で作り、メディアンカットを実行します。色の誤差が最も大きいバケット群を、広がりが最も大きいチャンネルに沿って、ピクセル数で重み付けした中央値で分割します。これを必要な数のグループができるまで繰り返し、各グループの平均色をパレットの 1 色にします。
フレーム最適化と破棄方法
最初のフレームはキャンバス全体を覆います。以降のフレームでは、エンコーダーが新しい絵と、その時点で閲覧側の画面に表示されている内容を比べ、異なるピクセルを囲む矩形(バウンディングボックス)だけを書き出します。矩形の中でも変化しなかったピクセルは、透明インデックスとして書きます。同じインデックスが長く続くと、数個のコードに圧縮されます。まったく変化のないフレームは、ディレイを保ったまま透明ピクセル 1 個になります。
フレームは破棄方法 1「do not dispose」(破棄しない)で書き出すので、各フレームは直前のフレームの上に積み重なります。透過にはもう 1 つルールが必要です。表示されていたピクセルを次のフレームで透明にしなければならないとき、上から透明ピクセルを描いても何も変わりません。そこでエンコーダーは、その 1 つ前のフレームに破棄方法 2「restore to background」(背景に戻す)を設定し、該当ピクセルを覆うようにそのフレームの矩形を広げます。ブラウザは、次のフレームを描く前に、破棄方法 2 の矩形を透明にクリアします。
入力側のフレームが破棄方法 3(「restore to previous」)やローカルカラーテーブルを使っていても問題ありません。ツールはまず、ブラウザが表示するのと同じ手順で全フレームをデコード・合成し、その結果を上記の方式でエンコードします。
ロッシー LZW
ロッシー LZW(非可逆 LZW)は、エンコーダーの手順を 1 か所だけ変えます。処理中の列に次のピクセルを足した列が辞書にないとき、可逆エンコーダーはそこで列を終えるしかありません。ロッシーエンコーダーはまず、処理中の列を 1 ピクセルだけ伸ばした辞書エントリを調べます。その中に、末尾の色がピクセルの色に十分近いものがあれば、代わりにその色を書き込み、列を伸ばし続けます。条件を満たすものが複数あれば、最も近いものを選びます。
「十分近い」の基準は ロッシー スライダー(0–100、デフォルト 40)で決まります。許容する最大のずれはロッシー × 0.6 で、RGB 空間での 2 色間の距離として測ります。黒から白までの距離は約 441 です。デフォルトでは 24、100 では 60 までのずれを許容します。ロッシー 0 では探索を行わず、ピクセルを正確に書き込みます。
同じしきい値はフレーム最適化にも使われます。新しい色が、画面にすでに表示されている色からロッシー距離以内のピクセルは「変化なし」とみなし、透明として書き込んでそのまま残します。エンコーダーは常に、それまでの置き換えも含めて、閲覧側に実際に見えている内容と比較します。そのため誤差がフレームをまたいで積み重なることはなく、本来割り当てられるパレット色からロッシー距離より離れるピクセルは出ません。透明ピクセルは置き換えの対象外です。透明であるべきピクセルは、必ず透明として書き込まれます。
gifsicle の --lossy オプションも同じ原理で動きます。gifsicle のマニュアルは、この機能を Kornel Lesiński の貢献としています。ただし 2 つの尺度は異なります。gifsicle はガンマ補正した色空間(デフォルトは sRGB)で誤差を測るため、--lossy=40 とこのツールのロッシー 40 は同じ設定ではありません。
リサイズとフレーム間引き
幅 は縮小専用です。空欄、0、元の幅より大きい値のいずれかなら元のサイズのままで、高さは縦横比に合わせて決まります。出力の各ピクセルは、覆っている元ピクセルの平均で、重なった面積と不透明度で重み付けします。不透明な元ピクセルに半分以上覆われていない出力ピクセルは透明になります。GIF には半透明がないためです。
フレーム は 2・3・4 フレームごとに 1 枚を残し、必ず最初のフレームから数えます。間引いたフレームの表示時間は、その直前に残したフレームに加算されます。そのため 6 秒のアニメーションは 6 秒のままです。動きはカクカクしますが、影響が大きいのは速い動きで、スライドショーやゆっくりスクロールするページではほとんど気になりません。
ディレイは 10 ms 刻みで、20 ms から 655.35 秒の範囲で書き込みます。元のディレイが 0 または 10 ms のフレームは、明示的に 100 ms として書き込みます。ブラウザもどのみちそのように再生します(後述の注意点を参照)。
GIF 圧縮ツールの使い方
- GIF をページにドロップするか、クリックして選ぶか、コピーした GIF ファイルを Ctrl/Cmd+V で貼り付けます。ファイルの先頭は
GIF87aかGIF89aである必要があります。 - サイズ、フレーム数、再生時間、繰り返し回数、ファイルサイズが表示され、選択中の設定で 1 回圧縮します。
- 元の GIF と結果が並んで再生されます。両方のサイズ、変化率、出力サイズ、フレーム数、色数をまとめた 1 行も表示されます。
- 設定を変えて 圧縮 を押すと、もう一度試せます。エンコードは重い処理なので、スライダーを動かすたびに再実行はしません。
- GIF をダウンロード で、結果を
{name}-compressed.gifとして保存します。
処理はページ上で短い単位に分けて実行され、デコード、パレット作成、エンコードの各段階で進捗バーが表示されます。ファイルは File API でディスクからページに読み込まれ、タブ内の JavaScript がデコードとエンコードを行います。ファイルや結果を送るネットワークリクエストは発生しません。ロッシー、色数、フレーム、ディザリングの設定は local storage に保存します。幅はファイルごとに設定するもので、保存しません。サイト内のほかのツールと同じく、ツール名とアクション(「compress」または「download」)だけを含む利用イベントを Google Analytics に送信します。
注意点とエッジケース
ディザリングでファイルが大きくなる
ディザリング(このツールでは Floyd–Steinberg 誤差拡散)は、隣り合うパレット色のピクセルを混ぜて、パレットにない色を擬似的に表現します。グラデーションの見た目は良くなりますが、バイト数では二重に損をします。フレーム内では、混ぜたピクセルが、LZW が長い列にするはずの連続を分断します。フレーム間では、絵が静止している部分でもパターンが動くため、「変化なし」とみなせるピクセルが大きく減り、各フレームで保存する範囲が大きく増えます。
実測値を見ると、その影響の大きさがわかります。16 色・ロッシー 0 のエディタ録画は、ディザリングなしで元の 67%、ありで 274% でした。ディザリングで出力が 4 倍になり、元のファイルを大きく上回ったことになります。グラデーションは 43% から 71% になりました。gifsicle のマニュアルにも同じ注意があります。ディザリングは「looks better, but makes bigger files and can cause animation artifacts, so it is off by default」(見た目は良くなるが、ファイルが大きくなり、アニメーションに乱れが出ることもあるため、デフォルトでは無効)とされています。
このツールでもディザリングはデフォルトで無効です。少ない色数でなめらかなグラデーションを扱い、増えるバイト数よりバンディングのほうが気になるときに有効にしてください。GIF の使用色数がもともとパレットに収まる場合、ディザリングは効果がありません。色が正確に保たれ、拡散する誤差がないからです。
結果が元より大きくなる
グラデーションをロッシー 0・256 色で再エンコードすると、元の 100.2% になりました。可逆の再エンコードで小さくなるのは、元ファイルの書き方が非効率だった場合だけです。そして多くの GIF は非効率ではありません。ffmpeg は変化した矩形を透過付きですでに保存していますし、gifsicle -O3 は複数の最適化手法を試します。フレームごとに専用のローカルカラーテーブルを持つファイルも、大きくなることがあります。このツールは最大 255 色の共有パレットを 1 つだけ書くため、フレーム全体でそれより多くの色を使っていれば、量子化をやり直す必要があるからです。
出力が小さくならなかったときは、ページにその旨が表示されます。ダウンロードはそのまま可能です。対処法は情報を削ることです。ロッシーを上げる、色数を下げる、幅を縮める、フレームを間引く、のいずれかを試してください。gifsicle のマニュアルも、自身の最適化について同じ限界を記しています。まれに「even -O3 may actually enlarge file size」(-O3 でもファイルサイズが大きくなることがある)とのことです。
指定より色数が少なくなる
色数 64 を指定したグラデーションは、21 色で出力されました。メディアンカットはチャンネルあたり 5 ビットのヒストグラムで動くため、1 バケット分(チャンネルあたり 8 段階)未満の差しかない色は同じバケットに入り、分けられません。互いに近い 131 色でできたなめらかなグラデーションは、約 20 バケットしか埋めません。もともとほぼ同じ色なので実際の影響は小さいものの、サマリー行には実際の色数が表示されます。
ごく短いフレームディレイ
GIF のディレイが 0 または 1(0 または 10 ms)でも、ブラウザはその速さでは再生しません。Chromium の deferred_image_decoder.cc には「We follow Firefox’s behavior and use a duration of 100 ms for any frames that specify a duration of <= 10 ms.」とあります。Firefox の挙動に合わせ、10 ms 以下を指定したフレームは 100 ms で表示するという意味です。Firefox の FrameTimeout.h も、0〜10 ms の生のタイムアウト値を 100 ms に正規化しています。理由は「broken tools generate these values when they actually want a ‘default’ value」(壊れたツールが、本当は「デフォルト」値を望んでいるときにこうした値を生成する)です。
GIF 圧縮ツールはファイルを読み込むときにブラウザのルールを適用するので、ページに表示される再生時間はブラウザでの再生と一致します。こうしたフレームは明示的な 100 ms のディレイで書き込むため、生の値を読むツールも含めて、どこでも同じように再生されます。20 ms 以上のディレイは、書かれている値のまま保持します。
透過は 1 ビットで、パレットの 1 枠を使う
GIF のピクセルは、完全に不透明か完全に透明かのどちらかです。このツールは、合成後のアルファが 128 未満のピクセルを透明として扱います。透過 GIF をリサイズすると、不透明ピクセルに半分以上覆われた縁のピクセルは平均色の不透明ピクセルになり、残りは透明になります。そのため縁は硬いままです。常に同じ背景の上に置くスタンプなら、先に画像編集ソフトでその背景と合成しておくと、ハードな透過よりなめらかな縁になります。
透過ピクセルのない GIF でも、パレットの 1 エントリは常に透明用に確保します。フレーム最適化で、変化のないピクセルを透明として書き込むためです。そのため色数 4 は、見える色が 3 色という意味になります。
サイズの上限
このツールが受け付けるのは、デコード後で最大 5,000 万ピクセル(幅 × 高さ × フレーム数)、1,000 フレーム、1 フレームあたり 16,777,216 ピクセルまでで、一辺は 16,384 px 以下です。デコードしたフレームは RGBA(1 ピクセル 4 バイト)でメモリに保持するため、5,000 万ピクセルは約 200 MB になります。スマートフォンのブラウザでもまだ扱える量です。60 フレームある 1280 × 720 の画面録画は 5,530 万ピクセルで、上限を超えます。この場合はデコードを始める前にファイルを拒否し、代わりにローカルで実行する gifsicle のコマンドを表示します。
gifsicle -O3 --lossy=80 --colors 128 input.gif -o output.gif
出力に残らないもの
出力は、グローバルカラーテーブル 1 つ、インターレースなし、ローカルカラーテーブルなしのシンプルな GIF89a です。ループ回数は保持します。NETSCAPE2.0 拡張は入力にあった場合だけ書き込むので(ANIMEXTS1.0 のループは NETSCAPE2.0 として書き込みます)、1 回だけ再生する GIF は出力でも 1 回だけ再生されます。コメントブロック、XMP データ、その他のアプリケーション拡張はコピーしません。ファイルが途中で終わっていたり壊れていたりする場合は、デコードできたフレームを圧縮し、最後のフレームが欠けている可能性があると表示します。
GIF 以外の形式を選ぶべき場合
このツールの出力は常に GIF です。写真的な動きなら、ほかの形式のほうがはるかに小さくなります。写真パンでは、gif2webp による非可逆のアニメーション WebP が元の 16%、ffmpeg のデフォルト設定で作った H.264 の MP4 が 1.4% でした。単色主体の UI では結果が逆転します。エディタ録画を MP4 にすると GIF の 137%、可逆のアニメーション WebP は 201%、非可逆の WebP は 674% でした。単色の上のアンチエイリアス文字は、動画コーデックより GIF のパレットとフレーム最適化のほうが得意です。コマンドは後述の Bash の節にあります。配信先が動画を受け付けるなら、<video autoplay loop muted playsinline> で MP4 を GIF のように再生できます。
コード例
Python:Pillow でリサイズと再パレット化
このスクリプトは、面積平均で GIF を幅半分に縮め、フレームのサンプルからメディアンカットのパレットを 1 つ作ります。そのパレットにディザリングなしで全フレームを割り当て、短いディレイにはブラウザの 100 ms ルールを適用します。
import sys
from PIL import Image, ImageSequence
src, dst = sys.argv[1], sys.argv[2]
scale, colors = 0.5, 128
frames, durations = [], []
with Image.open(src) as im:
loop = im.info.get("loop") # None:1 回だけ再生する GIF
for frame in ImageSequence.Iterator(im):
rgb = frame.convert("RGB") # 合成済みのフルフレーム。透過は捨てる
size = (max(1, round(rgb.width * scale)), max(1, round(rgb.height * scale)))
frames.append(rgb.resize(size, Image.Resampling.BOX)) # 面積平均
ms = frame.info.get("duration", 0)
durations.append(100 if ms <= 10 else ms) # ブラウザは 0〜10 ms を 100 ms で再生する
# アニメーション全体で 1 つのパレット。最大 16 フレームを縦に積んで作る
sample = frames[:: max(1, len(frames) // 16)]
w, h = frames[0].size
strip = Image.new("RGB", (w, h * len(sample)))
for i, f in enumerate(sample):
strip.paste(f, (0, i * h))
palette = strip.quantize(colors=colors, method=Image.Quantize.MEDIANCUT)
indexed = [f.quantize(palette=palette, dither=Image.Dither.NONE) for f in frames]
extra = {} if loop is None else {"loop": loop}
indexed[0].save(dst, save_all=True, append_images=indexed[1:],
duration=durations, optimize=False, **extra)
python shrink_gif.py input.gif output.gif で実行します。Pillow 12.1 では、写真パンは 1.49 MB(元の 22%)になりました。Pillow は各フレームを変化した矩形に切り詰め、同一のフレームを統合します(エディタ録画では 92 フレームが 85 フレームになりました)。ただしロッシー LZW は使いません。さらにエディタ録画では 2 フレーム目以降のすべてに 128 色のローカルテーブルを書いたため、出力は 43 KB と、入力の 20.1 KB の 2 倍以上になりました。このスクリプトが向いているのは不透明で写真的な GIF で、透過は設計上捨てています。フレーム最適化とロッシー LZW が必要なら、結果をさらに gifsicle に通してください。
JavaScript:sharp でリサイズと再エンコード
sharp(libvips と cgif エンコーダー)は、animated: true を渡すと全フレームを読み込みます。interFrameMaxError オプションは直前のフレームに近いピクセルを透明にするもので、前述したロッシーのフレーム比較に似ています。
// shrink-gif.mjs:sharp(libvips + cgif)でアニメーション GIF をリサイズ・再エンコードする
import sharp from 'sharp';
const [input, output, width = '240'] = process.argv.slice(2);
const { pages, loop, delay } = await sharp(input).metadata();
console.log(`${pages} frames, loop ${loop}, first delays ${delay?.slice(0, 3).join(', ')} ms`);
const info = await sharp(input, { animated: true }) // 最初のフレームだけでなく全フレームを読む
.resize({ width: Number(width), withoutEnlargement: true })
.gif({
colours: 128, // パレットのエントリ数(透明を含む)
dither: 0, // sharp はデフォルトでディザリングする(1.0)。0 なら変化のないピクセルは変化しないまま
interFrameMaxError: 8, // 直前のフレームとの差がこの値以内のピクセルは透明になる
effort: 7,
})
.toFile(output);
console.log(`${info.size} bytes`);
node shrink-gif.mjs input.gif output.gif 240 で実行します。sharp 0.34.5 では、幅 240 px の写真パンは 484 KB になりました。GIF 圧縮ツールで同じ幅、128 色、ロッシー 40 にすると 508 KB です。sharp はデフォルトでディザリングをかける点に注意してください。GIF 圧縮ツールや gifsicle とは逆です。
Bash:gifsicle、ffmpeg、GIF 以外への変換
# 1. gifsicle:フレーム最適化、ロッシー LZW、128 色、幅 240 px
gifsicle -O3 --lossy=80 --colors 128 --resize-width 240 input.gif -o output.gif
# 2. ffmpeg:パレット 1 つ、誤差拡散ディザリングなしで GIF を作り直す
ffmpeg -i input.gif -vf "scale=240:-1:flags=area,split[a][b];[a]palettegen=max_colors=128:stats_mode=diff[p];[b][p]paletteuse=dither=none:diff_mode=rectangle" output.gif
# 3. GIF をやめる:アニメーション WebP(libwebp)と MP4(H.264)
gif2webp -mixed -q 75 input.gif -o output.webp
ffmpeg -i input.gif -movflags +faststart -pix_fmt yuv420p \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" output.mp4
各コマンドの補足です。
- gifsicle:
-O3は複数の最適化手法を試します(遅くなりますが、小さくなることがあります)。--lossyを値なしで指定すると 20 になります。--colorsには 2〜256 を指定できます。--ditherはバンディングが見えるときだけ追加してください。--batchはファイルを直接書き換えます。 - ffmpeg:
stats_mode=diffは各フレームの変化する部分からパレットを作り、diff_mode=rectangleはpaletteuseに変化した矩形だけを処理させます。ffmpeg の GIF エンコーダーにはロッシーのオプションがありません。そのため写真パンでは、このレシピで幅 240 px にすると 1.49 MB となり、ロッシーの結果の 3 倍でした。エディタ録画では 9.3 KB、元の 46% です。単色主体の素材で最も効果を発揮します。 - gif2webp:
-mixedは、フレームごとに非可逆と可逆のどちらで圧縮するかを選びます。エディタ録画では、単純な-lossy -q 75で GIF の 6.7 倍、-mixedで 6.2 倍のファイルになりました。手元の素材で両方を試してください。 - MP4:
-pix_fmt yuv420p(4:2:0 クロマ)は、4:2:0 の H.264 しかデコードできないプレーヤーに向けた一般的な選択です。縦横が偶数である必要があり、scaleの式で偶数に丸めています。
ほかの GIF 圧縮ツールとの比較
| ZeroTool GIF 圧縮ツール | ezgif GIF optimizer | gifsicle | |
|---|---|---|---|
| 実行場所 | ブラウザのタブ。ファイルは端末の外に出ない | ファイルか URL でアップロード。ページによると、ファイルはアップロードの 1 時間後に削除される | ローカルのコマンドライン |
| 入力の上限 | デコード後 5,000 万ピクセル、1,000 フレーム | 200 MB | マシンのメモリ次第 |
| ロッシー LZW | あり、0–100 | あり。Lossy GIF エンコーダー付きの gifsicle をスライダーで調整 | --lossy、デフォルト 20 |
| 減色 | 256〜4 色、共有パレット 1 つ | あり。複数のバリエーションを表示 | --colors 2–256、複数の手法 |
| フレーム間引き | 2・3・4 フレームごとに 1 枚。表示時間は統合 | 2・3・4 フレームごとに 1 枚。重複フレームの削除 | フレーム選択と --delete |
| リサイズ | 幅、縮小のみ | 別のリサイズページ | --resize、--scale など |
| 一括処理 | 1 回に 1 ファイル | 別の一括最適化ページ | --batch |
どれも、向いている仕事がそれぞれ異なります。
ezgif optimizer は、アップロードされたファイルを、ページの説明によれば Gifsicle と Lossy GIF エンコーダーで圧縮します。このツールにない手法も備えています。fuzz 係数付きの「Optimize Transparency」、coalesce、重複フレームの削除、ファイルサイズの上限に合わせて自動で圧縮する目標サイズ指定などです。受け付ける GIF は 200 MB までです。GIF がこのツールのサイズ上限を超えるとき、目標サイズに合う設定を探してほしいとき、ファイルのアップロードが問題にならないときに使ってください。
gifsicle は、スクリプトで処理するときの基準になるツールです。ビルドパイプライン、バッチ処理、コミットした GIF を小さく保つ CI ステップ、非常に大きなファイルに向いています。複数のパレット・ディザリング手法やリサイズフィルターなど、このツールよりはるかに多くのオプションがあります。
GIF 圧縮ツールが担うのはその中間です。アップロードしたくない GIF(社内ダッシュボード、未発表の製品、顧客の画面)を、選んだ設定で元のファイルと並べて比較でき、インストールも要りません。できるのは GIF を小さくすることだけです。切り抜き、フレーム編集、ほかの形式への変換は対象外で、それらは ezgif、gifsicle、ffmpeg が担います。
関連ツールと参考資料
GIF や画像を扱う ZeroTool のツールです。
- GIF 分解ツール:GIF のフレームを PNG や JPG で書き出したり、フレーム時間付きのスプライトシートを作ったりできます。破棄方法やディレイの扱いは GIF 分解の解説記事 で詳しく取り上げています。
- 画像圧縮ツール:JPEG・PNG・WebP を圧縮・リサイズします。
- WebP 変換器:PNG、JPG、または GIF の最初のフレームを WebP に変換します。
この記事で参照した一次資料です。
- GIF89a Specification(W3C ミラー):ブロック構成、カラーテーブル、Graphic Control Extension、破棄方法、LZW、deferred clear code
- 米国特許 4,558,302(Google Patents):Welch の LZW 特許。出願日、成立日、米国での失効日
- Wikipedia: GIF, “Unisys and LZW patent enforcement”:米国以外の対応特許の失効日
- Gifsicle manual:
-O3、--lossy、--colors、--ditherとリサイズのオプション - FFmpeg filters: palettegen and paletteuse:
stats_mode、dither、diff_mode - gif2webp documentation:
-lossy、-mixed、-q - sharp output options: gif:
colours、dither、interFrameMaxError - Chromium
deferred_image_decoder.ccと FirefoxFrameTimeout.h:短いディレイに対する 100 ms ルール - ezgif GIF optimizer:オンラインでの比較対象
- GitHub Docs:ファイルを添付する:Issue・PR に添付できる画像と GIF のサイズ上限