web-files-image-handling について
web-files-image-handlingは、ブラウザーでの画像プレビューとCanvas処理を扱うガイドスキルです。表示と変換を分けた手順により、向き、メモリー、画質に関する問題を防ぎます。
主な機能
- 01オブジェクトURLのプレビューと解放
- 02Canvasによるリサイズと圧縮
- 03EXIF方向の処理
- 04段階的な縮小
- 05トリミングと形式変換
活用シーン
- 01Webアプリで選択画像をプレビュー
- 02アップロード前のリサイズや圧縮
- 03サムネイル生成や画像のトリミング
- 04出力画像データの向きの正規化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-files-image-handlingインストール後、Claude Code で/web-files-image-handlingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/canvas.mdcore.mdpreview.md詳細
画像処理のパターン
クイックガイド: 目的は2つあり、それぞれに適したツールも異なります。画像を表示する場合は
URL.createObjectURL()を使い、表示後に URL を無効化します。ブラウザーはすでに EXIF の回転を適用するため、 手動で回転すると二重に回転してしまいます。画像を処理する場合は Canvas を使います。サイズは 4096px に制限し、 50%を超えて縮小するときは複数回に分けて縮小し、JPEG に書き込む前に白で塗りつぶし、toDataURL()より非同期のtoBlob()を優先します。
詳細なリソース:
- examples/core.md — 共通の
loadImageヘルパー、プレビュー用フックとコンポーネント、寸法の抽出と検証、EXIF の解析、ギャラリーの状態 - examples/preview.md — 複数のサムネイルセット、ギャラリーグリッド
- examples/canvas.md — リサイズ処理、目標サイズへの圧縮、トリミング、透かし、フィルター
- reference.md — 形式と方法の選択、定数、メソッドの比較、EXIF 値、ブラウザーのサポート
どちらの方法を使うか
2つの方法では EXIF の扱いが異なります。これが最もよくあるバグの原因です。
- ユーザーに表示する場合 — オブジェクト URL をそのまま
<img>に渡し、置き換え時またはアンマウント時に無効化します。ブラウザーが回転してくれるため、向きには何もしません。examples/core.md から始めてください。 - 別の場所に送る前に処理する場合 — Canvas でリサイズ、圧縮、変換、またはトリミングします。ここでは自動で回転されないため、向きを明示的に正規化し、ブラウザー独自の回転を抑制せずに結果を表示しないでください。examples/canvas.md から始めてください。
<critical_requirements>
画像処理コードを書く前に
アンマウント時と、置き換え用の URL を作成する前に、すべてのオブジェクト URL を無効化してください。 createObjectURL は無効化されるまで Blob をメモリに保持するため、ユーザーが気を変えて選び直すファイル選択では、試行するたびに画像全体がメモリに残ります。
向きを扱う前に、どちらの方法を使うか決めてください。 2020 年以降、ブラウザーでは image-orientation: from-image がデフォルトになっているため、表示用に正規化すると画像が二重に回転します。正規化するのは、ブラウザーの外に送るバイトデータだけです。
Canvas の寸法は 4096px に制限してください。 これを超えると Canvas は失敗してもエラーを通知しないことがあり、タブがクラッシュする場合もあります。モバイルでは上限がさらに低くなります。
半分を超えて縮小する場合は、複数回に分けて縮小してください。 1回で縮小するとサンプリング不足になり、ぼやけたりエイリアスが発生したりします。中間段階を2回設けると鮮明さを保てます。
</critical_requirements>
自動検出: URL.createObjectURL、URL.revokeObjectURL、createImageBitmap、OffscreenCanvas、canvas.toBlob、canvas.toDataURL、convertToBlob、ctx.drawImage、imageSmoothingQuality、image-orientation、EXIF orientation、0x0112、FileReader.readAsDataURL、naturalWidth、image/webp、段階的縮小、サムネイル生成、クライアント側リサイズ、画像のトリミング
対象範囲:
- ユーザーが選択した画像のプレビューを表示する
- ブラウザーで画像をリサイズ、圧縮、または変換する
- EXIF の向きを読み取り、正規化する
- サムネイルの生成、トリミング、透かしの追加、フィルターの適用
- その他の処理を行う前に、寸法とアスペクト比を検証する
対象外:
- ファイルの選択と転送 — このスキルでは、すでに保持している
FileまたはBlobから始めます - サーバーでのリサイズやトランスコード、URL ベースの変換サービス
- レイヤー、選択範囲、取り消し機能を備えたインタラクティブな編集画面。生の Canvas 呼び出しではなく、Canvas ライブラリが適しています
## 基本方針
プレビューと処理は相反する方向を向いており、両者を混同することが画像に関するバグの多くの原因です。
プレビューにはコストがかからないはずです。URL.createObjectURL() は、すでにメモリ上にあるバイト列への参照を <img> に渡すため、JavaScript へのデコードもコピーも発生しません。その代わり、ライフタイムを手動で管理する必要があります。参照は無効化しない限り、コンポーネントの破棄後も残ります。
処理はその逆です。ピクセルをキャンバスにデコードし、変換して、再びエンコードする必要があります。負荷の高い処理はすべてここで行われるため、キャンバス処理はフル解像度で何度も行うのではなく、実際に必要なサイズで一度だけ行うのが適切です。
ブラウザー独自の EXIF 処理は、その境界をまたいでいます。表示時には画像を回転しますが、キャンバスでは回転しません。そのため、同じファイルでも、読み込み経路によって向きが異なります。
基本パターン
パターン 1:クリーンアップを伴うオブジェクト URL プレビュー
次の URL を作成する前に以前の URL を無効化し、アンマウント時にも無効化します。レンダー中に URL を作成すると、レンダーごとに 1 つずつリークします。
useEffect(() => {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
return () => URL.revokeObjectURL(url);
}, [file]);
コード全文:examples/core.md
パターン 2:キャンバスのリサイズ
ブラウザーの上限に合わせてサイズを制限し、アスペクト比を維持して、高品質のスムージングを指定します。出力形式が JPEG の場合は、描画前に白で塗りつぶしてください。JPEG にはアルファチャンネルがないため、透明部分は黒としてエンコードされます。
const MAX_CANVAS_DIMENSION = 4096;
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height);
const width = Math.round(img.width * Math.min(ratio, 1)); // never upscale
const height = Math.round(img.height * Math.min(ratio, 1));
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
if (mimeType === "image/jpeg") {
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
}
ctx.drawImage(img, 0, 0, width, height);
コード全文:examples/canvas.md
パターン 3:段階的な縮小
drawImage は固定サイズの近傍をサンプリングするため、一度に大きく縮小すると、元画像の大部分が失われます。何度も半分ずつ縮小すれば、すべてのピクセルを処理に反映できます。
const STEP_DOWN_THRESHOLD = 0.5;
if (targetWidth / img.width < STEP_DOWN_THRESHOLD) {
// 4000 → 400 → 100 rather than 4000 → 100
const factor = Math.pow(targetWidth / img.width, 1 / steps);
for (let i = 0; i < steps; i++) {
/* draw into a canvas scaled by factor, then use it as the next source */
}
}
コード全文:examples/canvas.md
パターン 4:EXIF の向き
// Display: the browser has already rotated it
<img src={URL.createObjectURL(file)} />
// Outbound bytes: normalise, because the next consumer may not rotate
const orientation = await getExifOrientation(file);
const normalized = orientation === 1 ? file : await normalizeOrientation(file);
// Displaying an already-normalised image: suppress the second rotation
<img src={url} style={{ imageOrientation: "none" }} />
コード全文:examples/core.md
パターン 5:形式の変換
品質の意味は形式によって異なり、PNG ではパラメーターが完全に無視されます。
const FORMAT_QUALITY_DEFAULTS: Record<string, number> = {
"image/jpeg": 0.85,
"image/webp": 0.82,
"image/png": 1, // lossless — the quality argument is ignored
};
WebP は、Safari 14 以降を含む最新の環境でエンコードできます。品質設定ではなくバイト数の上限を目指す場合は、品質パラメーターを二分探索します。
コード全文:examples/canvas.md
パターン 6:切り抜き
9 つの引数を使う drawImage は、ソースの矩形と描画先の矩形を受け取るため、切り抜きとリサイズを 1 回の呼び出しで行えます。範囲外の領域はエラーにならず何も描画されないため、まずソースの矩形を画像の範囲内に収めてください。
// drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh)
ctx.drawImage(
img,
cropX,
cropY,
cropWidth,
cropHeight,
0,
0,
outputWidth,
outputHeight,
);
コード全文:examples/canvas.md
<red_flags>
注意すべき点
実行時に問題が発生します:
URL.revokeObjectURL()がありません。ページが存続する間、選択するたびに別の画像がメモリに保持されます。エフェクトのクリーンアップ時と URL を置き換える前に取り消してください。- レンダリング中に
URL.createObjectURL()を呼び出しています。レンダリングごとに新しい URL が作成され、どれも取り消されません。エフェクト内に移動してください。 - キャンバスのサイズが 4096px を超えています。タブがクラッシュするか、キャンバスが何も出力せずに終わることがあります。
canvas.widthに代入する前に上限を設定してください。 - 向きを正規化した後、結果を
<img>でレンダリングしています。ブラウザーがもう一度回転させてしまいます。その要素にimage-orientation: noneを設定するか、表示用に正規化しないでください。 - 透明な PNG を背景色を塗りつぶさずに JPEG に変換しています。透明部分が黒くなります。先に白で
fillRectを実行してください。
予想外の動作:
- 1 回の処理で 50% を超えて縮小するとぼやけてギザギザになりますが、2 回に分けて縮小すると鮮明になります。
toBlob()は非同期ですが、toDataURL()は同期です。また、同期処理のほうがコストがかかります。- データ URL は、エンコード対象のファイルよりおよそ 3 分の 1 大きくなります。そのため、10MB の画像はメインスレッド上で 13MB の文字列になります。
- Node には
image-orientationがないため、サーバー側でキャンバスを処理する場合も EXIF を手動で処理する必要があります。 createImageBitmapはレイアウトとデコードを省略します。また、その結果はclose()が呼び出されるまでメモリを保持します。- 1 辺のサイズが上限内でも、画像がキャンバス全体の面積制限を超えることがあります。
toBlob()による AVIF エンコードは、すべてのブラウザーで利用できるわけではありません。デコードは可能です。- すでに最適化された画像を再エンコードすると、通常はサイズが小さくならず、むしろ大きくなります。