An image editor: a canvas where a person pans, zooms, rotates, flips and crops an image, and a helper that describes the edits to apply when saving it. Cropping is the first thing it does. The media editor is built on it.
This package is set to replace @wordpress/image-cropper. That package stays as it is for now. It will be rebuilt on the maths in this package’s core, and deprecated once it is.
Installation
Install the module
npm install @wordpress/image-editor --save
Styles
The canvas needs the editor’s styles to lay out at all, so load them. They ship in build-style/style.css:
@use "@wordpress/image-editor/build-style/style.css";
The focus ring and the focused resize handle use the admin theme colour. Set --wp-image-editor-focus-color on an ancestor to change it.
Outside WordPress
The package needs no WordPress site, and nothing it imports talks to one. Two things to know:
- The examples import React hooks from
@wordpress/element, which re-exports React. Import them fromreactinstead if you prefer. - The editor’s own text, such as the labels on the resize handles and what a screen reader announces, is in English unless you give
@wordpress/i18nyour translations.
Quick Start
You keep the editor’s state, and the controller hands you each change.
import { useState } from '@wordpress/element';
import {
ImageEditorCanvas,
createImageEditorState,
useImageEditor,
} from '@wordpress/image-editor';
function MyImageEditor( { src }: { src: string } ) {
const [ state, setState ] = useState( () => createImageEditorState() );
const controller = useImageEditor( { state, onChange: setState } );
return (
<div style={ { width: 600, height: 400 } }>
<ImageEditorCanvas
src={ src }
controller={ controller }
showGrid="interactive"
/>
</div>
);
}
API
ImageEditorCanvas
The canvas the person edits the image on. It fills its parent, so render it inside a sized element.
src(required): the image URL.controller(required): the controller fromuseImageEditor.showGrid: show the rule-of-thirds grid:true,falseor'interactive'(only while the crop is being changed). Defaults tofalse.showDimming: dim the image outside the crop area. Defaults totrue.showDimensions: show the crop size in pixels while resizing. Defaults totrue.freeformCrop: show resize handles so the crop area can be resized and moved. Defaults totrue. Whenfalse, the crop area is fixed and centred, and the image pans and zooms under it.focusOnMount: focus the crop area when it mounts. Defaults tofalse.disabled: ignore all crop interaction, for example while saving, so the edit cannot change after saving starts. A gesture already in progress is cancelled. Defaults tofalse.onImageLoaded( size ): called with the image’s natural size when the image loads.onImageError(): called when the image fails to load.onGestureStart(): called when a drag, resize or pinch starts. Use it withonGestureEndto record a gesture as one undo entry.onGestureEnd(): called when a drag, resize or pinch ends.className: an extra class for the container.
useImageEditor( { state, onChange } )
React hook that returns the editor’s controller. Pass the current state and a function that stores the next one. onChange( nextState, { isEdit } ) gets isEdit: false for changes the person did not make, such as an image loading or the crop being refitted after a window resize, which usually should not get an undo entry.
The controller has:
state: the current state.setPan( { x, y } ),setZoom( zoom ),setRotation( degrees ),snapRotate90( 1 | -1 ),setFlip( { horizontal, vertical } ),toggleFlip( 'horizontal' | 'vertical' ),setCropRect( { x, y, width, height } )andsetAspectRatio( ratio )to change the edit. The crop area is in fractions of the view, from 0 to 1. The pan is an offset from the centre, so it is signed, and how far it can go depends on the zoom and the crop, because the image has to cover the crop.setAspectRatio( ratio )reshapes the crop to the largest box of that ratio, andsetAspectRatio( undefined )unlocks it. While a ratio is locked, the crop always has that ratio: a crop that does not match, fromsetCropRector a state you pass in, keeps its centre and width and takes the height the ratio needs.setImage( image )to load an image. The canvas calls it whensrcloads. The first image keeps the state you started from, such as an edit you saved earlier. Replacing or clearing a loaded image starts fresh: no pan, zoom, rotation, flip or crop from the old image, but a locked ratio stays.reset()to go back to the clean state, andisDirty, which istruewhen the pan, zoom, rotation, flip or crop differs from that state. The clean state is the state you started from, as the editor settled it before the person made an edit: loading the image and fitting the crop to a locked ratio are not edits. Soreset()keeps a locked ratio only when the state you started from had one; a ratio locked later is an edit, and reset undoes it along with the rest. Locking a ratio usually makes the edit dirty, because it reshapes the crop. Unlocking does not, and nor does locking a ratio the crop already has.minZoom,maxZoomandmaxRotationOffset: the limits for building zoom and rotation controls.minZoomis the smallest zoom that still covers the crop area, so it changes with the edit.
ImageEditorState, createImageEditorState( options? )
The editor’s state, and the state an editor starts from. Pass image when you already have one, and aspectRatio to lock the crop from the start; both are optional. Read the state, store it and pass it back, but create it with createImageEditorState and change it only through the controller: it also carries values the editor keeps for itself.
areImageEditorStatesEqual( a, b )
Whether two states have the same public edit fields, ignoring tiny floating-point differences. The values the editor keeps for later rotations do not count. Use it to skip entries that change nothing in your own undo history.
getOutputSize( state )
The dimensions that the edits from mapEditToOriginal would produce on the image in state.image. A nearly full crop that the mapper omits gives the full image dimensions. Use this for a size readout or a minimum-size check against the loaded image. A processor that edits a different file or rounds pixels may produce different dimensions. Returns null when no image has loaded.
mapEditToOriginal( state )
Use it when saving. The editor stores where the image sits, how far it is zoomed, and a crop area measured against the view. mapEditToOriginal turns that state into operations for the full-size image loaded in state.image. Here, “original” means the image loaded into this editor, even if that file was edited before.
The returned list contains flip, rotate and crop operations as needed, in the order to apply them. Crop positions and sizes are fractions of the image after rotation. An empty list means no image has loaded or there is no flip, rotation or crop to apply. A locked ratio can produce a crop operation before the person edits anything. The caller applies the operations and saves the result.
Not Yet Included
The first version covers what the media editor needs. Two additions are likely next, for simpler editors:
- A self-contained mode, where
useImageEditor()keeps the state itself. An editor without its own undo history would not need to store the state. - Exporting the edited image in the browser as a
Blob. An editor that uploads the result, rather than asking a server to edit the original, would need it.
More
Contributing to this package
This is an individual package that’s part of the Gutenberg project. The project is organized as a monorepo. It’s made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to npm and used by WordPress as well as other software projects.
To find out more about contributing to this package or Gutenberg as a whole, please read the project’s main contributor guide.