秋が深まって、朝晩の寒さに驚いているOYMです。今回は実務で、canvasで作った画像をスマホから「シェア・保存」できるようにしたときに、悩んだ内容を共有したいと思います。
ページ上のテキストを背景画像に重ねて、1枚のカード画像にする機能です。画面下から出てくるシートに画像を表示して、「シェアする」と「保存する」の2つのボタンを置きました。
See the Pen blog-2026-11-13 by OYM (@oyama_stargate) on CodePen.
皆さんは、ブラウザで作った画像を「スマホの写真」に保存しようとして、思い通りにいかなかった経験はありませんか?
作るだけなら簡単そうだと思っていたのですが、スマホで試してみると、思わぬところで何度もつまずきました。同じところで悩む方の参考になればと思い、つまずいた順にご紹介します。
まずは背景画像を読み込んで、canvasに文字を描きます。ここで気をつけたいのは次の3つです。
const bg = new Image(); bg.crossOrigin = 'anonymous'; // これがないと、あとで画像を書き出せない bg.src = BG_URL;
画像が別ドメインにある場合、crossOrigin を付け忘れると、canvasが「汚染された」状態になり、toBlob で書き出すところでエラーになります。画像側のCORS設定も必要になるので、ここは忘れやすいポイントです。
await document.fonts.ready; // フォントの読み込みを待ってから描く
Webフォントの読み込みが終わる前に描いてしまうと、代わりのフォントで描かれてしまいます。そこで、document.fonts.ready を待ってから描くようにしました。
文字が長いときは、一番長い行の幅を measureText で測って、全行が同じ比率で収まるように縮めています。
let widest = 0;
for (const line of lines) {
widest = Math.max(widest, ctx.measureText(line).width);
}
if (widest > maxTextWidth) {
fontSize = fontSize * (maxTextWidth / widest);
}
共有にも保存にも使えるように、File にしておきます。
const canvasToFile = (canvas) =>
new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(new File([blob], 'card.jpg', { type: 'image/jpeg' }));
}, 'image/jpeg', 0.92);
});
最初はPNGでしたが、背景が全面を覆っていて透明部分がないので、JPG(画質0.92)に変えています。見た目はほとんど変わらず、ファイルサイズも小さくなるので、一石二鳥でした。
保存ボタンは、定番の <a download> で作りました。
const saveImage = (file) => {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
PCでは問題なく動いたので安心していたのですが、iPhoneで試してみると、画像は「写真」ではなく「ファイル」アプリに入ってしまいました。JavaScriptから写真アプリへ直接保存する方法は、今のところ用意されていません。
写真アプリに入れたい場合は、OSの共有シートを開いて、そこから「画像を保存」を選んでもらうしかなさそうです。
共有には Web Share API を使います。画像ファイルを渡すときは、先に canShare で判定します。
const shareImage = async (file) => {
if (!navigator.canShare || !navigator.canShare({ files: [file] })) {
return false; // この端末では使えない
}
await navigator.share({
files: [file],
text: 'タイトル\nhttps://example.com/',
});
return true;
};
ここで、実装のポイントが2つありました。
1つ目は、ボタンを押す前に画像を作っておくことです。navigator.share() は、ユーザー操作の直後でないと呼び出せません。ボタンを押してから画像を作ると、その間に有効期限が切れてしまい、共有シートが開かないことがあります。そのため、シートを開く時点で File まで作っておき、ボタンではそれを渡すだけにしました。
2つ目は、URLを別に渡さず、text にまとめたことです。タイトルとURLを別々に渡したところ、端末によって画像や文言が抜け落ちることがありました。そこで、text にまとめて渡しています。
実は、共有シートを閉じたとき(キャンセルしたとき)も、share() はエラーを返します。何も失敗していないのにコンソールが赤くなるので、最初は少し焦りました。
そこで、キャンセルのとき(AbortError)だけは無視するようにしています。
button.addEventListener('click', async () => {
try {
const shared = await shareImage(imageFile);
if (!shared) {
saveImage(imageFile); // 共有できない端末は、保存にフォールバック
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error); // キャンセル以外のときだけログに出す
}
}
});
保存ボタンを共有シート経由にして、写真に入れやすくする案も試しました。
ただ、運用上の都合や、別機能との競合も発生してしまったため、最終的には <a download> に戻して、「保存は端末のファイルに入る」と割り切りました。
canvasで画像を作るところまでは簡単でも、スマホで「保存」「共有」まで通そうとすると、ブラウザができることの境界にぶつかります。
ただ、仕様を先に知っておけば、無理に頑張りすぎず、代替案を提案するという判断もしやすくなるので知識としては知っておいて損はないかと思います。
ブラウザでできることの境界は、実機で確かめないと見えないことが多いので、画像まわりは、ぜひ早めにスマホで試してみてください。
同じところで悩んでいる方の参考になればうれしいです。