Canvas の保存処理で toBlob() を呼んだ後、必ず Blob が返るものとしてダウンロードを始めると、失敗時の扱いが曖昧になります。コールバックの引数には null が渡される場合があります。また、呼び出しそのものが例外を投げる経路もあります。
この二つを一つの Promise の失敗として受け取れる形にしておくと、画面側で成功と失敗を分けやすくなります。その際、呼び出し時に発生した例外は元のまま渡し、コールバックの空結果には説明のあるエラーを用意します。
最小のラッパー
以下は通常の HTMLCanvasElement を受け取る例です。toBlob() の戻り値を直接待っているわけではありません。コールバックで受け取った値を使って Promise を完了させています。
function canvasBlob(canvas, type = "image/png", quality) {
return new Promise((resolve, reject) => {
try {
canvas.toBlob((blob) => {
if (blob === null) {
reject(new Error("Canvas image encoding returned null"));
return;
}
resolve(blob);
}, type, quality);
} catch (error) {
reject(error);
}
});
}
たとえば、読み取りが許可されない画像を描いた Canvas は、オリジンに関する制約によって SecurityError を投げることがあります。catch は制約を解除する処理ではありません。呼び出し側が保存失敗を表示するために使います。問題のある画像を読み込む手順や CORS の設定は、別途確認する必要があります。
コールバックの null についても、常に同じ原因だとは決めつけません。ユーザー向けには保存できなかったことを伝え、開発時には入力条件と失敗経路を調べられるようにします。必要以上に元画像や個人情報をログへ残さず、調査に必要な寸法や要求形式から始めます。
成功してから表示を変える
保存ボタンを押した直後に「完了」を表示すると、後からエンコードが失敗しても成功したように見えます。処理中の表示に変え、Promise が成功した時点でダウンロード用リンクを用意します。失敗したらメッセージを表示し、ボタンを再度使える状態に戻します。
async function prepareDownload(canvas, link) {
const blob = await canvasBlob(canvas, "image/png");
if (blob.type !== "image/png") throw new Error("Unexpected output type");
const url = URL.createObjectURL(blob);
link.href = url;
link.download = "image.png";
link.hidden = false;
return () => {
link.removeAttribute("href");
link.hidden = true;
URL.revokeObjectURL(url);
};
}
この関数はリンクをクリックする前に URL を破棄しません。返した関数は、そのリンクを取り下げるときに呼びます。プレビューや右クリック保存で同じ URL を使う場合も、利用が終わるまで保持する必要があります。別の画像に置き換える操作では、古いリンクの後始末を忘れないようにします。
ファイル名は実際の出力形式に合わせる
この例は必須対応の PNG を指定しています。別形式を選べる UI に拡張するなら、要求した形式の文字列だけで拡張子を決めないようにします。対応していない形式では PNG にフォールバックする可能性があるため、実際の blob.type を見てファイル名を決めます。
私は画像変換プロジェクトの Morphio を作っています。変換結果を渡す処理では、完了表示のタイミングと保存ファイルの説明が一致しているかが気になります。ここに載せたコードは、API の失敗経路を確認するための独立したサンプルです。
ラッパーを使う画面では、通常の PNG 保存に加えて失敗時の表示も確認します。自動テストで toBlob() の代わりに null を返すテスト用関数を使う方法なら、失敗表示を再現できます。その確認と、実際のブラウザーで画像を保存して開く確認は分けて実施します。