Save and restore edits

The editor never changes the photo. Every edit is a change to an EditState: plain, versioned JSON describing geometry, colour, filter, the elements on the photo, frame, fill, watermark and output size. Save that JSON and you can reopen the photo later with every edit still editable.

When the user presses Done

import { ImageEditor, type ExportResult } from '@image-ultra/react';

declare const photoUrl: string;
declare function upload(image: Blob, fileName: string): Promise<void>;
declare function saveEdits(json: string): Promise<void>;

export function Editor() {
  return (
    <ImageEditor
      src={photoUrl}
      exportOptions={{ mimeType: 'image/jpeg', quality: 0.9, maxWidth: 4000 }}
      onSave={async (result: ExportResult) => {
        await upload(result.blob, result.fileName);
        await saveEdits(JSON.stringify(result.state));
      }}
    />
  );
}

While onSave runs, Done shows a spinner; if it throws, the editor tells the user saving failed.

result has:

Field
blobThe edited image.
fileNameThe source name plus the right extension, e.g. photo.jpg.
width, heightFinal size in pixels.
mimeTypeThe type actually produced (Safari on iOS gives PNG when asked for WebP).
stateThe EditState that produced it — save this to reopen.
downscaledtrue if the browser couldn't hold a canvas that big and the result is smaller.

Export options

OptionDefault
mimeTypethe source's type (JPEG / PNG / WebP), else PNG'image/jpeg', 'image/png' or 'image/webp'
quality0.92 JPEG, 0.9 WebP0…1
maxWidth, maxHeightnoneScale down (never up) to fit.
fileNamethe source's nameWithout extension.
background#ffffff for JPEGFills transparent areas.
keepMetadatafalseSee Photo metadata.

Reopen saved edits

<ImageEditor src={photoUrl} initialState={savedJson} />

initialState accepts an EditState or untrusted JSON — it's validated, unknown fields are dropped, and edits saved by older versions are migrated. It's read when src changes; to reopen the same photo with other edits, remount the editor (change its key) or call setState on the handle.

Every change, as it happens

onChange fires after each committed edit — once per slider drag, not on every frame — with the new EditState. Use it for autosave:

<ImageEditor src={photoUrl} onChange={(state) => saveDraft(JSON.stringify(state))} />

From your own buttons

import { ImageEditor, useImageEditor } from '@image-ultra/react';

export function Editor({ photoUrl }: { photoUrl: string }) {
  const editor = useImageEditor();
  return (
    <>
      <ImageEditor ref={editor} src={photoUrl} />
      <button onClick={() => editor.current?.save()}>Save</button>
      <button onClick={() => console.log(editor.current?.getState())}>Log edits</button>
    </>
  );
}
Handle method
getState()The current EditState.
setState(state)Replace all edits (validated). Clears undo history.
update(label, recipe)One undoable change: update('Rotate', (s) => { s.geometry.rotation = 90; }).
undo(), redo(), reset()Like the buttons.
exportImage(options?)Render and encode without calling onSave.
save()Same as pressing Done.
storeThe underlying store, for advanced use.

What's in the JSON

Images the user adds (stickers, logos, pasted images, a Fill image) are stored inside the state as data URLs — at most 1600 px on the long side — so the JSON is self-contained. Expect anywhere from a few hundred bytes (crop and colour only) to a few hundred kilobytes with images.