LoginSignup
1
2

More than 1 year has passed since last update.

画像を任意のフォーマット&サイズに変換するLambda関数を作成する

Last updated at Posted at 2022-10-16

任意のBase64にエンコードされた画像をリクエストで送るだけで、フォーマットとサイズを変換してレスポンスで返してくれるLambda関数を作成する方法です。

こういう汎用的なAPIが欲しくなる場面って割とあると思うので、すぐに作成できるように手順を残します。

変換前
3ce943ff-a81f-45bd-ab74-6560e0d33faf.jpeg

変換後(100x100に収まるように設定)
4f3654ed-8706-4854-8d5d-255548a126b3.jpeg

ユースケース

  • 画像形式もサイズもバラバラな画像を、512 x 512以下のJPEG画像に変換する
  • 画像処理のために、小さな画像を1000x1000のPNG画像にする
  • 例えば、ブラウザからプロフィール画像をS3や任意のAPIにアップロードする前に、このAPIでファイルを小さくしておく

開発環境

$ node -v
v16.15.1

nodeバージョンが 16 でない場合は、n などを使用してバージョンを 16 にしておいてください。

Lambda関数の作成手順

1. sharp のインストール

適当なプロジェクトを作って、sharpをインストールします。

$ npm init -y
$ SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp

Linux以外でビルドしたsharpはLambdaで動かないので、上記のように追加のコマンドを使用する必要があります。

参考: https://sharp.pixelplumbing.com/install#aws-lambda

2. Lambda Layer の作成

Layer にアップロードする zip ファイルを作成します。

$ mkdir nodejs
$ mv node_modules nodejs
$ zip -r layer.zip nodejs

AWSコンソールで Lambda Layer を作成し、zip ファイルをアップロードします。
この Lambda Layer は、Node.js 16.xx86_64 に互換性を持つように設定します。

3. Lambda関数の作成

AWS コンソールから、Lambda関数を作成します。
ランタイムは Node.js 16.x、 アーキテクチャは x86_64 を指定します。
また、「関数 URL を有効化」 にチェックを入れておきます。

作成したら、先ほど作成した Lambda Layer を紐付けます。
タイムアウトは 1分 に変更します。
メモリは 1024MB くらいにしておくと、3000 x 3000 ぐらいの大きな画像でも生成できます。

4. コードを書き込む

こんな感じでプログラムを設定しておきます。

const sharp = require("sharp");

exports.main = async (event) => {
  const { base64_image, toFormat, ...resizeOption } = JSON.parse(event.body);
  const base64 = base64_image.replace(/data:.+;base64,/, "");

  const resizedImage = sharp(Buffer.from(base64, "base64"))
    .resize(resizeOption)
    .toFormat(toFormat);

  const resizedImageBuf = await resizedImage.toBuffer();
  const resizedBase64 = resizedImageBuf.toString("base64");

  return {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Methods": "POST",
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Origin": "*",
    },
    body: JSON.stringify({ base64_image: resizedBase64 }),
  };
};

実際に使ってみる

React/TypeScriptから利用する例です。
ファイルを選択すると、800x800に収まるように変換された画像のBase64をコンソールに出力します。
長いですが、半分はSharpの型定義です。

some_page.tsx
type Input = {
  base64_image: string;
  toFormat:
    | "avif"
    | "dz"
    | "fits"
    | "gif"
    | "heif"
    | "input"
    | "jpeg"
    | "jpg"
    | "magick"
    | "openslide"
    | "pdf"
    | "png"
    | "ppm"
    | "raw"
    | "svg"
    | "tiff"
    | "tif"
    | "v"
    | "webp";
  width?: number | undefined;
  height?: number | undefined;
  // 指定した寸法に合わせてどうやって画像をリサイズするか。デフォルトでcover。
  fit?: "contain" | "cover" | "fill" | "inside" | "outside" | undefined;
  // fitがcoverかcontainのときにどこに画像を配置するか。
  position?: number | string | undefined;
  // fitにcontainを指定したときの背景色。デフォルトで#000。
  background?: string | undefined;
  // 画像縮小に使用するカーネル
  kernel?:
    | "nearest"
    | "cubic"
    | "mitchell"
    | "lanczos2"
    | "lanczos3"
    | undefined;
  // 画像の解像度を上げないようにする。
  withoutEnlargement?: boolean | undefined;
  // 画像の解像度を下げないようにする。
  withoutReduction?: boolean | undefined;
  // JPEGとWebPのシュリンクオンロード機能を使用する。画像によってはモアレが発生する可能性あり。
  fastShrinkOnLoad?: boolean | undefined;
};

const convert = async (input: Input): Promise<{ base64_image: string }> => {
  const response: Input = await fetch(
    "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/", // 関数URL
    {
      body: JSON.stringify(input),
      mode: "cors",
      method: "POST",
    }
  ).then((res) => res.json());
  return response;
};

const uploadToClient = (event: any) => {
  if (event.target.files[0]) {
    const reader = new FileReader();
    const file = (event.target.files as FileList)[0];

    reader.onload = async (event) => {
      const base64_image = (event.currentTarget as any).result;
      const { base64_image: resizedBase64 } = await convert({
        base64_image,
        width: 800,
        height: 800,
        fit: "inside",
        withoutEnlargement: true,
        toFormat: "jpg",
      });
      console.log(resizedBase64);
    };
    reader.readAsDataURL(file);
  }
};

const SomeComponent = () => <input type="file" onChange={uploadToClient} />;
export default SomeComponent;

fitプロパティの違いについて

言葉で説明するより、見た方が早いと思うので、実際にやってみます。
個人的に一番よく使うのは inside です。

元画像(1000x600)
ab898f5f-617b-4256-aeac-c82da0f3b9b1.jpeg

fit: contain

画像が収まるように、500x500の画像が生成される。
背景色は、background で指定した色になり、デフォルトでは黒になる。

const option = {
    width: 500,
    height: 500,
    fit: "contain",
}

77bb548d-6e49-4cdf-bacf-c0fb0753c470.jpeg

fit: cover

500x500になるように、画像が切り取られる。
画像の切り取り方は position で指定した方法になり、デフォルトでは "centre" が指定される。

const option = {
    width: 500,
    height: 500,
    fit: "cover",
}

cd32fe0d-cea7-476a-b445-9c43c5bc0c17.jpeg

fit: fill

縦横比は無視され、500x500の画像が生成される。

const option = {
    width: 500,
    height: 500,
    fit: "fill",
}

7d81dc26-e5d7-45eb-9497-9359e5b99769.jpeg

fit: inside

長い方の辺が500になるようにリサイズされる。
今回の例では横の方が長いので、500x300の画像が生成される。

const option = {
    width: 500,
    height: 500,
    fit: "inside",
}

4b6a1218-b030-4d00-a8b7-afe56ee57055.jpeg

fit: outside

短い方の辺が500になるようにリサイズされる。
今回の例では縦の方が短いので、833x500の画像が生成される。

const option = {
    width: 500,
    height: 500,
    fit: "outside",
}

e4d1477c-9c40-44bb-85c7-22a0b63e8d23.jpeg

変換にかかる時間

ほとんどの画像は2秒以内にレスポンスが返ります。
あまりにも大きい画像を処理する場合、タイムアウト上限が足りない、もしくはメモリが足りなくなることがあります。
その場合は、Lambdaの設定から上限を引き上げてください。
関数URLを使用せず、API Gatewayを使用する場合は、API Gatewayのタイムアウト上限が30秒なので、ご注意ください。

1
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2