← Images / images / tutorials
Трансформация изображений, загружаемых пользователями, перед отправкой в R2
В этом руководстве вы создадите приложение, которое принимает загружаемые изображения, накладывает на них визуальный водяной знак, а затем сохраняет преобразованное изображение в вашем бакете R2.
Images позволяет гибко выбирать, где хранить исходные изображения. Вы можете преобразовывать изображения, хранящиеся за пределами продукта Images, например в R2.
Если вы храните загружаемый пользователями медиаконтент в R2, вам может понадобиться оптимизировать или обработать изображения до их загрузки в бакет R2.
Вы узнаете, как подключать сервисы Developer Platform к Worker с помощью привязок, а также как использовать различные функции оптимизации в Images API.
Предварительные требования
Прежде чем начать, выполните следующее:
- Добавьте Images Paid подписку к своему аккаунту. Это позволяет привязать Images API к Worker.
- Создайте Бакет R2, куда будут загружены преобразованные изображения.
- Создайте новый проект Worker.
Если вы только начинаете, ознакомьтесь с тем, как создать первого Worker.
1: Настройте проект Worker
Для начала настройте проект на использование следующих ресурсов Developer Platform:
- Изображения чтобы преобразовывать, изменять размер и кодировать изображения напрямую из своего Worker.
- R2 чтобы подключить bucket для хранения преобразованных изображений.
- 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> именем 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.
- Транскодируйте изображение (вместе с водяным знаком) в
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>;Добавление водяного знака и перекодирование изображения
Взаимодействовать с привязкой 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.