別ドメインの画像ファイルをローカルに保存する（React+canvas）

Reactでcanvasを使用して「ボタンを押したら画像をダウンロードする機能」を実装しようとしたら詰まったので備忘録
かなりニッチなニーズ

aタグによる画像のダウンロード
---

通常aタグにdownload属性をつければファイルは簡単にダウンロードできるが

```haml

<a href="https://hoge.com/sample.png" download="saved.png">ダウンロード</a>
```

のように別ドメインのサーバーに存在する画像を指定すると別タブで開くだけでダウンロードされない
aタグのdownload属性は同一オリジンでのみ動作するので別ドメインの画像はダウンロードできない

canvasを使用してダウンロード
---

canvasを使用して画像をblobに変換してダウンロードする方法

```javascript

    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ヘッダを送信せずにオリジン許可が得られずに怒られる可能性がある

```html

<img src="https://hoge.com/sample.png?cache=none" crossOrigin="anonymous" alt=""/>
```

~~のようにすればキャッシュの問題も解決できる~~
S3の場合はこれでキャッシュの削除ができたが他のサーバーではできないこともあったので他の方法を検討したほうが良さそう


Reactで画像ダウンロード機能を実装してみる
---

今回は２枚の画像を同時にダウンロードする機能を作成する

まずはカスタムフック

```react:enhance.ts

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

```react:index.tsx
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>
  );
};

```


![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192773/75292dbc-ca04-4bd5-c73b-e440f20c2136.gif)

これにて一件落着

参考
---

[Google Chromeでのamazon S3画像へのクロスドメイン接続: stackoverflow](https://ja.stackoverflow.com/questions/33458/google-chrome%E3%81%A7%E3%81%AEamazon-s3%E7%94%BB%E5%83%8F%E3%81%B8%E3%81%AE%E3%82%AF%E3%83%AD%E3%82%B9%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E6%8E%A5%E7%B6%9A?rq=1)