大量画像の一括処理ワークフロー - 効率的なバッチ処理の設計と実装
バッチ処理が必要になる場面と設計の考え方
画像のバッチ処理が必要になる典型的な場面は、EC サイトの商品画像の一括リサイズ、ブログ移行時の画像フォーマット変換、写真アーカイブの整理、Web サイトリニューアル時の画像最適化などです。数枚であれば手動で処理できますが、100 枚を超えると自動化なしでは現実的ではありません。1000 枚規模になると、手動処理は数日を要し、ヒューマンエラーや品質のばらつきのリスクも無視できないレベルになります。
バッチ処理を設計する際の基本原則は「冪等性」と「再実行可能性」です。同じ処理を 2 回実行しても結果が変わらないこと、途中で失敗しても再開できることが重要です。具体的には、出力先を入力元とは別のディレクトリにする、処理済みファイルをスキップする仕組みを入れる、エラーログを残して失敗したファイルだけを再処理できるようにする、といった設計が必要です。
処理のパイプラインは「入力 → 変換 → 検証 → 出力」の 4 段階で構成します。入力段階ではファイルの存在確認とフォーマット判定を行い、変換段階で実際のリサイズやフォーマット変換を実行し、検証段階で出力ファイルの整合性 (ファイルサイズがゼロでないか、画像として読み込めるか) を確認し、出力段階で最終的なファイル配置を行います。検証段階を省略すると、破損したファイルが本番環境に紛れ込むリスクがあります。
ImageMagick によるコマンドライン一括処理
ImageMagick は 30 年以上の歴史を持つ画像処理ツールで、コマンドラインから 200 種類以上の画像フォーマットを操作できます。バッチ処理では mogrify コマンド (上書き変換) と convert コマンド (別ファイル出力) を使い分けます。
基本的なバッチ処理の例:
mogrify -resize 1920x1080\> -quality 82 -path ./output *.jpg- 全 JPEG を最大 1,920 × 1,080にリサイズ (拡大はしない)mogrify -format webp -quality 80 -path ./webp *.png- 全 PNG を WebP に変換find . -name "*.jpg" -exec convert {} -strip -resize 50% ./thumbs/{} \;- サブディレクトリを含めて処理
-resize オプションの末尾に \> を付けると、指定サイズより小さい画像は拡大されません。これは重要な安全策で、小さなアイコン画像が不必要に拡大されてぼやけることを防ぎます。-strip オプションは EXIF メタデータを除去し、ファイルサイズを削減すると同時に個人情報の漏洩を防止します。
大量ファイルを処理する場合、find と xargs を組み合わせた並列実行が効果的です: find . -name "*.jpg" | xargs -P 4 -I {} convert {} -resize 1920x1080\> ./output/{}。-P 4 で 4 プロセス並列実行となり、CPU コア数に応じて処理速度が向上します。
Node.js sharp ライブラリによる高速処理
sharp は libvips をバックエンドに持つ Node.js の画像処理ライブラリで、ImageMagick の 4〜5 倍高速に動作します。ストリーミング処理とメモリ効率の良さから、大量画像のバッチ処理に最適です。
基本的なバッチ処理スクリプト:
const sharp = require('sharp');const glob = require('glob');const path = require('path');const files = glob.sync('./input/**/*.{jpg,png}');await Promise.all(files.map(file =>sharp(file).resize(1920, null, { withoutEnlargement: true }).jpeg({ quality: 82, progressive: true, mozjpeg: true }).toFile(path.join('./output', path.basename(file, path.extname(file)) + '.jpg'))));
sharp の withoutEnlargement: true オプションは ImageMagick の \> フラグと同等で、元画像より大きくリサイズされることを防ぎます。mozjpeg: true を指定すると、mozjpeg エンコーダが使用され、同じ品質設定でも 5〜15% 小さいファイルが生成されます。
大量ファイルを処理する際は、Promise.all で全ファイルを同時に処理するとメモリが枯渇する可能性があります。p-limit ライブラリで同時実行数を制限するか、for...of ループで逐次処理する方が安全です。目安として、同時処理数は CPU コア数と同じか、メモリに余裕がない場合はその半分に設定します。
複数フォーマット・複数サイズの同時生成
Web サイト向けの画像最適化では、1 つの元画像から複数のフォーマット (JPEG, WebP, AVIF) と複数のサイズ (640w, 960w, 1280w, 1920w) を同時に生成する必要があります。組み合わせの数は「フォーマット数 × サイズ数」となり、3 フォーマット × 4 サイズ = 12 バリエーションを 1 枚の画像から生成することになります。
効率的な生成戦略として、元画像の読み込みは 1 回だけ行い、メモリ上のバッファから複数の出力を生成する方法が推奨されます。sharp では clone() メソッドでパイプラインを分岐できます:
const pipeline = sharp(inputBuffer);const sizes = [640, 960, 1280, 1920];const formats = [{ ext: 'jpg', opts: { quality: 82, progressive: true } },{ ext: 'webp', opts: { quality: 80 } },{ ext: 'avif', opts: { quality: 65 } }];
AVIF の品質値が低く見えますが、AVIF は同じ知覚品質を低い数値で実現できるため、品質 65 でも JPEG の品質 82 と同等以上の見た目になります。フォーマットごとに最適な品質値が異なる点に注意が必要です。
生成されたファイルの命名規則も重要です。{slug}-{width}w.{ext} (例: hero-1280w.webp) のような規則的な命名にすることで、HTML の srcset 生成を自動化しやすくなります。ビルドスクリプトがファイル名からサイズとフォーマットを推定できる命名規則を採用しましょう。
エラーハンドリングと進捗管理
数千枚の画像を処理する場合、一部のファイルで処理が失敗することは避けられません。破損した画像ファイル、サポートされていないフォーマット、ディスク容量不足など、様々な原因でエラーが発生します。堅牢なバッチ処理には、適切なエラーハンドリングと進捗管理が不可欠です。
エラーハンドリングの基本方針:
- 個別ファイルのエラーで全体を停止しない: try-catch で各ファイルの処理を囲み、エラーが発生してもログに記録して次のファイルに進む
- エラーログを構造化する: ファイルパス、エラー種別、エラーメッセージ、タイムスタンプを JSON 形式で記録し、後から失敗ファイルだけを再処理できるようにする
- リトライ機構を設ける: 一時的なエラー (ディスク I/O エラーなど) に対しては 1〜2 回のリトライを行う。リトライ間隔は指数バックオフ (1 秒、2 秒、4 秒) で増加させる
進捗管理については、処理済みファイル数 / 全ファイル数をリアルタイムで表示することで、処理完了までの見積もり時間を把握できます。Node.js では cli-progress ライブラリ、シェルスクリプトでは pv コマンドが便利です。大規模な処理では、中間チェックポイントを設けて処理状態を永続化し、プロセスが中断されても途中から再開できる仕組みを実装します。
CI/CD パイプラインへの組み込み
画像のバッチ処理をビルドパイプラインに組み込むことで、画像の追加・更新時に自動的に最適化が実行される仕組みを構築できます。手動実行の手間がなくなるだけでなく、最適化の適用漏れを防止できます。
GitHub Actions での実装例:
- name: Optimize imagesrun: |node scripts/optimize-images.jsgit diff --name-only | grep -E '\.(jpg|webp|avif)$' | wc -l
CI/CD に組み込む際の注意点:
- キャッシュの活用: 変更のない画像を毎回再処理しないよう、入力ファイルのハッシュ値と出力ファイルを対応付けてキャッシュする。ファイルの更新日時ではなく内容のハッシュ (MD5 や SHA-256) で判定することで、確実に変更を検出できる
- 処理時間の制限: CI の実行時間制限 (GitHub Actions は 6 時間) を超えないよう、差分のみを処理する設計にする。全画像の再処理が必要な場合は、ローカルで実行して結果をコミットする運用にする
- 成果物のコミット: 生成された画像をリポジトリにコミットするか、CDN に直接アップロードするかを決める。リポジトリサイズの肥大化を避けるなら、Git LFS の使用か CDN 直接アップロードが推奨される
Vercel や Netlify などのホスティングサービスでは、ビルド時に画像最適化プラグインが利用可能です。これらを活用すると、独自のバッチ処理スクリプトを書かずに自動最適化を実現できます。ただし、処理のカスタマイズ性は限定的なため、細かな制御が必要な場合は自前のスクリプトが必要です。