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