Processamento de imagens com Claude Code: Sharp, Canvas, WebP/AVIF e validação
Implemente imagens seguras com Claude Code: Sharp, Canvas, EXIF, WebP/AVIF, validação, jobs e testes.
Processamento de imagens parece simples em um ticket: o usuário envia uma foto, a aplicação cria miniaturas e a página fica mais rápida. Em produção, porém, isso vira uma fronteira técnica completa. Você precisa validar upload, evitar nomes de arquivo inseguros, remover EXIF, redimensionar, comprimir, escolher WebP ou AVIF, mover tarefas pesadas para background, proteger privacidade e escrever testes.
Claude Code ajuda porque a mudança passa por vários arquivos: política de upload, endpoint, módulo de otimização, resize no navegador, job e testes. O risco é pedir de modo vago. Um prompt como “otimize imagens” pode gerar código que confia em file.type, publica o nome original, mantém metadados, converte tudo para AVIF no request e nunca testa foto de celular rotacionada.
Use fontes primárias na revisão: documentação do Claude Code, Sharp resize API, Sharp output API, MDN File API, MDN Canvas toBlob e OWASP File Upload Cheat Sheet. Para tarefas pesadas no navegador, veja também o guia interno Web Worker com Claude Code.
Defina a fronteira
Não comece pelo formato. Primeiro defina qual camada faz cada coisa.
| Local | Bom uso | Evite |
|---|---|---|
| Navegador | Preview, resize leve, reduzir upload | Validação confiável, AVIF em massa, decisão de privacidade |
| Servidor síncrono | MIME, magic bytes, dimensões, EXIF, miniatura pequena | Muitas variações, AVIF pesado |
| Job em background | Catálogo, CMS, migração de imagens antigas | Resposta imediata de upload |
O padrão prático é: o navegador melhora a experiência, o servidor valida de novo e as variantes caras vão para uma fila. accept="image/*" e file.type ajudam a UI, mas não são fronteira de segurança.
flowchart LR
Browser["Browser preview / optional resize"]
Upload["Upload endpoint"]
Validate["Magic bytes, size, dimensions"]
Store["Private raw storage"]
Job["Background variants"]
Public["Public WebP/JPEG/AVIF"]
Browser --> Upload
Upload --> Validate
Validate --> Store
Store --> Job
Job --> Public
No prompt para Claude Code, escreva as regras: validar MIME e magic bytes no servidor, nunca usar nome original em URL pública, aplicar rotate(), não chamar .withMetadata() sem motivo claro e deixar AVIF como opção. Isso reduz código plausível, mas inseguro.
Casos reais de produto
O primeiro caso é ecommerce ou marketplace. Vendedores sobem fotos grandes do celular. O produto precisa de miniatura quadrada, imagem de card, imagem de detalhe e às vezes imagem social. Compressão demais prejudica confiança, porque o comprador quer ver textura, cor, etiqueta e condição. WebP é um bom começo; AVIF deve entrar depois de medir imagens reais.
O segundo caso é avatar ou foto de equipe. O que importa é crop quadrado, URL segura e privacidade. Um arquivo chamado cliente-contrato-final.png não pode virar caminho público. Mesmo que o navegador tenha reduzido a imagem, o servidor deve produzir saída sem EXIF.
O terceiro caso são screenshots de blog, help center ou curso. Texto legível vale mais que economia máxima. Uma captura que economiza 80 KB mas borra o nome do botão falhou. Se imagens fazem parte de documentos, leia também geração de PDF com Claude Code.
O quarto caso são anexos privados de SaaS: notas fiscais, imagens de verificação, capturas de suporte ou documentos de admin. Eles não pertencem a public/uploads. Precisam de storage privado, autorização, regra de exclusão e logs de auditoria.
Instalação
Os exemplos assumem Node.js 20 ou superior. Você pode adaptar os módulos para Next.js, Express, Hono, Astro API routes ou um worker de fila.
npm i sharp file-type p-limit
npm i -D tsx typescript @types/node
mkdir -p src public/uploads
Adicione um comando específico para rodar testes em CI.
{
"scripts": {
"test:images": "node --import tsx --test src/**/*.test.ts"
}
}
Validação de upload e nomes seguros
Este módulo é a fronteira de confiança. Ele não acredita na extensão; verifica magic bytes, lê dimensões com Sharp, recusa arquivos grandes e bloqueia imagens animadas ou multipágina neste fluxo.
// src/image-policy.ts
import { randomUUID } from "node:crypto";
import { fileTypeFromBuffer } from "file-type";
import sharp from "sharp";
const MAX_BYTES = 6 * 1024 * 1024;
const MAX_PIXELS = 24_000_000;
const EXTENSION_BY_MIME = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
"image/avif": ".avif",
} as const;
export type MimeType = keyof typeof EXTENSION_BY_MIME;
export type ImageUploadInfo = {
mime: MimeType;
extension: string;
width: number;
height: number;
bytes: number;
originalName: string;
};
function isAllowedMime(mime: string): mime is MimeType {
return mime in EXTENSION_BY_MIME;
}
export async function assertImageUpload(
buffer: Buffer,
originalName = "upload",
): Promise<ImageUploadInfo> {
if (buffer.byteLength === 0) {
throw new Error("Empty file");
}
if (buffer.byteLength > MAX_BYTES) {
throw new Error("Image must be 6 MB or smaller");
}
const detected = await fileTypeFromBuffer(buffer);
if (!detected || !isAllowedMime(detected.mime)) {
throw new Error("Unsupported image type");
}
const metadata = await sharp(buffer, { failOn: "error" }).metadata();
if (!metadata.width || !metadata.height) {
throw new Error("Image dimensions could not be read");
}
if (metadata.pages && metadata.pages > 1) {
throw new Error("Animated images are not allowed here");
}
const pixels = metadata.width * metadata.height;
if (pixels > MAX_PIXELS) {
throw new Error("Image dimensions are too large");
}
return {
mime: detected.mime,
extension: EXTENSION_BY_MIME[detected.mime],
width: metadata.width,
height: metadata.height,
bytes: buffer.byteLength,
originalName,
};
}
export function safeImageName(mime: MimeType): string {
return `${randomUUID()}${EXTENSION_BY_MIME[mime]}`;
}
O nome original pode ser guardado como dado privado de exibição. Ele não deve entrar na URL pública. Nomes aleatórios também evitam colisões e problemas com Unicode.
Sharp para resize, compressão e EXIF
Sharp é uma escolha forte para processamento no servidor Node.js. O detalhe principal é aplicar orientação com rotate() e não preservar metadados sem necessidade. Para imagens públicas, remover metadados costuma ser o padrão mais seguro.
// src/optimize-image.ts
import { mkdir } from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
type Variant = {
kind: "thumb" | "card" | "hero";
width: number;
height?: number;
};
const VARIANTS: Variant[] = [
{ kind: "thumb", width: 320, height: 320 },
{ kind: "card", width: 640 },
{ kind: "hero", width: 1280 },
];
export type OptimizedImage = {
src: string;
width: number;
height: number;
bytes: number;
format: "webp" | "avif";
};
export async function optimizeImage(
buffer: Buffer,
outputDir: string,
baseName: string,
makeAvif = false,
): Promise<OptimizedImage[]> {
await mkdir(outputDir, { recursive: true });
const results: OptimizedImage[] = [];
for (const variant of VARIANTS) {
const resized = sharp(buffer)
.rotate()
.resize({
width: variant.width,
height: variant.height,
fit: variant.height ? "cover" : "inside",
withoutEnlargement: true,
});
const webpName = `${baseName}-${variant.kind}.webp`;
const webpInfo = await resized
.clone()
.webp({ quality: 78, effort: 4 })
.toFile(path.join(outputDir, webpName));
results.push({
src: `/uploads/${webpName}`,
width: webpInfo.width,
height: webpInfo.height,
bytes: webpInfo.size,
format: "webp",
});
if (makeAvif) {
const avifName = `${baseName}-${variant.kind}.avif`;
const avifInfo = await resized
.clone()
.avif({ quality: 45, effort: 4 })
.toFile(path.join(outputDir, avifName));
results.push({
src: `/uploads/${avifName}`,
width: avifInfo.width,
height: avifInfo.height,
bytes: avifInfo.size,
format: "avif",
});
}
}
return results;
}
AVIF é útil, mas não é regra universal. Pode comprimir bem, porém a codificação é mais lenta. Para catálogo grande, meça antes de converter tudo e mantenha WebP ou JPEG como fallback.
Endpoint Next.js
Este é um endpoint mínimo de App Router. Em produção, troque public/uploads por storage privado se as imagens forem sensíveis.
// app/api/images/route.ts
import path from "node:path";
import { NextResponse } from "next/server";
import { assertImageUpload, safeImageName } from "@/src/image-policy";
import { optimizeImage } from "@/src/optimize-image";
export async function POST(request: Request) {
const form = await request.formData();
const file = form.get("image");
if (!(file instanceof File)) {
return NextResponse.json(
{ error: "image field is required" },
{ status: 400 },
);
}
const buffer = Buffer.from(await file.arrayBuffer());
const upload = await assertImageUpload(buffer, file.name);
const storedName = safeImageName(upload.mime);
const baseName = storedName.replace(/\.[^.]+$/, "");
const variants = await optimizeImage(
buffer,
path.join(process.cwd(), "public", "uploads"),
baseName,
false,
);
return NextResponse.json({
original: {
width: upload.width,
height: upload.height,
bytes: upload.bytes,
},
variants,
});
}
O exemplo é para mídia pública. Anexos de suporte, contratos e documentos internos exigem autorização e entrega separadas.
Resize no navegador
Resize no navegador reduz tráfego e melhora o preview. Ainda assim, o servidor precisa validar novamente.
// src/resize-in-browser.ts
export async function resizeInBrowser(
file: File,
maxSide = 1600,
): Promise<File> {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
const width = Math.round(bitmap.width * scale);
const height = Math.round(bitmap.height * scale);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Canvas 2D context is not available");
}
context.drawImage(bitmap, 0, 0, width, height);
bitmap.close();
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(result) => {
if (result) resolve(result);
else reject(new Error("Canvas export failed"));
},
"image/webp",
0.82,
);
});
const outputName = file.name.replace(/\.[^.]+$/, ".webp");
return new File([blob], outputName, {
type: blob.type || "image/webp",
lastModified: Date.now(),
});
}
Canvas normalmente remove muitos metadados ao exportar, mas privacidade deve ser garantida pelo servidor. Gerar AVIF no navegador também é dependente demais do ambiente.
Jobs e orçamento de performance
O upload síncrono deve ser curto: validar, salvar e criar a miniatura necessária. Imagens de detalhe, OGP, AVIF e regenerações em massa devem ir para job.
Como orçamento inicial, use 80 KB para avatar 320x320, 120 KB para card com largura 640 e 250 KB para hero com largura 1280. Os números mudam por produto, mas o orçamento evita parâmetros pesados.
// src/batch-optimize.ts
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import pLimit from "p-limit";
import { assertImageUpload, safeImageName } from "./image-policy";
import { optimizeImage } from "./optimize-image";
export async function batchOptimize(inputDir: string, outputDir: string) {
const files = await readdir(inputDir);
const limit = pLimit(3);
const jobs = files.map((file) =>
limit(async () => {
const sourcePath = path.join(inputDir, file);
const buffer = await readFile(sourcePath);
const upload = await assertImageUpload(buffer, file);
const baseName = safeImageName(upload.mime).replace(/\.[^.]+$/, "");
const variants = await optimizeImage(buffer, outputDir, baseName, true);
return {
file,
variants: variants.length,
};
}),
);
return Promise.allSettled(jobs);
}
Em uma fila real, registre job ID, imagem de origem, variant, motivo de falha, tentativas e caminhos gerados. Sem isso, arquivos acabam ficando órfãos.
Falhas comuns
As falhas aparecem de forma concreta: confiar em file.type, publicar nome original, ignorar orientação EXIF, manter metadados com .withMetadata(), gerar AVIF dentro do request ou comprimir screenshots até o texto ficar ilegível.
Também há falha de negócio. Se uma captura de curso fica borrada, o CTA para o produto pago perde confiança. Se a imagem de produto demora, o botão de compra aparece antes de o usuário entender o produto. Para medir carregamento de imagem, CTA e compra juntos, veja analytics com Claude Code.
Testes
Gere a imagem dentro do teste para não depender de fixtures manuais.
// src/image-policy.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import sharp from "sharp";
import { assertImageUpload, safeImageName } from "./image-policy";
import { optimizeImage } from "./optimize-image";
test("validates and optimizes a generated image", async () => {
const input = await sharp({
create: {
width: 1200,
height: 800,
channels: 3,
background: "#38bdf8",
},
})
.jpeg()
.toBuffer();
const info = await assertImageUpload(input, "masa-profile.jpg");
assert.equal(info.mime, "image/jpeg");
assert.equal(info.width, 1200);
const safeName = safeImageName(info.mime);
assert.match(safeName, /^[a-f0-9-]+\.jpg$/);
const outDir = await mkdtemp(path.join(tmpdir(), "images-"));
const baseName = safeName.replace(/\.[^.]+$/, "");
const variants = await optimizeImage(input, outDir, baseName, false);
assert.equal(variants.length, 3);
assert.ok(variants.every((item) => item.bytes > 0));
const thumb = await sharp(
path.join(outDir, `${baseName}-thumb.webp`),
).metadata();
assert.equal(thumb.width, 320);
assert.equal(thumb.height, 320);
assert.equal(thumb.exif, undefined);
});
A checagem manual deve cobrir mobile, rede lenta, arquivo quebrado, arquivo gigante, foto vertical de celular, PNG transparente e screenshot com texto pequeno. Depois peça a Claude Code uma revisão focada apenas em validação, nomes, metadados, CPU, fallback e testes.
CTA e nota prática
Processamento de imagens protege monetização quando ajuda o usuário a confiar no que vê: produto rápido, screenshot legível, avatar sem vazamento e OGP estável. Comece pelo checklist gratuito de Claude Code, use produtos ClaudeCodeLab para prompts e templates reutilizáveis, e avance para training / consultation quando sua equipe quiser regras de upload e revisão dentro de um repositório real.
Em 2 de junho de 2026, Masa testou este fluxo em um projeto pequeno de Next.js. O melhor resultado veio quando o prompt definiu a fronteira antes do código: resize no navegador é opcional, validação no servidor é obrigatória, AVIF é opcional e o nome original nunca é público. O prompt vago “crie upload de imagens” gerou código mais fraco com file.type, nomes públicos originais, orientação ausente e AVIF síncrono. Orçamento e exemplos de falha antes da implementação melhoraram muito a qualidade.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.