@rageshpikalmunde/rp-image-editor
| Install | |
|---|---|
npm install @rageshpikalmunde/rp-image-editor |
|
| Latest Version: | 1.5.10 |
| License: | MIT |
| Last Updated: | Sep 23, 2026 |
| Links: | GitHub · npm |
@rageshpikalmunde/rp-image-editor
An open-source, framework-agnostic JavaScript image editor, image annotation, and screenshot markup plugin built on Fabric.js. Crop, zoom, rotate, draw, add text, shapes (circle / ellipse / square / arrow), callout annotations, erase, undo/redo — all in a modal UI with a grouped toolbar. Always-on drag with a translucent ghost preview keeps the image visible while panning and zooming. Annotations are preserved across both crop and rotate (no drift past 360°), exports run at the image's native resolution by default, multi-image flows can show a built-in
current/totalheader counter, and themes auto-contrast so customized backgrounds always get a readable foreground. Works with Angular, React, Vue, Ionic, Capacitor, and plain JavaScript. Use it for photo editing, image markup, screenshot annotation, mobile upload flows, and in-browser image review tools without sending images to a backend editor.
🚀 ▶ Try the Live Demo →
One click opens a live browser demo with a sample image already loaded. No signup, no upload. Draw, crop, rotate, add callouts, apply filters, and test export quality right in your browser.
Features
| Feature | Description |
|---|---|
| ✂️ Crop | Free crop and aspect-ratio locked crop. Initial ratio chip matches the actual rectangle; switching ratios (e.g. 4:3 ↔ 16:9) refits into the same 80% image-bounds envelope so sizes stay consistent. Annotations are preserved across crops. |
| 🔍 Zoom | Zoom in/out with pinch-to-zoom gesture support |
| 🖐️ Pan/Drag | Always-on drag — no need to enable a tool. |
| 🔄 Rotate | Rotate left/right by 90° steps. Annotations are preserved across rotations and stay locked to the underlying pixels (no drift past 360°). Fast path skips PNG re-encoding so large 10–15 MB+ images rotate quickly; a loader overlay is shown during heavy renders. |
| ✏️ Freehand Draw | Configurable brush color & width |
| 🎨 Unified Active Color | One shared active color state across Draw, Text, Shapes, and Callout. Palette selection always reflects the true active color (including defaults). |
| 🔤 Add Text | Inline editing with color and font size |
| ⭕ Shapes | Circle, Ellipse, Square and Arrow primitives with resize handles. Circle/Square stay proportional, Ellipse resizes freely, Arrow has draggable start/end endpoints |
| 💬 Callout | Editable label with draggable tail, min-resize clamping, text constraints (40 chars, word-wrap), mobile double-tap support. Live color update: picking a new color from the palette recolors the currently selected callout — same behaviour as shape/text/draw tools. |
| 🧪 Custom Color Sync | Custom color picks are mirrored into the palette as a dynamic swatch and remain selected across tool switches. |
| 🗑️ Delete | Delete selected callout/annotation via toolbar trash button |
| 🧹 Eraser | Remove annotations without affecting the image |
| ↩️ Undo/Redo | Configurable stack depth (default: 20) |
| 🔁 Reset | Reset to original image |
| 🎯 Native-resolution export | Output preserves the source image's intrinsic resolution; annotations stay sharp |
| 🎛️ Grouped Toolbar | Compact toolbar with flyout menus (Zoom, Transform, Annotate, Shapes) |
| 🖼️ Multi-image flows | Pass currentImageIndex + totalImages to show a compact 2/5-style counter in the header while users step through a batch of images. |
| 🚫 Disable Features | Hide individual tools or groups via disabledFeatures config |
| 📱 HEIC Support | Auto-converts iPhone HEIC photos to JPEG |
| 📐 EXIF Orientation | Auto-corrects rotated photos |
| ⚡ Smart Resolution | Auto-downscales on iOS to stay within Safari canvas limits |
| 👆 Touch Gestures | Pinch zoom, drag, tap on mobile |
| 🎨 Theming | Fully customizable colors for header, footer, buttons, toolbar. Auto-contrast: customized backgrounds without an explicit text/icon color get a readable foreground derived from luminance. |
| 🌐 i18n (15 languages) | Bundled translations for da, de, en, es, fr, it, ko, nl, pl, pt, sv, th, tr, vi, zh selectable via language. sp aliases to es; regional variants (de-DE, pt_BR, zh-CN, …) fold to the primary tag. |
| 🏷️ Label overrides | labels (deep-partial LocalePack) lets you rebrand individual strings or add languages beyond the built-in set. |
| 🎚️ Filter presets config | Whitelist / reorder the built-in filter tiles via filterPresets, or rename them via filterPresetLabels. |
| 💬 Callout defaults config | Override the initial text, color, textColor, fontSize, maxChars, and lineBreakAt for new callouts. |
| 📝 Empty-state copy | Customize the drop-zone title/subtitle via strings.emptyStateTitle / strings.emptyStateSubtitle (empty string hides the subtitle). |
| 🧷 Bounds-safe annotations | Text, callouts, shapes, and draw strokes stay clipped/clamped to the visible image area, which keeps editing and exports clean even after zooming, panning, undo/redo, or fullscreen transitions. |
| 📦 Output | Base64, Blob, and File object |
Installation
npm install @rageshpikalmunde/rp-image-editor
Quick Start
Vanilla JavaScript / TypeScript
import { openEditorModal } from '@rageshpikalmunde/rp-image-editor';
const fileInput = document.querySelector<HTMLInputElement>('#fileInput');
fileInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const result = await openEditorModal({
image: file,
config: {
exportFormat: 'jpeg',
exportQuality: 0.92,
theme: {
headerTitle: 'Edit Photo',
applyButtonBackground: '#4a90d9',
},
},
});
if (result) {
console.log(result.file); // File object — upload via FormData
console.log(result.base64); // data:image/jpeg;base64,...
console.log(result.blob); // Blob
}
});
Angular / Ionic
import { openEditorModal } from '@rageshpikalmunde/rp-image-editor';
// Or use the ImageEditorService wrapper for centralized config:
// import { ImageEditorService } from './services/image-editor.service';
async onFileSelected(file: File) {
const result = await openEditorModal({
image: file,
config: { exportFormat: 'jpeg', exportQuality: 0.92 },
});
if (result) {
// Upload result.file to your backend
}
}
Multi-image header counter
If your app walks the user through several images in sequence, pass both currentImageIndex and totalImages to show a compact progress counter in the top bar:
await openEditorModal({
image: file,
config: {
currentImageIndex: 2,
totalImages: 5,
},
});
This renders 2/5 in the header. The values are clamped internally, so out-of-range inputs never produce invalid counters.
React
import { openEditorModal } from '@rageshpikalmunde/rp-image-editor';
function ImageUploader() {
const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const result = await openEditorModal({
image: file,
config: { exportFormat: 'jpeg' },
});
if (result) {
// Use result.file, result.base64, or result.blob
}
};
return <input type="file" accept="image/*" onChange={handleFile} />;
}
Programmatic API (Advanced)
import { RpImageEditor } from '@rageshpikalmunde/rp-image-editor';
const container = document.getElementById('editor-container');
const editor = new RpImageEditor(container, {
exportFormat: 'png',
maxUndoSteps: 30,
defaultBrushColor: '#ff0000',
showToolbar: true,
});
await editor.loadImage(file);
// Control the editor programmatically
editor.setMode('draw');
editor.zoomIn();
editor.rotate(90);
// Export
const result = await editor.getResult();
console.log(result.file);
// Clean up
editor.destroy();
Configuration
interface RpEditorConfig {
maxResolution?: number | null; // Max image resolution (auto-detect per platform)
cropAspectRatios?: CropAspectRatio[];
exportFormat?: 'png' | 'jpeg'; // Default: 'jpeg'
exportQuality?: number; // 0.0–1.0, Default: 0.92
exportPixelRatio?: number; // 1 = standard, 2 = retina. Default: 1
exportAtNativeResolution?: boolean; // Render export at the source image's native resolution. Default: true
maxUndoSteps?: number; // Default: 20
defaultBrushColor?: string; // Default: '#ff0000'
defaultBrushWidth?: number; // Default: 3
defaultTextColor?: string; // Default: '#ff0000'
defaultTextFontSize?: number; // Default: 24
defaultShapeColor?: string; // Default: matches defaultBrushColor
defaultShapeStrokeWidth?: number; // Default: 3
colorPalette?: string[];
showToolbar?: boolean; // Default: true
currentImageIndex?: number; // 1-based index for multi-image flows (e.g. 2)
totalImages?: number; // Total images in the flow (e.g. 5) → shows "2/5" in header
disabledFeatures?: string[]; // Default: [] — see below
filterPresets?: ImageFilterPreset[]; // Whitelist / order the filter tiles
filterPresetLabels?: Partial<Record<ImageFilterPreset, string>>; // Rename individual tiles
calloutDefaults?: { // Defaults for newly-placed callouts
text?: string; // Default: 'Label' (localised by `language`)
color?: string; // Default: matches defaultBrushColor
textColor?: string; // Default: '#ffffff'
fontSize?: number; // Default: 20
maxChars?: number; // Default: 40
lineBreakAt?: number; // Default: 15
};
strings?: { // Empty-state copy
emptyStateTitle?: string; // Default: 'Drop an image or click to upload'
emptyStateSubtitle?: string; // Default: 'Supported: PNG, JPEG, HEIC' (pass '' to hide)
};
theme?: RpEditorTheme;
locale?: string;
language?: LanguageCode; // Two-letter code — see "Internationalization" below
labels?: LocalePackOverrides; // Deep-partial per-key overrides on top of the language pack
}
Internationalization
Swap every user-facing string in the editor shell to any of 15 bundled languages with a single config key:
await openEditorModal({
image: file,
config: { language: 'de' }, // header, rails, props panel, callouts … all German
});
Supported codes: da, de, en, es, fr, it, ko, nl, pl, pt, sv, th, tr, vi, zh
spis accepted as a convenience alias fores.- Regional variants (
de-DE,pt_BR,zh-CN, …) are folded to their primary tag. - Unknown or missing codes fall back to English.
Per-key overrides (labels)
config.labels accepts any subset of LocalePack keys and layers them on top of the resolved language pack. Use it to rebrand individual strings, or to provide a translation for a language that isn't in the built-in set — missing keys fall through to English.
await openEditorModal({
image: file,
config: {
language: 'en',
labels: {
tool: { callout: 'Annotation' },
props: {
title: { callout: 'Annotation' },
deleteSelected: 'Remove',
},
},
},
});
Precedence (highest wins):
- Explicit fields on
theme,strings,filterPresetLabels,calloutDefaults config.labelsper-key overrides- Language pack from
config.language - English fallback pack
Public helpers (rarely needed — the editor calls these internally):
import { getLocalePack, resolveLanguage } from '@rageshpikalmunde/rp-image-editor';
import type { LanguageCode, LocalePack, LocalePackOverrides } from '@rageshpikalmunde/rp-image-editor';
resolveLanguage('pt-BR'); // → 'pt'
getLocalePack('de'); // → the full German LocalePack
Disabling Features
Hide individual tools or entire groups from the toolbar:
const result = await openEditorModal({
image: file,
config: {
// Hide individual tools
disabledFeatures: ['eraser', 'reset'],
// Or hide entire groups
// disabledFeatures: ['zoom', 'transform'],
},
});
Individual tool names: move, crop, zoomIn, zoomOut, rotateLeft, rotateRight, draw, text, callout, eraser, filters, adjust, shape-circle, shape-ellipse, shape-square, shape-rectangle, shape-arrow, shape-polyline, undo, redo, reset
Group names (disables all children): zoom (zoomIn + zoomOut), transform (rotateLeft + rotateRight), annotate (draw + text + callout + eraser), shapes (shape-circle + shape-ellipse + shape-square + shape-rectangle + shape-arrow + shape-polyline)
Common recipes:
// Hide the Filters menu entirely
disabledFeatures: ['filters']
// Hide both Filters and Adjust tiles (no photo effects at all)
disabledFeatures: ['filters', 'adjust']
// Hide the whole Annotate group (draw + text + callout + eraser)
disabledFeatures: ['annotate']
Toolbar Layout
The toolbar is organized into compact items with flyout menus:
| Button | Type | Contains |
|---|---|---|
| Move | Standalone | Pan / drag mode |
| Crop | Standalone | Free & aspect-ratio crop |
| Zoom ▾ | Flyout | Zoom In, Zoom Out |
| Transform ▾ | Flyout | Rotate Left, Rotate Right |
| Annotate ▾ | Flyout | Draw, Text, Callout, Eraser |
| Shapes ▾ | Flyout | Circle, Ellipse, Square, Arrow |
| Undo | Standalone | Undo last action |
| Redo | Standalone | Redo last undone action |
| Reset | Standalone | Reset to original image |
Theming
All theme tokens are optional. When you customize a background (headerBackground,
toolbarBackground, toolbarActiveBackground, or footerBackground) but do not
set its paired foreground (headerTextColor, toolbarIconColor,
toolbarActiveTextColor, cancelButtonTextColor), the editor will automatically
derive a readable foreground from the background's WCAG relative luminance — dark
backgrounds get white text/icons, light backgrounds get dark. Explicit overrides
always win.
Use toolbarActiveBackground to control the highlight color of the currently
selected toolbar tool. Pick a color with strong contrast against
toolbarBackground so the active tool stands out clearly to the user.
const result = await openEditorModal({
image: file,
config: {
theme: {
headerBackground: '#1a1a2e',
headerTextColor: '#ffffff',
headerTitle: 'Edit Image',
editorBackground: '#000000',
toolbarBackground: '#1a1a2e',
toolbarIconColor: '#cccccc',
// Highlight color for the currently selected toolbar tool.
// Choose a color with strong contrast against `toolbarBackground`.
toolbarActiveBackground: '#4a90d9',
toolbarActiveTextColor: '#ffffff',
footerBackground: '#1a1a2e',
applyButtonBackground: '#4a90d9',
applyButtonTextColor: '#ffffff',
cancelButtonBackground: 'transparent',
cancelButtonTextColor: '#ffffff',
modalBorderRadius: '12px',
buttonBorderRadius: '6px',
},
},
});
Note: The older
toolbarActiveIconColorprop is still supported as an alias fortoolbarActiveBackgroundfor backward compatibility, but new code should prefer the semantically-namedtoolbarActiveBackground/toolbarActiveTextColorpair.
Browser Support
| Browser | Version |
|---|---|
| Chrome | 60+ |
| Firefox | 60+ |
| Safari | 12+ |
| Edge | 79+ |
| iOS Safari | 12+ |
| Android Chrome | 60+ |
Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
Upgrading to 1.5.10
Nothing in your existing code has to change. 1.5.10 is backward-compatible with 1.4.x. If you install it and touch nothing, your existing crop / rotate / draw / filter / i18n behavior continues to work as before; the crop fix from 1.5.9 remains in place, and image flips now mirror annotations automatically.
1. Bump the version
npm install @rageshpikalmunde/rp-image-editor@^1.5.10
# and if you use the framework wrappers (they peer-depend on core ^1.5.10):
npm install @rageshpikalmunde/rp-image-editor-react@^1.5.10
npm install @rageshpikalmunde/rp-image-editor-angular@^1.5.10
2. Existing integrations keep working unchanged
There are no new required APIs or config keys in 1.5.10. If you already use the built-in flip controls, annotations now mirror with the image automatically. The broader 1.5.x additions below remain fully optional, and your existing config shape does not need to change.
Add any subset of the new keys to your existing config object. You do not need to remove or restructure anything you already pass:
await openEditorModal({
image: file,
config: {
// ...everything you already had in 1.4.x still works, unchanged...
// Optional image counter for multi-image flows
currentImageIndex: 2,
totalImages: 5,
// Localize the UI to any of 15 bundled languages
language: 'de',
// Per-key label overrides (rebrand, or translate to a language not bundled)
labels: { tool: { callout: 'Annotation' } },
// Whitelist / reorder the filter tiles, and rename them
filterPresets: ['none', 'grayscale', 'vintage'],
filterPresetLabels: { grayscale: 'B&W' },
// Defaults for newly-placed callouts
calloutDefaults: { text: 'Note', fontSize: 18, maxChars: 60 },
// Empty-state copy (drop-zone title/subtitle). Pass '' to hide the subtitle.
strings: {
emptyStateTitle: 'Drop a photo here',
emptyStateSubtitle: '',
},
// Header subtitle (below the header title). Pass '' to hide the row.
theme: { headerSubtitle: 'Edit and annotate' },
},
});
Hiding the Filters menu
Use disabledFeatures (available since 1.0.3 — see Disabling Features above) to hide the Filters tile:
await openEditorModal({
image: file,
config: {
disabledFeatures: ['filters'], // hide Filters only
// disabledFeatures: ['filters', 'adjust'], // hide Filters + Adjust
},
});
Rolling back
1.5.10 is additive — no data, export, or storage format changed — so you can downgrade at any time with no code changes:
npm install @rageshpikalmunde/rp-image-editor@1.5.9
npm install @rageshpikalmunde/rp-image-editor-react@1.5.9
npm install @rageshpikalmunde/rp-image-editor-angular@1.5.9
License
Related Packages
A powerful JavaScript image cropping library that supports GIF animations, PNG,...
Version History
| Version | Released | License |
|---|---|---|
| 1.5.10 | MIT | |
| 1.5.9 | MIT | |
| 1.5.8 | MIT | |
| 1.5.6 | MIT | |
| 1.5.5 | MIT | |
| 1.5.4 | MIT | |
| 1.5.3 | MIT | |
| 1.5.2 | MIT | |
| 1.5.1 | MIT | |
| 1.5.0 | MIT | |
| 1.4.1 | MIT | |
| 1.4.0 | MIT | |
| 1.3.0 | MIT | |
| 1.2.0 | MIT | |
| 1.1.0 | MIT | |
| 1.0.5 | MIT | |
| 1.0.4 | MIT | |
| 1.0.3 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |