42
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-06-03

変な向きで画像がプレビューされる・・・?!

前回書いたコードだと、Exif情報を持った画像を投稿すると変な向きでプレビュー表示されてしまいました。

(コーヒーの画像が横向きになってしまってますね・・)
ezgif.com-video-to-gif (1).gif

Exifとは

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

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

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

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

Orientation 撮影方向の定義
1 そのまま
2 上下反転(上下鏡像?)
3 180度回転
4 左右反転
5 上下反転、時計周りに270度回転
6 時計周りに90度回転
7 上下反転、時計周りに90度回転
8 時計周りに270度回転

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

Exif Orientationの対応方法

JavaScript-Load-Image を使う

解決してくれるライブラリがあります

$ 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:

スクリーンショット 2019-06-04 0.09.11.png

リサイズもしたい・・

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

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

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

最終的なコード

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 は数値の平方根を返します

toDataURL の第二引数にencoderOptionsを指定してあげると品質レベルを落とす事ができるようです

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

42
36
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
42
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?