跳转到内容
搜索文档

在上传到 R2 之前转换用户上传的图像

最后更新 查看 MarkdownAgent 设置

在本指南中,您将构建一个接受图像上传、在图像上叠加视觉水印,然后将转换后的图像存储在 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 引用这些资源。

设置 assets 目录

因为我们要为每张上传的图像应用视觉水印,所以需要一个存储叠加图像的位置。

项目的 assets 目录让您将静态资源作为 Worker 的一部分上传。部署项目时,这些上传的文件与 Worker 代码一起在单个操作中部署到 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 获取图像。相反,您将作为字节流操作图像 body。

为此,从 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:转换图像

对于每张上传的图像,您要执行以下操作:

  • 叠加我们添加到 assets 目录的视觉水印。
  • 将图像(带水印)转码为 AVIF。这压缩图像并减小文件大小。
  • 将转换后的图像上传到 R2。

设置叠加图像

要从 assets 目录获取叠加图像,创建 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>;

添加水印并转码图像

您可以通过 env.IMAGES 与 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 中的图像。

这篇文档对您有帮助吗?