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

[![](https://codesandbox.io/static/img/play-codesandbox.svg)][1]

# 問題

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

リサイズするのは色々なnpmがあるので簡単にできるが、Exif情報が飛んでしまう。
サーバー側でExif情報が必要な場合（GPS情報とかOrientationとか）困る。

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

[![](https://codesandbox.io/static/img/play-codesandbox.svg)][1]

1. ファイルを選択して
2. consoleを見てみる
3. 表示されている画像を右クリックで保存  ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16757/b1e5b44b-a48e-6427-f853-2eb7d170fdd9.png)
4. 画像の情報を見て、exifが残ってるか確認  ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16757/dcf9bcbd-5f8e-6106-1df6-63993708b144.png)


# 解決方法

![](https://hp.vector.co.jp/authors/VA032610/JPEGFormat/FrameStructure.gif)
1. Jpegである事（細かい事言うとTIFFとかも入るっぽいが...）
2. 画像データをバイナリで読み、Exifが入っているか確認（各セグメントを取っていく）
3. Exifが入ってたらそのセグメントをざっくり抜き出す
4. 抜き出したExifセグメントをリサイズしたバイナリデータに差し込む


# ソース

```ts: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/

[1]: https://codesandbox.io/s/charming-cherry-rgsqo?fontsize=14