Bildverarbeitung mit Claude Code: Sharp, Canvas, WebP/AVIF und Upload-Validierung
Sichere Bildverarbeitung mit Claude Code: Sharp, Canvas, EXIF, WebP/AVIF, Upload-Prüfung, Jobs und Tests.
Bildverarbeitung wirkt in einem Produkt-Ticket klein: Nutzer laden ein Foto hoch, die App erzeugt Thumbnails, die Seite wird schneller. In Produktion ist es aber eine echte Systemgrenze. Man braucht Upload-Validierung, sichere Dateinamen, EXIF-Bereinigung, Resize, Kompression, Format-Fallbacks, Hintergrundjobs, Datenschutz und Tests.
Claude Code ist hilfreich, weil diese Funktion viele kleine Dateien berührt. Ein ungenauer Prompt wie “optimiere Bilder” kann aber Code erzeugen, der file.type vertraut, den Originalnamen öffentlich macht, Metadaten erhält, jedes Bild synchron in AVIF wandelt und keine gedrehten Handyfotos testet. Deshalb muss die Architektur im Prompt stehen.
Nutze zur Prüfung die Primärquellen: Claude Code Dokumentation, Sharp resize API, Sharp output API, MDN File API, MDN Canvas toBlob und den OWASP File Upload Cheat Sheet. Für schwere Browserarbeit passt der interne Artikel Claude Code Web Worker Guide.
Die Grenze zuerst festlegen
Wähle nicht zuerst das Format. Lege zuerst fest, welche Schicht welche Verantwortung trägt.
| Ort | Geeignet | Nicht geeignet |
|---|---|---|
| Browser | Vorschau, leichter Resize, weniger Upload-Traffic | Vertrauenswürdige Prüfung, AVIF-Massenkonvertierung, Datenschutzentscheidung |
| Synchroner Server | MIME, Magic Bytes, Dimensionen, EXIF, kleine Thumbnails | Viele Varianten, langsame AVIF-Erzeugung |
| Hintergrundjob | Produktkatalog, CMS-Regeneration, Migration alter Bilder | Sofortige Upload-Antwort |
Das robuste Muster lautet: Der Browser verbessert die UX, der Server validiert immer neu, teure Varianten laufen im Job. accept="image/*" und file.type sind UI-Hinweise, keine Sicherheitsgrenze.
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
Schreibe diese Grenzen direkt in die Claude-Code-Aufgabe: MIME und Magic Bytes serverseitig prüfen, Originalnamen nie in öffentlichen URLs verwenden, rotate() anwenden, .withMetadata() vermeiden und AVIF optional machen. Dadurch sinkt die Wahrscheinlichkeit plausibler, aber unsicherer Implementierungen.
Produktnahe Use Cases
Der erste Fall ist Ecommerce oder Marketplace. Verkäufer laden große Smartphone-Fotos hoch. Das Produkt braucht quadratische Thumbnails, Kartenbilder, Detailbilder und oft Social-Images. Zu starke Kompression zerstört Vertrauen, weil Käufer Material, Farbe, Etikett und Zustand prüfen wollen. WebP ist ein guter Start; AVIF sollte erst nach Messung mit echten Bildern folgen.
Der zweite Fall ist ein Avatar oder Teamfoto. Wichtig sind quadratischer Crop, sichere URL und Privatsphäre. Ein Name wie kunde-vertrag-final.png gehört nicht in einen öffentlichen Pfad. Selbst wenn der Browser bereits verkleinert hat, muss der Server die Ausgabe ohne EXIF erzeugen.
Der dritte Fall sind Blog-, Hilfe- oder Kurs-Screenshots. Lesbarkeit ist wichtiger als maximale Kompression. Ein Screenshot, der 80 KB spart, aber Buttontexte unlesbar macht, ist fachlich schlechter. Wenn Bilder Teil eines Dokumentenflows sind, lies auch PDF-Erzeugung mit Claude Code.
Der vierte Fall sind private SaaS-Anhänge: Rechnungen, Prüfungsbilder, Support-Screenshots oder Admin-Dokumente. Diese Dateien gehören nicht in public/uploads. Sie brauchen privaten Speicher, Zugriffskontrolle, Löschregeln und Audit-Logs.
Installation
Die Beispiele setzen Node.js 20 oder neuer voraus. Du kannst die Module in Next.js, Express, Hono, Astro API Routes oder Queue Worker übernehmen.
npm i sharp file-type p-limit
npm i -D tsx typescript @types/node
mkdir -p src public/uploads
Füge einen Testbefehl hinzu, damit Bildverhalten in CI läuft.
{
"scripts": {
"test:images": "node --import tsx --test src/**/*.test.ts"
}
}
Upload validieren und sichere Namen erzeugen
Dieses Modul ist die Vertrauensgrenze. Es glaubt nicht an Dateiendungen, sondern prüft Magic Bytes, liest Dimensionen mit Sharp, lehnt zu große Dateien ab und blockiert animierte oder mehrseitige Bilder für diesen Upload-Pfad.
// 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]}`;
}
Der Originalname kann als privater Anzeigename gespeichert werden, aber nicht als öffentliche URL. Zufällige Namen verhindern außerdem Kollisionen und Unicode-Probleme.
Resize, Kompression und EXIF mit Sharp
Sharp ist eine solide Wahl für serverseitige Bildverarbeitung in Node.js. Wichtig ist: Orientierung mit rotate() anwenden und danach keine Metadaten behalten, solange es keinen klaren Grund gibt. Für öffentliche Webbilder ist das meist der richtige Datenschutz-Default.
// 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 ist nützlich, aber kein Pflichtformat. Die Kompression kann stark sein, die Kodierung ist jedoch oft deutlich langsamer. Miss mit echten Produktbildern und halte WebP oder JPEG als Fallback bereit.
Next.js Upload Route
Dies ist ein minimaler App-Router-Endpunkt. Für private Bilder solltest du public/uploads durch privaten Objektspeicher und autorisierte Auslieferung ersetzen.
// 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,
});
}
Dieses Beispiel gilt für öffentliche Medien. Support-Anhänge, Rechnungen oder Prüfdokumente brauchen ein anderes Zugriffskonzept.
Browser-Resize für UX
Browser-Resize spart Upload-Traffic und macht Vorschauen schneller. Es ersetzt keine Serverprüfung.
// 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-Reexport entfernt häufig viele Metadaten, aber Datenschutz darf nicht davon abhängen. AVIF-Erzeugung im Browser ist außerdem zu abhängig von der konkreten Umgebung.
Jobs und Performance-Budget
Die synchrone Upload-Route sollte kurz bleiben: prüfen, speichern, benötigte Miniatur erzeugen. Detailbilder, OGP, AVIF und Massenregenerationen gehören in Jobs.
Als Startbudget eignen sich 80 KB für Avatar 320x320, 120 KB für Kartenbilder mit Breite 640 und 250 KB für Hero-Bilder mit Breite 1280. Die Werte sind produktabhängig, aber ein Budget verhindert übermäßig schwere Claude-Code-Vorschläge.
// 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);
}
Bei einer echten Queue müssen Job-ID, Quellbild, Variant, Fehlergrund, Wiederholungen und erzeugte Pfade gespeichert werden. Sonst entstehen Dateien, die später keinem Datenbankeintrag mehr zugeordnet sind.
Typische Fehler
Typische Fehler sind konkret: file.type vertrauen, Originalnamen veröffentlichen, EXIF-Orientierung ignorieren, mit .withMetadata() versehentlich EXIF behalten, AVIF im Request erzeugen oder Screenshots so stark komprimieren, dass Text unlesbar wird.
Es gibt auch Produktfehler. Wenn ein Kurs-Screenshot unlesbar ist, verliert der CTA zum Template Vertrauen. Wenn Produktbilder spät laden, sieht der Nutzer den Kaufbutton, bevor das Produkt verständlich ist. Für gemeinsame Messung von Bild-Ladezeit, CTA und Kaufpfad hilft Analytics mit Claude Code.
Verhalten testen
Erzeuge das Testbild im Test, damit CI keine externen Fixtures braucht.
// 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);
});
Manuell solltest du Mobile-Breite, langsames Netz, defekte Dateien, riesige Dateien, vertikale Handyfotos, transparente PNGs und Screenshots mit kleinem Text prüfen. Danach kann Claude Code eine reine Reviewrunde für Validierung, Namen, Metadaten, CPU, Fallbacks und fehlende Tests machen.
CTA und Verifikationsnotiz
Bildverarbeitung schützt Monetarisierung, wenn Nutzer den sichtbaren Inhalt schneller verstehen und ihm vertrauen. Schnelle Produktbilder, lesbare Screenshots, sichere Avatare und stabile OGP-Bilder unterstützen den nächsten Klick. Starte mit der kostenlosen Claude-Code-Checkliste, nutze ClaudeCodeLab-Produkte für wiederverwendbare Prompts und Templates, und nutze Training / Consultation, wenn dein Team Upload-Regeln und Review-Gates in ein echtes Repository bringen will.
Am 2. Juni 2026 testete Masa diesen Ablauf in einem kleinen Next.js-Projekt. Das beste Ergebnis entstand, als der Prompt zuerst die Grenze festlegte: Browser-Resize optional, Servervalidierung Pflicht, AVIF optional, Originalname nie öffentlich. Der vage Prompt “baue Bild-Upload” erzeugte schwächeren Code mit file.type, öffentlichen Originalnamen, fehlender Orientierung und synchronem AVIF. Budgets und Fehlerfälle vor der Implementierung machten den größten Qualitätsunterschied.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.