# rn-image-editor URL: /docs/native/docyrus/image-editor Gesture-driven image crop editor with pan and pinch zoom, resizable crop box, circle stencil, rotate and flip, returning the edited image file. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-image-editor ``` **Dependencies:** - [react-native-gesture-handler](https://www.npmjs.com/package/react-native-gesture-handler) - [react-native-reanimated](https://www.npmjs.com/package/react-native-reanimated) - [react-native-svg](https://www.npmjs.com/package/react-native-svg) - [expo-image-manipulator (optional)](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/) - [expo-image-picker (optional)](https://docs.expo.dev/versions/latest/sdk/imagepicker/) `expo-image-manipulator` and `expo-image-picker` are **optional peers** (both work in Expo Go): ```bash npx expo install expo-image-manipulator expo-image-picker ``` - Without `expo-image-manipulator` the editor still renders and shows a hint; **Save returns the original image** unchanged. - Without `expo-image-picker` the Upload button is hidden. ## Usage ```tsx import { ImageEditor, type ImageEditorResult } from '@/components/docyrus-native/image-editor'; upload(result.uri)} onCancel={() => navigation.goBack()} /> ``` ### Circle avatar crop ```tsx setAvatar(result.uri)} /> ``` ### Crop limits and base64 output `minCropWidth` / `minCropHeight` / `maxCropWidth` / `maxCropHeight` are **source-image pixels** (after rotation), like `react-advanced-cropper` on web. They clamp both the crop box and the zoom range. ```tsx sendToApi(dataUrl)} /> ``` ### Replacing the image When `expo-image-picker` is installed the toolbar shows an Upload button. The picked image replaces the one being edited (all edits reset), and `onUpload` receives the asset. ```tsx console.log(asset.fileName, asset.mimeType, asset.width, asset.height)} onSave={handleSave} /> ``` ## Interactions | Gesture / control | Effect | |-------------------|--------| | Drag the image | Pans the image under the crop box (clamped so the image always covers the box). | | Pinch | Zooms around the focal point (range limited by the crop box and the min/max crop sizes). | | Drag a corner handle | Resizes the crop box from that corner; the aspect ratio is kept when set. | | Zoom in / Zoom out buttons | Zoom by 1.25× around the crop box centre. | | Reset button | Clears rotation, flips, zoom and the crop box. | | Rotate Left / Rotate Right | Rotates by 90°; the crop box resets to fit the rotated image. | | Flip Horizontal / Flip Vertical | Mirrors the image in its current on-screen orientation. | | Save | Rotate → flip → crop → optional resize via `expo-image-manipulator`, then `onSave(result)`. | ## API Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `src` | `string` | — | Image URI (remote `https://` or local `file://`). Changing it replaces the image and resets every edit. | | `source` | `{ uri: string }` | — | **Deprecated** alias of `src` (kept for backward compatibility). `src` wins when both are set. | | `onSave` | `(result: ImageEditorResult) => void` | — | Receives the **edited** image. Without `expo-image-manipulator` it receives the original URI and size. | | `onUpload` | `(asset: ImageEditorAsset) => void` | — | Receives the replacement image picked from the library (requires `expo-image-picker`). | | `onCancel` | `() => void` | — | Renders a Cancel button when provided. | | `onAction` | `(action: ImageEditorAction) => void` | — | Fired for every toolbar / overlay action (analytics, side effects). | | `disabled` | `boolean` | `false` | Disables gestures, tools and Save. | | `stencilShape` | `'rectangle' \| 'circle'` | `'rectangle'` | Crop stencil. `circle` draws a round mask (an ellipse when `aspectRatio` ≠ 1); the saved file is the bounding rectangle. | | `aspectRatio` | `number` | — | Crop width / height. Free when omitted; `circle` without a ratio uses `1`. | | `minCropWidth` | `number` | — | Minimum crop width in source-image pixels. | | `minCropHeight` | `number` | — | Minimum crop height in source-image pixels. | | `maxCropWidth` | `number` | — | Maximum crop width in source-image pixels. | | `maxCropHeight` | `number` | — | Maximum crop height in source-image pixels. | | `variant` | `'default' \| 'compact'` | `'default'` | `compact` uses a lighter frame and moves Save / Cancel into the toolbar as icons. | | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Cropper height: 300 / 400 / 500 points. | | `saveFormat` | `'jpeg' \| 'png' \| 'webp'` | `'png'` | Output encoding (`png` matches web `toDataURL()`). | | `compress` | `number` | `1` | Compression 0–1 (1 = best quality). | | `base64` | `boolean` | `false` | Also return `base64` and a `data:` URL (web `onSave(dataUrl)` parity). | | `maxOutputSize` | `number` | — | Downscales the crop so its longest edge is at most this many pixels. | | `className` | `string` | — | Additional classes for the root. | | `style` | `ViewStyle` | — | Root style for dynamic values. | ### ImageEditorResult | Property | Type | Description | |----------|------|-------------| | `uri` | `string` | Local file URI of the edited image (the original URI when `expo-image-manipulator` is missing). | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `base64` | `string \| undefined` | Raw base64 payload, only when `base64` is `true`. | | `dataUrl` | `string \| undefined` | `data:image/;base64,…`, only when `base64` is `true`. | ### ImageEditorAsset | Property | Type | Description | |----------|------|-------------| | `uri` | `string` | Local file URI. | | `fileName` | `string` | File name (derived from the URI when the picker gives none). | | `mimeType` | `string` | MIME type (derived from the extension when missing, default `image/jpeg`). | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `fileSize` | `number \| undefined` | Size in bytes, when known. | ### ImageEditorAction `'rotate-left' | 'rotate-right' | 'flip-horizontal' | 'flip-vertical' | 'zoom-in' | 'zoom-out' | 'reset' | 'upload'` ## Helpers The barrel also exports the optional-peer bridges, reused by `AvatarSelect`: | Export | Signature | Description | |--------|-----------|-------------| | `pickImageAsset` | `(options?: PickImageOptions) => Promise` | Opens the photo library (`mediaTypes: ['images']`). `null` when cancelled or `expo-image-picker` is missing. | | `isImagePickerAvailable` | `() => boolean` | Whether `expo-image-picker` is installed. | | `isImageManipulatorAvailable` | `() => boolean` | Whether `expo-image-manipulator` is installed. | | `toImageEditorAsset` | `(asset) => ImageEditorAsset` | Normalises a picker / manipulator result (fills `fileName` / `mimeType`). | | `imageEditorVariants` | `tv()` | Slot variants (`base`, `cropper`, `toolbar`, `footer`, `toolButton`, `overlayButton`). | | `DEFAULT_ADJUSTMENTS` | `Adjustments` | All-zero adjustments (web parity constant). | ### PickImageOptions | Property | Type | Default | Description | |----------|------|---------|-------------| | `allowsEditing` | `boolean` | `false` | Show the OS crop UI. | | `aspect` | `[number, number]` | — | Android crop aspect (iOS always crops square with `allowsEditing`). | | `quality` | `number` | `1` | 0–1 JPEG quality. | ## Differences from web - **Colour adjustments** (brightness, saturation, contrast, hue — the web `EditorMode` values other than `crop`) are **web-only**: React Native has no canvas filter pipeline. The `Adjustments` / `EditorMode` types are exported for shared code, but the native editor only crops. - Web `onSave` receives a data URL; native receives an `ImageEditorResult` file (set `base64` for a data URL). - Web `onUpload` receives a `File`; native receives an `ImageEditorAsset`. - Rotate and flip are native-only tools. ## i18n Copy goes through `useUiTranslation()`: `ui.common.save`, `ui.common.cancel`, `ui.imageEditor.uploadImage`, `ui.imageEditor.saveImage`, `ui.imageEditor.rotateLeft`, `ui.imageEditor.rotateRight`, `ui.imageEditor.flipHorizontal`, `ui.imageEditor.flipVertical`, `ui.imageEditor.zoomIn`, `ui.imageEditor.zoomOut`, `ui.imageEditor.reset`, `ui.imageEditor.noImage`, `ui.imageEditor.loadFailed`, `ui.imageEditor.manipulatorMissing`, `ui.imageEditor.saveFailed`. ## Type Exports | Type | Description | |------|-------------| | `ImageEditorProps` | Props for the ImageEditor component. | | `ImageEditorResult` | Edited image handed to `onSave`. | | `ImageEditorAsset` | Picked image handed to `onUpload`. | | `ImageEditorAction` | Toolbar / overlay action union. | | `ImageEditorSaveFormat` | `'jpeg' \| 'png' \| 'webp'`. | | `StencilShape` | `'rectangle' \| 'circle'` (web parity). | | `EditorMode` | `'crop' \| 'brightness' \| 'saturation' \| 'contrast' \| 'hue'` (web parity; only `crop` applies on native). | | `Adjustments` | `{ brightness, saturation, hue, contrast }` (web parity; web-only feature). | | `PickImageOptions` | Options for `pickImageAsset`. |