猫の手ツール

Pipeline & Memory Management

ブラウザ内画像変換の
パイプライン設計とメモリ最適化

iPhoneのHEIC画像やGoogleのWebP。これらを「外部サーバーへ一切送信せず」にJPGへ変換するためには、ブラウザの計算リソースをどう賢く使うかが鍵となります。本ツールが採用した、ブラウザ内で動く変換パイプラインの裏側を解説します。

このツールの入力から出力まで

現行コードの確認日:

入力
JPEGへ変えたい画像を選びます。HEICは変換ライブラリを取得してから読み込みます。
処理
長辺を3000px以下にし、Canvasを白く塗って画像を描き、JPEG品質0.9で再エンコードします。
出力
出力形式は JPEG、品質指定は 0.9、長辺は最大 3000px です。

まず確認する点:形式を変える過程で縮小・再圧縮も起きます。透明部分が白になり、元のメタデータをそのまま保持する経路ではないことを確認します。

仕上がりの例

WebPの猫写真を、構図と縦横比を保ってJPGに変換した例です。

変換前・変換後
左は前足をなめている茶トラの愛猫の元画像(WebP)。右は同じ写真を構図と縦横比を保ってJPGへ変換した画像。

JPEGの再圧縮により、画質やファイル容量が変わる場合があります。

変換前・変換後

左は前足をなめている茶トラの愛猫の元画像(WebP)。右は同じ写真を構図と縦横比を保ってJPGへ変換した画像。

掲載画像はサイトにある既存の公開作例です。保存済みの資料では運営者の愛猫の写真を使用しています。今回の確認日が、この作例を実行した日を表すわけではありません。

保存済み検証記録の条件を見る

資料の種類は「ツールのプレビュー・結果画面を保存した記録」です。公開作例と保存済み記録で素材や色などが異なる場合があるため、下の数値は保存済み検証記録の条件として読んでください。

記録の入力画像
800×805px (WEBP)
記録の画面キャプチャ
430×433px (JPEG)
保存済み記録の主な操作・出力条件
画面の項目記録の値現行コードの初期値・範囲
記録の入力形式WebP—
記録の出力形式JPG—
当時の画面に表示された容量148 KB—

資料ファイルの作成日は実行日として扱っていません。個別端末の処理速度や、ほかの写真に対する検出・加工の精度を測定した資料でもありません。

実装の確認箇所:src/pages/image/convert/index.astro。以下のコード抜粋と出力条件は、この本番実装に基づいて説明します。

仕組みを読み進めるときは、async / awaitと並行処理(画像やライブラリの取得を待つ処理の順序)、File APIとObject URL(選んだ画像の読み込みとBlob URLの解放)が参考になります。

1. Code Splittingを活用した動的ライブラリロード

HEIC形式のデコードは非常に複雑な計算を必要とし、そのためのライブラリ(heic2any)はファイルサイズが大きくなりがちです。これを初期ロードに含めると、ツール全体の起動が重くなってしまいます。本ツールでは、ファイルが選択された瞬間にのみ import() を行う動的ロード戦略をとっています。

メリット: HEIC変換を使わないユーザー(PNGやWebPのユーザー)に対して、不要な巨大スクリプトのダウンロードを回避させることができ、通常画像の変換時には HEIC 用コードの取得を省けます。

読み込み済みの画像を白背景 JPEG にする例です。toBlob が null を返す失敗も扱い、Blob を返します。

function jpegBlob(image) {
  const scale = Math.min(1, 3000 / Math.max(image.width, image.height));
  const canvas = document.createElement('canvas');
  canvas.width = Math.round(image.width * scale);
  canvas.height = Math.round(image.height * scale);
  const ctx = canvas.getContext('2d');
  if (!ctx) return Promise.reject(new Error('Canvas unavailable'));
  ctx.fillStyle = '#fff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
  return new Promise((resolve, reject) => canvas.toBlob(
    blob => blob ? resolve(blob) : reject(new Error('JPEG conversion failed')),
    'image/jpeg', 0.9));
}

2. 透過画像をJPG化する際の背景正規化ハック

JPG形式は「透明(Alpha)」をサポートしていません。そのため、WebPやPNGの透明な画像をそのままCanvasでJPGとして書き出すと、透過部分が黒くなってしまう問題が発生します。本ツールでは、描画前にキャンバス全体を「純白」で塗りつぶす正規化工程を挟んでいます。

メリット: 背景を白で初期化してから画像を描画することで、ロゴなどの透過画像でも違和感のない、自然なJPG画像を生成することが可能になります。

3. モバイル端末の処理する画素数を減らすスケーリング

最近のスマートフォンの写真は極めて高解像度ですが、巨大なキャンバスを生成しようとするとブラウザのメモリ上限を超え、タブがクラッシュ(リロード)することがあります。本ツールでは、読み込み時にアスペクト比を維持したまま、実装上の最大解像度(MAX_SIZE)へ自動スケーリングする処理を組み込んでいます。

メリット: 変換後に描く Canvas の長辺を 3000px 以下に抑えます。ただし、その前のデコードや HEIC 変換にもメモリを使い、破損ファイルや端末の制限によって失敗する場合があります。

出力の仕様と確認する点

出力形式は JPEG、品質指定は 0.9、長辺は最大 3000px です。透明部分は白になります。ファイル形式の変換に加えて、縮小や再圧縮が生じる点を確認してください。

HEIC は動的に読み込む heic2any で変換してから Canvas に描きます。複数画像を含む HEIC でも、全画像を別ファイルに書き出す機能ではありません。

Canvas から新しい画像を作る方式で、元ファイルの EXIF や位置情報などをそのまま引き継ぐ処理はありません。元画像は残し、変換後の向き・色・寸法を確認してから利用してください。

制作時に確かめたこと