0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザだけで完結!複数画像を一括リサイズできるWebツールを作った

0
Posted at

u3198448477_httpss.mj.runvUos-FacE7k_A_flat_design_illustrati_da9d79db-e4fa-4235-b772-e685d131fa94_0.png

複数の画像を一括でリサイズできる完全ブラウザベースのWebツールを開発しました。
サーバーにアップロードすることなく、クライアント側だけで処理が完結するので、プライバシーも安心です。

機能一覧

基本機能

  • 複数画像の同時アップロード(ドラッグ&ドロップ対応)
  • クリップボードから画像を直接ペースト(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

参考文献

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?