ReactでImageの傾きを正常に戻すHocを制作

iPhoneやAndroidなどの写真で撮った画像をサイトにアップロードするとその画像が傾いてしまうことがあります。
それはImageのExifのOrientationの数値が2〜8だと上下左右に反転してしまうためです。
(ググるとその現象の対応がたくさん出てきますね)

それをReactのHocを使って直して最終的にはこのComponent指定にします。

``` javascript:Sample
<Image src="/images/sample.jpg" alt="サンプル" />
```

src属性に画像pathを入れるだけで傾きが正常の画像が表示される仕組みを作ります。

## 方法

- パッケージ「blueimp-load-image」で画像のOrientationを調べ、画像の傾きがあれば修正するために画像をCanvasに変換します
- Canvasにした画像をバイナリURLとして作成します
- バイナリURLはUnMountしたときにRevoke(取り消す)します

※記述量を減らすためにTypeScript非対応となります

### パッケージ「blueimp-load-image」で画像のOrientationを調べる

- `blueimp-load-image`の使い方については公式サイトを参照してください
- `getImageInfo`の引数srcはImageの画像Pathになります
- ImageのExif情報が取得できて、かつOrientationの数値が2以上だったらOrientationの値と画像をCanvasにして返します

``` javascript:GetImageInfo.js
import loadImage from 'blueimp-load-image';

/**
 * 画像のEXIF情報があるか判定、あればOrientationとCanvasを返す
 * Orientationが0 or 1の場合は何も返さない
 * @param src
 */
const getImageInfo = async (src) => {
  let getImageInfo = {
    orientation: undefined,
    canvas: undefined,
  };

  try {
    const imageInfo = await new Promise((resolve, reject) => {
      loadImage(src, (img, data) => {
        // エラー処理
        if (img.type === 'error') {
          reject(`Error loading image ${src}`);
        }
        // EXIFがあればOrientation取得
        let imageData;
        if (data.exif.get) {
          imageData = {
            orientation: data.exif.get('Orientation'),
            canvas: img,
          };
        }
        resolve(imageData);
      },
      { orientation: true }
    });

    if (imageInfo.orientation) {
      const { orientation, canvas } = imageInfo;
      if (canvas && orientation > 1) {
        getImageInfo = {
          orientation,
          canvas,
        };
      }
    }
    return getImageInfo;
  } catch (e) {
    return getImageInfo;
  }
};

export default getImageInfo;
```

### 傾きを正常に修正する処理

- 上記のファイルでOrientaionの数値が`2〜8`だった場合、その数値を元に画像を修正します
- 数値によって反転が違うのでそれぞれにCanvasのtransformを使用して傾きを修正します
- 修正したCanvasをバイナリURLに変換するファイルに渡します


``` javascript:RepairOrientationImage.js
import canvasUtils from '@/helpers/image/CanvasUtils';

/**
 * 傾いた画像を補正して返却する
 * @param orientation
 * @param canvas
 */
const repairOrientationImage = async ({
  orientation,
  canvas,
}) => {
  let dataURL = '';
  try {
    if (canvas) {
      const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
      switch (orientation) {
        case 2: {
          ctx.transform(-1, 0, 0, 1, canvas.width, 0);
          break;
        }
        case 3: {
          ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height);
          break;
        }
        case 4: {
          ctx.transform(1, 0, 0, -1, 0, canvas.height);
          break;
        }
        case 5: {
          ctx.transform(0, -1, -1, 0, 0, 0);
          break;
        }
        case 6: {
          ctx.transform(0, 1, 1, 0, canvas.height, 0);
          break;
        }
        case 7: {
          ctx.transform(0, -1, -1, 0, canvas.height, canvas.width);
          break;
        }
        case 8: {
          ctx.transform(0, 1, 1, 0, 0, canvas.width);
          break;
        }
      }
      ctx.drawImage(canvas, 0, 0);
      dataURL = await canvasUtils.createObjectURL(canvas);
    }
    return dataURL;
  } catch (e) {
    return dataURL;
  }
};
 
export default repairOrientationImage;
```

### バイナリURLの作成とRevoke処理

- MDNの[サイト](HTMLCanvasElement.toBlob())を参考にCanvasをバイナリURLにする、Revokeする処理を記述
- IE、Edgeの場合でバイナリ作成を分岐

``` javascript:CanvasUtils.js
class CanvasUtils {
  createObjectURL = async (canvas) => {
    // Chrome,SafariなどのモダンブラウザでObjectURL生成
    if (canvas.toBlob) {
      return await new Promise((resolve) => {
        canvas.toBlob((blob: object | null) => {
          const objectURL = URL.createObjectURL(blob);
          resolve(objectURL);
        });
      });
      // IE、EdgeのブラウザでObjectURL生成
    } else if (canvas.msToBlob) {
      const blob = canvas.msToBlob();
      return URL.createObjectURL(blob);
    } else {
      return canvas.toDataURL();
    }
  };
  revokeObjectURL = (url: string) => {
    URL.revokeObjectURL(url);
  };
}

const canvasUtils = new CanvasUtils();
export default canvasUtils;
```

### Hocを作成

- 上記で作成した3つのファイルを読み込む
- useEffectでUnmount時の処理をします
- useStateで画像の傾きを修正したバイナリURL．もしくは傾きがない画像pathをsetします

``` javascript:ImageHoc
import React, { FC, useEffect, useState } from 'react';
import repairOrientationImage from 'RepairOrientationImage';
import getImageInfo from 'GetImageInfo';
import canvasUtils from 'CanvasUtils';

const ImageHoc = (FunctionComponent) => {
  const ImageWrapper = (props) => {
    const { src, ...rest } = props;
    const [imagePath, setImagePath] = useState('');
    
    useEffect(() => {
      let objectURL;
      const checkImage = async () => {
        const checkedImage = await getImageInfo(src);
        if (checkedImage.orientation) {
          objectURL = await repairOrientationImage(checkedImage);
          setImagePath(objectURL);
        } else {
          setImagePath(src);
        }
      };
      checkImage();
      return () => {
        canvasUtils.revokeObjectURL(objectURL);
      };
    }, [src]);

    return <FunctionComponent src={imagePath} {...rest} />;
  };
  return ImageWrapper;
};

export default ImageHoc;
```

### ImageComponentを作成

- 上記で作成したHocの`<FunctionComponent src={imagePath} {...rest} />`にあたるComponentになります

``` javascript:Image.js
import React, { FC, useMemo } from 'react';
import compose from 'recompose/compose';
import ImageHoc from 'ImageHoc';

const ImageComponent = ({src, alt, className }) => (
  <img src={src} alt={alt} className={className} />
);

const enhance = compose(ImageHoc);
const Image = enhance(ImageComponent);

export default Image;
```

以上です。
これで最初に書いたSampleの記述をすれば傾きがあった場合の対応が可能となります。