LoginSignup
9
12

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-13

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枚の画像を同時にダウンロードする機能を作成する

まずはカスタムフック

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

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

これにて一件落着

参考

Google Chromeでのamazon S3画像へのクロスドメイン接続: stackoverflow

9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12