複数の画像を一括でリサイズできる完全ブラウザベースのWebツールを開発しました。
サーバーにアップロードすることなく、クライアント側だけで処理が完結するので、プライバシーも安心です。
- 🔗 ツールURL: https://tools.easegis.jp/ja/tools/image/image-resizer
- 🚀 使い方: ファイル選択 → リサイズ設定 → 一括処理 → ダウンロード
- 📦 技術: Next.js 16 + React 19 + TypeScript + Canvas API
機能一覧
基本機能
- 複数画像の同時アップロード(ドラッグ&ドロップ対応)
- クリップボードから画像を直接ペースト(Ctrl+V/Cmd+V)
- アスペクト比を維持したリサイズ
- リアルタイムプレビュー(Before/After表示)
- 個別ダウンロード または ZIP一括ダウンロード
- リサイズ後の画像をクリップボードにコピー
UI/UX特徴
- サムネイル一覧で画像を選択・削除
- リサイズ完了時に「✓ 完了」バッジ表示
- ダークモード対応
- レスポンシブデザイン
実装手順
1. プロジェクトセットアップ
npx create-next-app@latest image-resizer --typescript --tailwind --app
cd image-resizer
npm install jszip lucide-react
2. Canvas APIを使った画像リサイズの実装
const resizeImage = (
img: HTMLImageElement,
width: number,
height: number
): string => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = width
canvas.height = height
ctx?.drawImage(img, 0, 0, width, height)
return canvas.toDataURL('image/png')
}
ポイント:
- Canvas APIを使うことでGPU加速された高速な画像処理が可能
-
drawImageで任意のサイズに描画 -
toDataURLでData URL形式に変換
3. アスペクト比を維持する計算
if (maintainAspect) {
const aspectRatio = img.width / img.height
if (targetWidth / targetHeight > aspectRatio) {
newWidth = Math.round(targetHeight * aspectRatio)
} else {
newHeight = Math.round(targetWidth / aspectRatio)
}
}
4. クリップボード操作の実装
// Data URLをBlobに変換
const dataUrlToBlob = (dataUrl: string): Promise<Blob> => {
return fetch(dataUrl).then((res) => res.blob())
}
// クリップボードにコピー
const copyFromDataUrl = async (dataUrl: string) => {
const blob = await dataUrlToBlob(dataUrl)
await navigator.clipboard.write([
new ClipboardItem({ [blob.type]: blob })
])
}
ポイント:
- Data URLを直接Blobに変換することで、Clipboard APIで扱いやすくする
- JPEG/WebPをPNGに自動変換して互換性を確保
5. 複数画像のZIPダウンロード
import JSZip from 'jszip'
const downloadAllAsZip = async () => {
const zip = new JSZip()
images.forEach((img, index) => {
if (img.resizedUrl) {
const base64Data = img.resizedUrl.split(',')[1]
zip.file(`resized_${index + 1}.png`, base64Data, { base64: true })
}
})
const content = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(content)
const a = document.createElement('a')
a.href = url
a.download = 'resized_images.zip'
a.click()
URL.revokeObjectURL(url)
}
6. FileReader APIでローカル画像を読み込み
const handleFileUpload = (files: FileList) => {
Array.from(files).forEach((file) => {
const reader = new FileReader()
reader.onload = (event) => {
const img = new Image()
img.src = event.target?.result as string
img.onload = () => {
setImages((prev) => [
...prev,
{
id: Date.now(),
originalUrl: img.src,
width: img.width,
height: img.height,
},
])
}
}
reader.readAsDataURL(file)
})
}
技術的な工夫ポイント
1. useRefで隠しCanvasを再利用
const canvasRef = useRef<HTMLCanvasElement | null>(null)
useEffect(() => {
if (!canvasRef.current) {
canvasRef.current = document.createElement('canvas')
}
}, [])
DOM作成コストを削減し、パフォーマンスを向上させています。
2. Promise.allで並列処理
await Promise.all(
images.map(async (img) => {
const resizedUrl = await resizeImage(img)
return { ...img, resizedUrl }
})
)
複数画像を同時に処理することで、処理時間を短縮しています。
3. セキュリティ対策
- クライアント側のみで処理(サーバー送信なし)
- Content Security Policy (CSP) でインラインスクリプト制限
- ファイルタイプのバリデーション
// next.config.jsでセキュリティヘッダー設定
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'",
},
]
Known Issues & Tips
Issue 1: クリップボード操作が失敗する場合
原因: HTTPSでない環境やブラウザの権限設定
解決策:
try {
await navigator.clipboard.write([...])
} catch (error) {
console.error('Failed to copy to clipboard:', error)
alert('クリップボードへのコピーに失敗しました。HTTPSで接続しているか確認してください。')
}
Issue 2: 大量の画像を処理するとメモリ不足
対策:
- Data URLではなくBlob URLを使用
- 処理後に
URL.revokeObjectURL()でメモリ解放
const blobUrl = URL.createObjectURL(blob)
// 使用後
URL.revokeObjectURL(blobUrl)
Tip: 画像品質の調整
// JPEGの場合、第2引数で品質を指定(0.0〜1.0)
canvas.toDataURL('image/jpeg', 0.9)
まとめ
ブラウザベースの画像リサイズツールを、Canvas APIとFileReader APIを活用して実装しました。完全なフロントエンド実装により、サーバー側の負荷を一切かけず、プライバシーを保ちながら高速に処理できるのが特徴ですね。
個人開発でこういったWebツールを作る際は、サーバーレスで完結する設計が手軽で良いなと実感しました。ZIP一括ダウンロードやクリップボード連携など、ユーザビリティを高める機能も意外と簡単に実装できます!
ぜひ試してみてください!
🔗 画像リサイズツール: https://tools.easegis.jp/ja/tools/image/image-resizer
