「アップロードできる画像は2MBまで」「プロフィール画像は正方形で」——画像のサイズを変えたい場面は地味に多いです。わざわざアプリを入れなくても、ブラウザ(JavaScript)だけで画像はリサイズできます。しかもファイルをサーバーに送らずに完結できます。
この記事では、Canvas を使った画像リサイズの基本と、縦横比を保つ計算やきれいに縮小するコツをまとめます。おまけに Python(Pillow)版も載せます。
ゴール:画像を指定した幅・高さに、縦横比を保ったまま、きれいにリサイズできるようになること。
基本:Image を読み込んで Canvas に描き直す
リサイズの流れは「画像を読み込む → 目的のサイズの canvas に描画する → 画像として書き出す」だけです。
<input type="file" id="file" accept="image/*">
document.getElementById("file").addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
// 1. 画像を読み込む
const img = await loadImage(file);
// 2. 目的のサイズを決める(ここでは幅400pxに、縦横比維持)
const targetW = 400;
const targetH = Math.round((targetW * img.naturalHeight) / img.naturalWidth);
// 3. canvasに描画してリサイズ
const canvas = document.createElement("canvas");
canvas.width = targetW;
canvas.height = targetH;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high"; // 縮小をなめらかに
ctx.drawImage(img, 0, 0, targetW, targetH);
// 4. 書き出し(Blob推奨。大きい画像でも軽い)
canvas.toBlob((blob) => download(blob, "resized.jpg"), "image/jpeg", 0.9);
});
function loadImage(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
img.onerror = reject;
img.src = url;
});
}
function download(blob, name) {
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement("a"), { href: url, download: name });
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
ポイントは toDataURL ではなく toBlob を使うこと。Data URL は文字列なので大きい画像だとメモリを食いますが、Blob なら効率的で、そのままダウンロードにも使えます。
縦横比を保つ計算
指定の仕方に応じて、もう片方の辺を計算します。
// 幅を基準に高さを求める
const h = Math.round((w * img.naturalHeight) / img.naturalWidth);
// 高さを基準に幅を求める
const w2 = Math.round((h2 * img.naturalWidth) / img.naturalHeight);
// 「長辺を maxSize に収める」パターン(サムネイル向け)
function fit(img, maxSize) {
const scale = Math.min(1, maxSize / Math.max(img.naturalWidth, img.naturalHeight));
return {
w: Math.round(img.naturalWidth * scale),
h: Math.round(img.naturalHeight * scale),
};
}
Math.min(1, …) を挟むと「元より大きくしない(拡大しない)」制御になります。
きれいに縮小するコツ
-
imageSmoothingQuality = "high"を指定する。 既定より補間がなめらかになります。 -
大幅な縮小は
createImageBitmapが手軽できれい。 ブラウザ側で高品質にリサイズしてくれます。
// 幅400pxへ高品質リサイズ(縦横比は自分で計算して渡す)
const bitmap = await createImageBitmap(file, {
resizeWidth: 400,
resizeQuality: "high",
});
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext("2d").drawImage(bitmap, 0, 0);
createImageBitmap はデコード&リサイズを一括で行うので、drawImage で一気に縮小するより輪郭のジャギーが出にくいです。
出力形式と容量
toBlob の第2・第3引数で形式と画質を選びます。
canvas.toBlob(cb, "image/jpeg", 0.8); // 写真向け。0〜1で画質
canvas.toBlob(cb, "image/webp", 0.8); // さらに軽い。透過も可
canvas.toBlob(cb, "image/png"); // 透過を保ちたいとき(qualityは無視)
写真はJPEGかWebP、透過が必要ならPNGかWebP、が基本の使い分けです。
つまずきやすいところ
-
iPhone写真などの向き(EXIF Orientation)。 生のピクセルを canvas に描くと、回転情報が反映されず横倒しになることがあります。
createImageBitmap(file, { imageOrientation: "from-image" })を使うと、EXIF の向きに合わせて起こしてくれます。 - 拡大は粗くなる。 元より大きくすると引き伸ばしになるので、基本は「縮小」で使うのが無難です。
-
toBlobは非同期。 コールバック(またはPromise化)で受け取ります。書き出し直後にサイズを測りたいときは順番に注意。 - 巨大画像はメモリに注意。 スマホで数千万画素を扱うと重いので、必要なら描画dpiや最大辺を制限します。
Python でやる場合(Pillow)
サーバーやバッチなら Pillow が定番です。
from PIL import Image, ImageOps
img = Image.open("input.jpg")
img = ImageOps.exif_transpose(img) # EXIFの向きを反映
# 幅400pxへ(縦横比維持)
w = 400
h = round(w * img.height / img.width)
img = img.resize((w, h), Image.LANCZOS) # LANCZOSできれいに縮小
img.save("resized.jpg", quality=90)
# 「長辺512pxに収める」ならthumbnailが手軽(アスペクト比を自動維持)
img.thumbnail((512, 512), Image.LANCZOS)
まとめ
- ブラウザだけなら Image → canvas に
drawImage→toBlobでリサイズできる - 縦横比は もう片方の辺を計算、
Math.min(1, …)で拡大を防げる - きれいに縮小したいなら
imageSmoothingQuality="high"やcreateImageBitmap(resizeQuality:"high") -
EXIFの向きは
imageOrientation:"from-image"/ Pillowのexif_transposeで対処 - Python は Pillow(
resize+LANCZOS/thumbnail)
「画像をちょっと小さくしたい」に、ブラウザ完結はよく合います。参考になれば嬉しいです。