画像をCanvasでリサイズしてbase64に変換する

# はじめに

アップロードした画像をHTML5 Canvasに描画し、特定の幅以内にリサイズしてからbase64に変換する方法をまとめていきます。

今回行うことは、

- 画像サイズが20MB以内か確認
- 画像の縦横幅が375px以上か確認
- 指定した幅より画像が大きい場合はアスペクト比を保ったままリサイズ
- 画像データをbase64に変換

です。

# 最終的なコード

まずは、最終的なコードを貼っておきます。[CodePen](https://codepen.io/Masaki_Yamanaka/pen/KKXpdBY)にも公開しているので、動作確認などで気になる方は使ってみてください。

```jsx
const inputElement = document.getElementById('input') as HTMLInputElement;

/**
 * Canvasの描画して画像をリサイズする
 * @param {object} image - img要素
 * @param {number} limitWidth - 最大横幅
 * @param {number} limitHeight - 最大縦幅
 * @returns {string} - base64
 */
export const resizeImage = async (
  image: HTMLImageElement,
  limitWidth: number,
  limitHeight: number
) => {
  const aspectRatio = image.width / image.height;
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d')!;
  let canvasWidth;
  let canvasHeight;

  if (image.width > limitWidth || image.height > limitHeight) {
    // 指定サイズよりも画像が大きい場合はトリミングする
    if (aspectRatio > 1) {
      // 横長画像の場合
      canvasWidth = limitWidth;
      canvasHeight = limitHeight * (image.height / image.width);
    } else {
      // 縦長画像の場合
      canvasWidth = limitWidth * (image.width / image.height);
      canvasHeight = limitHeight;
    }
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
  } else {
    // 指定サイズ内の場合
    canvas.width = image.width;
    canvas.height = image.height;
    canvasWidth = image.width;
    canvasHeight = image.height;
  }
  ctx.drawImage(
    image,
    0,
    0,
    image.width,
    image.height,
    0,
    0,
    canvasWidth,
    canvasHeight
  );

  return await canvas.toDataURL('image/jpeg', 0.85);
};

/**
 * 画像バリデーションとimg要素作成
 * @param {object} - ファイルオブジェクト
 * @returns {object} - HTMLImageElement
 */
const validateImage = (file: File) => {
  return new Promise((resolve) => {
    if (file.size / 1000000 <= 20) {
      const reader = new FileReader();
      reader.onload = (e) => {
        if (e.target === null) return;
        if (typeof e.target.result !== 'string') return;
        const image = new Image();
        image.src = e.target.result;
        image.onload = () => {
          if (image.width >= 375 && image.height >= 375) {
            resolve(image);
          } else {
            // 画像が375px未満の場合
            console.log('375px以上の画像を選択してください。');
          }
        };
      };
      reader.readAsDataURL(file);
    } else {
      // 画像が20MB以上の場合
      console.log('20MB以内の画像を選択してください。');
    }
  });
};

/**
 * ファイル選択時の変更イベント
 * @param {object} - イベントオブジェクト
 */
const handleFiles = async (e: Event) => {
  if (!(e.target instanceof HTMLInputElement)) return;
  const file = e.target.files;
  if (file === null) return;

  const image = (await validateImage(file[0])) as HTMLImageElement;
  console.log(image);
  if (image) {
    const base64Image = await resizeImage(image, 1500, 1500);
  }
};

inputElement.addEventListener('change', handleFiles);
```

# 選択した画像のファイルオブジェクトを取得

まずは、HTMLにinputタグを記述し、idにinputを指定します。

```jsx
<input type="file" id="input">
```

次に、TypescriptでgetElementByIdを用いてHTMLInputElementを変数に格納し、addEventListenerで変更検知のイベントリスナー（`handleFiles`）を登録します。

handleFilesでは、Typescriptエラーを回避するよう諸々の条件分岐を行い、最終的にファイルオブジェクト（`file[0]`）をvalidateImageに渡しています。

```jsx
const inputElement = document.getElementById('input') as HTMLInputElement;

/**
 * ファイル選択時の変更イベント
 * @param {object} - イベントオブジェクト
 */
const handleFiles = async (e: Event) => {
  if (!(e.target instanceof HTMLInputElement)) return;
  const file = e.target.files;
  if (file === null) return;

  const image = (await validateImage(file[0])) as HTMLImageElement;
};

inputElement.addEventListener('change', handleFiles);
```

# 画像バリデーションとimg要素作成

validateImageでは、画像のバリデーションとimg要素の作成を行なっています。ファイルオブジェクトを読み込んでimg要素を作成するためには時間がかかるため、Promiseを返すようにします。

```jsx
/**
 * 画像バリデーションとimg要素作成
 * @param {object} - ファイルオブジェクト
 * @returns {object} - HTMLImageElement
 */
const validateImage = (file: File) => {
  return new Promise((resolve) => {
    if (file.size / 1000000 <= 20) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const image = new Image();
        if (e.target === null) return;
        if (typeof e.target.result !== 'string') return;
        image.src = e.target.result;
        image.onload = () => {
          if (image.width >= 375 && image.height >= 375) {
            resolve(image);
          } else {
            // 画像が375px未満の場合
            console.log('375px以上の画像を選択してください。');
          }
        };
      };
      reader.readAsDataURL(file);
    } else {
      // 画像が20MB以上の場合
      console.log('20MB以内の画像を選択してください。');
    }
  });
};
```

ここで

- 画像サイズは20MB以内であるか
- 画像の横幅は375px以上、かつ縦幅が375px以上か

のバリデーションを行なっていきます。まずは、ファイルオブジェクトのsizeプロパティから画像サイズを取得し、20MBよりも大きい場合はconsoleログを出してあげます。

```jsx
// バイト単位のためメガバイトに変換
if (file.size / 1000000 <= 20) {
 ...
} else {
	// 画像が20MB以上の場合
  console.log('20MB以内の画像を選択してください。');
}
```

画像サイズが20MB以内の場合、FileReaderを用いてファイルオブジェクトをbase64 urlに変換します。reader.onloadは読み込みの成功と失敗両方で発火し、e.targetがnullの可能性があるので、その場合はreturnしています。

また、e.target.resultは

```jsx
string | ArrayBuffer | null
```

のいずれかのため、期待するstring以外の場合はreturnしています。

```jsx
const reader = new FileReader();
reader.onload = (e) => {
	if (e.target === null) return;
  if (typeof e.target.result !== 'string') return;
}
reader.readAsDataURL(file);
```

次にFileReaderから取得したbase64 urlをimg要素のsrcに追加していきます。

読み込みが完了したタイミングで、画像の縦幅と横幅を取得し、375未満の場合はconsoleログを出してあげます。375以上ある場合はresolveでimg要素を返してあげましょう。

```jsx
const image = new Image();
image.src = e.target.result;
image.onload = () => {
	if (image.width >= 375 && image.height >= 375) {
		resolve(image);
	} else {
		// 画像が375px未満の場合
		console.log('375px以上の画像を選択してください。');
  }
};
```

# 画像のリサイズ

ここから、いよいよ本題の画像のリサイズを行なっていきます。
validateImageの返り値で受け取ったimg要素を、resizeImageにリサイズする最大横幅、最大縦幅と一緒にを渡します。今回は1500px以内に収まるようにしてあげます。

```jsx
const handleFiles = async (e: Event) => {
 ...
  const image = (await validateImage(file[0])) as HTMLImageElement;
  if (image) {
    const base64Image = await resizeImage(image, 1500, 1500);
    console.log(base64Image);
  }
};
```

resizeImageの中身はこんな感じです。

```jsx
/**
 * Canvasの描画して画像をリサイズする
 * @param {object} image - img要素
 * @param {number} limitWidth - 最大横幅
 * @param {number} limitHeight - 最大縦幅
 * @returns {string} - base64
 */
const resizeImage = async (
  image: HTMLImageElement,
  limitWidth: number,
  limitHeight: number
) => {
  const aspectRatio = image.width / image.height;
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d')!;
  let canvasWidth;
  let canvasHeight;

  if (image.width > limitWidth || image.height > limitHeight) {
    // 指定サイズよりも画像が大きい場合はトリミングする
    if (aspectRatio > 1) {
      // 横長画像の場合
      canvasWidth = limitWidth;
      canvasHeight = limitHeight * (image.height / image.width);
    } else {
      // 縦長画像の場合
      canvasWidth = limitWidth * (image.width / image.height);
      canvasHeight = limitHeight;
    }
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
  } else {
    // 指定サイズ内の場合
    canvas.width = image.width;
    canvas.height = image.height;
    canvasWidth = image.width;
    canvasHeight = image.height;
  }
  ctx.drawImage(
    image,
    0,
    0,
    image.width,
    image.height,
    0,
    0,
    canvasWidth,
    canvasHeight
  );

  return await canvas.toDataURL('image/jpeg', 0.85);
};
```

resizeImageでは、まず`createElement` を用いてCanvas要素を作成します。このCanvasの縦横幅をリサイズしたい幅にして、`drawImage`で画像を描画していきます。

```jsx
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
```

画像の幅が指定した最大縦横幅よりも大きい場合はトリミング作業を行い、最大縦横幅以内の場合は、Canvasの幅と画像の幅を合わせていきます。`canvas.width, canvas.height`はCanvasそのものの幅、`canvasWidth, canvasHeight`は後でCanvasのどの範囲をbase64に変換するか指定する際に使います。

```jsx
if (image.width > limitWidth || image.height > limitHeight) {
	// 指定サイズよりも画像が大きい場合はトリミングする
	...
} else {
	// 指定サイズ内の場合
	canvas.width = image.width;
	canvas.height = image.height;
	canvasWidth = image.width;
	canvasHeight = image.height;
}
```

指定サイズよりも大きい場合はアスペクト比を保ったままリサイズします。

```jsx
const aspectRatio = image.width / image.height;
// 指定サイズよりも画像が大きい場合はトリミングする
if (aspectRatio > 1) {
  // 横長画像の場合
  canvasWidth = limitWidth;
  canvasHeight = limitHeight * (image.height / image.width);
} else {
  // 縦長画像の場合
  canvasWidth = limitWidth * (image.width / image.height);
  canvasHeight = limitHeight;
}
canvas.width = canvasWidth;
canvas.height = canvasHeight;
```

最後にdrawImageでCanvasに描画していきましょう。drawImageは、9個の引数を受け取ることが出来ます。第2~5引数は元画像の使用する範囲を指定できますが、今回は画像の全範囲を使用するので`(0, 0, image.width,  image.height)`としています。第6~9引数で描画する座標を指定してできるので、Canvas全体を指定します。

描画が完了すれば、`toDataURL`でCanvasデータをbase64に変換します。第2引数のencoderOptionsで0から1の間で画質を設定できます(デフォルトは0.92)。

```jsx
  ctx.drawImage(
    image,
    0,
    0,
    image.width,
    image.height,
    0,
    0,
    canvasWidth,
    canvasHeight
  );

  return await canvas.toDataURL('image/jpeg', 0.85);

```

これでbase64の画像データを受け取ることができました。

```jsx
const base64Image = await resizeImage(image, 1500, 1500);
console.log(base64Image);
```

consoleログにbase64の文字列が出れば成功です。

![スクリーンショット_2021-12-11_16.32.46.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349238/a0c0e475-7f3b-0270-3fe5-253a8338a3bc.jpeg)

# おわりに

アップロードした画像をHTML5 Canvasに描画し、クライアント側でリサイズする処理についてまとめていきました。画像サイズや画像幅のバリデーションもかけれるので、規定の条件で画像をサーバーへ送信することが可能になります。