画像選択時のプレビュー表示とExif対応のhook版


問題

Javascriptで画像ファイルを選択させた後に、クライアントのみで画像プレビューを出すのは簡単だが、縦横が実際の画像とは違って表示される(orientation)問題がある。

exifのorientationを考慮しないサンプル

PreviewImageTest.tsx
// 選択した画像ファイルを表示するだけ
// これだと画像のorientationを見ていないので、回転のかかった画像では縦横が変に表示される

import React from "react"
interface Props {}

const PreviewImageTest: React.FC<Props> = props => {
  const [url, setUrl] = React.useState(null)

  return (
    <>
      <input
        type="file"
        accept="image/*"
        onChange={({ target: { validity, files } }) => {
          if (validity.valid) {
            // 単純にFileオブジェクトから表示可能なURLを作ってるだけ
            setUrl(URL.createObjectURL(files[0]))
          }
        }}
      />

      {url ? (
        <img
          src={url}
          style={{ width: "100%" }}
          onLoad={() => {
            // メモリ解放
            URL.revokeObjectURL(url)
          }}
        />
      ) : null}
    </>
  )
}
export default PreviewImageTest

解決方法

を使う。

useExifOrientation.tsx
import React from "react"
import loadImage from "blueimp-load-image"
// canvas.toBlobが対応で無い場合はこれが必要
// https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/toBlob
// import "blueimp-canvas-to-blob" SSRでなければここに

interface Props {
  image: File | string
}
const useExifOrientation = ({ image }: Props) => {
  const [src, setSrc] = React.useState<null | string>(null)
  React.useEffect(() => {
    if (typeof image === "string") {
      // 単なるurl文字ならそのまま返す
      setSrc(image)
    }

    // SSRでは[window undefined]問題があるのでここで動的にロード
    // そうでなければファイル上部に通常通り
    // import "blueimp-canvas-to-blob"
    // で問題ない
    import("blueimp-canvas-to-blob").then(() => {
      // blueimp-load-imageの関数[loadImage]を使い、
      loadImage(
        image,
        canvas => {
          // このcallback内でcanvasが返ってくるので
          // toBlobを使ってblobをとり、そのblobからcreateObjectURLでurlを作る
          canvas.toBlob(
            blob => {
              const src = URL.createObjectURL(blob)
              setSrc(src)
            },
            // png画像の場合はblobにするのが遅いので、JPEG表示にする
            "image/jpeg",
            // JPEGの圧縮を利かせる 80%
            0.8
          )
        },
        // orientationを利かせる これだけで縦横がちゃんと表示される
        // orientationを設定すると、cropの指定が必要?みたい
        { orientation: true, crop: false }
      )
    })
  }, [image])

  return src
}
export default useExifOrientation

PreviewImageTest.tsx
import React from "react"
import useExifOrientation from "./useExifOrientation"

interface OrientedImgProps {
  image: File
}
const OrientedImg: React.FC<OrientedImgProps> = ({ image }) => {
  const src = useExifOrientation({ image })
  return (
    <img
      src={src}
      style={{ width: "100%" }}
      onLoad={() => {
        URL.revokeObjectURL(src)
      }}
    />
  )
}

interface Props {}
const PreviewImageTest: React.FC<Props> = props => {
  const [file, setFile] = React.useState<File | null>(null)

  return (
    <>
      <input
        type="file"
        accept="image/*"
        onChange={({ target: { validity, files } }) => {
          if (validity.valid) {
            setFile(files[0])
          }
        }}
      />

      <OrientedImg image={file} />
    </>
  )
}
export default PreviewImageTest