画像を「100KB以下」に確実に収める:品質の二分探索とリサイズの順番
社内システムや申請フォームでよくある「画像は100KB以下」という制限。ユーザーにとっては面倒ですし、作る側としても「アップロード前にクライアントで縮めてあげたい」場面は多いと思います。
この記事では、目標サイズに収める圧縮をどう組み立てるかを整理し、実際に動いている例として Nibbo の「画像を100KBに圧縮」 の挙動も紹介します。
なぜ「品質80で保存」ではダメなのか
JPEGやWebPの品質パラメータは見た目の品質を指定するもので、ファイルサイズを指定するものではありません。同じ品質80でも、青空の写真なら40KB、木の葉が写った写真なら400KBになります。サイズ上限がある場合は、品質を探索する必要があります。
基本アルゴリズム
async function encodeUnder(bitmap, maxBytes, type = 'image/jpeg') {
let lo = 0.3, hi = 0.95, best = null;
for (let i = 0; i < 7; i++) {
const q = (lo + hi) / 2;
const blob = await toBlob(bitmap, type, q);
if (blob.size <= maxBytes) { best = blob; lo = q; } else { hi = q; }
}
return best; // null なら解像度を下げて再挑戦
}
function toBlob(bitmap, type, q) {
const c = new OffscreenCanvas(bitmap.width, bitmap.height);
c.getContext('2d').drawImage(bitmap, 0, 0);
return c.convertToBlob({ type, quality: q });
}
ポイントは3つです。
- 二分探索:品質とサイズはおおむね単調なので、7回程度で十分に詰められます。
- 下限を設ける:品質0.3未満はブロックノイズが目立つので、そこまで下げても入らなければ品質ではなく解像度を下げます。
- OffscreenCanvas + Worker:メインスレッドを止めないようにWorkerで回します。
解像度を先に決めるべき理由
4000×3000(1200万画素)の写真を100KBに入れようとすると、1ピクセルあたり約0.07bitしか使えません。品質をいくら下げても破綻します。一方、1280×960にすれば約0.65bit/px。品質75前後で余裕を持って収まります。
実務では次の順番が安定します。
1. 用途から最大辺を決める(例:1280px)
2. その解像度で品質を二分探索
3. 下限品質でも入らなければ、最大辺を 0.85 倍して 2 に戻る
フォーマット選び
- JPEG:互換性最優先。申請フォームなどはほぼこれ。
- WebP:同画質でJPEGより25〜35%小さい。受け側が対応しているなら有力。
- AVIF:さらに小さいがエンコードが遅く、対応していないシステムも多い。
- PNG:写真には不向き。スクリーンショットや図版なら、色数削減で劇的に小さくなることがある。
フォーマットを変えるだけで制限を満たせることも多いので、画像変換ツールで試してみる価値はあります。
EXIFと向きの罠
-
createImageBitmap(file, { imageOrientation: 'from-image' })を使わないと、スマホ写真が横倒しになることがあります。 - Canvas経由で再エンコードするとEXIFは基本的に落ちます。位置情報を消したい場合は好都合ですが、撮影日時を残したい場合は別途扱いが必要です。
既存ツールを使う場合
自前で実装するまでもない、という場合はブラウザ完結型のツールが便利です。Nibbo は上記と同じ考え方で、目標サイズ(10KB〜2MBのプリセット)に向けて品質と解像度を自動調整します。処理はすべてタブ内で行われ、画像はサーバーに送信されません。ページを開いた後に機内モードにしても動作するので、業務で扱う画像でも安心して使えます。
使い方は、ページを開いて画像をドロップし、結果のサイズとプレビューを確認してダウンロードするだけです。HEICやWebPの入力にも対応しています。
まとめ
- サイズ上限があるなら、品質は「指定」ではなく「探索」する
- 先に解像度を決める。品質だけで頑張らない
- フォーマット変更も有効な選択肢
- 向きとEXIFの扱いに注意
フォームに「100KB以下」と書く側の人も、クライアント側で自動圧縮してあげるとユーザー体験がかなり良くなります。