INTEGRITY Документация

Трансформация изображений, загружаемых пользователями, перед отправкой в R2

В этом руководстве вы создадите приложение, которое принимает загружаемые изображения, накладывает на них визуальный водяной знак, а затем сохраняет преобразованное изображение в вашем бакете R2.


Images позволяет гибко выбирать, где хранить исходные изображения. Вы можете преобразовывать изображения, хранящиеся за пределами продукта Images, например в R2.

Если вы храните загружаемый пользователями медиаконтент в R2, вам может понадобиться оптимизировать или обработать изображения до их загрузки в бакет R2.

Вы узнаете, как подключать сервисы Developer Platform к Worker с помощью привязок, а также как использовать различные функции оптимизации в Images API.

Предварительные требования

Прежде чем начать, выполните следующее:

Если вы только начинаете, ознакомьтесь с тем, как создать первого Worker.

1: Настройте проект Worker

Для начала настройте проект на использование следующих ресурсов Developer Platform:

Добавьте привязки в конфигурацию 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> именем bucket R2, куда будут загружаться изображения после преобразования. В коде Worker к этому bucket можно обращаться, используя env.R2.

Замените ./<DIRECTORY> именем каталога проекта, где будет храниться накладываемое изображение. В коде Worker к этим ресурсам можно обращаться, используя env.ASSETS.

Настройте каталог assets

Поскольку визуальный водяной знак должен применяться к каждому загружаемому изображению, вам понадобится место для хранения изображения наложения.

Каталог assets вашего проекта позволяет загружать статические ресурсы вместе с Worker. При развёртывании проекта эти файлы вместе с кодом Worker развёртываются в инфраструктуре Cloudflare за одну операцию.

После настройки файла Wrangler загрузите оверлейное изображение в указанный каталог. В нашем примере приложения этот каталог называется ./assets содержит изображение наложения.

2: Создайте фронтенд

Вам потребуется создать интерфейс приложения, который позволит пользователям загружать изображения.

В этом примере фронтенд отрисовывается непосредственно из скрипта Worker.

Для этого создайте новый html переменная, которая содержит form элемент для приёма загружаемых файлов. В fetch, создайте новый Response с Content-Type: text/html заголовок для передачи статического 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: Преобразуйте изображение

Для каждого загруженного изображения нужно выполнить следующие действия:

Настройте изображение для наложения

Чтобы получить изображение наложения из каталога 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>;

Добавление водяного знака и перекодирование изображения

Взаимодействовать с привязкой 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.