Web制作の画像最適化を効率化する「画像変換ツール」を作りました

Web制作の画像最適化を効率化する「画像変換ツール」を作りました

Web制作をしていると、画像の最適化作業は避けて通れません。

「この画像、WebPに変換して」  
「AVIFにも対応したいけど、どうやって作るの?」  
「リサイズもしたいし、ちょっと明るくしたい」

こういった要望に対応するため、これまでは複数のツールを行き来していました。  
Squooshで圧縮、Photoshopでリサイズ、別のツールでフィルター適用...

そこで、ブラウザ上で完結する多機能な画像変換ツールを作成しました。
本格的なレタッチや補正は専門家にお任せしたいところですが、フィルターやリサイズは、これだけで賄えるようにしたいと思います。

まずは完成品

https://mifa.tokyo/tools/image-converter

主な機能:

  • PNG/JPG/WebP/AVIFへの相互変換
  • リアルタイムプレビュー
  • リサイズ(パーセント指定/ピクセル指定)
  • 回転・反転
  • フィルター(グレースケール、セピア、明度、コントラスト、彩度、ぼかし)
  • 複数ファイル一括処理(最大10枚)
  • ファイル名保持(拡張子のみ変更)
  • 完全ブラウザ内処理(プライバシー保護)

技術的な実装

1. フォーマット変換の基本実装

Canvas APIを使った基本的な画像変換処理。

javascript
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

// リサイズ計算
const { width, height } = calculateDimensions(img.width, img.height);
canvas.width = width;
canvas.height = height;

// 画像を描画
ctx.drawImage(img, 0, 0, width, height);

// Blob化して変換
canvas.toBlob(
 (blob) => {
  const file = new File([blob], newFileName, { type: mimeType });
  // ダウンロード処理
 },
 mimeType,
 quality
);

PNG/JPG/WebPは、この方法で問題なく変換できます。

2. AVIF変換の特別対応

AVIFの場合、ブラウザのCanvas API `toBlob()`では品質パラメータが正しく反映されないという問題があります。  
そのため、AVIF変換時のみ`@jsquash/avif`ライブラリを使用しています。

javascript
if (targetFormat === 'avif') {
 // ImageDataを取得
 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  
 // AVIFエンコード(quality: 0-100)
 const avifData = await encodeAvif(imageData, { 
  quality: quality * 100 
 });
  
 const blob = new Blob([avifData], { type: 'image/avif' });
 // ファイル化
} else {
 // 通常のCanvas API
}

使い分けの理由:
- Canvas API:PNG/JPG/WebPは標準APIで十分
- @jsquash/avif:AVIFは専用ライブラリで品質調整が確実

3. リアルタイムプレビューの実装

設定変更時に即座にプレビューを更新することで、結果を確認しながら調整できます。

javascript
useEffect(() => {
 if (!previewImage || !canvasRef.current) return;

 const canvas = canvasRef.current;
 const ctx = canvas.getContext('2d');

 // リサイズ計算
 const { width, height } = calculateDimensions(
  previewImage.width, 
  previewImage.height
 );

 // 回転を考慮したキャンバスサイズ
 if (rotation === 90 || rotation === 270) {
  canvas.width = height;
  canvas.height = width;
 } else {
  canvas.width = width;
  canvas.height = height;
 }

 // 変形処理
 ctx.save();
 ctx.translate(canvas.width / 2, canvas.height / 2);
 ctx.rotate((rotation * Math.PI) / 180);
 ctx.scale(flipH ? -1 : 1, flipV ? -1 : 1);
  
 // フィルター適用
 applyFilters(ctx);
  
 // 画像描画
 ctx.drawImage(previewImage, -width / 2, -height / 2, width, height);
 ctx.restore();
  
}, [previewImage, resizeOptions, filterOptions, transformOptions]);

ポイント:
- useEffectで依存配列を監視し、設定変更時に自動再描画
- 座標変換(translate/rotate/scale)で回転・反転を実現
- 複数の変形を組み合わせても正しく描画

4. リサイズ機能の実装

パーセント指定とピクセル指定の2つのモードを用意。

javascript
const calculateDimensions = (originalWidth, originalHeight) => {
 if (resizeOptions.mode === 'none') {
  return { width: originalWidth, height: originalHeight };
 }

 // パーセント指定
 if (resizeOptions.mode === 'percent') {
  const scale = resizeOptions.percent / 100;
  return {
   width: Math.round(originalWidth * scale),
   height: Math.round(originalHeight * scale),
  };
 }

 // ピクセル指定
 if (resizeOptions.maintainAspect) {
  const aspectRatio = originalWidth / originalHeight;
  if (resizeOptions.width && !resizeOptions.height) {
   return {
    width: resizeOptions.width,
    height: Math.round(resizeOptions.width / aspectRatio),
   };
  }
 }

 return {
  width: resizeOptions.width || originalWidth,
  height: resizeOptions.height || originalHeight,
 };
};

実装の工夫:
- パーセント指定:直感的なサイズ調整
- ピクセル指定:正確なサイズ指定
- アスペクト比維持オプション:縦横比を保ったまま調整

5. フィルター機能の実装

CSS Filtersを活用して6種類のフィルターを実装。

javascript
const applyFilters = (ctx) => {
 const filters = [];
  
 if (filterOptions.grayscale > 0) {
  filters.push(`grayscale(${filterOptions.grayscale}%)`);
 }
 if (filterOptions.sepia > 0) {
  filters.push(`sepia(${filterOptions.sepia}%)`);
 }
 if (filterOptions.brightness !== 100) {
  filters.push(`brightness(${filterOptions.brightness}%)`);
 }
 if (filterOptions.contrast !== 100) {
  filters.push(`contrast(${filterOptions.contrast}%)`);
 }
 if (filterOptions.saturate !== 100) {
  filters.push(`saturate(${filterOptions.saturate}%)`);
 }
 if (filterOptions.blur > 0) {
  filters.push(`blur(${filterOptions.blur}px)`);
 }

 ctx.filter = filters.length > 0 ? filters.join(' ') : 'none';
};


ポイント:
- ブラウザネイティブのCSS Filtersを使用
- 高速処理でリアルタイムプレビューも快適
- 複数フィルターの組み合わせが可能

Photoshopのような高度な編集はできませんが、  
「ちょっと明るくしたい」「セピア調にしたい」といったよくある要望には十分対応できます。

6. 回転・反転の実装

座標変換を使って回転と反転を実現。

javascript
// 回転の中心を設定
ctx.translate(canvas.width / 2, canvas.height / 2);

// 回転(ラジアンに変換)
ctx.rotate((transformOptions.rotation * Math.PI) / 180);

// 反転(スケールを-1にすることで実現)
ctx.scale(
 transformOptions.flipHorizontal ? -1 : 1,
 transformOptions.flipVertical ? -1 : 1
);


実装のポイント:
- 回転は0°/90°/180°/270°の4段階
- 反転は水平・垂直を個別に設定可能
- 座標変換の順序が重要(translate → rotate → scale)

7. ファイル名保持の実装

拡張子のみ変更し、元のファイル名を保持します。

javascript
// 拡張子を除去
const originalName = file.name.replace(/\.[^/.]+$/, '');

// 新しい拡張子を付与(jpegの場合はjpgに統一)
const newFileName = `${originalName}.${
 targetFormat === 'jpeg' ? 'jpg' : targetFormat
}`;


実装理由:
ファイル名が変わると管理が面倒になるため、  
`hero-image.png` → `hero-image.webp` のように、  
元のファイル名を維持することで管理しやすくしています。

8. パフォーマンス制限

ブラウザのメモリを考慮した制限を設定。

javascript
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const MAX_FILES = 10; // 10枚まで

// ファイル選択時のバリデーション
const validFiles = allFiles
 .filter(file => file.type.startsWith('image/') && file.size <= MAX_FILE_SIZE)
 .slice(0, MAX_FILES);

if (allFiles.length > MAX_FILES) {
 alert(`最大${MAX_FILES}枚まで選択できます`);
}

制限内容:
- 1ファイル:10MB以下
- 最大枚数:10枚まで
- 合計最大100MBの処理

この制限により、ほとんどの環境で快適に動作します。

9. UIレイアウトの工夫

Tailwind CSSのグリッドシステムで、レスポンシブなレイアウトを実現。

jsx
<div className="grid md:grid-cols-2 gap-6">
 {/* プレビューエリア */}
 <div>
  <h3 className="text-sm font-semibold mb-3">
   プレビュー {files.length > 1 && '(最初のファイル)'}
  </h3>
  <canvas ref={canvasRef} className="max-w-full" />
 </div>

 {/* 設定エリア */}
 <div className="space-y-6">
  {/* フォーマット選択 */}
  {/* 品質調整 */}
  {/* 詳細設定 */}
 </div>
</div>

レイアウトの特徴:
- PC:プレビューと設定を横並び
- タブレット・スマホ:縦積み
- 複数ファイル選択時は「(最初のファイル)」と表示

10. Astro統合

ReactコンポーネントをAstroページに統合。

astro
---
// src/pages/tools/image-converter.astro
import ImageConverter from '../../components/ImageConverter';
---

<!DOCTYPE html>
<html lang="ja">
<head>
 <title>画像変換ツール | mifa.tokyo</title>
</head>
<body>
 <ImageConverter client:load />
</body>
</html>


`client:load`が必須:
クライアントサイドでのインタラクションが必要なため、hydrationディレクティブを指定。

実際の使用例

パターン1: 単純な変換

  1. PNG画像をWebPに変換したい
  2. ツールで画像を選択
  3. フォーマットを「WebP」に選択
  4. 品質を調整(デフォルト75%)
  5. 「変換する」→ダウンロード

作業時間: 30秒

パターン2: リサイズ+変換

  1. 大きすぎる画像をリサイズしてWebPに
  2. 「詳細設定を開く」
  3. リサイズ:50%に設定
  4. プレビューで確認
  5. 変換

作業時間: 1分

パターン3: 一括処理

  1. 10枚の画像をまとめてAVIFに
  2. 10枚選択(ドラッグ&ドロップ)
  3. フォーマットを「AVIF」に
  4. 「変換する」
  5. 「すべてダウンロード」

作業時間: 2分

技術的な課題と解決策

AVIF品質問題

課題:
ChromeのCanvas API `toBlob()`では、AVIFのqualityパラメータが正しく反映されない。

解決策:
AVIF変換時のみ`@jsquash/avif`ライブラリを使用し、WebAssemblyベースのエンコーダーで確実に品質調整を行う。

bash
npm install @jsquash/avif

プレビュー更新のパフォーマンス

課題:
スライダーを動かすたびに再描画すると、大きな画像では処理が重い。
解決策:
useEffectの依存配列で必要な時だけ再描画。Canvas APIの描画は十分高速なため、特別な最適化は不要。

TypeScriptエラー

課題:
JSXファイルでTypeScriptの型エラーが発生。
解決策:
ファイル先頭に `// @ts-nocheck` を追加し、型チェックを無効化。  
開発速度を優先する判断。

まとめ

このツールを作ったことで、

  • 複数ツールの往復が不要に
  • リアルタイムプレビューで結果を確認しながら調整
  • ブラウザ完結でプライバシー保護


という利点が得られました。

Squooshのような高度な圧縮ツールには及びませんが、  
日常的な画像変換作業には十分な機能を備えています。

今後も実際に使いながら、必要な機能を追加していく予定です。

参考リンク