【簡単】画像選択時のプレビュー表示とExif対応【JavaScript-Load-Image/Vue】


## 変な向きで画像がプレビューされる・・・？！
[前回書いたコード](https://qiita.com/kawa64372358/items/492c89db7f96823a26c0)だと、Exif情報を持った画像を投稿すると変な向きでプレビュー表示されてしまいました。

（コーヒーの画像が横向きになってしまってますね・・）
![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239038/3738e117-8796-bba4-ee88-34e52869b769.gif)

## Exifとは

写真のメタデータ等が含まれる画像ファイル形式のことです
全ての画像ファイルに含まれているわけではなく、スマートフォンで撮影した画像などに含まれます

* [Exifとは何？Weblio辞書](https://www.weblio.jp/content/Exif)

Exif情報の中には `ファイルサイズ` `撮影した位置（緯度・経度）` `撮影日時` 等が含まれます
iPhoneのアルバムだと、撮影地ごとにフォルダ分けされるのもExif情報があるからですね

### Exif情報の中のOrientationが悪さの原因

Orientationとは写真の撮影方向のことです


| Orientation      | 撮影方向の定義 |
|:-----------------|----------------------------:|
| 1                |                      そのまま|
| 2                |             上下反転(上下鏡像?)|
| 3                |                     180度回転|
| 4                |                      左右反転|
| 5                |    上下反転、時計周りに270度回転|
| 6                |             時計周りに90度回転|
| 7                |     上下反転、時計周りに90度回転|
| 8                |            時計周りに270度回転|
* [JPEGのExifタグ情報のOrientaionの定義の早見表 · DQNEO起業日記](http://dqn.sakusakutto.jp/2009/02/jpegexiforientaion.html) より

Orientaiotn情報を持った画像をアップロードすると回転されて表示されてしまうという事でした

## Exif Orientationの対応方法

### JavaScript-Load-Image を使う

解決してくれるライブラリがあります
* [blueimp/JavaScript-Load-Image](https://github.com/blueimp/JavaScript-Load-Image)

```bash
$ yarn add -D blueimp-load-image
```

主に `loadImage` を使ったコードだけ抜き出して説明します。（全体のコードは後述）
まず、inputのchangeイベント（ファイルを選択した時）に発火される`attachImg` 処理についてです

```index.vue
<input @change="attachImg">
```


```index.vue
<script>
import loadImage from 'blueimp-load-image';

export default {
  methods: {
    attachImg(e) {
      const file = e.target.files[0];

      loadImage.parseMetaData(file, (data) => {
        const options = {
          canvas: true
        };
        if (data.exif) {
          options.orientation = data.exif.get('Orientation');
        }
        this.displayImage(file, options);
      });
    }
  }
};
</script>
```

parseMetaDataを使うと `data.exif` でExif情報が取れます
もし画像にExif情報があれば `Orientation` をoptionに指定してあげます

次に、実際にプレビュー表示する `displayImage` にfileとoptionを渡します。

```index.vue
<script>
export default {
  methods: {
    displayImage(file, options) {
      loadImage(
        file,
        async (canvas) => {
          const data = canvas.toDataURL(file.type);
          // data_url形式をblob objectに変換
          const blob = this.base64ToBlob(data, file.type);
          // objectのURLを生成
          const url = window.URL.createObjectURL(blob);

          this.resizedImg = url; // resizedImgはdataで定義
        },
        options
      );
    }
  }
};
</script>
```
displayImageではファイル系式を変換しているだけです
`data_url` -> `blob object` -> `blob url`
最終的に生成されたurlをdataで定義したresizedImgに入れることでimg srcにバインドされます

```index.vue
<template>
 <img :src="resizedImg">
</template>

<script>
export default {
  data() {
    return {
      resizedImg: null
    };
  }
 }
}
</script>
```

### 正しい向きでプレビュー表示された :tada:
<img width="356" alt="スクリーンショット 2019-06-04 0.09.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239038/ee6f8372-ab0c-d776-12ef-5d88065efee0.png">

## リサイズもしたい・・

JavaScript-Load-Imageを使うと簡単にリサイズもできます

```index.vue
<script>
// 略
loadImage.parseMetaData(file, (data) => {
  const options = {
    maxHeight: 500,
    maxWidth: 500,
    canvas: true
  };
</script>
```

さっきのoptionオブジェクトの中に `maxHeight` と `maxWidth` を指定するだけ
縦横比を保ったままいい感じにリサイズしてくれます

## 最終的なコード
```index.vue
<template>
  <div class="resize-img">
    <!-- 画像選択 -->
    <div v-show="!resizedImg" class="resize-img__post">
      <label for="file" class="resize-img__post__label">画像
        <input
          id="file"
          ref="fileInput"
          type="file"
          accept=".jpeg, .png"
          @change="attachImg">
      </label>
    </div>
    <!-- プレビュー -->
    <div v-show="resizedImg" class="resize-img__preview">
      <div class="resize-img__preview__circle" @click="clearAttachImg">
        <span class="resize-img__preview__circle__close-icon">×</span>
      </div>
      <img :src="resizedImg" class="resize-img__preview__img">
    </div>
  </div>
</template>

<script>
import loadImage from 'blueimp-load-image';

export default {
  data() {
    return {
      resizedImg: null
    };
  },
  destroyed() {
    this.clearAttachImg();
  },
  methods: {
    attachImg(e) {
      const file = e.target.files[0];

      loadImage.parseMetaData(file, (data) => {
        const options = {
          maxHeight: 500,
          maxWidth: 500,
          canvas: true
        };
        if (data.exif) {
          options.orientation = data.exif.get('Orientation');
        }
        this.displayImage(file, options);
      });
    },
    displayImage(file, options) {
      loadImage(
        file,
        async (canvas) => {
          const data = canvas.toDataURL(file.type);
          // data_url形式をblob objectに変換
          const blob = this.base64ToBlob(data, file.type);
          // objectのURLを生成
          const url = window.URL.createObjectURL(blob);

          this.resizedImg = url;
        },
        options
      );
    },
    clearAttachImg() {
      this.resizedImg = null;
      if (this.$refs.fileInput && this.$refs.fileInput.value !== undefined) {
        this.$refs.fileInput.value = '';
        window.URL.revokeObjectURL(this.resizedImg);
      }
    },
    base64ToBlob(base64, fileType) {
      const bin = atob(base64.replace(/^.*,/, ''));
      const buffer = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
      }
      return new Blob([buffer.buffer], {
        type: fileType ? fileType : 'image/png'
      });
    }
  }
};
</script>

<style lang="scss" scoped>
.resize-img {
  width: 300px;
  height: 300px;
  margin: 0 auto;
  margin-top: 20px;

  &__post {
    border: 1px solid rgba(#000, 0.16);
    line-height: 30rem;

    &__label {
      display: inline-block;
      width: 100%;
      color: rgba(0, 0, 0, 0.4);
      text-align: center;

      & > input {
        display: none;
      }
    }
  }

  &__preview {
    width: 300px;
    height: 300px;

    &__circle {
      position: absolute;
      right: 37px;
      width: 27px;
      height: 27px;
      margin: 5px;
      padding: 2px 9px;
      border-radius: 50%;
      background-color: rgba(0, 0, 0, 0.3);

      &__close-icon {
        color: #fff;
      }
    }

    &__img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }
}
</style>
```

## おまけ
### maxWidth・maxHeightじゃなくてmaxByteで指定したい

JavaScript-Load-Imageのread meを見ると `downsamplingRatio` で出来そう
https://github.com/blueimp/JavaScript-Load-Image

でも自分はうまく出来なかった（泣）なので苦し紛れ実装しました

```index.vue
<script>
export default {
  methods: {
    displayImage(file, options) {
      loadImage(
        file,
        async (canvas) => {
          const data = canvas.toDataURL(file.type);
          const blob = this.base64ToBlob(data, file.type);
          let url = window.URL.createObjectURL(blob);

          // 追記箇所ここから ->
          const maxSize = 1000000 // 1MB
          if (blob.size >= maxSize) {
            const capacity = Math.sqrt(maxSize / blob.size);
            const binary = canvas.toDataURL(file.type, capacity);
            const resized = this.base64ToBlob(binary, file.type);
            url = window.URL.createObjectURL(resized);
          }
          // 追記箇所ここまで

          this.resizedImg = url;
        },
        options
      );
    }
  }
};
</script>
```

`Math.sqrt` は数値の平方根を返します
* [Math.sqrt() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt)

`toDataURL` の第二引数にencoderOptionsを指定してあげると品質レベルを落とす事ができるようです
* [HTMLCanvasElement.toDataURL() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)

なんとなく間違った実装な気がするので
もっと良い書き方があれば教えていただけると嬉しいです