ReactNative(Expo)でImageManipulatorを使用して画像を任意のサイズまで圧縮する

# やりたいこと
- Expoを使ったアプリで、画像をサーバにアップロードする時に任意のファイルサイズまで圧縮したい。
- なぜなら、昨今のスマホで撮影した写真はファイルサイズがエグいため、大きなファイルをそのままアップロードされたらサーバを圧迫してしまうため。

# 使うライブラリ
- 画像の操作のために、Expoでは公式で[ImageManipulator](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/)の利用を推奨しており、このライブラリには圧縮率を指定して画像を圧縮する機能が含まれている。
- 圧縮率は1.0~0.1まで0.1刻みで指定できる。
![スクリーンショット 2022-08-21 14.32.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268579/44389aac-5e00-b962-e704-4e9841b44daa.png)


今回はこのライブラリの圧縮処理を再帰的に呼び出すことで、任意のファイルサイズまで圧縮できるようにすることを目指す。

# 問題点
## その1：SaveOptionsのcompressを用いた圧縮は一度しか機能しない
- 最初は、SaveOptionsのcompressを0.4などに設定して何度も圧縮処理をかければファイルサイズは毎回小さくなるだろうと思っていた。
- しかし、SaveOptionsのcompressを使った圧縮は、オリジナルの画像に対しての一回目しか機能しない。一度圧縮した画像に対して再びSaveOptionsのcompressを使った圧縮を行ってもさらに圧縮されるということはなかった。
- 具体的には以下のようなことをしてもファイルサイズは変わらないということ。

```compress.ts
import ImageManipulator from "expo-image-manipulator";

const imageUri = "圧縮したいローカル画像のURI"

const onceCompressedImage = await ImageManipulator.manipulateAsync(imageUri, [], {
  compress: 0.4,
  format: ImageManipulator.SaveFormat.JPEG,
});

// ↑で圧縮した画像のuriを与えてもう一度圧縮する
const twiceCompressedImage = await ImageManipulator.manipulateAsync(onceCompressedImage.uri, [], {
  compress: 0.4,
  format: ImageManipulator.SaveFormat.JPEG,
});

// 二回圧縮してもonceCompressedImageとtwiceCompressedImageのファイルサイズは同じ
```

## その2: FileSystem.getInfoAsync().sizeで取得できるファイルサイズが正しくない
- Expoではローカルファイルの操作には[FileSystem](https://docs.expo.dev/versions/latest/sdk/filesystem/)を推奨している。
- FileSystemを使うことで、以下のようにローカル画像のファイルサイズを取得できるが、ここで取得したファイルサイズと実際にサーバにアップロードした画像のファイルサイズは異なっている。FileSystemで取得したファイルサイズは実際より小さい。
- そのため、任意のサイズより小さいかをFileSystemを使って検証すると、実際より大きいファイルを許容してしまうことになる。

```FileSize.ts
import FileSystem from "expo-file-system";

const imageUri = "圧縮したいローカル画像のURI"
const fileSize = await FileSystem.getInfoAsync(imageUri).size;
```


# 問題点を考慮した実装
- 以上の問題点を考慮して、以下のように実装を行った。

```compressImage.ts

// 指定したファイルサイズより小さいかをblobから取得したサイズで判定する
const isLessThanTargetSizeMB = async (
  imageUri: string,
  targetSizeMB: number
): Promise<boolean> => {
  const response = await fetch(imageUri);
  const blob = await response.blob();
  const fileSizeByte = blob.size;

  return fileSizeByte / 1024 / 1024 < targetSizeMB;
};

// maximumSizeMBで指定したファイスサイズ（例えば1メガバイト）以下に圧縮する
const compressImage = async (
  imageUri: string,
  maximumSizeMB: number, 
  sizeReduction: boolean = false
): Promise<string | undefined> => {
  // 元々指定したサイズより小さい時はそのままimageUriを返す
  if (await isLessThanTargetSizeMB(imageUri, maximumSizeMB)) return imageUri;

  
  const compressedImage = await ImageManipulator.manipulateAsync(imageUri, [], {
    compress: 0.4,
    format: ImageManipulator.SaveFormat.JPEG,
  });

  const reductionRatio = sizeReduction ? 0.8 : 1.0;
  const resizedImage = await ImageManipulator.manipulateAsync(
    compressedImage.uri,
    [
      {
        resize: { ←ファイルの縦横自体をリサイズすることでファイルサイズを圧縮する
          width: compressedImage.width * reductionRatio,
          height: compressedImage.height * reductionRatio,
        },
      },
    ],
    { compress: 0.4, format: ImageManipulator.SaveFormat.JPEG }
  );

  const resizedImageURI = resizedImage.uri;
  // 指定したサイズより小さくなった時はresizedImageUriを返す
  if (await isLessThanTheMB(resizedImageURI, maximumSizeMB)) return resizedImageURI;
  
  // 元々指定したサイズより小さくなってない時には、
  // sizeReductionをtrueに（つまり画像の縦横のリサイズする設定）にして再帰的にcompressImageを呼び出す
  return resizeImage(resizedImageURI, true);
};
```
# まとめ
- ファイルの縦横をリサイズしているあたり力技感があり、ちょっと気になる



Twitterもやってるので、よければフォローお願いします。
→https://twitter.com/ObataGenta