Edited at

EXIFは残したまま、デカイ画像をリサイズしたい


問題

モバイル端末からなどの画像のアップロードは、そのままの画像サイズだと相当デカイ。そのままサーバーにアップロードするには1枚ならまだしも複数枚だと軽く10MBとか超える。

リサイズするのは色々なnpmがあるので簡単にできるが、Exif情報が飛んでしまう。

サーバー側でExif情報が必要な場合(GPS情報とかOrientationとか)困る。

結構色々探してみたが、リサイズしつつExifは残すって言うのをサクッとやってくれるようなnpmが見つからなかった..ので作った。


  1. ファイルを選択して

  2. consoleを見てみる

  3. 表示されている画像を右クリックで保存


  4. 画像の情報を見て、exifが残ってるか確認



解決方法



1. Jpegである事(細かい事言うとTIFFとかも入るっぽいが...)

2. 画像データをバイナリで読み、Exifが入っているか確認(各セグメントを取っていく)

3. Exifが入ってたらそのセグメントをざっくり抜き出す

4. 抜き出したExifセグメントをリサイズしたバイナリデータに差し込む


ソース


ResizeKeepingExif


import loadImage, { LoadImageOptions } from "blueimp-load-image"

export default class ResizeKeepingExif {
file: File
jpegQuality: number
options: LoadImageOptions
binaryData: Uint8Array
resizedBinary: Uint8Array
segments: Uint8Array[] = []

constructor(file: File, options: LoadImageOptions, jpegQuality = 0.8) {
this.file = file
this.jpegQuality = jpegQuality
this.options = options
}

setup() {
const p1 = new Promise((resolve, reject) => {
// リサイズはloadImageに任せる
loadImage(
this.file,
canvas => {
// JPEGに強制
const resizedDataUrl = canvas.toDataURL(
"image/jpeg",
this.jpegQuality
)
// カンマ以降のデータ部分となるbase64をデコード
const base64Data = resizedDataUrl.split(",")[1]
const raw = atob(base64Data)

//デコードしたバイナリをUint8Arrayに入れていく
this.resizedBinary = new Uint8Array(raw.length)
Array.from(raw).forEach(
(_, index) => (this.resizedBinary[index] = raw.charCodeAt(index))
)

resolve()
},
this.options
)
})

const p2 = new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = event => {
// resultはArrayBufferで、このままでは使えないのでUint8Arrayに変換
this.binaryData = new Uint8Array((event as any).target
.result as ArrayBuffer)
resolve()
}
reader.readAsArrayBuffer(this.file)
}).then(() => {
let index = 0

if (!this.isJpeg) {
return
}

// 各segmentを画像データの始まりとなるSOSが出てくるまで探していく
// Exifのsegment以外要らないが、とりあえずどこで出てくるのか分からないので全部取っておく
while (index < this.binaryData.length) {
const marker = [this.binaryData[index], this.binaryData[index + 1]]
const markerSize = [
this.binaryData[index + 2],
this.binaryData[index + 3],
]

switch (marker.toString()) {
//
case [0xff, 0xd8].toString(): // SOI
break
case [0xff, 0xda].toString(): // SOS
return

default:
const [second, first] = markerSize
const size = (second << 8) + first

this.segments.push(
this.binaryData.slice(index, index + size + markerSize.length)
)
index += size
break
}
index += 2 //next
}
})

return Promise.all([p1, p2])
}

get isJpeg() {
return this.binaryData.slice(0, 2).toString() == [0xff, 0xd8].toString()
}

get hasExif() {
return !!this.exifRawData
}

get exifRawData() {
return this.segments.find(
segment =>
Array.from(segment)
.slice(0, 2)
.toString() == [0xff, 0xe1].toString()
)
}

get resizedFile() {
let blob: Blob = new Blob([this.resizedBinary])

if (this.isJpeg && this.hasExif) {
// JPEGでExifがあるなら、SOIの後ろに取っておいたExifを差し込む
const [SOI0, SOI1, ...rest] = Array.from(this.resizedBinary)
const data = [SOI0, SOI1]
.concat(Array.from(this.exifRawData))
.concat(rest)

blob = new Blob([new Uint8Array(data)])
}

return new File([blob], this.file.name, {
type: this.file.type,
lastModified: this.file.lastModified,
})
}
}



参考

https://digitalexploration.wordpress.com/2009/11/17/jpeg-header-definitions/

https://hp.vector.co.jp/authors/VA032610/JPEGFormat/StructureOfJPEG.htm

https://hp.vector.co.jp/authors/VA032610/operation/MessageList.htm

https://beyondjapan.com/blog/2016/11/start-binary-reading-with-jpeg-exif/

https://www.setsuki.com/hsp/ext/jpg.htm

http://elicon.blog57.fc2.com/blog-entry-206.html

https://otounow.jimdo.com/exif%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88/

https://otounow.jimdo.com/exif%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88/app1%E9%A0%98%E5%9F%9F%E3%81%A8ifd%E9%A0%98%E5%9F%9F/