API reference

Generated from the published TypeScript declarations, so it always matches the package. Most apps only need @image-ultra/react; it re-exports the parts of @image-ultra/core you're likely to use.

@image-ultra/react

Components

AspectGlyph

Tiny rectangle showing an aspect ratio (or a circle).

function AspectGlyph({ aspect, round }: {
  aspect: number | null;
  round?: boolean;
}): react.JSX.Element;

ColorButton

Round colour button that shows the current colour (or "none").

const ColorButton: react.ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
  swatch: string | null;
  label: string;
  ring?: boolean;
} & react.RefAttributes<HTMLButtonElement>>;

ColorStrip

Colours laid out inline, like the filter strip: one tap picks a swatch; the last button opens the full picker (HSV, hex, eyedropper) for any other colour. Scrolls sideways when narrow.

function ColorStrip({ label, value, onChange, swatches, }: ColorStripProps): react.JSX.Element;

HsvPicker

Saturation/brightness area, hue slider, hex input and eyedropper.

function HsvPicker({ value, onChange }: {
  value: string;
  onChange: (hex: string) => void;
}): react.JSX.Element;

IconButton

A square icon button with a tooltip, styled like the editor's own.

const IconButton: react.ForwardRefExoticComponent<IconButtonProps & react.RefAttributes<HTMLButtonElement>>;

ImageEditor

The image editor. Give it a photo (src); get the edited image and the edits as JSON (onSave).

const ImageEditor: react.ForwardRefExoticComponent<ImageEditorProps & react.RefAttributes<ImageEditorHandle>>;

MaskBrushOverlay

A paintable mask over the stage (for tools like the future AI object eraser). Strokes are in oriented image space — rasterise them with rasterizeMask from @image-ultra/core. Render it as (part of) a tool's StageOverlay.

function MaskBrushOverlay({ strokes, onChange, size, mode, color }: MaskBrushOverlayProps): react.JSX.Element | null;

NumberField

Compact numeric input. Commits on Enter/blur (so typing "1" on the way to "1080" doesn't change the image); ↑/↓ step by 1, Shift by 10.

function NumberField({ label, value, min, max, unit, onChange }: NumberFieldProps): react.JSX.Element;

Popover

Floating panel for secondary controls (colours, widths, fonts). Radix handles focus, Escape and outside clicks; it renders inside the editor root so it keeps the theme.

function Popover({ trigger, label, children, side, open, onOpenChange, }: PopoverProps): react.JSX.Element;

PresetStrip

Horizontally scrolling row of choice chips (UI_VISION §5 PresetStrip, compact variant).

function PresetStrip<T extends string>({ label, presets, value, onSelect, variant, onRemove, removeLabel, }: PresetStripProps<T>): react.JSX.Element;

RulerSlider

Pintura-style ruler: a tick scale scrolls under a fixed centre marker (UI_VISION §5). Drag it, use the arrow keys (Shift = ×10), or double-click to reset.

function RulerSlider({ label, value, min, max, step, tickEvery, defaultValue, unitWidth, majorEvery, snapTo, format, onChange, onChangeStart, onChangeEnd, disabled, }: RulerSliderProps): react.JSX.Element;

SegmentedControl

2–5 mutually exclusive options (UI_VISION §5). Arrow keys move the selection.

function SegmentedControl<T extends string>({ label, options, value, onChange, }: SegmentedControlProps<T>): react.JSX.Element;

SwatchPicker

Swatches + a compact HSV picker with hex input and eyedropper (UI_VISION §5).

function SwatchPicker({ value, onChange, allowNone, swatches, }: SwatchPickerProps): react.JSX.Element;

Hooks

useEditorState

Subscribe to a slice of editor state; re-renders only when that slice changes.

function useEditorState<T>(selector: (state: EditorStoreState) => T): T;

useEditorStore

The editor's store, inside a custom tool: read with getState(), change edits with update / beginChange.

function useEditorStore(): EditorStore;

useFonts

The fonts offered for text (the fonts prop, or the defaults), inside a custom tool.

function useFonts(): readonly FontOption[];

useImageEditor

A typed ref for controlling the editor from your own UI:

const editor = useImageEditor();
<ImageEditor ref={editor} … />
<button onClick={() => editor.current?.undo()}>Undo</button>
function useImageEditor(): RefObject<ImageEditorHandle | null>;

useLabels

The editor's labels (after your labels overrides), inside a custom tool.

function useLabels(): Labels;

useLooks

The saved looks and a setter, inside a custom tool.

function useLooks(): [readonly Look[], (looks: Look[]) => void];

useStickers

Your stickers (the stickers prop), inside a custom tool.

function useStickers(): readonly StickerOption[];

useToolState

Transient UI state shared by a tool's Controls and StageOverlay (e.g. current drawing tool, selection). Lives in the store, outside the edit history.

function useToolState<T>(initial: T): [T, (next: T | ((prev: T) => T)) => void];

useWatermarkLocked

Whether the watermark is locked by the app (lockWatermark), inside a custom tool.

function useWatermarkLocked(): boolean;

Functions

defineTool

Identity helper that gives you type-checking and autocompletion for a tool definition.

function defineTool<T extends ToolDefinition>(tool: T): T;

formatCount

Fills a CountLabel for count.

function formatCount(label: CountLabel, count: number): string;

Constants

BUILT_IN_TOOLS

The built-in tools by id, in their default order.

const BUILT_IN_TOOLS: Record<ToolId, ToolDefinition>;

DEFAULT_FONTS

Default text fonts: system stacks only, so they render without downloads on every OS.

const DEFAULT_FONTS: readonly (FontOption & {
  id: keyof Labels['fontNames'];
})[];

DEFAULT_SWATCHES

Default annotation palette (the last one is the brand plum).

const DEFAULT_SWATCHES: readonly ["#ffffff", "#000000", "#ff3b30", "#ff9500", "#ffcc00", "#34c759", "#00c7be", "#0a84ff", "#5856d6", "#af52de", "#ff2d55", "#4d194d"];

defaultLabels

The English labels: every string the editor shows. A template for your own translations.

const defaultLabels: Labels;

SIZE_PRESETS

Common social/web sizes. Picking one crops to its aspect ratio (as large as possible) and resizes to exactly this size.

const SIZE_PRESETS: readonly SizePreset[];

Types

ColorStripProps

Props for ColorStrip.

interface ColorStripProps {
  /** Accessible name of the group, e.g. "Fill colour". */
  label: string;
  /** Hex colour. */
  value: string;
  onChange: (value: string) => void;
  swatches?: readonly string[];
}

CountLabel

A label with a number in it: a template with {count}, or a function for plural rules.

type CountLabel = string | ((count: number) => string);

FontOption

A font offered for text shapes and the watermark (fonts prop).

interface FontOption {
  /** Shown in the font menu. */
  label: string;
  /** CSS font-family value. Web fonts must be loaded by your app (e.g. next/font). */
  family: string;
}

IconButtonProps

Props for IconButton.

interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** Accessible name, also shown as the hover tooltip. */
  label: string;
  icon: ReactNode;
  /** Show the label next to the icon (TopBar text buttons). */
  showLabel?: boolean;
  variant?: 'ghost' | 'primary';
  /** `sm` = 24px, for secondary actions inside dense panels. Default `md` (40px touch target). */
  size?: 'md' | 'sm';
}

IconProps

Props every icon accepts (custom tool icons too).

interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'children'> {
  /** Rendered size in CSS px. Icons are drawn on a 24px grid. */
  size?: number;
}

ImageEditorHandle

Control the editor from your own code: const editor = useImageEditor() + ref={editor}.

interface ImageEditorHandle {
  /** The underlying store, for advanced use (subscribe, custom tools). */
  readonly store: EditorStore;
  /** Current edits. Serializable — `JSON.stringify` it to save. */
  getState(): EditState;
  /** Replace all edits (e.g. restore saved JSON). Clears undo history. */
  setState(state: EditState | unknown): void;
  /** One undoable change: `update('Rotate', (s) => { s.geometry.rotation = 90; })`. */
  update(label: string, recipe: EditRecipe): void;
  undo(): void;
  redo(): void;
  reset(): void;
  /** Render and encode without triggering `onSave`. */
  exportImage(options?: ExportOptions): Promise<ExportResult>;
  /** Same as pressing Done: exports with `exportOptions` and calls `onSave`. */
  save(): Promise<ExportResult | null>;
}

ImageEditorProps

Props for <ImageEditor>.

interface ImageEditorProps {
  /** Image to edit. Omit to show the drop zone. */
  src?: ImageSource | undefined;
  /**
   * Edits to restore when the image opens — an `EditState` or its JSON (e.g. from `onSave`'s
   * `result.state`). Read whenever `src` changes.
   */
  initialState?: EditState | unknown;
  /** `'dark'` (default), `'light'`, or `'auto'` to follow the OS setting. */
  theme?: ThemeMode;
  /**
   * Text direction. Default: the page's (inherited). `rtl` mirrors the layout (tool rail on the
   * right, rows reversed); the photo, sliders and curves stay left-to-right.
   */
  dir?: 'ltr' | 'rtl';
  /** Typed shortcuts for common design tokens, e.g. `{ accent: '#ff5a1f' }`. */
  themeOverrides?: ThemeOverrides;
  /**
   * Tools shown in the ToolRail, in this order: built-in ids and/or your own `defineTool(...)`
   * definitions. Defaults to all built-in tools.
   */
  tools?: readonly ToolInput[];
  /** Tool selected when the editor opens. Defaults to the first of `tools`. */
  defaultTool?: ToolId | (string & {});
  /** Translate or rename any text. */
  labels?: LabelOverrides;
  /** Output format, quality and size used when the user presses Done. */
  exportOptions?: ExportOptions;
  /** Called with the exported image when the user presses Done. */
  onSave?: (result: ExportResult) => void | Promise<void>;
  /** Called after every committed edit (not on every frame of a slider drag). */
  onChange?: (state: EditState) => void;
  /**
   * Saved colour looks shown in the Filter tool. Pass with `onLooksChange` to store them yourself
   * (e.g. per user in your database); otherwise they're kept in this browser's localStorage.
   */
  looks?: Look[];
  onLooksChange?: (looks: Look[]) => void;
  /** localStorage key for uncontrolled looks; `false` keeps them in memory only. */
  persistLooks?: boolean | string;
  /** Fonts offered for text annotations (default: system font stacks). */
  fonts?: FontOption[];
  /**
   * Looks in the Filter tool (default: `FILTER_PRESETS`). A built-in id shows its name from
   * `labels.filterNames`; your own presets show their `name`.
   */
  filterPresets?: readonly FilterPreset[];
  /**
   * Output sizes in Resize (default: `SIZE_PRESETS`). A built-in id shows its name from
   * `labels.sizePresetNames`; your own sizes show their `label`.
   */
  sizePresets?: readonly SizePreset[];
  /**
   * A watermark to start with — text, or a logo via `logo` (image URL). With `lockWatermark` the
   * user can't change or remove it, and every export applies it.
   */
  watermark?: WatermarkInput;
  lockWatermark?: boolean;
  /** Your own stickers, shown first in the Sticker tool. */
  stickers?: StickerOption[];
  /**
   * The 3D sticker library (Microsoft Fluent Emoji 3D, MIT), loaded on demand. Default: the pinned
   * jsDelivr copy. Pass your own base URL to self-host the `assets/` folder, or `false` to turn it off.
   */
  stickerLibrary?: string | false;
  /** Shows a Cancel button in the TopBar when provided. */
  onCancel?: () => void;
  /**
   * Load, export or `initialState` errors. The editor already shows load errors on screen, so
   * without this they're only logged with `console.warn`.
   */
  onError?: (error: Error) => void;
  className?: string;
  style?: CSSProperties;
}

LabelOverrides

Any subset of Labels, also inside groups: pass to labels to translate or rename text.

type LabelOverrides = Partial<Omit<Labels, NestedKey>> & {
  [K in NestedKey]?: Partial<Labels[K]>;
};

Labels

Every user-facing string. Pass a partial object to labels to translate or rename.

interface Labels {
  /** Built-in filter names, by preset id (`FILTER_PRESETS`). */
  filterNames: Record<string, string>;
  /** Built-in Resize size names, by id (`SIZE_PRESETS`). */
  sizePresetNames: Record<string, string>;
  /** Built-in sticker names, by id. */
  stickerNames: Record<string, string>;
  /** The default text fonts (when the app passes no `fonts`). */
  fontNames: Record<'sans' | 'serif' | 'mono' | 'rounded' | 'hand', string>;
  /** Layer names for elements the user hasn't named. `{text}` = a text box's first words. */
  shapeNames: Record<'rectangle' | 'ellipse' | 'line' | 'arrow' | 'drawing' | 'polygon' | 'text' | 'image', string>;
  /** Undo-history step names (History panel, "Undone: …"). */
  steps: Record<'crop' | 'moveCrop' | 'resizeCrop' | 'rotate' | 'flip' | 'aspectRatio' | 'resetAdjust' | 'autoEnhance' | 'resetFinetune' | 'resize', string>;
  /** Key names in the shortcuts panel (⌘ ⇧ ⌫ ↵ are symbols and stay). */
  keys: Record<'ctrl' | 'space' | 'drag' | 'click' | 'tab' | 'esc', string>;
  /** Pixel unit after a number field. */
  unitPx: string;
  cancel: string;
  reset: string;
  undo: string;
  redo: string;
  done: string;
  saving: string;
  zoomIn: string;
  zoomOut: string;
  zoomFit: string;
  zoomLevel: string;
  zoomActual: string;
  /** Compare button tooltip ("hold" = press and hold). */
  compare: string;
  showOriginal: string;
  before: string;
  after: string;
  /** Accessible name of the split-view divider. */
  compareDivider: string;
  history: string;
  historyOriginal: string;
  /** Screen-reader announcements (7.5c). `{step}` = a history step label. */
  announceUndo: string;
  announceRedo: string;
  announceHistory: string;
  /** `{percent}` */
  announceZoom: string;
  /** `{width}`, `{height}` */
  announceCrop: string;
  /** `{name}` = the element's name */
  announceSelected: string;
  announceNothingSelected: string;
  announceSaved: string;
  saveFailed: string;
  /** The photo on the stage: `{width}`, `{height}` = the result's size. */
  photoLabel: string;
  /** The photo in Annotate / Sticker / Redact (a Tab stop), and how to use it. */
  elementsLabel: string;
  elementsHint: string;
  /** Curve point value: `{in}`, `{out}` (0–255). */
  curvePointValue: string;
  shortcuts: string;
  shortcutsGeneral: string;
  shortcutsShow: string;
  nudge: string;
  finishOrEdit: string;
  /** Shortcuts panel: Tab / Shift+Tab on the photo. */
  nextElement: string;
  deselect: string;
  /** Built-in tool names. Custom tools pass their own `label`. */
  tools: Record<ToolId, string>;
  toolbarLabel: string;
  loading: string;
  loadError: string;
  /** `{format}` is replaced with the file type, e.g. `HEIC`. */
  loadErrorUnsupported: string;
  loadErrorDamaged: string;
  loadErrorNotImage: string;
  loadErrorNetwork: string;
  emptyTitle: string;
  emptyHint: string;
  browse: string;
  comingSoon: string;
  rotateLeft: string;
  flipHorizontal: string;
  flipVertical: string;
  straighten: string;
  tiltVertical: string;
  tiltHorizontal: string;
  resetTool: string;
  aspectRatio: string;
  aspectFree: string;
  aspectOriginal: string;
  aspectCircle: string;
  cropArea: string;
  /** Screen-reader hint on the crop area. */
  cropAreaHint: string;
  width: string;
  height: string;
  keepAspect: string;
  sizePresets: string;
  originalSize: string;
  upscaleWarning: string;
  /** Names of the 16 adjustments. */
  finetune: Record<keyof FinetuneState, string>;
  modeAdjust: string;
  modeCurves: string;
  modeLevels: string;
  adjustments: string;
  auto: string;
  autoHint: string;
  curveChannels: Record<CurveChannel, string>;
  curvePoint: string;
  curveHint: string;
  levelsBlack: string;
  levelsMid: string;
  levelsWhite: string;
  saveLook: string;
  /** `{name}` is replaced with the look's name. */
  lookSaved: string;
  viewInFilters: string;
  lookName: string;
  save: string;
  cancelEdit: string;
  annotateTools: string;
  annotateModes: Record<'select' | 'pen' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'polygon' | 'text', string>;
  insertImage: string;
  strokeColor: string;
  fillColor: string;
  textColor: string;
  textBackground: string;
  strokeWidth: string;
  fontSize: string;
  opacity: string;
  cornerRadius: string;
  font: string;
  bold: string;
  alignLeft: string;
  alignCenter: string;
  alignRight: string;
  arrowStart: string;
  arrowEnd: string;
  duplicate: string;
  copy: string;
  zoomCrop: string;
  resizeModeSize: string;
  resizeModeCanvas: string;
  /** Aspect chips for the canvas; `canvasOriginal` = no shape change, padding only. */
  canvasShape: string;
  canvasOriginal: string;
  canvasPadding: string;
  canvasAnchor: string;
  canvasReset: string;
  canvasHint: string;
  selectAll: string;
  /** Shift-click hint in the shortcuts panel. */
  addToSelection: string;
  panPhoto: string;
  /**
   * `{count}` = number of selected shapes. A function gets the number, for languages whose wording
   * changes with it (plurals): `(n) => …`.
   */
  selectedCount: CountLabel;
  resizeSelection: string;
  arrange: string;
  alignEdges: Record<'left' | 'centerX' | 'right' | 'top' | 'centerY' | 'bottom', string>;
  distributeX: string;
  distributeY: string;
  renameLayer: string;
  groupMenu: string;
  editInRedact: string;
  editInWatermark: string;
  lockAll: string;
  unlockAll: string;
  hideAll: string;
  reorderLayer: string;
  showAllLayers: string;
  cut: string;
  paste: string;
  deleteShape: string;
  layers: string;
  noLayers: string;
  bringForward: string;
  sendBackward: string;
  bringToFront: string;
  sendToBack: string;
  showInLayers: string;
  /** Accessible name of the "⋯" menu button on each layer and of the canvas context menu. */
  shapeMenu: string;
  showLayer: string;
  hideLayer: string;
  lockLayer: string;
  unlockLayer: string;
  sizeSmall: string;
  sizeMedium: string;
  sizeLarge: string;
  sizeHuge: string;
  annotateHint: string;
  polygonHint: string;
  textPlaceholder: string;
  /** Content of a new text box (selected, so typing replaces it). */
  textDefault: string;
  editText: string;
  rotate: string;
  redactTools: string;
  redactBox: string;
  redactBrush: string;
  redactStyle: string;
  redactStyles: Record<RedactStyle, string>;
  redactStrength: string;
  brushSize: string;
  redactColor: string;
  redactClear: string;
  redactDelete: string;
  /** History label for a new area. */
  redactArea: string;
  redactMove: string;
  redactHint: string;
  redactBlurHint: string;
  stickers: string;
  stickerTabs: Record<'stickers' | 'emoji', string>;
  stickerUpload: string;
  stickerBasic: string;
  stickerSearch: string;
  stickerCategories: string;
  emojiGroups: Record<EmojiGroup, string>;
  stickerNoResults: string;
  /** History label for placing a sticker. */
  stickerAdd: string;
  watermarkKind: string;
  watermarkKinds: Record<'none' | 'text' | 'logo', string>;
  watermarkText: string;
  watermarkColor: string;
  watermarkChooseLogo: string;
  watermarkPosition: string;
  watermarkPositions: Record<WatermarkPosition, string>;
  watermarkSize: string;
  watermarkBold: string;
  watermarkLocked: string;
  /** Accessible name of the draggable watermark on the photo. */
  watermarkDrag: string;
  frames: string;
  frameNone: string;
  frameStyles: Record<FrameStyle, string>;
  frameSize: string;
  frameColor: string;
  fillKind: string;
  fillKinds: Record<'none' | 'color' | 'image' | 'blur', string>;
  backgroundColor: string;
  fillChooseImage: string;
  fillHint: string;
  color: string;
  colorNone: string;
  colorCustom: string;
  colorSaturation: string;
  colorHue: string;
  colorPick: string;
  filters: string;
  filterNone: string;
  intensity: string;
  myLooks: string;
  removeLook: string;
}

MaskBrushOverlayProps

Props for MaskBrushOverlay.

interface MaskBrushOverlayProps {
  strokes: readonly MaskStroke[];
  onChange: (strokes: MaskStroke[]) => void;
  /** Brush diameter in image (oriented) pixels. */
  size: number;
  mode: 'paint' | 'erase';
  /** Any CSS colour for the mask preview. Default: accent at 45%. */
  color?: string;
}

NumberFieldProps

Props for NumberField.

interface NumberFieldProps {
  label: string;
  value: number;
  min: number;
  max: number;
  /** Shown after the number, e.g. `px`. */
  unit?: string;
  onChange: (value: number) => void;
}

PopoverProps

Props for Popover.

interface PopoverProps {
  /** The element that opens the popover (usually an `IconButton` or `ColorButton`). */
  trigger: ReactNode;
  /** Accessible name of the popover panel. */
  label: string;
  children: ReactNode;
  side?: 'top' | 'bottom';
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
}

Preset

One chip in a PresetStrip.

interface Preset<T extends string> {
  value: T;
  label: string;
  /** Optional small visual before the label, e.g. an aspect-ratio glyph. */
  glyph?: ReactNode;
  /** Extra text for screen readers, e.g. "1080 by 1350 pixels". */
  description?: string;
  /** Shows a remove badge (and Delete key removes it) — needs `onRemove` on the strip. */
  removable?: boolean;
  /** Draw a thin divider before this item (to separate groups). */
  separatorBefore?: boolean;
  /** Small marker in the corner, e.g. a bookmark for the user's own looks. */
  badge?: ReactNode;
}

PresetStripProps

Props for PresetStrip.

interface PresetStripProps<T extends string> {
  label: string;
  presets: readonly Preset<T>[];
  /** `null` when no preset matches the current state. */
  value: T | null;
  onSelect: (value: T) => void;
  /** `thumbs`: large glyph (e.g. a preview image) above the label. */
  variant?: 'chips' | 'thumbs';
  onRemove?: (value: T) => void;
  /** Accessible name of the remove badge, e.g. "Remove look". */
  removeLabel?: string;
}

RulerSliderProps

Props for RulerSlider.

interface RulerSliderProps {
  /** Accessible name. */
  label: string;
  value: number;
  min: number;
  max: number;
  /** Precision of the value when dragging. Default 1. */
  step?: number;
  /** Distance between ticks, also the arrow-key step (Shift = ×10). Default `step`. */
  tickEvery?: number;
  /** Double-click / Enter resets to this. Default 0. */
  defaultValue?: number;
  /** Screen pixels per unit of value. Default 6. */
  unitWidth?: number;
  /** Draw a taller tick every N units. Default 5. */
  majorEvery?: number;
  /** Values that attract the ruler when dragged close (e.g. 0). Default `[defaultValue]`. */
  snapTo?: number[];
  format?: (value: number) => string;
  onChange: (value: number) => void;
  /**
   * A drag or key-press burst begins — use it to open one undo step. Separate key presses each
   * start one; `undoStep()` merges them in the store.
   */
  onChangeStart?: (source: ChangeSource) => void;
  /** …and ends. */
  onChangeEnd?: () => void;
  disabled?: boolean;
}

SegmentedControlProps

Props for SegmentedControl.

interface SegmentedControlProps<T extends string> {
  /** Accessible name of the group. */
  label: string;
  options: readonly SegmentedOption<T>[];
  value: T;
  onChange: (value: T) => void;
}

SizePreset

Built-in output sizes for the Resize tool (data only, so the labels can list their names).

interface SizePreset {
  id: string;
  /** Chip text. */
  label: string;
  width: number;
  height: number;
}

StickerOption

One of your own stickers (stickers prop).

interface StickerOption {
  id: string;
  /** Accessible name / tooltip. */
  label: string;
  /** Image URL (same-origin or CORS-enabled so exports can read it) or data URL. */
  src: string;
}

SwatchPickerProps

Props for SwatchPicker.

interface SwatchPickerProps {
  /** Hex colour or `null` (none). */
  value: string | null;
  onChange: (value: string | null) => void;
  /** Offer "none" (e.g. for fills). */
  allowNone?: boolean;
  swatches?: readonly string[];
}

ThemeMode

dark (default), light, or auto to follow the OS setting.

type ThemeMode = 'dark' | 'light' | 'auto';

ThemeOverrides

Typed shortcuts for the most common CSS variables. Anything else can be overridden in CSS: .iu-root { --iu-surface-1: #000; } — see docs/internal/THEMING.md for the full token list.

interface ThemeOverrides {
  accent?: string;
  accentHover?: string;
  accentContrast?: string;
  /** Accent-coloured text and icons on surfaces (e.g. selected tool label). Defaults to `accent`. */
  accentText?: string;
  bg?: string;
  stage?: string;
  surface1?: string;
  surface2?: string;
  surface3?: string;
  text?: string;
  textMuted?: string;
  border?: string;
  fontFamily?: string;
  /** Base control radius, e.g. `'10px'`. Small and large radii scale from it. */
  radius?: string;
}

ToolDefinition

A tool in the ToolRail. Built-in tools use exactly this API, so custom tools are first-class:

const stamp = defineTool({
  id: 'stamp',
  label: 'Stamp',
  icon: MyStampIcon,
  Controls: () => <button onClick={…}>Add stamp</button>,
});
<ImageEditor tools={['adjust', 'finetune', stamp]} />

Inside Controls / StageOverlay, use useEditorStore(), useEditorState() and useLabels().

interface ToolDefinition {
  /** Unique id. Built-in ids: see `ToolId`. */
  id: string;
  /** Text under the icon. Built-in tools read theirs from `labels.tools`. */
  label?: string;
  icon: ComponentType<IconProps>;
  /** Rendered in the ControlBar while the tool is active. */
  Controls: ComponentType;
  /** Rendered on top of the stage while the tool is active (e.g. the crop box). */
  StageOverlay?: ComponentType;
}

ToolInput

An entry of the tools prop: a built-in tool id or your own defineTool(...).

type ToolInput = ToolId | ToolDefinition;

WatermarkInput

The watermark prop: any watermark fields, plus logo (an image URL) for a logo watermark. Missing fields use the defaults (bottom-right, 5 % high, 70 % opacity).

type WatermarkInput = Partial<Omit<WatermarkState, 'assetId' | 'kind'>> & {
  logo?: string;
};

@image-ultra/core

Functions

applyLook

state with the look's colour settings (geometry, resize and assets untouched).

function applyLook(state: EditState, look: Look): EditState;

autoEnhance

Auto-enhance for the current image and geometry.

function autoEnhance(image: LoadedImage, state: EditState): Promise<AutoEnhanceResult>;

createEditorStore

Creates the editor's state store (image, edits, undo history, viewport, tools) with no UI attached.

function createEditorStore(options?: CreateEditorStoreOptions): EditorStore;

createEditState

A fresh state with no edits.

function createEditState(): EditState;

createLook

Saves the colour part of state (finetune, levels, curves, filter) as a named look.

function createLook(state: EditState, name: string, id?: string): Look;

detectImageFormat

Reads a file's header to tell what kind of image it really is (ignores the extension).

function detectImageFormat(blob: Blob): Promise<ImageFormat | null>;

exportImage

Renders the edited image at full quality and encodes it. Browser only.

function exportImage(image: LoadedImage, state: EditState, options?: ExportOptions): Promise<ExportResult>;

filterFromPreset

Turns a preset into the self-contained EditState.filter value.

function filterFromPreset(preset: FilterPreset, intensity?: number): FilterState;

getBeforeState

The "before" side of compare: the same framing (geometry + resize) with every look and overlay removed, so it lines up exactly with the edited result.

function getBeforeState(state: EditState): EditState;

loadImage

Decodes any supported source into an ImageBitmap, applying EXIF orientation. Browser only — call it from effects/event handlers, never during SSR. Every failure is an ImageLoadError (or the abort reason when signal fires).

function loadImage(source: ImageSource, options?: LoadImageOptions): Promise<LoadedImage>;

lookMatches

true when the state's colour settings are exactly the look's.

function lookMatches(state: EditState, look: Look): boolean;

parseEditState

Turns untrusted input (e.g. JSON from your database) into a valid EditState. Missing fields get defaults and numbers are clamped to their ranges. Throws EditStateError if the input isn't an edit state at all or comes from a newer version.

function parseEditState(input: unknown): EditState;

parseLooks

Validates untrusted looks (e.g. from localStorage); invalid entries are dropped.

function parseLooks(input: unknown): Look[];

renderImage

Headless rendering: apply a saved EditState to an image without any UI. state may be an EditState or untrusted JSON (it is validated).

function renderImage(source: ImageSource | LoadedImage, state?: EditState | unknown, options?: ExportOptions): Promise<ExportResult>;

renderToCanvas

Renders the edited image onto a 2D canvas (WebGL2 first, Canvas2D fallback). background flattens transparency (used for JPEG). Browser only.

function renderToCanvas(image: LoadedImage, state: EditState, options?: Pick<ExportOptions, 'maxWidth' | 'maxHeight' | 'renderer'> & {
  background?: string;
}): Promise<RenderedCanvas>;

selectCanRedo

Store selector: is there a step to redo?

const selectCanRedo: (s: EditorState) => boolean;

selectCanUndo

Selectors for common derived values.

const selectCanUndo: (s: EditorState) => boolean;

selectIsDirty

Store selector: do the edits differ from the ones the photo opened with?

const selectIsDirty: (s: EditorState) => boolean;

Classes

EditStateError

Thrown by parseEditState when the input can't be read as edits.

class EditStateError extends Error {
  readonly name = "EditStateError";
}

ImageLoadError

Why a photo didn't open (code): unsupported format, damaged file, not an image, or a download error.

class ImageLoadError extends Error {
  readonly name = "ImageLoadError";
  readonly code: ImageLoadErrorCode;
  /** Detected format (e.g. `ico`, `heic`), when known. */
  readonly format: ImageFormat | null;
  constructor(message: string, code: ImageLoadErrorCode, options?: {
    format?: ImageFormat | null;
    cause?: unknown;
  });
}

Constants

DEFAULT_WATERMARK

The watermark a new one starts from.

const DEFAULT_WATERMARK: WatermarkState;

EDIT_STATE_VERSION

EditState — the single, serializable description of every edit. The source image is never modified; rendering = source image + EditState. Save it with JSON.stringify, restore it with parseEditState.

const EDIT_STATE_VERSION = 1;

FILTER_PRESETS

The built-in filter looks.

const FILTER_PRESETS: readonly FilterPreset[];

FINETUNE_KEYS

Display order of the adjustments.

const FINETUNE_KEYS: readonly ["brightness", "contrast", "saturation", "vibrance", "exposure", "highlights", "shadows", "temperature", "tint", "hue", "gamma", "clarity", "sharpen", "blur", "grain", "vignette"];

FINETUNE_RANGES

Allowed range of each adjustment.

const FINETUNE_RANGES: Record<keyof FinetuneState, readonly [min: number, max: number]>;

FRAME_STYLES

Display order in the Frame tool.

const FRAME_STYLES: readonly ["border", "rounded", "bevel", "line", "double", "inset", "plus", "lumber", "corners", "polaroid"];

MAX_OUTPUT_SIDE

Largest output side we accept (also the practical GPU limit on most devices).

const MAX_OUTPUT_SIDE = 16384;

REDACT_STYLES

Redaction styles: pixelate, blur or solid.

const REDACT_STYLES: readonly ["pixelate", "blur", "solid"];

TOOL_IDS

Built-in tool ids, in their default ToolRail order.

const TOOL_IDS: readonly ["adjust", "finetune", "filter", "annotate", "redact", "sticker", "frame", "fill", "resize", "watermark"];

WATERMARK_ELEMENT_ID

Id of the watermark's element (marker, and its selection on the photo).

const WATERMARK_ELEMENT_ID = "watermark";

WATERMARK_POSITIONS

Where a watermark can go: 9 anchors, custom and tile.

const WATERMARK_POSITIONS: readonly ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right", "custom", "tile"];

Types

AutoEnhanceResult

Suggested settings from Auto-enhance. Only the listed finetune keys are touched.

interface AutoEnhanceResult {
  finetune: Pick<FinetuneState, 'temperature' | 'tint' | 'vibrance' | 'contrast' | 'shadows' | 'highlights'>;
  levels: LevelsState;
}

BackgroundState

What shows through transparent parts of the result (PNGs, round crops, rounded frames). image refers to an entry in EditState.assets; blur is a blurred copy of the result.

type BackgroundState = {
  kind: 'color';
  color: string;
} | {
  kind: 'image';
  assetId: string;
} | {
  kind: 'blur';
};

CanvasState

Extra space around the photo (DECISIONS #83), e.g. to make a landscape photo square for a post without cutting anything off. The added area is transparent, so the Fill shows there, and annotations can sit on it. Measured around the crop; the output covers crop + space.

interface CanvasState {
  /** Width ÷ height of the whole canvas, or `null` to only add `padding`. Only ever adds space. */
  aspect: number | null;
  /** Space on every side, as a share of the photo's short side (0…1). */
  padding: number;
  /** Where the photo sits in the extra space: 0 = left / top, 0.5 = centre, 1 = right / bottom. */
  anchor: {
    x: number;
    y: number;
  };
}

ChangeOptions

Options for update / beginChange.

interface ChangeOptions {
  /**
   * Merge with the previous step when it had the same label, was also coalesced, and ended less
   * than `coalesceMs` ago with nothing recorded since — e.g. repeated arrow-key presses on one
   * control become one undo step.
   */
  coalesce?: boolean;
}

CreateEditorStoreOptions

Options for createEditorStore.

interface CreateEditorStoreOptions {
  defaultTool?: string;
  viewport?: ViewportOptions;
  /** Maximum undo steps kept. Default 250. */
  historyLimit?: number;
  /** How long after a coalesced step the next one still merges with it. Default 600 ms. */
  coalesceMs?: number;
}

CropShape

Crop outline: rect or ellipse (a round crop).

type CropShape = 'rect' | 'ellipse';

CropView

Transform used while cropping: stage px = oriented px × scale + (x, y). The crop rectangle is fitted and centred on the stage; the rest of the image extends around it.

interface CropView {
  scale: number;
  x: number;
  y: number;
}

CurveChannel

A tone curve: rgb (all) or one colour channel.

type CurveChannel = keyof CurvesState;

CurvePoint

A tone-curve control point [input, output], both 0…1.

type CurvePoint = [x: number, y: number];

CurvesState

Tone curves. Each list is sorted by x and always includes x = 0 and x = 1.

interface CurvesState {
  rgb: CurvePoint[];
  red: CurvePoint[];
  green: CurvePoint[];
  blue: CurvePoint[];
}

DrawnShape

Shapes you draw and style (everything except redaction areas and the watermark marker).

type DrawnShape = RectShape | EllipseShape | LineShape | PathShape | TextShape | ImageShape;

EditAsset

An image stored in the edits (stickers, logos, pasted images, a Fill image).

type EditAsset = RasterAsset;

EditorActions

Everything the store can do: load, change edits, undo / redo, zoom, export.

interface EditorActions {
  /** Opens an image. `state` restores previously saved edits. Clears history. */
  load(source: ImageSource, options?: {
    state?: EditState;
  }): Promise<void>;
  /** Show the error screen, e.g. when the preview can't be drawn. */
  fail(error: Error): void;
  setActiveTool(tool: string): void;
  setCropView(view: CropView | null): void;
  /** Replace one tool's transient UI state. */
  setToolState(toolId: string, value: unknown): void;
  setStageSize(size: Size): void;
  setViewport(viewport: Viewport, options?: Pick<ViewportChangeOptions, 'animate'>): void;
  zoomTo(scale: number, options?: ViewportChangeOptions): void;
  zoomBy(factor: number, options?: ViewportChangeOptions): void;
  fit(options?: Pick<ViewportChangeOptions, 'animate'>): void;
  setAnimationMs(ms: number): void;
  /** Applies one undoable change. During `beginChange`/`endChange` it applies live instead. */
  update(label: string, recipe: EditRecipe, options?: ChangeOptions): void;
  /** Start a continuous change (e.g. slider drag). All `update` calls merge into one undo step. */
  beginChange(label: string, options?: ChangeOptions): void;
  /** Finish the continuous change and record it (if anything changed). */
  endChange(): void;
  /** Abort the continuous change and restore the state from `beginChange`. */
  cancelChange(): void;
  undo(): void;
  redo(): void;
  /** Jump through history: negative = back, positive = forward (one change, one `onChange`). */
  jump(steps: number): void;
  setCompare(value: number | null): void;
  /** Back to `initialEdit`, as an undoable step. `label`: the step's name (UI language). */
  reset(label?: string): void;
  /** Replace the whole edit state, e.g. from saved JSON. Clears history. */
  replaceEdit(state: EditState): void;
  /** Renders and encodes the current result. */
  exportImage(options?: ExportOptions): Promise<ExportResult>;
  /** Runs a cancellable job and tracks its progress in `tasks`. */
  runTask<T>(label: string, run: (context: TaskContext) => Promise<T>): Promise<T>;
  cancelTask(id: string): void;
  /** Frees the decoded image, cancels tasks and animations. Call when unmounting. */
  destroy(): void;
}

EditorState

The store's data: status, image, edits, history, viewport, active tool.

interface EditorState {
  status: EditorStatus;
  image: LoadedImage | null;
  error: Error | null;
  /** Id of the selected tool: a built-in `ToolId` or a custom tool's id. */
  activeTool: string;
  /**
   * Set while a crop-style tool is active: the stage then shows the whole image framed around the
   * crop instead of the edited result. `null` = normal result view.
   */
  cropView: CropView | null;
  /** Size of the stage element in CSS px. */
  stageSize: Size;
  /** Where the edited result sits on the stage. */
  viewport: Viewport;
  /** True while the result is shown at "fit" — it then re-fits when the stage resizes. */
  isFitted: boolean;
  /** Duration used for animated zooms. The UI sets 0 for `prefers-reduced-motion`. */
  animationMs: number;
  /** All current edits. Immutable — replace it through the actions below. */
  edit: EditState;
  /** The state the image was opened with; "Reset" returns here. */
  initialEdit: EditState;
  history: History<EditState>;
  /** An open continuous change (slider drag) that becomes one history step on `endChange`. */
  pendingChange: {
    base: EditState;
    label: string;
    coalesce: boolean;
    /** Set when the change re-opened the previous step: what `cancelChange` goes back to. */
    reopened?: {
      edit: EditState;
      history: History<EditState>;
    };
  } | null;
  tasks: EditorTask[];
  /**
   * Transient UI state owned by tools (e.g. Annotate's current drawing tool and selection),
   * shared between a tool's Controls and StageOverlay. Not part of the edit or history.
   */
  toolState: Record<string, unknown>;
  /**
   * Before/after compare (UI only, never part of the edit): `null` = off, `1` = the whole image
   * shows "before", `0…1` = split view with the divider at that fraction of the stage width.
   */
  compare: number | null;
}

EditorStatus

idle (no photo), loading, ready or error.

type EditorStatus = 'idle' | 'loading' | 'ready' | 'error';

EditorStore

The editor's store (a zustand store): getState(), subscribe().

type EditorStore = StoreApi<EditorStoreState>;

EditorStoreState

The store's data and actions together (EditorState & EditorActions).

type EditorStoreState = EditorState & EditorActions;

EditorTask

A long-running job (export, AI…) the UI can show progress for and cancel.

interface EditorTask {
  id: string;
  label: string;
  /** 0…1, or `null` when the duration is unknown. */
  progress: number | null;
}

EditRecipe

Changes an EditState. Mutate the draft (Immer) or return a whole new state: store.update('Rotate', (s) => { s.geometry.rotation = 90; })

type EditRecipe = (draft: Draft<EditState>) => void | EditState;

EditState

Every edit, as serializable JSON: geometry, colour, filter, elements, frame, fill, watermark, output size and the images they use. The source photo is never changed.

interface EditState {
  version: typeof EDIT_STATE_VERSION;
  geometry: GeometryState;
  finetune: FinetuneState;
  levels: LevelsState;
  curves: CurvesState;
  /** Applied before finetune, so adjustments tweak the filtered look. */
  filter: FilterState | null;
  /** Vector shapes on top of the photo, bottom → top (see `annotations.ts`). */
  annotations: Shape[];
  /** Decorative frame over the photo's edges (see `frame.ts`). */
  frame: FrameState | null;
  /** What shows through transparent parts (colour, image or a blurred copy). */
  background: BackgroundState | null;
  /** Text or logo on top of everything (see `watermark.ts`). */
  watermark: WatermarkState | null;
  /** Space added around the photo, or `null` for none (see `CanvasState`). */
  canvas: CanvasState | null;
  /** Output size in pixels, or `null` to keep the canvas's (crop + added space) size. */
  resize: ResizeState | null;
  assets: Record<string, EditAsset>;
}

EllipseShape

An ellipse element.

interface EllipseShape extends ShapeBase {
  type: 'ellipse';
  /** Bounding box. */
  x: number;
  y: number;
  width: number;
  height: number;
  fill: Paint;
  stroke: Paint;
  strokeWidth: number;
}

ExportMimeType

Output formats: JPEG, PNG or WebP.

type ExportMimeType = 'image/png' | 'image/jpeg' | 'image/webp';

ExportOptions

How an export is encoded and sized.

interface ExportOptions {
  /** Default: the source type when it's PNG/JPEG/WebP, otherwise PNG. */
  mimeType?: ExportMimeType;
  /** 0…1 for JPEG/WebP. Default 0.92 (JPEG) / 0.9 (WebP). */
  quality?: number;
  /** Scale the result down (never up) to fit these bounds. */
  maxWidth?: number;
  maxHeight?: number;
  /** File name without extension. Default: the source name, else `image`. */
  fileName?: string;
  /** Fills transparent areas for formats without alpha (JPEG). Default `#ffffff`. */
  background?: string;
  /** Force a renderer; default tries WebGL2 then Canvas2D. */
  renderer?: RendererKind | 'auto';
  /**
   * Keep the photo's EXIF (camera, lens, date, copyright…) — JPEG to JPEG only. Default `false`:
   * exports carry no metadata. GPS location is removed even when `true`; pass
   * `{ location: true }` to keep it too. The embedded thumbnail and maker notes are never kept,
   * orientation is reset (the pixels are already turned) and the size fields are updated.
   */
  keepMetadata?: boolean | {
    location?: boolean;
  };
}

ExportResult

The exported image, its size and type, and the edits that produced it.

interface ExportResult {
  blob: Blob;
  width: number;
  height: number;
  /** The type actually produced (browsers may fall back to PNG for unsupported types). */
  mimeType: string;
  /** File name with extension, e.g. `photo-edited.jpg`. */
  fileName: string;
  /** The edits that produced this image — store it to re-open the editor later. */
  state: EditState;
  renderer: RendererKind;
  /**
   * The result is smaller than asked for: the browser can't hold a canvas that big (e.g. ~16 MP
   * on iPhones, ~268 MP on desktop browsers), so it was scaled down to fit.
   */
  downscaled: boolean;
}

FilterCategory

Filter groups in the Filter tool.

type FilterCategory = 'color' | 'film' | 'mono';

FilterPreset

Built-in filter looks. Each is a colour matrix and/or tone curves — no bitmaps, so they're tiny, resolution-independent and stored in full inside EditState.filter.

interface FilterPreset {
  id: string;
  name: string;
  category: FilterCategory;
  matrix?: number[];
  curves?: Partial<CurvesState>;
}

FilterState

A filter look. The full definition is stored (not just an id) so a saved EditState renders the same anywhere, even without the preset list that created it.

interface FilterState {
  /** Preset id, e.g. `chrome` — used to highlight the chip. */
  id: string;
  /** Display name at the time it was applied. */
  name: string;
  /** 0…1 blend between the original and the filtered colours. */
  intensity: number;
  /**
   * Row-major 3×4 colour matrix on 0…1 RGB: `r' = m0·r + m1·g + m2·b + m3`, etc.
   * Omit for "curves only".
   */
  matrix?: number[];
  /** Tone curves applied after the matrix. */
  curves?: Partial<CurvesState>;
}

FinetuneState

Colour and detail adjustments. 0 always means "unchanged". Most values are −1…1; sharpen, blur and grain are 0…1 (see FINETUNE_RANGES).

interface FinetuneState {
  brightness: number;
  contrast: number;
  saturation: number;
  /** Boosts muted colours more than already-saturated ones. */
  vibrance: number;
  /** ±2 photographic stops at the extremes. */
  exposure: number;
  /** Brightens (positive) or recovers (negative) the brightest tones. */
  highlights: number;
  /** Lifts (positive) or deepens (negative) the darkest tones. */
  shadows: number;
  /** Negative = cooler (blue), positive = warmer (amber). */
  temperature: number;
  /** Negative = green, positive = magenta. */
  tint: number;
  /** Rotates all hues, ±180° at the extremes. */
  hue: number;
  /** Positive brightens mid-tones, negative darkens them. */
  gamma: number;
  /** Local contrast (mid-tone detail). Negative softens. */
  clarity: number;
  sharpen: number;
  blur: number;
  /** Film grain amount. */
  grain: number;
  /** Positive darkens the edges, negative lightens them. */
  vignette: number;
}

FrameState

A frame around the photo.

interface FrameState {
  style: FrameStyle;
  /** Thickness as a fraction of the output's short side (0.005…0.2). */
  size: number;
  color: string;
}

FrameStyle

Frame styles, drawn over the photo's edges (the output size never changes).

type FrameStyle = 'border' | 'rounded' | 'bevel' | 'line' | 'double' | 'inset' | 'plus' | 'lumber' | 'corners' | 'polaroid';

GeometryState

Rotation, flips, straighten, perspective and crop.

interface GeometryState {
  /** Clockwise rotation in 90° steps, applied first. */
  rotation: QuarterTurn;
  /** Mirror horizontally / vertically, as seen after `rotation`. */
  flipX: boolean;
  flipY: boolean;
  /** Fine rotation in degrees (−45…45, clockwise), around the image centre. */
  straighten: number;
  /**
   * Perspective correction: `x` tilts around the vertical axis (left/right edges),
   * `y` around the horizontal axis (top/bottom edges). −1…1, 0 = none.
   */
  perspective: {
    x: number;
    y: number;
  };
  /**
   * Crop in "oriented" space: the image after rotation, flip, straighten and perspective, with the
   * origin at the top-left of the rotated-but-not-straightened frame. `null` keeps the whole image.
   */
  crop: Rect | null;
  /** Locked crop aspect ratio (width ÷ height), or `null` for free-form. */
  cropAspect: number | null;
  /** `ellipse` makes the result round (transparent corners, or `background` for JPEG). */
  cropShape: CropShape;
}

History

Undo / redo steps.

interface History<T> {
  past: HistoryEntry<T>[];
  future: HistoryEntry<T>[];
}

HistoryEntry

Snapshot history. Each entry stores a full immutable state; Immer's structural sharing makes that cheap (unchanged parts are shared between snapshots). See DECISIONS.md #20.

interface HistoryEntry<T> {
  state: T;
  /** What the change *after* this snapshot did, e.g. "Rotate". Shown in the history panel. */
  label: string;
}

ImageFormat

Image formats recognised from a file's first bytes.

type ImageFormat = 'jpeg' | 'png' | 'gif' | 'webp' | 'avif' | 'heic' | 'bmp' | 'ico' | 'tiff' | 'svg' | 'psd';

ImageLoadErrorCode

Why an image didn't open: - unsupported — a real image, but in a format this browser can't show (e.g. HEIC in Chrome). - damaged — looks like a supported image but can't be decoded (truncated/corrupt file). - not-image — the file isn't an image at all (PDF, text…). - network — the URL couldn't be downloaded (404, offline, blocked by CORS).

type ImageLoadErrorCode = 'unsupported' | 'damaged' | 'not-image' | 'network';

ImageShape

An image element (sticker, logo, pasted image).

interface ImageShape extends ShapeBase {
  type: 'image';
  x: number;
  y: number;
  width: number;
  height: number;
  /** Key into `EditState.assets`. */
  assetId: string;
}

ImageSource

Anything the editor can open. - string: an http(s) URL, data: URL or blob: URL - Blob / File: e.g. from an <input type="file"> - DOM image sources you already have in memory

type ImageSource = string | Blob | HTMLImageElement | HTMLCanvasElement | ImageBitmap;

LevelsState

Input levels: black/white points (0…1) and a mid-tone shift (−1…1, positive = brighter).

interface LevelsState {
  black: number;
  white: number;
  mid: number;
}

LineCap

Line ends: none, arrow or circle.

type LineCap = 'none' | 'arrow' | 'circle';

LineShape

A line or arrow element.

interface LineShape extends ShapeBase {
  type: 'line';
  /** Start and end. */
  points: [Point, Point];
  stroke: string;
  strokeWidth: number;
  startCap: LineCap;
  endCap: LineCap;
}

LoadedImage

The decoded source image. Never mutated — every edit is described separately.

interface LoadedImage {
  /** Decoded pixels with EXIF orientation already applied. */
  bitmap: ImageBitmap;
  width: number;
  height: number;
  /** e.g. `image/jpeg`; `null` when unknown (DOM sources). */
  mimeType: string | null;
  /** Original file name without extension, when known. */
  name: string | null;
  /**
   * The source JPEG's EXIF block (TIFF bytes), read only for `ExportOptions.keepMetadata`.
   * Missing for other formats and DOM sources.
   */
  exif?: Uint8Array;
}

LoadImageOptions

Options for loadImage.

interface LoadImageOptions {
  /** Abort a slow load, e.g. when the user picks another image. */
  signal?: AbortSignal;
  /** Sent with URL requests. Defaults to `anonymous` so the canvas stays exportable. */
  crossOrigin?: 'anonymous' | 'use-credentials';
}

Look

A saved colour "look": finetune + levels + curves + filter. Geometry is not part of a look, so it can be applied to any photo.

interface Look {
  id: string;
  name: string;
  finetune: FinetuneState;
  levels: LevelsState;
  curves: CurvesState;
  filter: FilterState | null;
}

Paint

Any CSS colour, or null for none.

type Paint = string | null;

PathShape

Freehand pen stroke or polygon.

interface PathShape extends ShapeBase {
  type: 'path';
  points: Point[];
  closed: boolean;
  /** Draw with smooth curves through the points (pen) instead of straight segments (polygon). */
  smooth: boolean;
  fill: Paint;
  stroke: Paint;
  strokeWidth: number;
}

Point

A point in CSS pixels.

interface Point {
  x: number;
  y: number;
}

QuarterTurn

Clockwise quarter turns, in degrees.

type QuarterTurn = 0 | 90 | 180 | 270;

RasterAsset

A raster produced during editing (e.g. by an AI plugin): a background mask, an erased patch, an upscaled base image. Referenced from other parts of the state by its id in assets.

interface RasterAsset {
  kind: 'raster';
  /** `data:` URL, `blob:` URL or remote URL. */
  src: string;
  width: number;
  height: number;
  mimeType: string;
  /** Who made it, e.g. `plugin-ai/remove-background@1`. */
  createdBy?: string;
  /** Hash of the inputs, so a result can be reused or re-generated. */
  inputHash?: string;
}

Rect

Axis-aligned rectangle in pixels.

interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

RectShape

A rectangle element.

interface RectShape extends ShapeBase {
  type: 'rect';
  x: number;
  y: number;
  width: number;
  height: number;
  fill: Paint;
  stroke: Paint;
  strokeWidth: number;
  cornerRadius: number;
}

RedactBox

A rectangular area.

interface RedactBox extends RedactionBase {
  kind: 'box';
  x: number;
  y: number;
  width: number;
  height: number;
}

RedactBrush

A painted area: a round brush along points, size = diameter (oriented px).

interface RedactBrush extends RedactionBase {
  kind: 'brush';
  points: Point[];
  size: number;
}

Redaction

A redaction area (box or brush stroke).

type Redaction = RedactBox | RedactBrush;

RedactShape

A redaction area (pixelate / blur / solid) as an element (DECISIONS #88): it hides the photo and every element below it in the list. Box or brush stroke — the fields of RedactBox / RedactBrush.

type RedactShape = (RedactBox | RedactBrush) & Omit<ShapeBase, 'id' | 'rotation' | 'type'> & {
  type: 'redact';
};

RedactStyle

Redactions hide parts of the photo (faces, plates, names). They live in oriented space, like annotations, and are drawn after the colour pipeline and before annotations (DECISIONS #68).

type RedactStyle = 'pixelate' | 'blur' | 'solid';

RenderedCanvas

A rendered result on a 2D canvas, before encoding.

interface RenderedCanvas {
  canvas: AnyCanvas;
  width: number;
  height: number;
  renderer: RendererKind;
  /** Smaller than asked for (see `ExportResult.downscaled`). */
  downscaled: boolean;
}

RendererKind

webgl2, or canvas2d where WebGL2 isn't available.

type RendererKind = 'webgl2' | 'canvas2d';

ResizeState

Final pixel size. Aspect may differ from the crop, which stretches the result.

interface ResizeState {
  width: number;
  height: number;
}

Shape

Any element on the photo: shapes, text, images, redaction areas, the watermark's place.

type Shape = RectShape | EllipseShape | LineShape | PathShape | TextShape | ImageShape | RedactShape | WatermarkShape;

ShapeType

Element types.

type ShapeType = 'rect' | 'ellipse' | 'line' | 'path' | 'text' | 'image' | 'redact' | 'watermark';

Size

A width/height pair in pixels.

interface Size {
  width: number;
  height: number;
}

TaskContext

Passed to runTask jobs: report progress, check for cancellation.

interface TaskContext {
  /** Aborted when the user cancels or the editor closes. Pass it to fetch/models. */
  signal: AbortSignal;
  progress(value: number | null): void;
}

TextAlign

Text alignment in a text element.

type TextAlign = 'left' | 'center' | 'right';

TextShape

A text element.

interface TextShape extends ShapeBase {
  type: 'text';
  /** Top-left of the text box; the height follows the wrapped text. */
  x: number;
  y: number;
  /** Wrap width. */
  width: number;
  text: string;
  fontFamily: string;
  fontSize: number;
  fontWeight: 400 | 700;
  fontStyle: 'normal' | 'italic';
  align: TextAlign;
  /** Line height as a multiple of the font size. */
  lineHeight: number;
  color: string;
  /** Optional box behind the text (label / highlight style). */
  background: Paint;
}

ToolId

Ids of the built-in tools.

type ToolId = (typeof TOOL_IDS)[number];

Viewport

Where the image sits on the stage. x/y are the screen position (CSS px, relative to the stage) of the image's top-left corner, scale is CSS px per image px.

interface Viewport {
  scale: number;
  x: number;
  y: number;
}

ViewportChangeOptions

Options for zoom / pan changes.

interface ViewportChangeOptions {
  /** Stage point that stays fixed while zooming. Defaults to the stage centre. */
  anchor?: Point;
  /** Animate the change using `animationMs`. */
  animate?: boolean;
}

WatermarkPosition

Nine spots on the photo, custom = wherever the user dragged it (x, y), or tile = a repeating, tilted pattern across all of it.

type WatermarkPosition = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | 'custom' | 'tile';

WatermarkShape

Where the watermark is drawn in the element order. Its look and layout stay in EditState.watermark; with no marker in the list it's drawn on top of everything. Only one; ignored when there's no watermark (and for an app-locked one, which is always on top).

interface WatermarkShape extends ShapeBase {
  type: 'watermark';
}

WatermarkState

A text or logo watermark: position, size, opacity, colour.

interface WatermarkState {
  kind: 'text' | 'image';
  /** For `text`. */
  text: string;
  fontFamily: string;
  fontWeight: 400 | 700;
  color: string;
  /** For `image`: an entry in `EditState.assets`. */
  assetId: string | null;
  position: WatermarkPosition;
  /** Centre of the mark for `custom`, as fractions of the output's width / height. */
  x: number;
  y: number;
  /** Degrees clockwise around the mark's centre (not for `tile`, which has its own tilt). */
  rotation: number;
  /**
   * 0.01…1: share of the largest size that fits — 1 = the mark fills the photo's width (or height)
   * inside the margin. For `tile`, 1 = marks half that big.
   */
  size: number;
  opacity: number;
  /** Distance from the edges as a fraction of the output's short side (0…0.25). */
  margin: number;
}