©️

Reactで画像のコピーとダウンロード機能を実装してみた

に公開

この記事ではReactを用いた画像のダウンロード・コピー機能についてお話しします。

ごりごりの初心者向けです。

使用技術について

今回使用する技術は以下です。

  • React(v18.2.15)
  • TypeScript(v5.0.2)
  • axios(v1.5.1)

画像のダウンロード処理

初めは画像のダウンロード処理についてです!

全部のコード載せちゃいます。
これです。

export const downloadImage = async (src: string) => {
  try {
    const response = await axios.get(src, {
      responseType: "blob",
    });
    const fileName = src.substring(src.lastIndexOf("/") + 1);
    saveAs(response.data, fileName);
  } catch (error) {
    console.error("Image download failed", error);
  }
};

意外と短いですね。

引数としてsrcを受け取っているのはダウンロードした時の画像ファイルの名前を指定するためです。

データを非同期で取得

以下の箇所では、srcからデータを非同期で取得しています。

    const response = await axios.get(src, {
      responseType: "blob",
    });

responseTypeをblobとしているので、サーバーから返ってくる値の型はBlobとなります。
主に画像やビデオといった大きなバイナリデータを扱うファイル形式です。

もちょっとBlobについて詳しく

1 : 🐶 にーな
responseType: blobっていうのは、WebAPIやサーバーからデータを取得するときに使うオプションの一つだよ。Blobっていうのは、Binary Large OBjectの略で、画像や動画などの大きなバイナリデータを扱うためのフォーマットなんだ。このオプションを使うと、テキストではなくバイナリデータとしてデータを直接受け取れるから、画像ダウンロードなどに便利なんだよ。」

2 : 🦊 もんた
「バイナリデータって、具体的にはどういうものなの?」

3 : 🐶 にーな
「バイナリデータっていうのは、0と1のみで表されるデータのことだよ。テキストファイルと違って、人が直接読んで理解することは難しいけど、画像や動画、音声ファイルなど、多くのファイル形式がこのバイナリデータで構成されているんだ。だから、responseType: blobを使うと、こういったファイルを効率良くダウンロードして扱えるようになるんだ。」

4 : 🧑🏾‍🦱 のぎお
「へぇ〜、blobってなんかスライムみたいだね!でも、どうやってそれで画像をダウンロードするの?」

5 : 🐶 にーな
「いい質問だね!例えば、JavaScriptでaxiosを使って画像をダウンロードするときはこんな感じに書くんだ。」

// axiosを使って画像のURLからデータを取得する
axios.get('画像のURL', {
  responseType: 'blob'  // レスポンスタイプとしてblobを指定
}).then(function (response) {
  // ダウンロードしたblobデータはresponse.dataに格納されている
  // ここでダウンロードしたデータを扱う処理を書く
});

6 : 🐭 せんぱい
「responseTypeをblobにすると、どんなメリットがあるの?」

7 : 🐶 にーな
blobにするメリットとしては、まずデータの損失なく元の状態でファイルをダウンロードできることが挙げられるよ。テキストとして受け取ると、エンコーディングの問題でデータが壊れるリスクがあるけど、blobならその心配がないんだ。さらに、大きなデータも効率的に扱えるから、高画質の画像や長い動画でもスムーズにダウンロードできるよ。」

8 : 🐶 にーな
「まとめると、responseType: blobを指定することで、バイナリデータとして直接ファイルをダウンロードできるんだ。これにより、データの損失なく、効率的に大きなファイルを扱うことができるようになる。画像や動画などのメディアファイルを扱うときに特に便利だね。」

ファイル名を取得する

続いて、以下の箇所ではsrcの最後に出現する/の位置を探します。

    const fileName = src.substring(src.lastIndexOf("/") + 1);

例えばsrcの中身が以下だとします。

https://example.com/images/photo.jpg

そうすると、lastIndexOf("/")https://example.com/images/photo.jpgの間のスラッシュのインデックスを返します。

これだとsubstring() した時に、/photo.jpgを返すのでfilenameとして使えません。
そこで+1することで開始位置をpとし、それをsubstring()することでphoto.jgpを返し、filenameとして使えるようになるというわけです。

取得したファイル名で画像を保存

サイトはsaveAs()ですがめちゃくちゃ簡単です。

    saveAs(response.data, fileName);

これでユーザーのデバイスにresponse.data(画像のバイナリデータ)fileNameで保存します。

画像のダウンロード周りは以上!

画像のコピー処理

続いて画像のコピーですね。

全体はこんな感じです。

export const copyImageToClipboard = async (src: string) => {
  try {
    const response = await axios.get(src, { responseType: "blob" });
    const blob = response.data;

    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const image = await createImageBitmap(blob);
    canvas.width = image.width;
    canvas.height = image.height;
    if (ctx) {
      ctx.drawImage(image, 0, 0);
    }

    canvas.toBlob(async (newBlob) => {
      if (newBlob) {
        const clipboardItem = new ClipboardItem({ [newBlob.type]: newBlob });
        await navigator.clipboard.write([clipboardItem]);
      }
    }, blob.type);
  } catch (err) {
    console.error("Failed to copy on clipboard", err);
  }
};

以下より詳しく説明していきます!

画像を取得する

以下では画像のデータを非同期で取得しています。
先ほどの画像ダウンロードの説明でもありましたね。

非同期で取得した画像データをblobという変数に代入しています。

const response = await axios.get(src, { responseType: "blob" });
const blob = response.data;

画像をCanvasに描画する

以下ではcanvas要素を作成し、blobの内容を描画するという作業を行なっています。

canvas.getContext("2d")では、2Dグラフィックを描画するためのツールを取得しています。
最後のctx.drawImage(image, 0, 0);で描画する際に使います。

await createImageBitmap(blob)は先ほど取得したblobデータから画像ビットマップを作成します。
このビットマップは、描画に使用できる画像データです。

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const image = await createImageBitmap(blob);
canvas.width = image.width;
canvas.height = image.height;
if (ctx) {
  ctx.drawImage(image, 0, 0);
}

ちなみにcanvasを使用している理由ですが、Blobデータを直接クリップボードにコピーする機能はブラウザによってはサポートされていないからです。

サポートされていないと画像データが不安定になる可能性があるらしく、それを防ぐためにcanvasを使用しています。

ビットマップってなんじゃ?

1 : 🐶 にーな
「画像ビットマップっていうのはね、簡単に言うと画像が点々(ピクセル)でできているということだよ。コンピューターの画面上で見る写真や絵は、全部小さな点の集まりからできているんだ。ビットマップとは、それらの点の色や位置を記録したデータのことを指すんだよ。」

2 : 🦊 もんた
「へー、じゃあ画面上で見えてる画像って、全部小さな点々でできてるんだ。でも、どうやってそれぞれの点の色を記録してるの?」

3 : 🐶 にーな
「いい質問だね!各ピクセルには色情報があって、RGB(赤、緑、青)の値を使って、そのピクセルがどんな色をしているかを表しているんだ。たとえば、RGBで(255, 0, 0)っていう値だったら、そのピクセルは赤色ってことになるよ。」

4 : 🧑🏾‍🦱 のぎお
「RGBって何?なんか飲み物?」

5 : 🐶 にーな
「はは、飲み物じゃないよ、のぎお。RGBっていうのは Red(赤)、Green(緑)、Blue(青) の頭文字を取ったもので、これらの色を組み合わせることで、私たちが普段見ている様々な色を作り出しているんだ。」

6 : 🐭 せんぱい
「ビットマップのデータはどれくらいの大きさになるの?」

7 : 🐶 にーな
「それは画像の解像度と色深度によって変わってくるよ。解像度が高く、色深度が深いほど、ファイルのサイズは大きくなるんだ。色深度っていうのは、1つのピクセルが表現できる色の数のこと。たとえば、24ビット色深度なら、約1677万色を表現できるよ。」

8 : 🧑🏾‍🦱 のぎお
「1677万色もあるの?全部見たことある?」

9 : 🐶 にーな
「実際には、私たち人間の目で区別できる色はそれよりも少ないんだけど、コンピューターはとても細かい色の違いを扱えるんだよ。」

10 : 🐶 にーな
「まとめると、画像ビットマップとは、画面上の画像を構成する小さな点(ピクセル)の集まりで、それぞれのピクセルが特定の色情報(RGB値)を持っているってこと。この方式で、デジタルデバイスは様々な画像を表示できるんだよ。」

Canvasの内容をクリップボードにコピーする

最後です。

最後は先ほどcanvas上に画像データをblobデータとして取得します。

非同期処理でnewBlobが取得できたら、それをクリップボードに書き込んでいます。
const clipboardItem = new ClipboardItem({ [newBlob.type]: newBlob });でコピー前の準備を行い、await navigator.clipboard.write([clipboardItem]);でクリップボードにコピーをしています。

canvas.toBlob(async (newBlob) => {
  if (newBlob) {
    const clipboardItem = new ClipboardItem({ [newBlob.type]: newBlob });
    await navigator.clipboard.write([clipboardItem]);
  }
}, blob.type);
navigatorってなんや?

1 : 🐶 にーな
navigatorっていうのは、ブラウザが提供するオブジェクトの一つで、ブラウザ自体やその機能に関する情報を扱うためのものだよ。navigatorオブジェクトを使うと、ブラウザのバージョンやユーザーエージェント(ブラウザが自身をどう識別しているか)の情報を取得できるんだ。」

2 : 🦊 もんた
「じゃあ、navigator.clipboard.writeっていうのはどういう意味?」

3 : 🐶 にーな
「それはね、navigatorオブジェクトのclipboardプロパティを通じて、ブラウザのクリップボードAPIにアクセスする方法の一つだよ。writeメソッドを使うと、ユーザーのクリップボードにデータを書き込むことができる。この場合、[clipboardItem]という配列に格納されたデータをクリップボードにコピーしているんだ。」

4 : 🧑🏾‍🦱 のぎお
「クリップボードって何?」

5 : 🐶 にーな
「クリップボードっていうのは、コンピューターで一時的にデータを保存しておく場所のこと。例えば、テキストや画像をコピーして、別の場所にペーストするときに使うんだ。ブラウザが提供するクリップボードAPIを使うと、ウェブページからもそのクリップボードにアクセスできるようになるんだ。」

6 : 🐭 せんぱい
「セキュリティは大丈夫なの?」

7 : 🐶 にーな
「いい質問だね。クリップボードAPIを使うときは、ユーザーの許可が必要だったり、HTTPSで暗号化された安全な接続が必要になることが多いよ。これは、ユーザーのデータを保護するための措置だね。ウェブページが勝手にクリップボードにアクセスして、ユーザーの情報を読み取ったり書き込んだりすることを防ぐためだよ。」

8 : 🐶 にーな
「要するに、navigatorはブラウザの機能にアクセスするためのオブジェクトで、navigator.clipboard.writeはその中のクリップボードAPIを使って、ユーザーのクリップボードにデータを安全に書き込むためのメソッドってわけ。セキュリティのために、ユーザーの許可が必要になることがあるから、使うときは注意が必要だよ。」

オワリ

意外と簡単でしたね。
余談ですが、チャットGPT使ってキャラクター同士の会話形式で説明してもらうのにハマってます。

Discussion