アップロードされた画像のリサイズをJSでおこなう

実務のフロント側で画像のリサイズをおこなう必要があったため忘備録として書くことにしました。

おおまかな実装の流れ

  1. リサイズ関数の実装
    1. Fileオブジェクトを引数としたリサイズ関数の定義
    2. FileReaderオブジェクトを作成
    3. ファイルの読み込み完了時に呼び出される関数の実装
    4. Canvas を用いて画像をリサイズ
  2. 呼び出し元の実装

実装

まずはresizeImageという関数を定義します 以降asyncは動作には必要ないですが、非同期関数だということを明示するキーワードとして記述しています。 ファイルの読み込みや画像リサイズを非同期でおこなうのでPromiseオブジェクトを作成しています。 リサイズされた画像を返り値とします。

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
  }
}

続いて、ファイルを読み込むためのFileReaderオブジェクトを作成し、ファイルの読み込みと、それが完了した時とエラーの時の関数を割り当てています。 readAsDataURLは、データURLとしてfileをBase64エンコードします

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {};
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

reader.onloadに割り当てる関数を実装します。 まずはFileReaderによって読み込んだ画像データ(Base64形式)を画像として処理するためにImgオブジェクトを生成します。 FileReader同様、完了時とエラー時の関数を割り当てます。 img.srcでImgオブジェクトに画像データを読み込ませ、その後onloadにて処理が行われます。

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const img = new Image();
      img.onload = () => {}
      img.onerror = reject;
      img.src = event.target?.result as string;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

Canvas を用いて画像をリサイズします。 新しいcanvas要素を作成し、アスペクト比を維持するため画像のサイズ計算をおこないます。 canvasに描画するために必要なctx(context)を取得し、drawImageで画像を描画します。 最後にcanvas.toBlobでcanvasの内容をBlobオブジェクトとして取得し、Fileオブジェクトに変換します。 resolveを使ってFileオブジェクトを返します。

const resizeImage = async (file: File): Promise<File> => {
  const maxWidth = 1024;
  const maxHeight = 768;

  return new Promise<File>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement("canvas");
        // 画像のサイズ計算
        let width = img.width;
        let height = img.height;
        if (width > height) {
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;

        const ctx = canvas.getContext("2d");
        ctx?.drawImage(img, 0, 0, width, height);

        canvas.toBlob((blob) => {
          if (blob) {
            // BlobをFileオブジェクトに変換して返す
            const resizedFile = new File([blob], file.name, {
              type: file.type,
            });
            resolve(resizedFile);
          } else {
            reject(new Error("canvasからFileへの変換に失敗"));
          }
        }, file.type);
      };
      img.onerror = reject;
      img.src = event.target?.result as string;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

最後に呼び出し側の実装です。 複数のファイルがアップロードされるのも考慮し、Promise.allで実装をしています。 完了すると、アップロード関数にリサイズ画像を渡し完了です。

  const hoge = async (files: File[]) => {
    try {
      const resizedImages = await Promise.all(files.map((file) => resizeImage(file)));
      handleUpload(resizedImages);
    } catch {
      console.log('エラー')
    }
  };