[JavaScript]リアルタイム反映されるアップロード画像の拡張子変換が意外とめんどかった

# TL;DR

- `<input type='file'>`で受け取ったfileの拡張子を.jpg→.pngの変更って簡単にできない
- GUIなら簡単なのに…
- フロント制御だけならHTMLのタグがあるが。。。

https://webbibouroku.com/Blog/Article/html5-file-accept

# はじめに

## やりたいこと

- フロントでjpg, jpeg, pngを受け取れる`<input type='file'>`を作成
- 受け取ったfileをサーバーに送信する際はpngに変換して、base_64文字列でリクエストしたい
    - 生fileでリクエストするとバリデーション内容膨大、ローカルストレージに保存される無駄、jpeg exifの考慮（画像の回転情報補正処理）があるのでかなり避けたい…

## 悩み

- JSでサクッとできる方法がない…
- GUIならサクッとできるのに…
    - 拡張子を.jpgから.pngにするだけ

## 仕方なしの対応

- `<input type='file'>`で取得したファイルデータをblobに変換
- canvasからblobへの変換はデフォルトでpngになる

https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#:~:text=for%20any%20reason.-,type,-Optional

### 補足

- blobとは

https://e-words.jp/w/BLOB.html](https://e-words.jp/w/BLOB.html

- JSにおけるblob

https://ja.javascript.info/blob](https://ja.javascript.info/blob

### 流れ

①`<input type='file'>`で選択されたFileオブジェクト取得
②Fileオブジェクトで取得した画像データをFielReader APIを使ってImageオブジェクトに変換
③②のImageオブジェクトをCanvasに転写
④CanvasをBlobに変換
⑤BlobはデフォルトでmimeTypeがpngなので、ここからdataUrl（＝base64）で取得

```mermaid
flowchart TD
    A[<input type='file'> に画像選択]--> B;
		B[chnageイベント発火 + jpg or jpeg or pngの画像がfileオブジェクトで渡る]-->C
    C[FileReader APIでImageオブジェクトに変換]-->D;
		D[Canvasに転写: Imageオブジェクトのリアルタイム反映]-->E;
    E[CanvasをBlobに変換]-->F;
    F[BlobからDataUrl=base64のURLを取得]-->G
    G[base64Urlをtype='hidden'のinputに渡す]-->H
    H[End: サーバーにbase64のURLをPOST送信];
```

## ブラウザ

### 初回画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780929/f9cde58a-7a86-d60e-fe34-0ea637021d8f.png)

### 画像選択後

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780929/60dab9bd-061c-4378-f8e0-030fc34c6893.png)

### ソースコード
- 「めんどくさいことしてんな」というイメージだけ捉えてもらえば大丈夫です。
```vuejs
<!-- 「保存する」ボタンはコンポーネント外で実装 -->
<template>
  <div>
    <div>
      <input
        type="file"
        @change="uploadFile"
      >
      <!-- サーバーにリクエストする値 -->
      <input
        type="hidden"
        :name="inputName"
        :value="urlBase64"
      >
      <!-- 初回描画時の画像 -->
      <div v-if="urlBase64 === ''">
        <img
          v-lazy="url"
        >
      </div>
      <!-- ファイル選択後の画像 -->
      <div v-show="urlBase64">
        <canvas
          :id="canvasId"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    url: {
      type: String,
      default: '',
    },
    canvasId: {
      type: String,
      default: '',
    },
    inputName: {
      type: String,
      default: 'url_base64',
    },
  },
  data () {
    return {
      urlBase64: '',
    }
  },
  methods: {
    uploadFile(event) {
      // onload内でVueComponentインスタンスを参照するために変数に代入
      const vm = this;
      // canvasからblob, base64へ変換
      const file = event.currentTarget.files[0];

      const readerForFile = new FileReader();
      readerForFile.onload = function() {
        // ファイルが読み込まれたあとの処理
        const image = new Image();
        image.onload = async function() {
          // imageを転写するcanvasの取得、加工
          const canvas = document.getElementById(vm.canvasId);

          canvas.width = image.width;
          canvas.height = image.height;

          // canvas(CanvasRenderingContext2D)に画像を転写
          const context = canvas.getContext("2d");
          context.drawImage(image, 0, 0);

          // canvas -> blobの変換
          const blob = await vm.canvasToBlob(canvas);
          // blob -> base64の変換してプロパティに代入
          vm.urlBase64 = await vm.readAsDataURL(blob);
          console.log(vm.urlBase64);
        }
        image.onerror = function() {
          alert('ファイルはjpg, jpeg, pngしか選べません。');
        }
        // 画像がimageタグに読み込み->image.onloadイベント発火
        image.src = readerForFile.result;
      }
      readerForFile.onerror = function() {
        alert('ファイルを読み込めませんでした。');
      }
      // Fileオブジェクトを読み込む->readerForFile.onloadイベント発火
      readerForFile.readAsDataURL(file);
    },
    canvasToBlob(canvas) {
      return new Promise((resolve) => {
        // mimtypeを分かりやすいように明示
        canvas.toBlob(resolve, 'image/png');
      });
    },
    readAsDataURL(blob) {
      return new Promise((resolve, reject) => {
        const readerForBlob = new FileReader();
        readerForBlob.onloadend = () => {
          resolve(readerForBlob.result);
        };
        readerForBlob.onerror = () => {
          reject(readerForBlob.error);
        };
        readerForBlob.readAsDataURL(blob);
      });
    },
  },
}
</script>
```

## 参考

https://qiita.com/_shimizu/items/7df9add540984eba8786

# 所見

「サクッと拡張子変更したい」なんてよくある要件だよなと思いつつ、ライブラリないか探しましたが意外となかった…

結構「リアルタイム反映」と「pngへの変換」をごっちゃになっている可能性も否めないのですが、

> 「リアルタイム反映」という要件でcanvusに変換する必要がある
  →どうせcanvas取得できるならそっからblobに変換しちゃったほうがいいや

という思考の流れです。

「jpgからpngの変換はおまえが思っているより簡単なことではない」

「こんなことせずとももっと簡単にできる」

などご助言あれば幸いです…