base64エンコードして画像をアップロード

その他

画像をbase64エンコードしてアップロードするプログラムを実装したい、けど全然わからないや。

saku
saku

大丈夫!わかりやすく解説するね!

画像をAPI経由で送信し、サーバーやS3に保存する処理の実装は実務でもよくあると思います。

方法としては下記の2種類があります。

  • formDataで送る形式
  • base64エンコードして送る形式

今回はbase64エンコード形式で、実装方法を説明します!

formData形式を見たい方はこちら↓

サンプルアプリのgithub

GitHub - shinsaku-sasaki/ImageUpload: 画像アップロード処理のサンプル
画像アップロード処理のサンプル. Contribute to shinsaku-sasaki/ImageUpload development by creating an account on GitHub.

こちらのbase64ディレクトリにコードが入っています。

READMEに起動手順が書いてあるので、クローンしたら起動してみてください。

サンプルアプリの構成

下記の構成で説明します。

  • フロントエンド React
  • バックエンド Express

アプリの説明

画面はこんな感じです。

画像を選択してみるとプレビューが出ます↓

送信ボタンを押すと、アップロードされサーバー側のディレクトリに保存されます。

起動中のExpress側のターミナルに、アップロードしたファイルの情報が文字列で表示されます。

base64エンコードって何?

画像などのバイナリデータを、base64と呼ばれる文字列形式に変換することです。

画像を文字列に変えているんだな、くらいの理解で大丈夫です。

実装手順としてはこんな感じです👇

  • ブラウザ上で、画像をbase64形式の文字列にする。
  • ブラウザからサーバーに、base64形式に変換した文字列形式で画像を送信
  • サーバー側で受け取った文字列を、元の画像データに戻す
  • 画像データをディレクトリやS3に保存する

フロント側のアップロード処理

ImageUploadFormコンポーネントに処理が書いてあります。

import React, { useState, ChangeEvent } from "react";
import "./ImageUploadForm.css";

const ImageUploadForm: React.FC = () => {
  const [selectedImage, setSelectedImage] = useState<string | null>(null);
  const [submissionMessage, setSubmissionMessage] = useState<string | null>(
    null
  );

  const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = () => {
        setSelectedImage(reader.result as string);
      };
      reader.readAsDataURL(file);
    }
  };

  const handleSubmit = () => {
    // サーバーへの送信ロジック
    fetch("http://localhost:3000/upload", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        image: selectedImage,
      }),
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
        setSubmissionMessage("アップロードに成功しました!");
      })
      .catch((error) => {
        console.error(error);
        setSubmissionMessage(
          "アップロードに失敗しました。エラーが発生しました。"
        );
      });
  };

  return (
    <div className="image-upload-form">
      <input
        type="file"
        id="image-input"
        accept="image/*"
        onChange={handleImageChange}
      />
      {selectedImage && (
        <div className="preview-container">
          <img src={selectedImage} alt="Preview" className="preview-image" />
        </div>
      )}
      <button onClick={handleSubmit}>送信</button>
      {submissionMessage && <p>{submissionMessage}</p>}
    </div>
  );
};

export default ImageUploadForm;

handleImageChangeで、アップロードする画像を選択した時の処理、

handleSubmith関数で画像の送信処理をしています。

画像選択時

input type=”file”のonChangeに指定している関数を見てみます。

  const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = () => {
        setSelectedImage(reader.result as string);
      };
      reader.readAsDataURL(file);
    }
  };

引数で渡ってきたイベントハンドラの、e.target.files

に、配列形式で選択した画像の情報が渡ってきます。

試しに、e.target.filesをコンソールで見てみます。

  const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.files);
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = () => {
        setSelectedImage(reader.result as string);
      };
      reader.readAsDataURL(file);
    }
  };

次に、FileReaderでファイルを読み取ります。

読み取り完了時(render.onload)に、render.resultで、読み取り結果を取得しています。

今回はreader.readAsDataURLを使用し、base64エンコードされた文字列形式で読み取ります。

これで画像をbase64文字列に変換することができます。

  const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = () => {
        console.log(reader.result);
        setSelectedImage(reader.result as string);
      };
      reader.readAsDataURL(file);
    }
  };

reader.resultをコンソールに出力してみます。

このように、base64エンコードされた画像のデータが出力されました。

この文字列をステートとして管理し、imgタグのsrc属性に当てることで画像を表示できます。

あとは、この文字列をサーバーに送ります。

画像送信時

送信ボタンを押した時の処理はこちらです↓

  const handleSubmit = () => {
    // サーバーへの送信ロジック
    fetch("http://localhost:3000/upload", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        image: selectedImage,
      }),
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
        setSubmissionMessage("アップロードに成功しました!");
      })
      .catch((error) => {
        console.error(error);
        setSubmissionMessage(
          "アップロードに失敗しました。エラーが発生しました。"
        );
      });
  };

こちらは特別なことはしていません。

文字列になったので、テキスト情報を送るのと同じように送ればOKです。

あとはリクエストヘッダーに

"Content-Type": "application/json"

を指定するのを忘れないようにしてください!

フロントエンドの実装はこれで終わりです!

API側のアップロード処理

主要な処理はこのようになっています。

app.post('/upload', async (req, res) => {
  const base64Data = req.body.image;
  // 先頭のdata:image/png;base64を除いたもの
  const fileData = base64Data.replace(/^data:\w+\/\w+;base64,/, '');
  // 拡張子を取得、/と;の間にある部分をsliceする
  const fileExtension = base64Data
    .toString()
    .slice(base64Data.indexOf('/') + 1, base64Data.indexOf(';'));

  // 注意:実際には送られたファイルの検証をしますが、ここでは省いています。
  const binaryData = Buffer.from(fileData, 'base64');
  fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, binaryData);
  // または下記で、デコードとファイルの書き込みを一発でできる
  // fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, fileData, {
  //   encoding: 'base64'
  // });
  res.json(`アップロード成功!`);
});

まずはリクエストの受け取り部分です👇

const base64Data = req.body.image;

リクエストbodyにあるbase64エンコードされた画像を取得しています。

ブラウザでコンソールに出力したものが、

Express側のコンソールで受け取れました👇

画像形式に復元したいのですが、data:image/png;base64,の部分は不要です。

そこで、正規表現を使ってトリミングしています。

const fileData = base64Data.replace(/^data:\w+\/\w+;base64,/, '');

fileDataにはカンマ以降の値が入ります。

これを

const binaryData = Buffer.from(fileData, 'base64');

で、base64文字列からバイナリデータに復元します。

また、画像にはpngやjpegなどの拡張子があります。

data:image/png;base64の部分から、拡張子「png」を取得するコードがこちら👇

  const fileExtension = base64Data
    .toString()
    .slice(base64Data.indexOf('/') + 1, base64Data.indexOf(';'));

toString()で、TypeScript上で文字列型として扱えるようにし、sliceで「/」と「;」の間を切り取っています。

これで拡張子が判別できました。

あとはアップロードして終わりです。

  fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, binaryData);

ファイル名はrandomUUID()で、ランダムな値にしています。

または、writeFileSyncでエンコード方式を指定し、デコードとファイルの書き込みの2行分を1つにまとめることができます。

  // 2行で書いた場合
  const binaryData = Buffer.from(fileData, 'base64');
  fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, binaryData);

  // または下記で、デコードとファイルの書き込みを一発でできる
  fs.writeFileSync(`./uploads/${randomUUID()}.${fileExtension}`, fileData, {
    encoding: 'base64'
  });

フロントエンドからbase64文字列形式で画像を送信する説明は以上です!

formDataの実装より難しいですが、こちらで送信するケースもあると思います。

どのような処理が必要か、流れを押さえておけば実務では焦る心配はないでしょう👍