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

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

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

# 使用技術について
今回使用する技術は以下です。

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

# 画像のダウンロード処理

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

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

```typescript
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からデータを非同期で取得しています。

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

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


<details><summary>もちょっとBlobについて詳しく</summary>

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

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

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

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

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

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

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

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

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

## ファイル名を取得する

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

```typescript
    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()`ですがめちゃくちゃ簡単です。

```typescript
    saveAs(response.data, fileName);
```

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

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


# 画像のコピー処理

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

全体はこんな感じです。

```typescript
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という変数に代入しています。

```typescript
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データから画像ビットマップを作成します。
このビットマップは、描画に使用できる画像データです。




```typescript
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を使用しています。



<details><summary>ビットマップってなんじゃ？</summary>

**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値）を持っているってこと。この方式で、デジタルデバイスは様々な画像を表示できるんだよ。」
</details>

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

最後です。

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

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

```typescript
canvas.toBlob(async (newBlob) => {
  if (newBlob) {
    const clipboardItem = new ClipboardItem({ [newBlob.type]: newBlob });
    await navigator.clipboard.write([clipboardItem]);
  }
}, blob.type);
```

<details><summary>navigatorってなんや？</summary>

**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**を使って、ユーザーのクリップボードにデータを安全に書き込むためのメソッドってわけ。セキュリティのために、ユーザーの許可が必要になることがあるから、使うときは注意が必要だよ。」
</details>


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