Reactでcanvasを使用して「ボタンを押したら画像をダウンロードする機能」を実装しようとしたら詰まったので備忘録
かなりニッチなニーズ
aタグによる画像のダウンロード
通常aタグにdownload属性をつければファイルは簡単にダウンロードできるが
<a href="https://hoge.com/sample.png" download="saved.png">ダウンロード</a>
のように別ドメインのサーバーに存在する画像を指定すると別タブで開くだけでダウンロードされない
aタグのdownload属性は同一オリジンでのみ動作するので別ドメインの画像はダウンロードできない
canvasを使用してダウンロード
canvasを使用して画像をblobに変換してダウンロードする方法
const c = document.getElementById('canvas');
c.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'foo.png';
a.href = url;
a.click();
}, 'image/png');
こんな感じでblobに変換してダウンロードすることが可能なのでこちらを試してみる
しかし、、
Tainted canvases may not be exported
こんな感じのエラーが発生
キャンバスは別オリジンの画像を入れると汚染されてしまいtoBlobなどのメソッドが使えなくなるそう
crossOrigin="anonymous"
canvas内のimgタグに crossOrigin="anonymous"
をつけると別オリジン間でのダウンロードが可能になる
余計な画像のキャッシュ
crossOrigin="anonymous"
をつけたらダウンロードできるようになる場合とならない場合がある
crossOrigin属性を付ける前に画像のリクエストを送っている場合、キャッシュを保存している可能性がある
特にS3に画像を保存している場合、S3は Orgin ヘッダが含まれていないと Access-Control-
系のヘッダを返さない
crossOrigin属性を付ける前のimageタグではOriginヘッダを送信せずにオリジン許可が得られずに怒られる可能性がある
<img src="https://hoge.com/sample.png?cache=none" crossOrigin="anonymous" alt=""/>
のようにすればキャッシュの問題も解決できる
S3の場合はこれでキャッシュの削除ができたが他のサーバーではできないこともあったので他の方法を検討したほうが良さそう
Reactで画像ダウンロード機能を実装してみる
今回は2枚の画像を同時にダウンロードする機能を作成する
まずはカスタムフック
import { useRef, RefObject } from 'react';
const useEnhancer = () => {
const canv = useRef<HTMLCanvasElement>(null);
const canv2 = useRef<HTMLCanvasElement>(null);
const img = useRef<HTMLImageElement>(null);
const img2 = useRef<HTMLImageElement>(null);
const downloadImage = (
canvas: RefObject<HTMLCanvasElement>,
image: RefObject<HTMLImageElement>,
id: string
) => {
if (canvas.current !== null) {
const currentCnavas = canvas.current;
const ctx = currentCnavas.getContext('2d');
if (ctx && image.current !== null) {
const currentImage = image.current;
currentCnavas.width = currentImage.width;
currentCnavas.height = currentImage.height;
ctx.drawImage(currentImage, 0, 0, currentImage.width, currentImage.height);
}
const anchor: HTMLAnchorElement = document.createElement('a');
currentCnavas.toBlob((blob: any) => {
if (anchor !== null && blob) {
anchor.href = window.URL.createObjectURL(blob);
anchor.download = `${id}.png`;
anchor.click();
}
});
}
};
return {
downloadImage,
canv,
canv2,
img,
img2,
};
};
export default useEnhancer;
View
import React from 'react';
import useEnhancer from './enhance';
const DownloadImage = () => {
const enhance = useEnhancer();
return (
<div>
<button
onClick={() => {
enhance.downloadImage(enhance.canv, enhance.img, '1');
enhance.downloadImage(enhance.canv2, enhance.img2, '2');
}}
type="button"
>
ダウンロード
</button>
<canvas ref={enhance.canv} style={{ display: 'none' }}>
<img
ref={enhance.img}
src="https://cdn.qiita.com/assets/qiita-fb-fe28c64039d925349e620ba55091e078.png?cache=none"
alt=""
crossOrigin="anonymous"
/>
</canvas>
<canvas ref={enhance.canv2} style={{ display: 'none' }}>
<img
ref={enhance.img2}
src="https://cdn.qiita.com/assets/qiita-fb-2887e7b4aad86fd8c25cea84846f2236.png?cache=none"
alt=""
crossOrigin="anonymous"
/>
</canvas>
</div>
);
};
これにて一件落着