photocn

Search

Search the docs

0

Saving edits

Save the finished image, the edits, or both.

Upload the result

onSave adds a Save button to the export panel. It receives the image as a Blob. JPEG exports keep the original EXIF data.

<ImageEditor
  src={photo.url}
  onSave={async ({ blob, filename }) => {
    const body = new FormData();
    body.append("file", blob, filename);
    await fetch("/api/photos", { method: "POST", body });
  }}
/>

Or export from code: await editor.exportImage({ format: "webp", quality: 0.9, width: 1600 }).

Save the edits instead

Edits are plain data. onParamsChange fires on every change, and buildRecipe(params) turns the edits into a small JSON object you can store and apply later with editor.recipes.apply(recipe). To reopen a whole session, pass defaultParams.

"use client";

import { useState } from "react";
import { buildRecipe, type EditorParams, type RecipeV1 } from "photocn";

import { ImageEditor } from "@/components/image-editor/image-editor";

// Persist edits anywhere: `onParamsChange` fires with every change and
// `buildRecipe` turns params into a small, serializable diff.
export default function ControlledExample() {
  const [recipe, setRecipe] = useState<RecipeV1 | null>(null);
  return (
    <div className="grid gap-4 lg:grid-cols-[1fr_280px]">
      <ImageEditor
        className="h-[600px] overflow-hidden rounded-xl border"
        onParamsChange={(params: EditorParams) => setRecipe(buildRecipe(params))}
        src="/samples/mountain-lake.jpg"
      />
      <pre className="max-h-[600px] overflow-auto rounded-xl border bg-muted/40 p-3 font-mono text-xs">
        {recipe ? JSON.stringify(recipe, null, 2) : "// Move a slider…"}
      </pre>
    </div>
  );
}

Older saved sessions

Edits saved by earlier versions are converted automatically when you pass them to defaultParams or setParams. To convert them yourself, call normalizeEditorParams(saved) from photocn.