問題
モバイル端末からなどの画像のアップロードは、そのままの画像サイズだと相当デカイ。そのままサーバーにアップロードするには1枚ならまだしも複数枚だと軽く10MBとか超える。
リサイズするのは色々なnpmがあるので簡単にできるが、Exif情報が飛んでしまう。
サーバー側でExif情報が必要な場合(GPS情報とかOrientationとか)困る。
結構色々探してみたが、リサイズしつつExifは残すって言うのをサクッとやってくれるようなnpmが見つからなかった..ので作った。
解決方法
- Jpegである事(細かい事言うとTIFFとかも入るっぽいが...)
- 画像データをバイナリで読み、Exifが入っているか確認(各セグメントを取っていく)
- Exifが入ってたらそのセグメントをざっくり抜き出す
- 抜き出したExifセグメントをリサイズしたバイナリデータに差し込む
ソース
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/