Use Cases (Aktualisiert: 2.6.2026)

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 mit Claude Code: Sharp, Canvas, WebP/AVIF und Upload-Validierung

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.

OrtGeeignetNicht geeignet
BrowserVorschau, leichter Resize, weniger Upload-TrafficVertrauenswürdige Prüfung, AVIF-Massenkonvertierung, Datenschutzentscheidung
Synchroner ServerMIME, Magic Bytes, Dimensionen, EXIF, kleine ThumbnailsViele Varianten, langsame AVIF-Erzeugung
HintergrundjobProduktkatalog, CMS-Regeneration, Migration alter BilderSofortige 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.

#Claude Code #Bildverarbeitung #Sharp #WebP #Upload-Validierung
Kostenlos

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.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.