猫の手ツール

Step 3: Core Technology

14. File APIとBlob URL:選んだ画像を端末内で表示する

File APIは、利用者が選んだファイルをブラウザで扱う入口です。Object URLで画像を読み込めますが、画像形式への対応や、参照をいつ解放するかも考える必要があります。実ツールの読み込み関数と、愛猫の写真を使う完全サンプルで、データの流れを確認します。

1. 「アップロード」という概念をアップデートする

サーバー保存型のサービスでは、画像をアップロードし、保存先のURLを img 要素へ渡す方式があります。ただし、選択した画像をプレビューするだけならサーバーへの送信は必要ありません。ブラウザ内のFileを参照する方法を使えます。

サーバー保存型では、画像を送る通信と保存先の管理が必要です。表示と加工をブラウザ内で行う設計なら、その画像の送信を省けます。File API自体が後続の送信を禁止する仕組みではありません。

2. URL.createObjectURL という画期的な仕組み

FileやBlobを画像の読み込みへ渡すために使えるのが URL.createObjectURL() というメソッドです。

これは、JavaScriptが読み取ったローカル上のファイルオブジェクトに対して、ブラウザ内だけで有効な一時的なURL(Blob URL)を発行する仕組みです。このURLは blob:http://localhost... といった形式で、ブラウザのメモリ内に直接マッピングされています。

コードで比較する Before / After

【Before】サーバーへ送る、従来の方法

// 1. サーバーへアップロード(時間がかかる & セキュリティリスク)
const formData = new FormData();
formData.append('photo', file);
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();

// 2. サーバーから返ってきたURLをセット
previewImg.src = data.url;

【After】Blob URLによる端末内のプレビュー

// 1. 選択済みFileを参照するURLを発行
const blobUrl = URL.createObjectURL(file);

// 2. そのまま img タグにセット
previewImg.src = blobUrl;

// このURL生成と画像参照のために、画像をサーバーへ送る必要はない

3. 実装上の注意点:メモリリークという罠

この技術は非常に便利ですが、フロントエンドエンジニアとして絶対に忘れてはいけないのが「メモリの解放」です。

URL.createObjectURL() で生成されたURLは、明示的に破棄されるか、ブラウザのタブが閉じられるまでメモリを専有し続けます。特に「猫の手ツール」のように何度も写真を入れ替えるツールの場合、古いURLを放置するとブラウザのメモリを食い潰し、動作が重くなる原因になります。

// 新しい画像を表示する前に、古いURLをメモリから解放する
if (oldBlobUrl) {
  URL.revokeObjectURL(oldBlobUrl);
}

Object URLを作った場所と、それを使い終えた場所を対応付けます。表示中のURLを早く破棄しすぎる問題と、不要なURLを残す問題の両方を確認します。

4. 技術が支える「利用者の自由」

画像の読み取りと加工自体は端末内で行えます。ただしオフラインで新たにページを開けるかは、HTML・JavaScript・AIモデルなどのキャッシュやService Workerの実装に依存します。端末内処理だけでオフライン対応になるわけではありません。

Blob URLは画像を公開するインターネット上の共有URLではありません。一方、File APIで読み取った画像を別のコードから送信することは可能です。画像を送らないという性質は、File APIの存在だけでなく、サイト全体の実装とデータの流れで確認します。

プレビューの差し替えとURLの寿命

以下は選択した画像を表示し、差し替え時に前のURLを破棄する例です。Blob URLを作るだけではブラウザが非対応の画像形式を表示できるようにはならないため、読み込み失敗も扱います。

const picker = document.createElement('input');
picker.type = 'file';
picker.accept = 'image/*';
const preview = document.createElement('img');
preview.alt = '選択した画像のプレビュー';
document.body.append(picker, preview);
let currentUrl = null;
picker.addEventListener('change', () => {
  const file = picker.files?.[0];
  if (!file) return;
  preview.removeAttribute('src');
  if (currentUrl) URL.revokeObjectURL(currentUrl);
  currentUrl = URL.createObjectURL(file);
  preview.src = currentUrl;
});
preview.addEventListener('error', () => {
  preview.removeAttribute('src');
  if (currentUrl) URL.revokeObjectURL(currentUrl);
  currentUrl = null;
  console.error('この画像は表示できません');
});

読み込み直後にURLを破棄すると、画像を別タブで開く操作などに影響することがあります。表示中は保持し、差し替えや画面を破棄する時点で不要な参照を片づけます。revokeObjectURL はURLによる参照を解除する操作であり、すべての画像メモリを直ちに消す命令ではありません。

背景透過ツールは、一時URLを画像デコード後に解放する

背景透過ツールの共有decodeImage()は、Blobを受け取ったときだけObject URLを作り、画像が読めた場合も失敗した場合もfinallyで解放します。URL文字列を直接受け取った場合は、そのURLを所有していないので解放しません。

const ownedUrl = typeof source === 'string' ? null : URL.createObjectURL(source);
try {
  // new Image()のonload/onerrorをPromiseへ包み、読み込み済み画像を返す。
} finally {
  if (ownedUrl) URL.revokeObjectURL(ownedUrl);
}

これは本番関数のURL所有と後処理を抜き出した例で、画像を読む中身は省略しています。読み込み済み画像の画素を使うための一時URLと、利用者が別タブで開く表示用のURLは寿命が違います。背景透過の制作解説で推論へ進む前に、以下のサンプルで読み込みからCanvasへの転写を確かめます。

愛猫の写真で、Fileの読み込みを実際に動かす

愛猫写真の動作サンプルを開く / 完全サンプルZIPを保存する

ZIPをすべて展開し、index.html・app.js・cat.jpg・README.txtがあるフォルダーでターミナルを開きます。Node.jsが未導入ならNode.js公式サイト(別タブで開く)のLTS版を導入してターミナルを開き直します。次を1行ずつ実行し、サーバーを開いたままにします。

node --version
npm.cmd --version
npx.cmd --yes http-server . -p 8080 -c-1

ブラウザでhttp://localhost:8080/を開きます。macOS・Linuxはnpm・npxへ読み替えます。8080が使用中ならコマンドとURLを8081へ変更し、終了はCtrl+Cです。file://では起動しません。初回のHTTPサーバー取得には通信が必要です。

実行ボタンを押すと、内蔵のcat.jpgを取得してメモリ上のFileへ包み、Object URLでデコードして入力Canvasへ描きます。利用者が選んだファイルを送信する処理ではありません。自分のJPEG・PNG・WebPを選んだ場合は、選択されたFileを同じ関数へ渡します。猫の写真はこのプロジェクトの運営者の愛猫です。

const url = URL.createObjectURL(file);
try {
  const image = new Image();
  image.src = url;
  await image.decode();
  // 画像を入力Canvasへ転写する。
} finally {
  URL.revokeObjectURL(url);
}

これは教材のreadFileIntoCanvas()の待機と解放を抜き出した例です。画素はCanvasに移したので入力URLを解放できます。選択を変えたら前の保存リンクを無効にし、まだ実行していない結果を保存しないようにします。続きはCanvasで4枚配置する章、読み込み待ちの意味はasync/awaitの章へ進みます。完全コードはCanvas記事の完全コードとZIPにあります。

今回の入力をJSONで確かめる

Windows・Chrome 154で内蔵の愛猫写真を実行したところ、入力はcat.jpg、image/jpeg、190,818バイト、800×589 pxでした。長辺960 pxより小さいので入力Canvasも同じ寸法です。内蔵写真はHTTPで取得してメモリ上のFileへ包む教材で、OS上の原本を変更していません。生成したPNGと実行画面はCanvasの記事の確認結果へ進めます。

まとめ

画像の参照をURLとして渡せることと、そのURLをインターネット上で公開することは別です。必要な期間だけ参照を保持する設計にします。

ファイル入力、Object URL、画像デコード、Canvasへの描画を順に追うと、画像を送信する処理がどこにあるかも確認しやすくなります。

参考資料

制作時に確かめたこと

ツールを作る途中で実際に確認した症状と、変更を選んだ経緯です。日付は当時の記録日です。現在の実装や、このページの学習サンプルの検証とは分けて読んでください。

プレビューが見えることと、写真へ保存できることは違った

iPhoneの長押しと、一括保存を別々に確かめる

公開初期から、iPhoneで「ファイル」ではなく「写真」へ保存する方法を分かりやすく案内したいと考え、画面の注意書きや説明マークを依頼しました。3月22日には、機内モードでは写真の長押し保存ができないと報告しています。

4月6日のAI背景ツールでも、長押し保存が効かず、最初のサンプルではできていた方式を使えないかと確認しました。18日には複数画像の保存で、iPhoneでは最後の画像しかダウンロードされないという報告もあります。これらの記録だけで、各保存問題の最終的な解消は確認できません。

一時URLで表示できたこと、出力データを作れたこと、利用者が目的の場所へ保存できたことは、それぞれ別の確認です。画像を表示し続けるためのURLは、長押しなどの操作が終わる前に破棄しない設計を検討します。ただし、当時の障害がすべてURLの早期解放で起きたと断定したものではありません。