@rageshpikalmunde/rp-image-editor

Lightweight JavaScript image editor with crop, zoom, rotate, draw, text, shapes (circle / ellipse / square / arrow), callout annotations, eraser, undo/redo. Always-on drag with translucent ghost preview so panning and zooming never hide the underlying ima
4,552 8
Install
npm install @rageshpikalmunde/rp-image-editor
Latest Version:1.5.10
License:MIT
Last Updated:Sep 23, 2026
Links: GitHub  ·  npm
Maintainer: rpragesh

@rageshpikalmunde/rp-image-editor

npm version npm downloads license bundle size

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/total header 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

  • sp is accepted as a convenience alias for es.
  • 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):

  1. Explicit fields on theme, strings, filterPresetLabels, calloutDefaults
  2. config.labels per-key overrides
  3. Language pack from config.language
  4. 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 toolbarActiveIconColor prop is still supported as an alias for toolbarActiveBackground for backward compatibility, but new code should prefer the semantically-named toolbarActiveBackground / toolbarActiveTextColor pair.

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

MIT © Ragesh Pikalmunde

Related Packages

super-image-cropper

A powerful JavaScript image cropping library that supports GIF animations, PNG,...

27,932 23
ionic-appauth

Integration for OpenId/AppAuth-JS into Ionic

366,380 105

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