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: 単純な変換
- PNG画像をWebPに変換したい
- ツールで画像を選択
- フォーマットを「WebP」に選択
- 品質を調整(デフォルト75%)
- 「変換する」→ダウンロード
作業時間: 30秒
パターン2: リサイズ+変換
- 大きすぎる画像をリサイズしてWebPに
- 「詳細設定を開く」
- リサイズ:50%に設定
- プレビューで確認
- 変換
作業時間: 1分
パターン3: 一括処理
- 10枚の画像をまとめてAVIFに
- 10枚選択(ドラッグ&ドロップ)
- フォーマットを「AVIF」に
- 「変換する」
- 「すべてダウンロード」
作業時間: 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のような高度な圧縮ツールには及びませんが、
日常的な画像変換作業には十分な機能を備えています。
今後も実際に使いながら、必要な機能を追加していく予定です。