LoginSignup
4
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-11

問題

モバイル端末からなどの画像のアップロードは、そのままの画像サイズだと相当デカイ。そのままサーバーにアップロードするには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/

4
1
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
4
1