任意のBase64にエンコードされた画像をリクエストで送るだけで、フォーマットとサイズを変換してレスポンスで返してくれるLambda関数を作成する方法です。
こういう汎用的なAPIが欲しくなる場面って割とあると思うので、すぐに作成できるように手順を残します。
例
ユースケース
- 画像形式もサイズもバラバラな画像を、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で動かないので、上記のように追加のコマンドを使用する必要があります。
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.x
と x86_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の型定義です。
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
です。
fit: contain
画像が収まるように、500x500の画像が生成される。
背景色は、background
で指定した色になり、デフォルトでは黒になる。
const option = {
width: 500,
height: 500,
fit: "contain",
}
fit: cover
500x500になるように、画像が切り取られる。
画像の切り取り方は position
で指定した方法になり、デフォルトでは "centre"
が指定される。
const option = {
width: 500,
height: 500,
fit: "cover",
}
fit: fill
縦横比は無視され、500x500の画像が生成される。
const option = {
width: 500,
height: 500,
fit: "fill",
}
fit: inside
長い方の辺が500になるようにリサイズされる。
今回の例では横の方が長いので、500x300の画像が生成される。
const option = {
width: 500,
height: 500,
fit: "inside",
}
fit: outside
短い方の辺が500になるようにリサイズされる。
今回の例では縦の方が短いので、833x500の画像が生成される。
const option = {
width: 500,
height: 500,
fit: "outside",
}
変換にかかる時間
ほとんどの画像は2秒以内にレスポンスが返ります。
あまりにも大きい画像を処理する場合、タイムアウト上限が足りない、もしくはメモリが足りなくなることがあります。
その場合は、Lambdaの設定から上限を引き上げてください。
関数URLを使用せず、API Gatewayを使用する場合は、API Gatewayのタイムアウト上限が30秒なので、ご注意ください。