@bensitu/image-editor 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,27 @@
1
1
  # ImageEditor
2
+
2
3
  [![npm](https://img.shields.io/npm/l/image-editor.svg)](https://github.com/bensitu/image-editor)
3
4
  [![npm](https://img.shields.io/npm/v/@bensitu/image-editor.svg)](https://www.npmjs.com/package/@bensitu/image-editor)
4
5
  [![](https://data.jsdelivr.com/v1/package/npm/@bensitu/image-editor/badge)](https://www.jsdelivr.com/package/npm/@bensitu/image-editor)
5
6
 
6
- A lightweight JavaScript wrapper around fabric.js that provides comprehensive image editing capabilities including loading, zooming, rotation, and mask management.
7
+ A lightweight JavaScript wrapper around fabric.js that provides image loading, scaling, rotation, cropping, mask management, history, and export helpers.
7
8
 
8
9
  ## Overview
9
10
 
10
11
  ImageEditor offers:
11
12
 
12
- - Image loading from base64 or file input
13
- - Zoom in/out and reset scale functionality
14
- - Rotation (with custom degrees or step-based)
15
- - Mask creation, selection, and removal
16
- - Optional mask labels
17
- - Merge/export helpers
18
- - Basic UI element binding
13
+ - Image loading from base64 data URLs or file input
14
+ - Zoom in/out and reset transform helpers
15
+ - Rotation with custom degrees or step-based controls
16
+ - Crop mode with optional mask preservation
17
+ - Mask creation, selection, removal, and label support
18
+ - Undo/redo history helpers
19
+ - Merge, download, base64 export, and `File` export helpers
20
+ - Optional DOM/UI binding for common editor controls
21
+ - Large-image downsampling to reduce browser memory pressure
22
+ - Centralized error and warning callbacks
19
23
 
20
- **Note**: This library requires fabric.js v5.x to be loaded before instantiating the editor.
24
+ **Note:** This library requires **fabric.js v5.x** to be loaded before creating or initializing the editor.
21
25
 
22
26
  ## Demo
23
27
 
@@ -25,20 +29,41 @@ ImageEditor offers:
25
29
 
26
30
  ## Features
27
31
 
28
- - **Fabric.js-powered canvas** - Built on top of the robust fabric.js library
29
- - **Image scaling** - Configurable min/max limits with smooth animation
30
- - **Image rotation** - Step control and animated transitions
31
- - **Auto-resizing** - Optional canvas resizing to match image or container size
32
- - **Image crop** - Enter a crop mode to crop image
33
- - **Mask management** - Add, remove, remove all, with draggable/resizable masks
34
- - **Mask labels** - Auto-sync with mask movement/scaling
35
- - **Performance optimization** - Downsampling on load to prevent large image performance issues
36
- - **Export & Download** - Base64 output or direct file save support
37
- - **DOM/UI binding** - Easy integration with buttons, inputs, and placeholders
32
+ - **Fabric.js-powered canvas** - Built on top of fabric.js.
33
+ - **Image scaling** - Configurable min/max limits with smooth animation.
34
+ - **Image rotation** - Step control and animated transitions.
35
+ - **Auto-resizing** - Optional canvas resizing to match the image, fit the viewport, or cover the viewport.
36
+ - **Image crop** - Enter a temporary crop mode and apply or cancel the crop.
37
+ - **Mask management** - Add, remove, remove all, drag, resize, and optionally rotate masks.
38
+ - **Mask labels** - Auto-sync labels with mask movement and scaling.
39
+ - **History** - Undo and redo supported editor state changes.
40
+ - **Performance optimization** - Downsample large images on load.
41
+ - **Export & download** - Export as base64 data URL, `File`, or direct download.
42
+ - **DOM/UI binding** - Bind common buttons, inputs, and placeholders by element ID.
38
43
 
39
44
  ## Installation
40
45
 
41
- Include fabric.js and the ImageEditor class script in your HTML:
46
+ ### npm / pnpm / yarn
47
+
48
+ ```bash
49
+ npm i @bensitu/image-editor fabric
50
+ # or
51
+ pnpm add @bensitu/image-editor fabric
52
+ # or
53
+ yarn add @bensitu/image-editor fabric
54
+ ```
55
+
56
+ ### ESM / bundler usage
57
+
58
+ ```javascript
59
+ import ImageEditor, {
60
+ ImageEditor as NamedImageEditor,
61
+ } from "@bensitu/image-editor";
62
+ ```
63
+
64
+ ### Browser global usage
65
+
66
+ Include fabric.js first, then ImageEditor:
42
67
 
43
68
  ```html
44
69
  <!-- Fabric.js (required) -->
@@ -50,11 +75,7 @@ Include fabric.js and the ImageEditor class script in your HTML:
50
75
  <script src="https://cdn.jsdelivr.net/npm/@bensitu/image-editor/dist/image-editor.min.js"></script>
51
76
  ```
52
77
 
53
- For ESM/bundler usage:
54
-
55
- ```javascript
56
- import ImageEditor, { ImageEditor as NamedImageEditor } from '@bensitu/image-editor';
57
- ```
78
+ Use `dist/image-editor.js` or `dist/image-editor.min.js` for browser global script usage. Use `dist/image-editor.esm.mjs` or `dist/image-editor.esm.min.mjs` for standards-compliant ESM imports. Matching `.js` ESM builds are also generated for browser and bundler compatibility.
58
79
 
59
80
  ## Quick Start
60
81
 
@@ -65,20 +86,31 @@ import ImageEditor, { ImageEditor as NamedImageEditor } from '@bensitu/image-edi
65
86
  <canvas id="fabricCanvas"></canvas>
66
87
 
67
88
  <!-- Optional Controls -->
68
- <button id="zoomInBtn">Zoom In</button>
69
- <button id="zoomOutBtn">Zoom Out</button>
70
- <button id="rotateLeftBtn">Rotate Left</button>
71
- <input id="rotationLeftInput" type="number" value="90">
72
- <button id="rotateRightBtn">Rotate Right</button>
73
- <input id="rotationRightInput" type="number" value="90">
89
+ <button id="zoomInButton">Zoom In</button>
90
+ <button id="zoomOutButton">Zoom Out</button>
91
+
92
+ <button id="rotateLeftButton">Rotate Left</button>
93
+ <input id="rotateLeftDegreesInput" type="number" value="90" />
94
+
95
+ <button id="rotateRightButton">Rotate Right</button>
96
+ <input id="rotateRightDegreesInput" type="number" value="90" />
97
+
98
+ <button id="createMaskButton">Add Mask</button>
99
+ <button id="removeSelectedMaskButton">Remove Mask</button>
100
+ <button id="removeAllMasksButton">Remove All Masks</button>
74
101
 
75
- <button id="addMaskBtn">Add Mask</button>
76
- <button id="removeMaskBtn">Remove Mask</button>
102
+ <button id="enterCropModeButton">Crop</button>
103
+ <button id="applyCropButton">Apply Crop</button>
104
+ <button id="cancelCropButton">Cancel Crop</button>
77
105
 
78
- <button id="mergeBtn">Merge</button>
79
- <button id="downloadBtn">Download</button>
106
+ <button id="undoButton">Undo</button>
107
+ <button id="redoButton">Redo</button>
80
108
 
81
- <input id="imageInput" type="file" accept="image/*">
109
+ <button id="mergeMasksButton">Merge</button>
110
+ <button id="resetImageTransformButton">Reset</button>
111
+ <button id="downloadImageButton">Download</button>
112
+
113
+ <input id="imageInput" type="file" accept="image/*" />
82
114
  ```
83
115
 
84
116
  ### JavaScript Implementation
@@ -88,126 +120,408 @@ import ImageEditor, { ImageEditor as NamedImageEditor } from '@bensitu/image-edi
88
120
  const editor = new ImageEditor({
89
121
  canvasWidth: 800,
90
122
  canvasHeight: 600,
91
- backgroundColor: '#ffffff',
92
- initialImageBase64: null // optional
93
- });
123
+ backgroundColor: "#ffffff",
124
+ initialImageBase64: null,
125
+
126
+ onError: (error, message) => {
127
+ console.error("[ImageEditor]", message, error);
128
+ },
94
129
 
95
- // Initialize (binds to DOM elements)
96
- editor.init({
97
- canvas: 'fabricCanvas',
98
- zoomInBtn: 'zoomInBtn',
99
- zoomOutBtn: 'zoomOutBtn',
100
- rotateLeftBtn: 'rotateLeftBtn',
101
- rotationLeftInput: 'rotationLeftInput',
102
- rotateRightBtn: 'rotateRightBtn',
103
- rotationRightInput: 'rotationRightInput',
104
- addMaskBtn: 'addMaskBtn',
105
- mergeBtn: 'mergeBtn',
106
- downloadBtn: 'downloadBtn',
107
- imageInput: 'imageInput'
130
+ onWarning: (error, message) => {
131
+ console.warn("[ImageEditor]", message, error);
132
+ },
108
133
  });
109
134
 
110
- // Load an image manually (base64 string)
111
- // editor.loadImage('data:image/jpeg;base64,...');
135
+ try {
136
+ editor.init({
137
+ canvas: "fabricCanvas",
138
+ zoomInButton: "zoomInButton",
139
+ zoomOutButton: "zoomOutButton",
140
+ rotateLeftButton: "rotateLeftButton",
141
+ rotateLeftDegreesInput: "rotateLeftDegreesInput",
142
+ rotateRightButton: "rotateRightButton",
143
+ rotateRightDegreesInput: "rotateRightDegreesInput",
144
+ createMaskButton: "createMaskButton",
145
+ removeSelectedMaskButton: "removeSelectedMaskButton",
146
+ removeAllMasksButton: "removeAllMasksButton",
147
+ enterCropModeButton: "enterCropModeButton",
148
+ applyCropButton: "applyCropButton",
149
+ cancelCropButton: "cancelCropButton",
150
+ undoButton: "undoButton",
151
+ redoButton: "redoButton",
152
+ mergeMasksButton: "mergeMasksButton",
153
+ resetImageTransformButton: "resetImageTransformButton",
154
+ downloadImageButton: "downloadImageButton",
155
+ imageInput: "imageInput",
156
+ });
157
+ } catch (error) {
158
+ console.error("Failed to initialize ImageEditor:", error);
159
+ }
160
+ ```
161
+
162
+ ### Loading an Image Manually
163
+
164
+ ```javascript
165
+ async function loadImageDataUrl(imageBase64) {
166
+ try {
167
+ await editor.loadImage(imageBase64);
168
+ } catch (error) {
169
+ console.error("Image could not be loaded:", error);
170
+ }
171
+ }
172
+
173
+ loadImageDataUrl("data:image/jpeg;base64,...");
112
174
  ```
113
175
 
114
176
  ## Configuration Options
115
177
 
116
- When creating the editor instance, you can pass an options object to override defaults:
117
-
118
- | Option | Default | Description |
119
- |--------|---------|-------------|
120
- | `canvasWidth` | `800` | Initial canvas width (px) |
121
- | `canvasHeight` | `600` | Initial canvas height (px) |
122
- | `backgroundColor` | `transparent` | Canvas background color |
123
- | `animationDuration` | `300` | Animation duration for scale/rotation (ms) |
124
- | `minScale` | `0.1` | Minimum scale factor |
125
- | `maxScale` | `5.0` | Maximum scale factor |
126
- | `scaleStep` | `0.05` | Scale step for zoom buttons |
127
- | `rotationStep` | `90` | Default rotation step in degrees |
128
- | `expandCanvasToImage` | `true` | Expand canvas to image size on load |
129
- | `fitImageToCanvas` | `false` | Fit image to the visible canvas viewport |
130
- | `coverImageToCanvas` | `false` | Fit image to cover canvas (at least one side fits, allowing overflow). |
131
- | `downsampleOnLoad` | `true` | Downsample large images before rendering |
132
- | `downsampleMaxWidth` | `4000` | Max width count before downsampling |
133
- | `downsampleMaxHeight` | `3000` | Max height count before downsampling |
134
- | `downsampleQuality` | `0.92` | JPEG quality when downsampling |
135
- | `imageLoadTimeoutMs` | `30000` | Timeout for image decode operations |
136
- | `exportMultiplier` | `1` | Scale factor for export |
137
- | `exportImageAreaByDefault` | `true` | Export only the image area (clipped to masks) |
138
- | `defaultMaskWidth` | `50` | Default mask width (px) |
139
- | `defaultMaskHeight` | `80` | Default mask height (px) |
140
- | `maskRotatable` | `false` | Whether masks can be rotated |
141
- | `maskLabelOnSelect` | `true` | Show label when mask is selected |
142
- | `maskLabelOffset` | `3` | Offset for mask labels from top-left corner |
143
- | `maskName` | `mask` | Prefix for mask names/labels |
144
- | `label.getText` | `(mask) => mask.maskName` | Callback for custom label text. The second argument is the mask's stable zero-based creation index (`mask.maskId - 1`). |
145
- | `showPlaceholder` | `true` | Shows placeholder when no image is loaded |
146
- | `initialImageBase64` | `null` | Base64 string to auto-load as initial image |
147
- | `defaultDownloadFileName` | `edited_image.jpg` | Default file name for downloads |
148
- | `crop.preserveMasksAfterCrop` | `false` | Keep masks that intersect the crop area, shifted into the cropped canvas. Merge masks first if they should be baked into the image pixels. |
178
+ When creating the editor instance, pass an options object to override defaults.
179
+
180
+ | Option | Default | Description |
181
+ | ----------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
182
+ | `canvasWidth` | `800` | Initial canvas width in pixels. |
183
+ | `canvasHeight` | `600` | Initial canvas height in pixels. |
184
+ | `backgroundColor` | `transparent` | Canvas background color. |
185
+ | `animationDuration` | `300` | Animation duration for scale and rotation operations, in milliseconds. |
186
+ | `minScale` | `0.1` | Minimum image scale factor. |
187
+ | `maxScale` | `5.0` | Maximum image scale factor. |
188
+ | `scaleStep` | `0.05` | Scale increment/decrement used by zoom controls. |
189
+ | `rotationStep` | `90` | Default rotation step in degrees. |
190
+ | `expandCanvasToImage` | `true` | Expand the canvas to the loaded image size. |
191
+ | `fitImageToCanvas` | `false` | Fit the loaded image inside the visible canvas viewport. |
192
+ | `coverImageToCanvas` | `false` | Scale the image to cover the visible canvas viewport, allowing overflow when needed. |
193
+ | `downsampleOnLoad` | `true` | Downsample large images before rendering. |
194
+ | `downsampleMaxWidth` | `4000` | Maximum source image width before downsampling. |
195
+ | `downsampleMaxHeight` | `3000` | Maximum source image height before downsampling. |
196
+ | `downsampleQuality` | `0.92` | JPEG/WebP quality used when downsampling. `0` is valid and is preserved. |
197
+ | `preserveSourceFormat` | `true` | Preserve the source image format where possible during downsampling. |
198
+ | `downsampleMimeType` | `null` | Optional output MIME type for downsampled images. Supported values include `jpeg`, `jpg`, `png`, `webp`, `image/jpeg`, `image/png`, and `image/webp`. |
199
+ | `imageLoadTimeoutMs` | `30000` | Timeout for image decode/load operations. |
200
+ | `exportMultiplier` | `1` | Default scale multiplier for export. |
201
+ | `exportImageAreaByDefault` | `true` | Export only the image area by default instead of the full canvas. |
202
+ | `defaultMaskWidth` | `50` | Default mask width in pixels. |
203
+ | `defaultMaskHeight` | `80` | Default mask height in pixels. |
204
+ | `maskRotatable` | `false` | Whether masks can be rotated. |
205
+ | `maskLabelOnSelect` | `true` | Show the mask label when a mask is selected. |
206
+ | `maskLabelOffset` | `3` | Offset for mask labels from the mask's top-left corner. |
207
+ | `maskName` | `mask` | Prefix for mask names and labels. |
208
+ | `groupSelection` | `false` | Whether Fabric can select multiple masks as an active selection. |
209
+ | `label.getText` | `(mask) => mask.maskName` | Callback for custom label text. The second argument is the mask's stable zero-based creation index (`mask.maskId - 1`). |
210
+ | `showPlaceholder` | `true` | Show a placeholder when no image is loaded. |
211
+ | `initialImageBase64` | `null` | Base64 data URL to auto-load during initialization. |
212
+ | `defaultDownloadFileName` | `edited_image.jpg` | Default file name for downloads. |
213
+ | `crop.preserveMasksAfterCrop` | `false` | Keep masks that intersect the crop area, shifted into the cropped canvas. Merge masks first if they should be baked into the image pixels. |
214
+ | `onImageLoaded` | `null` | Callback invoked after an image finishes loading. |
215
+ | `onError` | `null` | Callback invoked for recoverable internal errors. |
216
+ | `onWarning` | `null` | Callback invoked for recoverable internal warnings. |
149
217
 
150
218
  `expandCanvasToImage`, `fitImageToCanvas`, and `coverImageToCanvas` are mutually exclusive layout modes. If more than one is enabled, the editor reports a warning and uses the first active mode in its existing load order.
151
219
 
220
+ ## DOM Binding Keys
221
+
222
+ `init(idMap)` binds editor behavior to DOM elements by ID. All keys are optional.
223
+
224
+ | Key | Description |
225
+ | --------------------------- | -------------------------------------------------------------- |
226
+ | `canvas` | Required canvas element ID. |
227
+ | `canvasContainer` | Optional scrollable viewport/container element ID. |
228
+ | `imagePlaceholder` | Optional placeholder element shown when no image is loaded. |
229
+ | `scalePercentageInput` | Optional element used to display the current scale percentage. |
230
+ | `rotateLeftDegreesInput` | Optional input used by the rotate-left button. |
231
+ | `rotateRightDegreesInput` | Optional input used by the rotate-right button. |
232
+ | `rotateLeftButton` | Rotate image left. |
233
+ | `rotateRightButton` | Rotate image right. |
234
+ | `createMaskButton` | Create a new mask. |
235
+ | `removeSelectedMaskButton` | Remove the currently selected mask. |
236
+ | `removeAllMasksButton` | Remove all masks. |
237
+ | `mergeMasksButton` | Merge masks into the base image. |
238
+ | `downloadImageButton` | Download the edited image. |
239
+ | `maskList` | Optional mask list container. |
240
+ | `zoomInButton` | Zoom in. |
241
+ | `zoomOutButton` | Zoom out. |
242
+ | `resetImageTransformButton` | Reset scale and rotation. |
243
+ | `undoButton` | Undo the last state change. |
244
+ | `redoButton` | Redo the next state change. |
245
+ | `imageInput` | File input used to load images. |
246
+ | `uploadArea` | Optional clickable upload area that triggers the file input. |
247
+ | `enterCropModeButton` | Enter crop mode. |
248
+ | `applyCropButton` | Apply the current crop rectangle. |
249
+ | `cancelCropButton` | Cancel crop mode. |
250
+
251
+ ### Legacy DOM Binding Keys
252
+
253
+ The following DOM binding keys remain supported in `1.x` for compatibility, but are deprecated and will be removed in `v2.0.0`.
254
+
255
+ | Deprecated key | Use instead |
256
+ | -------------------- | --------------------------- |
257
+ | `imgPlaceholder` | `imagePlaceholder` |
258
+ | `scaleRate` | `scalePercentageInput` |
259
+ | `rotationLeftInput` | `rotateLeftDegreesInput` |
260
+ | `rotationRightInput` | `rotateRightDegreesInput` |
261
+ | `rotateLeftBtn` | `rotateLeftButton` |
262
+ | `rotateRightBtn` | `rotateRightButton` |
263
+ | `addMaskBtn` | `createMaskButton` |
264
+ | `removeMaskBtn` | `removeSelectedMaskButton` |
265
+ | `removeAllMasksBtn` | `removeAllMasksButton` |
266
+ | `mergeBtn` | `mergeMasksButton` |
267
+ | `downloadBtn` | `downloadImageButton` |
268
+ | `zoomInBtn` | `zoomInButton` |
269
+ | `zoomOutBtn` | `zoomOutButton` |
270
+ | `resetBtn` | `resetImageTransformButton` |
271
+ | `undoBtn` | `undoButton` |
272
+ | `redoBtn` | `redoButton` |
273
+ | `cropBtn` | `enterCropModeButton` |
274
+ | `applyCropBtn` | `applyCropButton` |
275
+ | `cancelCropBtn` | `cancelCropButton` |
276
+
152
277
  ## API Methods
153
278
 
154
- | Method | Description |
155
- |--------|-------------|
156
- | `init(idMap)` | Bind the editor to DOM elements. Pass IDs in an object (optional). |
157
- | `loadImage(imageBase64, options)` | Load an image from a base64 data string. Resolves after the Fabric image is on the canvas. |
158
- | `scaleImage(factor)` | Scale image to the given factor (relative to base scale). |
159
- | `rotateImage(degrees)` | Rotate image to the given angle in degrees. |
160
- | `resetImageTransform()` | Reset scale to 1 and rotation to 0. |
161
- | `undo()` | Undo the last state change. Resolves after the canvas state is restored. |
162
- | `redo()` | Redo the next state change. Resolves after the canvas state is restored. |
163
- | `createMask(config)` | Create a mask on the canvas. Config can include width, height, color. |
164
- | `removeSelectedMask()` | Remove the currently selected mask. |
165
- | `removeAllMasks()` | Remove all masks from the canvas. |
166
- | `enterCropMode()` | Create a resizable/movable selection rect on top of the image. |
167
- | `cancelCrop()` | Cancel crop mode and remove the temporary selection rect. |
168
- | `applyCrop()` | Apply the current crop rectangle in the canvas. |
169
- | `mergeMasks()` | Merge masks with the base image in the canvas. |
170
- | `downloadImage()` | Download the merged image as a file. |
171
- | `exportImageBase64(options)` | Export an image data URL. `fileType` can be `jpeg`, `png`, or `webp`. |
172
- | `exportImageFile(options)` | Exports the current canvas (with or without masks) as a `File` object. `fileType` is exported directly when supported. |
173
-
174
- Deprecated aliases are still available for compatibility and will be removed in `v2.0.0`: `reset()`, `addMask(config)`, `merge()`, and `getImageBase64(options)`.
175
-
176
- By default, applying crop removes unmerged masks. Set `crop.preserveMasksAfterCrop` to keep intersecting masks, or use `mergeMasks()` before cropping when masks should become part of the image pixels.
279
+ | Method | Returns | Description |
280
+ | --------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
281
+ | `init(idMap)` | `void` | Bind the editor to DOM elements. Pass IDs in an object. |
282
+ | `loadImage(imageBase64, options)` | `Promise<void>` | Load an image from a base64 data URL. Resolves after the Fabric image is on the canvas. |
283
+ | `isImageLoaded()` | `boolean` | Return whether a valid image is loaded on the canvas. |
284
+ | `isBusy()` | `boolean` | Return whether the editor is loading, animating, cropping, or running another compound operation. |
285
+ | `scaleImage(factor)` | `Promise<void>` | Scale the image to the given factor relative to the base scale. |
286
+ | `rotateImage(degrees)` | `Promise<void>` | Rotate the image to the given angle in degrees. |
287
+ | `resetImageTransform()` | `Promise<void>` | Reset scale to `1` and rotation to `0`. |
288
+ | `undo()` | `Promise<void>` | Undo the last state change. Resolves after the canvas state is restored. |
289
+ | `redo()` | `Promise<void>` | Redo the next state change. Resolves after the canvas state is restored. |
290
+ | `createMask(config)` | `fabric.Object \| null` | Create a mask on the canvas. Config can include shape, width, height, color, opacity, position, style, and callbacks. |
291
+ | `removeSelectedMask()` | `void` | Remove the currently selected mask or selected masks. |
292
+ | `removeAllMasks(options)` | `void` | Remove all masks from the canvas. |
293
+ | `enterCropMode()` | `void` | Create a resizable/movable selection rectangle on top of the image. |
294
+ | `cancelCrop()` | `void` | Cancel crop mode and remove the temporary selection rectangle. |
295
+ | `applyCrop()` | `Promise<void>` | Apply the current crop rectangle to the canvas. |
296
+ | `mergeMasks()` | `Promise<void>` | Merge masks into the base image on the canvas. |
297
+ | `downloadImage(fileName)` | `void` | Download the edited image as a file. |
298
+ | `exportImageBase64(options)` | `Promise<string>` | Export an image data URL. `fileType` can be `jpeg`, `jpg`, `png`, `webp`, or a supported image MIME type. |
299
+ | `exportImageFile(options)` | `Promise<File>` | Export the current canvas as a `File` object. |
300
+ | `saveState()` | `void` | Save the current editor state to history. Usually called internally after supported edits. |
301
+ | `loadFromState(serializedState)` | `Promise<void>` | Restore the editor from a serialized canvas/editor state. |
302
+
303
+ Deprecated method aliases are still available for compatibility and will be removed in `v2.0.0`.
304
+
305
+ | Deprecated method | Use instead |
306
+ | ------------------------- | ---------------------------- |
307
+ | `reset()` | `resetImageTransform()` |
308
+ | `addMask(config)` | `createMask(config)` |
309
+ | `merge()` | `mergeMasks()` |
310
+ | `getImageBase64(options)` | `exportImageBase64(options)` |
311
+
312
+ ## Mask Configuration
313
+
314
+ `createMask(config)` accepts an optional configuration object.
315
+
316
+ | Option | Description |
317
+ | ------------------ | ------------------------------------------------------------------------------------------ |
318
+ | `shape` | Mask shape. Built-in values include `rect`, `circle`, `ellipse`, and `polygon`. |
319
+ | `width` / `height` | Mask size in pixels, percentage string, or resolver callback. |
320
+ | `radius` | Circle radius in pixels, percentage string, or resolver callback. |
321
+ | `rx` / `ry` | Ellipse radius or rectangle corner radius. |
322
+ | `points` | Polygon points as `{ x, y }` objects or `[x, y]` tuples. |
323
+ | `left` / `top` | Mask position in pixels, percentage string, or resolver callback. |
324
+ | `angle` | Initial rotation angle in degrees. |
325
+ | `color` | Fill color. |
326
+ | `alpha` | Opacity from `0` to `1`. |
327
+ | `selectable` | Whether the mask can be selected. |
328
+ | `hasControls` | Whether Fabric transform controls are shown. |
329
+ | `styles` | Additional Fabric style properties, such as `stroke`, `strokeWidth`, or `strokeDashArray`. |
330
+ | `fabricGenerator` | Factory callback for creating a custom Fabric object. |
331
+ | `onCreate` | Callback invoked after the mask is added to the canvas. |
332
+
333
+ Example:
334
+
335
+ ```javascript
336
+ const mask = editor.createMask({
337
+ shape: "rect",
338
+ width: 120,
339
+ height: 80,
340
+ left: 20,
341
+ top: 20,
342
+ color: "rgba(0, 0, 0, 0.5)",
343
+ alpha: 0.5,
344
+ });
345
+ ```
346
+
347
+ ## Crop Behavior
348
+
349
+ By default, applying crop removes unmerged masks.
350
+
351
+ This is intentional: an unmerged mask is still an editable overlay object, not part of the image pixels. When `crop.preserveMasksAfterCrop` is `false`, applying crop discards unmerged masks instead of trying to keep or partially crop those overlay objects.
352
+
353
+ Choose the workflow based on the result you want:
354
+
355
+ - **Crop first, then add masks** when masks should be placed only on the final cropped image.
356
+ - **Merge masks before cropping** when masks should become part of the image pixels and appear in the cropped result.
357
+ - **Enable `crop.preserveMasksAfterCrop`** when masks should remain editable after crop. Only masks that intersect the crop area are kept and shifted into the cropped canvas.
358
+ - **Keep the default `crop.preserveMasksAfterCrop: false`** when unmerged masks should be discarded by crop.
359
+
360
+ ### Preserve editable masks after crop
361
+
362
+ ```javascript
363
+ const editor = new ImageEditor({
364
+ crop: {
365
+ preserveMasksAfterCrop: true,
366
+ },
367
+ });
368
+ ```
369
+
370
+ ### Bake masks into the image before crop
371
+
372
+ ```javascript
373
+ try {
374
+ await editor.mergeMasks();
375
+ editor.enterCropMode();
376
+ await editor.applyCrop();
377
+ } catch (error) {
378
+ console.error("Crop workflow failed:", error);
379
+ }
380
+ ```
381
+
382
+ ## Export Examples
383
+
384
+ ### Export as Base64
385
+
386
+ ```javascript
387
+ try {
388
+ const dataUrl = await editor.exportImageBase64({
389
+ fileType: "jpeg",
390
+ quality: 0.92,
391
+ multiplier: 1,
392
+ });
393
+
394
+ console.log(dataUrl);
395
+ } catch (error) {
396
+ console.error("Export failed:", error);
397
+ }
398
+ ```
399
+
400
+ ### Export as File
401
+
402
+ ```javascript
403
+ try {
404
+ const file = await editor.exportImageFile({
405
+ fileName: "edited_image.jpg",
406
+ fileType: "jpeg",
407
+ quality: 0.92,
408
+ mergeMask: true,
409
+ });
410
+
411
+ console.log(file);
412
+ } catch (error) {
413
+ console.error("File export failed:", error);
414
+ }
415
+ ```
416
+
417
+ ## Error Handling
418
+
419
+ Some ImageEditor API methods may throw synchronously or reject their returned Promise when the operation cannot be completed.
420
+
421
+ Recommended usage:
422
+
423
+ - Wrap initialization and manual API calls in `try...catch`.
424
+ - Always `await` Promise-based methods such as `loadImage()`, `scaleImage()`, `rotateImage()`, `resetImageTransform()`, `mergeMasks()`, `applyCrop()`, `undo()`, `redo()`, `exportImageBase64()`, `exportImageFile()`, and `loadFromState()`.
425
+ - Use `onError` and `onWarning` for centralized logging or UI notifications.
426
+ - Do not rely only on `onError`; manual API calls should still handle thrown errors or rejected Promises at the call site.
427
+ - Check `editor.isBusy()` before starting user-triggered operations when needed, especially before loading, cropping, rotating, scaling, merging, undoing, redoing, or exporting.
428
+ - Show user-friendly messages in your application instead of exposing raw error text directly.
429
+
430
+ Example:
431
+
432
+ ```javascript
433
+ const editor = new ImageEditor({
434
+ onError: (error, message) => {
435
+ console.error("[ImageEditor]", message, error);
436
+ showEditorMessage("The image editor could not complete the operation.");
437
+ },
438
+
439
+ onWarning: (error, message) => {
440
+ console.warn("[ImageEditor]", message, error);
441
+ },
442
+ });
443
+
444
+ async function rotateSafely(degrees) {
445
+ if (editor.isBusy()) {
446
+ return;
447
+ }
448
+
449
+ try {
450
+ await editor.rotateImage(degrees);
451
+ } catch (error) {
452
+ console.error("Rotate failed:", error);
453
+ showEditorMessage("Rotation failed. Please try again.");
454
+ }
455
+ }
456
+
457
+ function showEditorMessage(message) {
458
+ // Replace this with your own toast, alert, or inline message UI.
459
+ console.log(message);
460
+ }
461
+ ```
462
+
463
+ ### Common Failure Cases
464
+
465
+ Common failure cases include:
466
+
467
+ - fabric.js is not loaded before creating or initializing the editor.
468
+ - The configured canvas element cannot be found.
469
+ - The image data URL is invalid, unsupported, too large, or times out while loading.
470
+ - Another operation is already running, such as image loading, animation, crop, undo, redo, merge, or export.
471
+ - The editor has been disposed before the operation completes.
472
+ - The browser cannot create a canvas export because of platform, memory, or security restrictions.
473
+ - A custom `fabricGenerator`, label callback, or external event handler throws an error.
177
474
 
178
475
  ## Example Workflow
179
476
 
180
- 1. **Load an image** - Via file input or base64 string
181
- 2. **Adjust positioning** - Zoom in/out or rotate as needed
182
- 3. **Add masks** - Highlight or cover specific areas (drag, resize)
183
- 4. **Merge result** - Create a flattened export (optional)
184
- 5. **Download / export** - Save the final image
477
+ The recommended order depends on whether masks should remain editable, be baked into the image, or be discarded.
185
478
 
186
- ## Installing
479
+ ### Common workflow: crop first, then add masks
187
480
 
188
- ### npm / pnpm / yarn
189
- ```bash
190
- npm i @bensitu/image-editor fabric
191
- # or
192
- pnpm add @bensitu/image-editor fabric
193
- # or
194
- yarn add @bensitu/image-editor fabric
481
+ Use this flow when masks are only needed on the final cropped image.
482
+
483
+ 1. **Load an image** - Via file input or base64 data URL.
484
+ 2. **Adjust positioning** - Zoom in/out, rotate, or reset as needed.
485
+ 3. **Crop** - Optionally crop the image area.
486
+ 4. **Add masks** - Highlight or cover specific areas on the cropped image.
487
+ 5. **Merge result** - Flatten masks into the base image when needed.
488
+ 6. **Download / export** - Save the final image.
489
+
490
+ ### Alternative workflow: add masks before crop
491
+
492
+ If masks are added before crop, choose one of these behaviors before applying crop:
493
+
494
+ - Call `mergeMasks()` before `applyCrop()` if masks should become part of the cropped image pixels.
495
+ - Set `crop.preserveMasksAfterCrop: true` if masks should stay editable after crop.
496
+ - Keep the default `crop.preserveMasksAfterCrop: false` if unmerged masks should be removed by crop.
497
+
498
+ Example:
499
+
500
+ ```javascript
501
+ // Masks become part of the image pixels before crop.
502
+ await editor.mergeMasks();
503
+ editor.enterCropMode();
504
+ await editor.applyCrop();
195
505
  ```
196
506
 
197
- ### Local build
507
+ ## Local Build
508
+
198
509
  ```bash
199
510
  npm run build
200
511
  ```
201
512
 
202
- ### Load UMD js file:
203
- Use `dist/image-editor.js` or `dist/image-editor.min.js` for browser global script usage. Use `dist/image-editor.esm.mjs` or `dist/image-editor.esm.min.mjs` for standards-compliant ESM imports. Matching `.js` ESM builds are also generated for browser and bundler compatibility.
513
+ ## Tests
514
+
515
+ ```bash
516
+ npm test
517
+ ```
204
518
 
205
519
  ## Browser Support
206
520
 
207
- * Chrome 100+
208
- * Firefox 100+
209
- * Safari 15+
210
- * Edge 100+
521
+ - Chrome 100+
522
+ - Firefox 100+
523
+ - Safari 15+
524
+ - Edge 100+
211
525
 
212
526
  The package build targets these modern browsers and uses modern DOM and JavaScript features.
213
527
 
@@ -215,7 +529,7 @@ IE11 and old mobile Safari are not supported by the distributed build. If you ne
215
529
 
216
530
  ## Dependencies
217
531
 
218
- - **fabric.js v5.x** — Must be loaded before ImageEditor
532
+ - **fabric.js v5.x** — Must be loaded before ImageEditor.
219
533
 
220
534
  ## License
221
535