@bensitu/image-editor
A lightweight, TypeScript-first canvas image editor built on top of
Fabric.js v7. ImageEditor wraps a Fabric canvas
with image loading, scale and rotation, mask creation, Text and Draw
annotations, crop, Mosaic mode, base-image flips, history (undo/redo), layer operations, and
base64/file export — exposed as a single canonical class
with a stable public surface.
Demo
https://bensitu.github.io/image-editor/
Features
- TypeScript source with
.d.tsdeclarations published alongside the runtime - Single canonical class
ImageEditorexported as both default and named - Fabric.js v7 declared as a peer dependency (no bundled Fabric copy)
- Multi-format publish: ESM (
import), CommonJS (require), UMD (<script>), TypeScript declarations (types) - Transactional
loadImagewith rollback on decode, Fabric, downsample, or timeout failures - Animation queue serializes
scaleImage,rotateImage,resetImageTransform,undo, andredoso concurrent clicks never interleave - Bounded history stack with idempotent dispose
- Crop session with mask preservation toggle and atomic apply/cancel
- Mosaic mode with circular brush preview, runtime brush/block controls, and one undo step per successful pixelation click
- Unified editor-owned object model for base images, masks, annotations, and session overlays
- Text annotations, Draw mode, annotation update/delete APIs, and layer operations
- Base64 and
Fileexports with PNG/JPEG/WebP support, configurable multiplier, independent mask/annotation rendering toggles, and state-mutating mask/annotation merge APIs
Requirements
- Node.js:
>= 20for development / building from source - Fabric.js: peer dependency
^7.0.0(must be installed by the consumer) - Browsers: modern evergreen (Chrome, Firefox, Safari, Edge). The library ships ES2019-targeted JavaScript and uses the Fabric v7 promise-based API.
- TypeScript: strict consumers that compile dependencies with
skipLibCheck: falseshould include the ES2019 and DOM libraries intsconfig.json. Fabric v7.4 declarations also referencejsdomtypes, so install@types/jsdomwhen your project type-checks Fabric's declaration files.
Installation
npm install @bensitu/image-editor fabric
# or
pnpm add @bensitu/image-editor fabric
# or
yarn add @bensitu/image-editor fabric
fabric@^7.0.0 is a peer dependency: install it explicitly so the editor
resolves the exact version your application uses.
Module formats and entry points
The package ships a single public entry, resolved by tooling via the
exports map in package.json:
| Consumer | Resolves to |
|---|---|
ESM (import) |
dist/esm/index.js |
CommonJS (require) |
dist/cjs/index.cjs |
TypeScript (types) |
dist/types/index.d.ts |
UMD (<script>, unpkg, jsdelivr) |
dist/umd/image-editor.umd.js |
default fallback |
dist/esm/index.js |
The UMD bundle exposes a global named ImageEditor and treats fabric as an
external global named fabric.
Framework integration
The core editor is framework-agnostic and can be mounted with string IDs or HTMLElement refs. React, Vue, Next.js, Nuxt, and other frameworks should create and dispose the editor inside client-side lifecycle hooks.
Runnable examples:
Dual entry-point convention
ImageEditor's constructor accepts the Fabric module either explicitly (ESM
consumers) or via globalThis.fabric (UMD consumers). The same source ships
in all four formats:
Explicit module form (recommended for bundled apps): pass the Fabric module as the first argument.
import * as fabric from 'fabric'; import { ImageEditor } from '@bensitu/image-editor'; const editor = new ImageEditor(fabric, { canvasWidth: 800, canvasHeight: 600, });Global form (UMD
<script>consumers): omit the first argument; the constructor readsglobalThis.fabric.<script src="https://cdn.jsdelivr.net/npm/fabric@7/dist/index.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@bensitu/image-editor/dist/umd/image-editor.umd.js"></script> <script> const editor = new ImageEditor.ImageEditor({ canvasWidth: 800, canvasHeight: 600, }); </script>The UMD global is a namespace object.
ImageEditor.ImageEditorandImageEditor.defaultboth reference the constructor.
If neither form yields a usable Fabric module, the constructor logs a single
descriptive console.error and init() and loadImage() become no-ops that
resolve to undefined.
Quick start
HTML
<canvas id="canvas"></canvas>
<button id="zoomInButton">Zoom In</button>
<button id="zoomOutButton">Zoom Out</button>
<button id="rotateLeftButton">Rotate Left</button>
<input id="rotateLeftDegreesInput" type="number" value="90" />
<button id="rotateRightButton">Rotate Right</button>
<input id="rotateRightDegreesInput" type="number" value="90" />
<button id="flipHorizontalButton">Flip Horizontal</button>
<button id="flipVerticalButton">Flip Vertical</button>
<button id="createMaskButton">Add Mask</button>
<button id="removeSelectedMaskButton">Remove Mask</button>
<button id="removeAllMasksButton">Remove All Masks</button>
<ul id="maskList"></ul>
<button id="enterTextModeButton">Text</button>
<button id="exitTextModeButton">Exit Text</button>
<input id="textColorInput" type="color" value="#ff0000" />
<input id="textFontSizeInput" type="number" min="8" max="160" value="32" />
<button id="enterDrawModeButton">Draw</button>
<button id="exitDrawModeButton">Exit Draw</button>
<input id="drawColorInput" type="color" value="#ff0000" />
<input id="drawBrushSizeInput" type="range" min="1" max="80" value="8" />
<button id="removeSelectedAnnotationButton">Remove Annotation</button>
<button id="removeAllAnnotationsButton">Remove All Annotations</button>
<button id="deleteSelectedObjectButton">Delete Selected</button>
<button id="bringSelectedObjectForwardButton">Forward</button>
<button id="sendSelectedObjectBackwardButton">Backward</button>
<button id="bringSelectedObjectToFrontButton">Front</button>
<button id="sendSelectedObjectToBackButton">Back</button>
<ul id="annotationList"></ul>
<button id="enterCropModeButton">Crop</button>
<select id="cropAspectRatioSelect">
<option value="free">Free</option>
<option value="1:1">1:1</option>
<option value="3:4">3:4</option>
<option value="4:3">4:3</option>
<option value="3:2">3:2</option>
<option value="2:3">2:3</option>
<option value="9:16">9:16</option>
<option value="16:9">16:9</option>
</select>
<button id="applyCropButton">Apply Crop</button>
<button id="cancelCropButton">Cancel Crop</button>
<button id="enterMosaicModeButton">Mosaic</button>
<button id="exitMosaicModeButton">Exit Mosaic</button>
<label>
Brush size
<input id="mosaicBrushSizeInput" type="range" min="8" max="160" step="1" value="48" />
</label>
<label>
Block size
<input id="mosaicBlockSizeInput" type="range" min="2" max="40" step="1" value="8" />
</label>
<button id="mergeMasksButton">Merge Masks</button>
<button id="mergeAnnotationsButton">Merge Annotations</button>
<button id="downloadImageButton">Download</button>
<button id="undoButton">Undo</button>
<button id="redoButton">Redo</button>
<button id="resetImageTransformButton">Reset</button>
<input id="imageInput" type="file" accept="image/*" />
TypeScript / ESM
import * as fabric from 'fabric';
import { ImageEditor } from '@bensitu/image-editor';
import type { ImageEditorOptions, MaskConfig } from '@bensitu/image-editor';
const editor = new ImageEditor(fabric, {
canvasWidth: 800,
canvasHeight: 600,
backgroundColor: '#ffffff',
defaultMosaicConfig: {
brushSize: 48,
blockSize: 8,
},
defaultTextConfig: {
fill: '#ff0000',
fontSize: 32,
},
defaultDrawConfig: {
color: '#ff0000',
brushSize: 8,
},
} satisfies ImageEditorOptions);
editor.init({
canvas: 'canvas',
zoomInButton: 'zoomInButton',
zoomOutButton: 'zoomOutButton',
rotateLeftButton: 'rotateLeftButton',
rotateLeftDegreesInput: 'rotateLeftDegreesInput',
rotateRightButton: 'rotateRightButton',
rotateRightDegreesInput: 'rotateRightDegreesInput',
flipHorizontalButton: 'flipHorizontalButton',
flipVerticalButton: 'flipVerticalButton',
createMaskButton: 'createMaskButton',
removeSelectedMaskButton: 'removeSelectedMaskButton',
removeAllMasksButton: 'removeAllMasksButton',
maskList: 'maskList',
enterCropModeButton: 'enterCropModeButton',
cropAspectRatioSelect: 'cropAspectRatioSelect',
applyCropButton: 'applyCropButton',
cancelCropButton: 'cancelCropButton',
enterMosaicModeButton: 'enterMosaicModeButton',
exitMosaicModeButton: 'exitMosaicModeButton',
mosaicBrushSizeInput: 'mosaicBrushSizeInput',
mosaicBlockSizeInput: 'mosaicBlockSizeInput',
enterTextModeButton: 'enterTextModeButton',
exitTextModeButton: 'exitTextModeButton',
textColorInput: 'textColorInput',
textFontSizeInput: 'textFontSizeInput',
enterDrawModeButton: 'enterDrawModeButton',
exitDrawModeButton: 'exitDrawModeButton',
drawColorInput: 'drawColorInput',
drawBrushSizeInput: 'drawBrushSizeInput',
removeSelectedAnnotationButton: 'removeSelectedAnnotationButton',
removeAllAnnotationsButton: 'removeAllAnnotationsButton',
deleteSelectedObjectButton: 'deleteSelectedObjectButton',
mergeAnnotationsButton: 'mergeAnnotationsButton',
bringSelectedObjectForwardButton: 'bringSelectedObjectForwardButton',
sendSelectedObjectBackwardButton: 'sendSelectedObjectBackwardButton',
bringSelectedObjectToFrontButton: 'bringSelectedObjectToFrontButton',
sendSelectedObjectToBackButton: 'sendSelectedObjectToBackButton',
annotationList: 'annotationList',
mergeMasksButton: 'mergeMasksButton',
downloadImageButton: 'downloadImageButton',
undoButton: 'undoButton',
redoButton: 'redoButton',
resetImageTransformButton: 'resetImageTransformButton',
imageInput: 'imageInput',
});
// Load an image programmatically (base64 data URL).
await editor.loadImage('data:image/jpeg;base64,...');
// Add a rectangular mask, then export the result as base64.
const mask: MaskConfig = { shape: 'rect', width: 120, height: 80, left: '25%', top: '25%' };
editor.createMask(mask);
editor.createTextAnnotation({ text: 'Label', left: 120, top: 80 });
editor.enterDrawMode();
editor.setDrawConfig({ color: '#00aaff', brushSize: 10 });
const dataUrl = await editor.exportImageBase64({ fileType: 'png' });
CommonJS
const fabric = require('fabric');
const { ImageEditor } = require('@bensitu/image-editor');
const editor = new ImageEditor(fabric, { canvasWidth: 800, canvasHeight: 600 });
In v2, require('@bensitu/image-editor') returns a namespace object with
ImageEditor, default, and the editor object guards
(isBaseImageObject, isMaskObject, isAnnotationObject,
isTextAnnotationObject, isDrawAnnotationObject, isSessionObject, and
isEditableOverlayObject); it does not return the constructor directly.
Public API
ImageEditor is the only public class. The package barrel re-exports it as
both the default export and a named export, alongside the editor object guards
and the documented public types. Internal helpers (animation queue, command, history
manager, controllers, services, managers, utility modules) are intentionally
not exported and may change without notice.
Object model
Every editor-owned Fabric object carries strict editorObjectKind metadata:
| Kind | Meaning |
|---|---|
baseImage |
The committed image at the bottom of the stack. |
mask |
Editable mask overlay with required maskId, maskUid, and maskName. |
annotation |
Editable Text or Draw overlay. Masks are not annotations. |
session |
Internal crop labels, mask labels, Mosaic previews, and tool previews. |
Session objects are never persisted, exported, or user-deletable. Strict type
guards reject legacy mask-like objects that do not carry editorObjectKind.
Constructor
new ImageEditor(fabric: FabricModule, options?: ImageEditorOptions)
new ImageEditor(options?: ImageEditorOptions) // UMD: reads globalThis.fabric
Lifecycle
| Method | Description |
|---|---|
init(elementMap?) |
Bind the editor to DOM elements. Pass string IDs, HTMLElement refs, or null for unmanaged optional controls. |
dispose() |
Tear down the editor, drain DOM bindings, and dispose the Fabric canvas. Idempotent. |
disposeAsync() |
Same teardown as dispose(), resolving after Fabric canvas disposal settles. Idempotent. |
dispose() is synchronous and starts Fabric canvas teardown. If an integration
must immediately create another editor on the same <canvas> element, wait for
the next microtask or animation frame before reusing that element, or call
await disposeAsync().
Image loading
| Method | Description |
|---|---|
loadImage(base64, options?) |
Load a supported raster image data URL (png, jpeg, or webp). Returns Promise<void>. Transactional: any failure restores the prior canvas, scroll, overflow, and snapshot state. |
isImageLoaded() |
Returns true if a valid image is currently loaded on the canvas. |
isBusy() |
Returns true while the editor is loading, animating, or in Crop, Mosaic, Text, or Draw mode. |
isProcessing() |
Returns true while an async load, export/merge transaction, or animation is active, excluding tool modes. |
setLayoutMode(mode) |
Select the layout strategy for future image loads. mode is 'fit', 'cover', or 'expand'. |
setCanvasSize(width, height) |
Resize the Fabric canvas to explicit positive pixel dimensions. Invalid values warn and no-op. |
resizeToContainer(options?) |
Resize the canvas to canvasContainer.clientWidth/clientHeight, optionally using fallback dimensions for hidden containers. |
relayout(options?) |
Re-measure the host layout and refresh canvas geometry without reloading the current image or dropping overlays. |
LoadImageOptions currently includes preserveScroll?: boolean for
preserving the container's scroll position across both successful loads and
rollback paths.
File-input loading normalizes supported JPEG EXIF orientation by default, so
phone photos with sideways encoded pixels are displayed upright. Set
autoOrientImage: false to preserve the raw encoded orientation. Non-identity
orientations are normalized through a canvas and re-encoded as JPEG; set
autoOrientImageQuality to control that JPEG quality independently, or leave it
null to use downsampleQuality. This applies only to JPEG files loaded
through the file-input path; PNG/WebP files and loadImage(dataUrl) use the
existing path, and arbitrary EXIF metadata is not preserved. If the browser
cannot decode the raw JPEG orientation with createImageBitmap(..., { imageOrientation: 'none' }), auto-orientation is skipped with an onWarning
and the original file data is loaded.
Input size guards run before browser image decode. maxInputBytes limits the
encoded file size or decoded base64 payload size, and maxInputPixels rejects
PNG/JPEG/WebP files whose header dimensions exceed the configured source-pixel
budget when those dimensions can be read cheaply.
Read-only state
| Method | Description |
|---|---|
getEditorState() |
Return a safe snapshot of image, transform, tool-mode, busy, and history state. |
getImageInfo() |
Return committed image dimensions/display geometry, or null before image load. |
getMasks() |
Return a shallow array snapshot of current live mask objects in canvas order. |
getAnnotations() |
Return a shallow array snapshot of current live annotation objects. |
getSelection() |
Return the current selected masks/annotations in the onSelectionChange shape. |
getActiveToolMode() |
Return 'crop', 'mosaic', 'text', 'draw', or null. |
const state = editor.getEditorState();
const imageInfo = editor.getImageInfo();
const masks = editor.getMasks();
const selection = editor.getSelection();
const activeToolMode = editor.getActiveToolMode();
getEditorState() and getImageInfo() return defensive data snapshots.
getMasks(), getAnnotations(), and object references inside
getSelection() / lifecycle callbacks are different: they return new arrays or
payload objects, but the mask and annotation elements are the live Fabric
objects on the canvas. Treat those objects as read-only from integration code.
Direct mutations such as mask.set(...) or annotation.set(...) bypass editor
history, metadata synchronization, and change callbacks.
The read-only methods and lifecycle callbacks use these public payload types:
interface ImageInfo {
width: number;
height: number;
displayWidth: number;
displayHeight: number;
scale: number;
rotation: number;
canvasWidth: number;
canvasHeight: number;
}
interface ImageEditorState {
hasImage: boolean;
image: ImageInfo | null;
maskCount: number;
annotationCount: number;
currentScale: number;
currentRotation: number;
isFlippedHorizontally: boolean;
isFlippedVertically: boolean;
isBusy: boolean;
activeToolMode: EditorToolMode | null;
isCropMode: boolean;
isMosaicMode: boolean;
isTextMode: boolean;
isDrawMode: boolean;
canUndo: boolean;
canRedo: boolean;
canvasWidth: number;
canvasHeight: number;
}
interface ImageEditorSelection {
selectedMask: MaskObject | null;
selectedMasks: MaskObject[];
selectedAnnotation: AnnotationObject | null;
selectedAnnotations: AnnotationObject[];
selectedObjectKind: 'mask' | 'annotation' | null;
}
interface ImageEditorCallbackContext {
operation: ImageEditorOperation;
isInternalOperation?: boolean;
}
ImageEditorOperation is the public union of operation names that can trigger
callbacks, such as 'loadImage', 'createMask', 'undo', and 'dispose'.
Use defaultLayoutMode to choose the initial image-load strategy, then call
setLayoutMode() when a UI should change how future images are placed:
const editor = new ImageEditor(fabric, {
defaultLayoutMode: 'fit',
});
await editor.loadImage(imageA);
// Future loads use cover. The current image is not re-laid out immediately.
editor.setLayoutMode('cover');
await editor.loadImage(imageB);
Invalid JavaScript defaultLayoutMode values fall back to 'expand'.
Invalid setLayoutMode() calls are ignored and preserve the current mode.
File-input helpers accept JPG, PNG, and WebP files. Export output remains controlled by the JPEG, PNG, or WebP export options.
Transforms
| Method | Description |
|---|---|
scaleImage(factor) |
Scale to factor (clamped to [minScale, maxScale]). Non-finite values are no-ops. Animated. |
rotateImage(degrees) |
Rotate to degrees. Non-finite values resolve without changing canvas state. Animated. |
flipHorizontal() |
Toggle horizontal flip on the base image only. Masks, annotations, and session overlays are not mirrored. Returns Promise<void>. |
flipVertical() |
Toggle vertical flip on the base image only. Masks, annotations, and session overlays are not mirrored. Returns Promise<void>. |
resetImageTransform() |
Animate to scale 1, rotation 0, and an unflipped state. Records exactly one history entry covering the entire transform. |
await editor.flipHorizontal();
await editor.flipVertical();
Masks
| Method | Description |
|---|---|
createMask(config?) |
The single mask-creation entry point. Returns the new MaskObject or null. |
removeSelectedMask() |
Remove the currently selected mask and push one history entry. |
removeAllMasks(options?) |
Remove every mask. options.saveHistory defaults to true. |
MaskConfig supports rect, circle, ellipse, polygon, and a custom
fabricGenerator. Falsy values in styles (0, false, null, '',
NaN) are applied verbatim.
Every mask is marked as editorObjectKind: 'mask' and includes required
maskId, maskUid, and maskName metadata.
Use defaultMaskConfig to define constructor-level defaults for masks created
through either createMask() or the built-in createMaskButton. Per-call
createMask(config) values override defaultMaskConfig.
const editor = new ImageEditor(fabric, {
defaultMaskConfig: {
color: 'rgba(255, 0, 0, 0.35)',
alpha: 0.35,
styles: {
stroke: '#ff0000',
strokeWidth: 2,
strokeDashArray: [6, 4],
},
},
});
editor.createMask(); // Uses defaultMaskConfig.
editor.createMask({ color: 'rgba(0, 128, 255, 0.35)' }); // Per-call override.
Crop
| Method | Description |
|---|---|
enterCropMode(options?) |
Add an interactive crop rectangle on top of the image. |
setCropAspectRatio(ratio) |
Update the active crop rectangle ratio while crop mode is open. |
applyCrop() |
Apply the current crop region. Atomic: failure rolls back to the pre-crop snapshot. |
cancelCrop() |
Cancel crop mode and restore the prior canvas state without pushing a history entry. |
enterCropMode({ aspectRatio }) locks the crop rectangle to a preset or custom
ratio. Supported preset strings are 'free', '1:1', '3:4', '4:3',
'3:2', '2:3', '16:9', and '9:16'. Custom ratios use
{ width, height }. Per-call options override crop.aspectRatio from the
constructor.
When cropAspectRatioSelect is bound through init(elementMap), the built-in Crop
button uses the select's current value and changing the select while crop mode
is open calls setCropAspectRatio() to resize the active crop rectangle.
editor.enterCropMode({ aspectRatio: '1:1' });
editor.enterCropMode({ aspectRatio: '16:9' });
editor.setCropAspectRatio('4:3');
editor.enterCropMode({ aspectRatio: { width: 2, height: 1 } });
Mosaic mode
| Method | Description |
|---|---|
enterMosaicMode() |
Enter circular-brush Mosaic mode and show the hover preview on canvas. |
exitMosaicMode() |
Leave Mosaic mode and remove preview/session handlers. |
isMosaicMode() |
Returns true while a Mosaic session is active. |
getMosaicConfig() |
Returns a defensive copy of the current runtime Mosaic config. |
setMosaicConfig(config) |
Patch current Mosaic config without creating a history entry. |
resetMosaicConfig() |
Restore current Mosaic config from constructor defaults. |
setMosaicBrushSize(size) |
Set brush diameter in canvas pixels. |
setMosaicBlockSize(size) |
Set source-pixel block size; values are floored to integers. |
defaultMosaicConfig initializes the current runtime Mosaic config. Runtime
setters update only the current config and never mutate constructor defaults.
resetMosaicConfig() clones the constructor defaults back into the current
config.
const editor = new ImageEditor(fabric, {
defaultMosaicConfig: {
brushSize: 48,
blockSize: 8,
},
});
editor.init({
canvas: 'canvas',
enterMosaicModeButton: 'enterMosaicModeButton',
exitMosaicModeButton: 'exitMosaicModeButton',
mosaicBrushSizeInput: 'mosaicBrushSizeInput',
mosaicBlockSizeInput: 'mosaicBlockSizeInput',
});
editor.enterMosaicMode();
editor.setMosaicConfig({ brushSize: 64, blockSize: 12 });
editor.resetMosaicConfig();
brushSize is the circular brush diameter in canvas pixels. blockSize is
the source-image pixel block size; larger values produce chunkier pixelation.
Clicking outside the image is a no-op. Each successful Mosaic click bakes the
pixelated region into the base image and creates exactly one undo step. Because
Mosaic edits replace base image pixels rather than adding Fabric overlay
objects, exported images include the Mosaic naturally while the preview circle
is never exported or saved in history.
Text and Draw annotations
Tool modes are mutually exclusive: Crop, Mosaic, Text, and Draw cannot be
active at the same time. getEditorState() reports activeToolMode plus
isCropMode, isMosaicMode, isTextMode, and isDrawMode.
While Text or Draw mode is active, unrelated image operations are blocked:
export, merge, undo/redo, delete, transform, loadImage, and loadFromState
no-op through the normal guard. Exit the active mode before running those
operations. Text mode still allows exitTextMode, createTextAnnotation, and
Text config setters; Draw mode still allows exitDrawMode and Draw config
setters.
| Method | Description |
|---|---|
getAnnotations() |
Return a shallow array snapshot of current live annotation objects. Masks are not included. |
enterTextMode() / exitTextMode() |
Click empty canvas space to create editable text annotations. |
isTextMode() |
Returns true while Text mode is active. |
createTextAnnotation(config?) |
Create a text annotation directly and return it. |
getTextConfig() |
Return a defensive copy of the current Text config. |
setTextConfig(config) |
Patch current Text config without history. |
resetTextConfig() |
Restore Text config from constructor defaults. |
setTextColor(color) |
Convenience setter for text fill color. |
setTextFontSize(size) |
Convenience setter for text font size. |
enterDrawMode() / exitDrawMode() |
Use Fabric free drawing; each stroke becomes a Draw annotation. |
isDrawMode() |
Returns true while Draw mode is active. |
getDrawConfig() |
Return a defensive copy of the current Draw config. |
setDrawConfig(config) |
Patch current Draw config without history. |
resetDrawConfig() |
Restore Draw config from constructor defaults. |
setDrawColor(color) |
Convenience setter for brush color. |
setDrawBrushSize(size) |
Convenience setter for brush size. |
updateAnnotation(id, config) |
Update an annotation by id. |
updateSelectedAnnotation(config) |
Update selected annotation objects. |
removeSelectedAnnotation() |
Remove selected unlocked annotations. |
removeAllAnnotations(options?) |
Remove annotations only. Masks are preserved. |
deleteSelectedObject() |
Convenience deletion for selected masks and unlocked annotations. |
editor.enterTextMode();
editor.setTextConfig({ fill: '#ff0000', fontSize: 32 });
editor.updateSelectedAnnotation({ fill: '#00aaff' });
editor.enterDrawMode();
editor.setDrawConfig({ color: '#00aaff', brushSize: 10 });
Annotations carry annotationHidden and annotationLocked metadata. Hidden
annotations remain in state and annotation lists, but are not visible or
rendered during export until shown again. Locked annotations are non-interactive
(selectable, evented, transform controls, movement/scaling/rotation, and
text editability are disabled) and are skipped by selected-annotation
update/delete operations unless an API explicitly opts into forced removal.
Unlocking restores the annotation's intended base interactivity flags, including
non-default selectable, evented, text editable, and hasControls values
provided at creation or through supported update paths.
Layer operations
Editable overlays include masks and annotations. Layer operations keep the base image below overlays and session objects above overlays.
| Method | Description |
|---|---|
bringSelectedObjectForward() |
Move selected editable overlays one step up. |
sendSelectedObjectBackward() |
Move selected editable overlays one step down. |
bringSelectedObjectToFront() |
Move selected editable overlays to overlay front. |
sendSelectedObjectToBack() |
Move selected editable overlays to overlay back. |
Merge and export
| Method | Description |
|---|---|
mergeMasks() |
Bake masks into the base image atomically. Returns Promise<void>. |
mergeAnnotations() |
Bake annotations into the base image atomically. Returns Promise<void>. |
exportImageBase64(options?) |
Returns Promise<string> (data URL). Rejects when no image is loaded or the editor is not ready. |
exportImageFile(options?) |
Returns Promise<File>. Rejects when no image is loaded. |
downloadImage(options?) |
Returns Promise<void> and triggers a browser download. No-op when no image is loaded. |
All export APIs use the same ImageExportOptions shape:
| Option | Default | Description |
|---|---|---|
mergeMasks |
true |
Render masks into exported pixels. Mask labels are never exported. |
mergeAnnotations |
true |
Render non-hidden annotations into exported pixels. |
exportArea |
'image' |
'image' clips to the image bounding box; 'canvas' exports the canvas. |
fileType |
'jpeg' |
'png', 'jpeg', 'jpg', 'webp', or matching full MIME strings. |
format |
'jpeg' |
Alias for fileType on all export APIs; fileType wins when both set. |
quality |
0.92 |
Lossy quality clamped to [0, 1]; ignored for PNG. |
multiplier |
1 |
Output resolution multiplier. |
fileName |
option | exportImageFile() and downloadImage(). Defaults to defaultDownloadFileName. |
Unknown runtime fileType / format values preserve the compatibility fallback
to JPEG. If the browser cannot encode the resolved target MIME type and
canvas.toDataURL() falls back to another MIME such as PNG, export rejects
instead of returning mismatched Base64/File metadata.
await editor.exportImageBase64({ mergeMasks: true, mergeAnnotations: true });
await editor.exportImageBase64({ mergeMasks: false, mergeAnnotations: true });
await editor.exportImageBase64({ mergeMasks: true, mergeAnnotations: false });
await editor.exportImageBase64({ mergeMasks: false, mergeAnnotations: false });
const dataUrl = await editor.exportImageBase64({ fileType: 'png', exportArea: 'image' });
const file = await editor.exportImageFile({
fileType: 'webp',
quality: 0.85,
fileName: 'edited',
});
await editor.downloadImage({
fileType: 'png',
fileName: 'edited',
mergeMasks: false,
mergeAnnotations: false,
});
mergeMasks and mergeAnnotations in export options affect the rendered output
only. They do not mutate editor state, remove objects, or push history entries.
State-mutating merge APIs are mergeMasks() and mergeAnnotations().
mergeMasks() preserves annotations; mergeAnnotations() preserves masks.
State and history
| Method | Description |
|---|---|
saveState() |
Capture a snapshot of the canvas plus editor metadata into the history stack. |
loadFromState(snapshot) |
Restore canvas, masks, and editor metadata from a snapshot. Returns Promise<void>. |
undo() |
Undo the last state change. Routed through the animation queue. No-op while disposed. |
redo() |
Redo the next state change. Routed through the animation queue. No-op while disposed. |
loadFromState() is designed for snapshots produced by this editor's
saveState(). If snapshots come from external storage or user-controlled
input, validate or reject untrusted JSON before passing it to the editor.
Configuration options
Pass an ImageEditorOptions object as the second constructor argument
(or as the only argument when using the UMD global form). Unknown keys are
ignored, unsupported runtime values fall back to documented defaults, and nested
label and crop objects are deep-merged with the defaults.
| Option | Default | Description |
|---|---|---|
canvasWidth |
800 |
Initial and hidden-container fallback canvas width in pixels. |
canvasHeight |
600 |
Initial and hidden-container fallback canvas height in pixels. |
backgroundColor |
'transparent' |
Fabric canvas background color. |
animationDuration |
300 |
Duration of scale and rotate animations (ms). |
minScale |
0.1 |
Minimum scale factor. |
maxScale |
5.0 |
Maximum scale factor. |
scaleStep |
0.05 |
Scale delta per zoom step. |
rotationStep |
90 |
Rotation step in degrees. |
defaultLayoutMode |
'expand' |
Initial layout mode for image loads until changed by setLayoutMode(). Use 'fit', 'cover', or 'expand'. Invalid runtime values fall back to 'expand'. |
downsampleOnLoad |
true |
Downsample large images on load. |
downsampleMaxWidth |
4000 |
Max width before downsampling kicks in. |
downsampleMaxHeight |
3000 |
Max height before downsampling kicks in. |
downsampleQuality |
0.92 |
Lossy quality used when downsampling and exporting. |
preserveSourceFormat |
true |
Preserve PNG/WebP MIME through downsampling unless downsampleMimeType is set. |
downsampleMimeType |
null |
Explicit downsample MIME type. Overrides preserveSourceFormat. |
autoOrientImage |
true |
Normalize supported JPEG EXIF orientation during file-input loading. Set to false to preserve raw encoded orientation. |
autoOrientImageQuality |
null |
JPEG quality used when autoOrientImage re-encodes a rotated or mirrored file-input JPEG. null falls back to downsampleQuality. |
maxInputBytes |
50000000 |
Maximum encoded file bytes or decoded base64 payload bytes accepted before image decode. Invalid values fall back to this default. |
maxInputPixels |
50000000 |
Maximum source image pixel count accepted from PNG/JPEG/WebP headers before image decode when dimensions are available. Invalid values fall back to this default. |
imageLoadTimeoutMs |
30000 |
Maximum duration for both decode and Fabric image creation during loadImage. |
exportMultiplier |
1 |
Output resolution multiplier. |
maxExportPixels |
50000000 |
Maximum output pixel count after applying the export multiplier. Invalid values fall back to this default. |
maxExportDimension |
16384 |
Maximum output width or height after applying the export multiplier. Guards browser canvas single-dimension limits; invalid values fall back to this default. |
maxHistorySize |
50 |
Maximum undo-history entries. Snapshots may include full image data URLs, so large images can duplicate memory across history entries. Lower this for memory-constrained pages. |
exportAreaByDefault |
'image' |
Default export region for exportImageBase64, exportImageFile, and downloadImage. |
mergeMasksByDefault |
true |
Default mask rendering behavior for exportImageBase64, exportImageFile, and downloadImage. |
mergeAnnotationsByDefault |
true |
Default annotation rendering behavior for exportImageBase64, exportImageFile, and downloadImage. |
defaultMaskWidth |
50 |
Default mask width. |
defaultMaskHeight |
80 |
Default mask height. |
defaultMaskConfig |
{} |
Defaults applied by createMask() after defaultMaskWidth / defaultMaskHeight and before per-call config. Supports MaskConfig fields except onCreate and fabricGenerator. |
defaultMosaicConfig |
see source | Defaults used to initialize the current Mosaic tool config. Supports brushSize, blockSize, preview circle styling, outputFileType, and outputQuality. Runtime Mosaic setters update the current config only. |
defaultTextConfig |
see source | Defaults used to initialize the current Text annotation config. Runtime Text setters update the current config only. |
defaultDrawConfig |
see source | Defaults used to initialize the current Draw mode config. Runtime Draw setters update the current config only. |
maskRotatable |
false |
Allow masks to be rotated by the user. |
maskLabelOnSelect |
true |
Show a label above a selected mask. |
maskLabelOffset |
3 |
Pixel offset of the label from the mask's top-left corner. |
maskName |
'mask' |
Prefix used for auto-generated mask names. |
textAnnotationName |
'text' |
Prefix used for auto-generated text annotation names. |
drawAnnotationName |
'draw' |
Prefix used for auto-generated draw annotation names. |
maskListOrder |
'front-to-back' |
Mask list DOM order. 'front-to-back' shows the topmost mask first; 'back-to-front' preserves Fabric's bottom-to-top object order. |
annotationListOrder |
'front-to-back' |
Annotation list DOM order. 'front-to-back' shows the topmost annotation first; 'back-to-front' preserves Fabric's bottom-to-top object order. |
groupSelection |
false |
Allow Fabric multi-object group selection on the canvas. |
showPlaceholder |
true |
Show a placeholder element while no image is loaded. |
initialImageBase64 |
null |
Base64 data URL auto-loaded after construction. |
defaultDownloadFileName |
'edited_image' |
Default filename base used by exportImageFile() and downloadImage(). The resolved export format supplies or corrects the extension. |
onImageLoadStart |
null |
Called as (context) before a valid image load begins. |
onImageLoaded |
null |
Called as (imageInfo, context) once after a successful loadImage. Extra arguments are ignored by existing zero-argument JavaScript handlers. |
onImageCleared |
null |
Called as (previousImage, context) when a committed image is replaced or cleared. |
onImageChanged |
null |
Called as (state, context) with a safe editor state snapshot after visible editor state changes. |
onBusyChange |
null |
Called as (isBusy, context) only when the public busy state changes. |
onToolModeChange |
null |
Called as (activeToolMode, previousToolMode, context) only when the active tool mode changes. |
onHistoryChange |
null |
Called as ({ canUndo, canRedo }, context) only when undo/redo availability changes. |
onEditorDisposed |
null |
Called as (context) once when dispose() performs teardown. |
onMasksChanged |
null |
Called as (masks, context) with a shallow copy of current mask objects after the mask collection changes. |
onAnnotationsChanged |
null |
Called as (annotations, context) with a shallow copy of current annotation objects after the annotation collection changes. |
onSelectionChange |
null |
Called as (selection, context) with selected mask and annotation payload after selection changes. |
onError |
null |
Called as (error, message) when the editor reports an error. |
onWarning |
null |
Called as (error, message) when the editor reports a recoverable warning. |
label |
see source | LabelConfig for selected-mask labels (getText, textOptions, create). |
crop |
see source | CropConfig (minWidth, minHeight, padding, aspectRatio, hideMasksDuringCrop, preserveMasksAfterCrop, allowRotationOfCropRect, exportFileType, exportQuality). applyCrop() preserves the current image format by default ('source') and falls back to PNG when unknown. |
const editor = new ImageEditor(fabric, {
onToolModeChange(activeToolMode, previousToolMode, context) {
console.log('tool mode changed', {
activeToolMode,
previousToolMode,
operation: context.operation,
});
},
onHistoryChange(history, context) {
console.log('history changed', {
canUndo: history.canUndo,
canRedo: history.canRedo,
operation: context.operation,
});
},
});
Lifecycle callback exceptions are caught and logged so a faulty host callback
does not replace or mask the editor operation. onError and onWarning
callbacks use the same isolation.
maskListOrder and annotationListOrder affect only the sidebar DOM order. They do not change canvas z-order, object IDs, history, or export output. Invalid runtime values fall back to 'front-to-back'.
crop.exportFileType defaults to 'source'. Supported explicit values are
'png', 'jpeg', 'jpg', 'webp', and full image MIME strings. PNG is
lossless and ignores crop.exportQuality; JPEG/WebP use crop.exportQuality
when finite, otherwise downsampleQuality, otherwise 0.92. Choose JPEG/WebP
only when smaller intermediate crop output is preferred.
defaultMosaicConfig.outputFileType also defaults to 'source'. Mosaic commits
preserve the current image MIME type when known and fall back to PNG when the
source format cannot be determined. JPEG/WebP commits use
defaultMosaicConfig.outputQuality when finite, otherwise downsampleQuality.
Breaking changes in v2
Base64ExportOptions,ImageFileExportOptions, andDownloadImageOptionswere replaced byImageExportOptions.downloadImage(fileName: string)was removed. UsedownloadImage({ fileName }).downloadImage()now returnsPromise<void>.defaultDownloadFileNamenow defaults to'edited_image'; export format resolution supplies or corrects the final extension.- All editor-owned Fabric objects now require
editorObjectKind. isMaskObject()is strict and rejects legacy objects with onlymaskId.MaskObject.maskUidis required.- Serialized states without
editorObjectKindare not migrated. - Export option
mergeMaskwas removed; usemergeMasks. - Constructor option
mergeMaskByDefaultwas removed; usemergeMasksByDefault.
Example workflow
- Construct
ImageEditorwith options and callinit(elementMap)to wire it up. - Load an image via
loadImage(base64)or the bound file input. - Adjust with
scaleImage,rotateImage,flipHorizontal,flipVertical,resetImageTransform, Crop mode, Mosaic mode, Text mode, or Draw mode. - Add
createMaskand annotation calls, then inspect viamaskListandannotationList. - Use
mergeMasks()ormergeAnnotations()to bake overlays into the image, thenexportImageBase64,exportImageFile, ordownloadImageto produce the final output. - Call
dispose()when the editor is unmounted, ordisposeAsync()if the wrapper must await Fabric canvas teardown before immediate remount.
Building from source
npm install
npm run build
npm run build runs clean → build:esm → build:cjs → build:types → build:umd in order, emitting:
dist/esm/index.js(and the rest of the decomposed source tree)dist/cjs/index.cjsdist/types/index.d.tsdist/umd/image-editor.umd.js
npm test runs the Node-based unit and property tests under tests/.
Browser tests
npm run test:e2e
npm run test:browser
E2E browser tests validate core editor behavior in Chromium through Playwright.
Visual regression tests
npm run test:visual
npm run test:visual:update
Visual tests compare deterministic exported-image screenshots. Run
npm run test:visual:update after intentional rendering changes, then review
the updated snapshots before committing them.
For the full local release gate, run:
npm run format:check
npm run lint
npm run typecheck
npm test
npm run build
npm run package:check
npm run test:e2e
npm audit --audit-level=high
npm pack --dry-run
npm run ci combines format, lint, typecheck, tests, build, and package
linting. Playwright visual tests are kept outside the default CI command until
they are stable across supported environments. The test suite also supports a
clean checkout where dist/ has not been built yet; integration helpers use
source modules until build artifacts exist, while partial dist/ trees still
fail the artifact checks.
Browser support
- Chrome 100+
- Firefox 100+
- Safari 15+
- Edge 100+
The distributed JavaScript targets ES2019 and modern DOM APIs. Older runtime targets must be transpiled by the consumer.
Type declarations
Public types are re-exported from the package root:
import type {
ImageEditorOptions,
ResolvedOptions,
LabelConfig,
CropConfig,
MosaicConfig,
ResolvedMosaicConfig,
LoadImageOptions,
RemoveAllMasksOptions,
DefaultMaskConfig,
MaskConfig,
MaskObject,
MaskNumericProp,
ResolvedMaskConfig,
ImageMimeType,
ImageFileType,
NormalizedImageFormat,
ExportArea,
OverlayListOrder,
CropExportFileType,
MosaicOutputFileType,
ImageExportOptions,
CropAspectRatioPreset,
CropAspectRatio,
CropModeOptions,
ImageInfo,
ImageEditorState,
ImageEditorSelection,
ImageEditorCallbackContext,
ImageEditorOperation,
ElementTarget,
ElementMap,
ElementIdMap,
ResizeToContainerOptions,
RelayoutOptions,
FabricModule,
} from '@bensitu/image-editor';
License
MIT Ben Situ.
Fabric.js is distributed under its own MIT license.