React+canvasで別ドメインの画像ファイルをローカルに保存する
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"
をつけたらダウンロードできるようになる場合とならない場合が
前提として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> ); };
これにて一件落着