変な向きで画像がプレビューされる・・・?!
前回書いたコードだと、Exif情報を持った画像を投稿すると変な向きでプレビュー表示されてしまいました。
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
処理についてです
<input @change="attachImg">
<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を渡します。
<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にバインドされます
<template>
<img :src="resizedImg">
</template>
<script>
export default {
data() {
return {
resizedImg: null
};
}
}
}
</script>
正しい向きでプレビュー表示された
リサイズもしたい・・
JavaScript-Load-Imageを使うと簡単にリサイズもできます
<script>
// 略
loadImage.parseMetaData(file, (data) => {
const options = {
maxHeight: 500,
maxWidth: 500,
canvas: true
};
</script>
さっきのoptionオブジェクトの中に maxHeight
と maxWidth
を指定するだけ
縦横比を保ったままいい感じにリサイズしてくれます
最終的なコード
<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
でも自分はうまく出来なかった(泣)なので苦し紛れ実装しました
<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を指定してあげると品質レベルを落とす事ができるようです
なんとなく間違った実装な気がするので
もっと良い書き方があれば教えていただけると嬉しいです