ZIPを開いて、中の画像だけ入れ替えて詰め直す

Shrinkumaは、画像を何枚も投げ込むと、圧縮してZIPにまとめて返すツールです。

今回足したのは、その逆向きの入口でした。 ZIPを投げ込むと、中の画像だけが軽くなったZIPが返ってきます。

「任意のZIP」を条件にした

作りはじめる前に決めたのは、対象を特定の形式に絞らないことでした。

  • フォルダ構造をそのまま保つ
  • 画像以外のファイルには手を触れない
  • 日本語のファイル名を壊さない

この3つを満たせば、中身が何であっても通せます。 逆に言うと、どれか1つでも欠けると「使えるZIPと使えないZIPがある」という説明の面倒なツールになります。

日本語のファイル名は、放っておくと壊れる

3つのうち、いちばん厄介だったのがファイル名でした。

Windowsのエクスプローラで作ったZIPは、ファイル名がUTF-8ではなくCP932で入っています。 ZIPの仕様にはUTF-8であることを示すフラグがあるのですが、それが立っていません。

このとき、ZIPを読み書きするライブラリ(fflate)は、フラグが無い名前をLatin-1として解釈して返します。 1バイトが1文字に対応するので、生のバイト列自体は保持されます。ただし文字としては化けます。

元:          立ち絵_ナナ.png
読み込み後:  §¿G_ii.png

問題はこの先です。 化けたままの名前でZIPを作り直すと、今度はUTF-8として符号化されます。 バイト列が化けた状態で確定してしまうので、もう元には戻せません。

なので、読み込んだ名前をいったんバイト列に戻して、UTF-8として読めるか試し、駄目ならShift_JISとして読み直す、という復元を挟んでいます。 ※ 中身が正しく圧縮できていても、ファイル名が化けたZIPは使い物になりません。ここは飛ばせない工程でした。

1枚ずつ処理する

ZIPを展開すると、当然ながら中の画像が全部メモリに載ります。 80MBのZIPに346枚入っていたりするので、展開したものを全部持ったまま処理すると落ちます。

1枚取り出して、圧縮して、元のバイト列への参照を捨てる。 これを順番に繰り返す形にしました。

画像処理を並列にすると速くはなりますが、同時に持つ枚数が増えるので、そこは諦めています。

元より大きくなったら、元を使う

画像はPNGからWebPに変換しています。 寸法は変えません。形式を変えるだけで落とす方針です。

ただ、これが全部の画像で効くわけではありませんでした。 100×100くらいの小さなアイコンは、WebPにすると逆に太ります。

なので、変換結果が元のバイト数以上だった場合は、変換せずに元のファイルをそのまま入れています。 1枚ごとに大きいほうを捨てる、という単純な判定です。

追加のライブラリは入れていない

新しく入れたパッケージはありません。

ZIPの読み書きは、まとめてダウンロードする機能で既に使っていたものがあります。 画像1枚を圧縮する処理も、アニメーション画像を変換する経路も、すでにあるものがそのまま使えました。

足したのは、ZIPを開いて1枚ずつ渡し、返ってきたものを詰め直す、という前後の殻だけです。 機能としては大きく見えますが、中身は既存の部品の並べ替えに近いものでした。

実測

検証には、TRPGのオンラインセッションで使う部屋のデータを使いました。 画像が数百枚まとめて入っていて、実際に配布されるものなので、テスト用に作った理想的なZIPよりも当てになります。

  • 画像183枚・72.2MBのZIPが、15.9MB
  • ZIPの中にZIPが入っている80.4MBのもの(画像346枚)が、46.9MB

2つ目はAndroidの実機でも通しました。 処理の途中で他のアプリに切り替えて戻ってきても、続きから進みます。

※ iPhoneは手元に実機が無いため、確認できていません。サイトの注意書きでその旨を伝える形にしています。

これから

ZIPを投げられるようにしたことで、「まず解凍して、画像を選んで、圧縮して、また圧縮する」という手順が1回で済むようになりました。

もともとは自分が使う場面があって足した機能なので、そこはひとまず目的を果たしています。 似たような形のデータは他にもありそうなので、要望があれば個別に見ていくつもりです。