このガイドでは、画像のアップロードを受け付け、画像にウォーターマークを重ね、変換後の画像を R2 バケットに保存するアプリを作ります。
Images では、元画像の保存場所を選べます。Images 製品の外、たとえば R2 に保存した画像も変換できます。
ユーザーがアップロードしたメディアを R2 に保存する場合、R2 バケットへアップロードする前に、画像の最適化や加工をしたくなることがあります。
バインディングで Developer Platform のサービスを Worker に接続する方法と、Images API の各種最適化機能の使い方を学びます。
始める前に、次を済ませてください。
- アカウントに Images Paid のサブスクリプションを追加します。これで Images API を Worker にバインドできます。
- 変換後の画像をアップロードする R2 バケット を作成します。
- 新しい Worker プロジェクトを作成します。
初めての場合は、最初の Worker の作り方 を確認してください。
まず、Developer Platform 上の次のリソースをプロジェクトで使えるようにします。
- Images — Worker から直接、画像の変換、リサイズ、エンコードを行います。
- R2 — 変換後の画像を保存するバケットを接続します。
- Assets — ウォーターマークとして使う静的画像にアクセスします。
Wrangler 設定ファイルを編集し、Images、R2、Assets のバインディングを追加します。
{
"images": {
"binding": "IMAGES"
},
"r2_buckets": [
{
"binding": "R2",
"bucket_name": "<BUCKET>"
}
],
"assets": {
"directory": "./<DIRECTORY>",
"binding": "ASSETS"
}
}[images]
binding = "IMAGES"
[[r2_buckets]]
binding = "R2"
bucket_name = "<BUCKET>"
[assets]
directory = "./<DIRECTORY>"
binding = "ASSETS"<BUCKET> は、変換後の画像をアップロードする R2 バケット名に置き換えます。Worker コードでは、このバケットを env.R2 で参照できます。
./<DIRECTORY> は、オーバーレイ画像を置くプロジェクトディレクトリ名に置き換えます。Worker コードでは、これらのアセットを env.ASSETS で参照できます。
アップロードされたすべての画像にウォーターマークを重ねるため、オーバーレイ画像の置き場所が必要です。
プロジェクトのアセットディレクトリを使うと、静的アセットを Worker の一部としてアップロードできます。プロジェクトをデプロイすると、これらのファイルは Worker コードと一緒に、1 回の操作で Cloudflare のインフラにデプロイされます。
Wrangler ファイルを設定したら、指定したディレクトリにオーバーレイ画像をアップロードします。この例のアプリでは、ディレクトリ ./assets にオーバーレイ画像があります。
ユーザーが画像をアップロードできるアプリの画面を作ります。
この例では、フロントエンドは Worker スクリプトから直接レンダリングします。
そのため、アップロードを受け付ける form 要素を含む新しい html 変数を作ります。fetch では、Content-Type: text/html ヘッダー付きの新しい Response を組み立て、静的 HTML サイトをクライアントに返します。
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Upload Image</title>
</head>
<body>
<h1>Upload an image</h1>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required />
<button type="submit">Upload</button>
</form>
</body>
</html>
`;
export default {
async fetch(request, env) {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
// This is called when the user submits the form
}
},
};const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Upload Image</title>
</head>
<body>
<h1>Upload an image</h1>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required />
<button type="submit">Upload</button>
</form>
</body>
</html>
`;
interface Env {
IMAGES: ImagesBinding;
R2: R2Bucket;
ASSETS: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
// This is called when the user submits the form
}
},
} satisfies ExportedHandler<Env>;form を用意したら、アップロードされた画像を変換できるようにします。
この form ではユーザーがディスクから直接アップロードするため、fetch() で URL から画像を取得できません。代わりに、画像本文をバイトのストリームとして扱います。
そのため、form からアップロードファイルを解析し、そのストリームを取得します。
export default {
async fetch(request, env) {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
} catch (err) {
console.log(err.message);
}
}
},
};export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
} catch (err) {
console.log((err as Error).message);
}
}
},
} satisfies ExportedHandler<Env>;アップロードされたすべての画像に対して、次の処理を行います。
- アセットディレクトリに追加したウォーターマークを重ねます。
- ウォーターマーク付きの画像を
AVIFにトランスコードします。これで画像が圧縮され、ファイルサイズが小さくなります。 - 変換後の画像を R2 にアップロードします。
アセットディレクトリからオーバーレイ画像を取得するには、関数 assetUrl を作り、env.ASSETS で watermark.png 画像を取得します。
function assetUrl(request, path) {
const url = new URL(request.url);
url.pathname = path;
return url;
}
export default {
async fetch(request, env) {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
// Fetch image as watermark
const watermarkResponse = await env.ASSETS.fetch(
assetUrl(request, "watermark.png"),
);
const watermarkStream = watermarkResponse.body;
} catch (err) {
console.log(err.message);
}
}
},
};function assetUrl(request: Request, path: string): URL {
const url = new URL(request.url);
url.pathname = path;
return url;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
// Fetch image as watermark
const watermarkResponse = await env.ASSETS.fetch(
assetUrl(request, "watermark.png"),
);
const watermarkStream = watermarkResponse.body;
} catch (err) {
console.log((err as Error).message);
}
}
},
} satisfies ExportedHandler<Env>;Images バインディングは env.IMAGES 経由で操作します。
ここで、画像に対して行いたい最適化をすべて指定します。.draw() 関数でアップロード画像の上にウォーターマークを重ね、.output() で画像を AVIF としてエンコードします。
function assetUrl(request, path) {
const url = new URL(request.url);
url.pathname = path;
return url;
}
export default {
async fetch(request, env) {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
// Fetch image as watermark
const watermarkResponse = await env.ASSETS.fetch(
assetUrl(request, "watermark.png"),
);
const watermarkStream = watermarkResponse.body;
if (!watermarkStream) {
return new Response("Failed to fetch watermark", { status: 500 });
}
// Apply watermark and convert to AVIF
const imageResponse = (
await env.IMAGES.input(fileStream)
// Draw the watermark on top of the image
.draw(
env.IMAGES.input(watermarkStream).transform({
width: 100,
height: 100,
}),
{ bottom: 10, right: 10, opacity: 0.75 },
)
// Output the final image as AVIF
.output({ format: "image/avif" })
).response();
} catch (err) {
console.log(err.message);
}
}
},
};function assetUrl(request: Request, path: string): URL {
const url = new URL(request.url);
url.pathname = path;
return url;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
// Fetch image as watermark
const watermarkResponse = await env.ASSETS.fetch(
assetUrl(request, "watermark.png"),
);
const watermarkStream = watermarkResponse.body;
if (!watermarkStream) {
return new Response("Failed to fetch watermark", { status: 500 });
}
// Apply watermark and convert to AVIF
const imageResponse = (
await env.IMAGES.input(fileStream)
// Draw the watermark on top of the image
.draw(
env.IMAGES.input(watermarkStream).transform({
width: 100,
height: 100,
}),
{ bottom: 10, right: 10, opacity: 0.75 },
)
// Output the final image as AVIF
.output({ format: "image/avif" })
).response();
} catch (err) {
console.log((err as Error).message);
}
}
},
} satisfies ExportedHandler<Env>;変換後の画像を R2 にアップロードします。
fileName 変数を作ると、変換後の画像名を指定できます。この例では、R2 にアップロードする前に、元画像の名前に日付を付けます。
例の完全なコードは次のとおりです。
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Upload Image</title>
</head>
<body>
<h1>Upload an image</h1>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required />
<button type="submit">Upload</button>
</form>
</body>
</html>
`;
function assetUrl(request, path) {
const url = new URL(request.url);
url.pathname = path;
return url;
}
export default {
async fetch(request, env) {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
// Fetch image as watermark
const watermarkResponse = await env.ASSETS.fetch(
assetUrl(request, "watermark.png"),
);
const watermarkStream = watermarkResponse.body;
if (!watermarkStream) {
return new Response("Failed to fetch watermark", { status: 500 });
}
// Apply watermark and convert to AVIF
const imageResponse = (
await env.IMAGES.input(fileStream)
// Draw the watermark on top of the image
.draw(
env.IMAGES.input(watermarkStream).transform({
width: 100,
height: 100,
}),
{ bottom: 10, right: 10, opacity: 0.75 },
)
// Output the final image as AVIF
.output({ format: "image/avif" })
).response();
// Add timestamp to file name
const fileName = `image-${Date.now()}.avif`;
// Upload to R2
await env.R2.put(fileName, imageResponse.body);
return new Response(`Image uploaded successfully as ${fileName}`, {
status: 200,
});
} catch (err) {
console.log(err.message);
return new Response("Internal error", { status: 500 });
}
}
return new Response("Method not allowed", { status: 405 });
},
};interface Env {
IMAGES: ImagesBinding;
R2: R2Bucket;
ASSETS: Fetcher;
}
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Upload Image</title>
</head>
<body>
<h1>Upload an image</h1>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required />
<button type="submit">Upload</button>
</form>
</body>
</html>
`;
function assetUrl(request: Request, path: string): URL {
const url = new URL(request.url);
url.pathname = path;
return url;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "GET") {
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
if (request.method === "POST") {
try {
// Parse form data
const formData = await request.formData();
const file = formData.get("image");
if (!file || typeof file.stream !== "function") {
return new Response("No image file provided", { status: 400 });
}
// Get uploaded image as a readable stream
const fileStream = file.stream();
// Fetch image as watermark
const watermarkResponse = await env.ASSETS.fetch(
assetUrl(request, "watermark.png"),
);
const watermarkStream = watermarkResponse.body;
if (!watermarkStream) {
return new Response("Failed to fetch watermark", { status: 500 });
}
// Apply watermark and convert to AVIF
const imageResponse = (
await env.IMAGES.input(fileStream)
// Draw the watermark on top of the image
.draw(
env.IMAGES.input(watermarkStream).transform({
width: 100,
height: 100,
}),
{ bottom: 10, right: 10, opacity: 0.75 },
)
// Output the final image as AVIF
.output({ format: "image/avif" })
).response();
// Add timestamp to file name
const fileName = `image-${Date.now()}.avif`;
// Upload to R2
await env.R2.put(fileName, imageResponse.body);
return new Response(`Image uploaded successfully as ${fileName}`, {
status: 200,
});
} catch (err) {
console.log((err as Error).message);
return new Response("Internal error", { status: 500 });
}
}
return new Response("Method not allowed", { status: 405 });
},
} satisfies ExportedHandler<Env>;このチュートリアルでは、Worker を Developer Platform 上の各種リソースに接続し、画像アップロードを受け付け、画像を変換し、出力を R2 にアップロードするアプリの作り方を学びました。
次は、変換 URL を設定 して、R2 に保存した画像を動的に最適化できます。