Saving edits
Save the rendered image, the edit itself, or both.
Upload the result
onSave adds a Save button to the export dialog. It receives the encoded Blob at full resolution; for JPEG, the original EXIF is kept.
<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 call it yourself: await editor.exportImage({ format: "webp", quality: 0.9 }).
Save the edit, not the pixels
Edits are plain data. onParamsChange fires on every change, and buildRecipe(params) turns the params into a small JSON diff that you can store and re-apply later with editor.recipes.apply(recipe). Pass defaultParams to restore a full session.
"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>
);
}Sessions saved by older versions
Params from before the geometry rewrite are migrated automatically when you pass them to defaultParams or setParams. Rotation, flips, crop, zoom and perspective all carry over. To convert them yourself, call normalizeEditorParams(saved) from photocn.