Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

ユーザーアップロード画像を変換してから R2 にアップロードする

最終更新 Markdown で表示Agent セットアップ

このガイドでは、画像のアップロードを受け付け、画像にウォーターマークを重ね、変換後の画像を R2 バケットに保存するアプリを作ります。


Images では、元画像の保存場所を選べます。Images 製品の外、たとえば R2 に保存した画像も変換できます。

ユーザーがアップロードしたメディアを R2 に保存する場合、R2 バケットへアップロードする前に、画像の最適化や加工をしたくなることがあります。

バインディングで Developer Platform のサービスを Worker に接続する方法と、Images API の各種最適化機能の使い方を学びます。

前提条件

始める前に、次を済ませてください。

  • アカウントに Images Paid のサブスクリプションを追加します。これで Images API を Worker にバインドできます。
  • 変換後の画像をアップロードする R2 バケット を作成します。
  • 新しい Worker プロジェクトを作成します。

初めての場合は、最初の Worker の作り方 を確認してください。

1: Worker プロジェクトをセットアップする

まず、Developer Platform 上の次のリソースをプロジェクトで使えるようにします。

  • Images — Worker から直接、画像の変換、リサイズ、エンコードを行います。
  • R2 — 変換後の画像を保存するバケットを接続します。
  • Assets — ウォーターマークとして使う静的画像にアクセスします。

Wrangler 設定にバインディングを追加する

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 にオーバーレイ画像があります。

2: フロントエンドを作る

ユーザーが画像をアップロードできるアプリの画面を作ります。

この例では、フロントエンドは 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>;

3: アップロードされた画像を読み取る

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>;

4: 画像を変換する

アップロードされたすべての画像に対して、次の処理を行います。

  • アセットディレクトリに追加したウォーターマークを重ねます。
  • ウォーターマーク付きの画像を AVIF にトランスコードします。これで画像が圧縮され、ファイルサイズが小さくなります。
  • 変換後の画像を R2 にアップロードします。

オーバーレイ画像を用意する

アセットディレクトリからオーバーレイ画像を取得するには、関数 assetUrl を作り、env.ASSETSwatermark.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>;

5: R2 にアップロードする

変換後の画像を 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 に保存した画像を動的に最適化できます。

役に立ちましたか?