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.