import { NativeModulesProxy, EventEmitter, requireNativeViewManager } from 'expo-modules-core' import { ViewProps } from 'react-native' import React from 'react' // Get the native module const ImageEditorNativeModule = NativeModulesProxy.ImageEditor // Helper to check module availability function ensureModuleAvailable() { if (!ImageEditorNativeModule) { throw new Error( 'ImageEditorSDK native module is not available. ' + 'Make sure you have:\n' + '1. Installed the package: npm install @vitaliez/react-native-image-editor-sdk\n' + '2. Run: npx expo prebuild --clean\n' + '3. Rebuilt the app: npx expo run:ios\n' + '4. Restarted Metro bundler' ) } } // Create event emitter lazily to avoid init errors const createEventEmitter = () => { ensureModuleAvailable() return new EventEmitter(ImageEditorNativeModule as any) } // MARK: - Types export interface ImageAdjustments { brightness?: number // -1.0 to 1.0 contrast?: number // 0.0 to 2.0 saturation?: number // 0.0 to 2.0 exposure?: number // -2.0 to 2.0 highlights?: number // 0.0 to 1.0 shadows?: number // -1.0 to 1.0 temperature?: number // -1.0 to 1.0 tint?: number // -150 to 150 sharpness?: number // 0.0 to 2.0 noiseReduction?: number // 0.0 to 1.0 vignette?: number // 0.0 to 1.0 } export type FilterPreset = | 'vintage' | 'dramatic' | 'mono' | 'noir' | 'fade' | 'chrome' | 'process' | 'transfer' | 'instant' | 'warm' | 'cool' export interface FilterOptions { type: FilterPreset intensity?: number // 0.0 to 1.0, default 1.0 } export interface CropParams { x: number y: number width: number height: number } export interface ResizeParams { width: number height: number } export type ExportFormat = 'jpeg' | 'png' | 'heic' export interface ExportOptions { format?: ExportFormat quality?: number // 0.0 to 1.0, for JPEG and HEIC maxDimension?: number // Maximum width/height (for thumbnails/previews) } export interface ExportResult { success: boolean uri: string size: number } export interface ImageLoadedEvent { width: number height: number } export interface ImageSource { uri?: string path?: string } /** * LUT (Look-Up Table) options for color grading */ export interface LUTOptions { name: string // Unique identifier for the LUT filePath: string // Path to .cube file } export interface LUTApplyOptions { name: string // Previously loaded LUT name intensity?: number // 0.0 to 1.0, default 1.0 } // MARK: - Module API export const ImageEditorModule = { /** * Load an image from a file path or URI */ async loadImage(path: string): Promise { ensureModuleAvailable() return await ImageEditorNativeModule.loadImage(path) }, /** * Apply adjustments to the current image */ async applyAdjustments(adjustments: ImageAdjustments): Promise<{ success: boolean }> { ensureModuleAvailable() return await ImageEditorNativeModule.applyAdjustments(adjustments) }, /** * Apply a preset filter */ async applyFilter(filterType: FilterPreset, intensity: number = 1.0): Promise<{ success: boolean }> { ensureModuleAvailable() return await ImageEditorNativeModule.applyFilter(filterType, intensity) }, /** * Crop the image to the specified rectangle */ async crop(params: CropParams): Promise { ensureModuleAvailable() return await ImageEditorNativeModule.crop(params.x, params.y, params.width, params.height) }, /** * Rotate the image by the specified angle (in radians) */ async rotate(angle: number): Promise<{ success: boolean }> { ensureModuleAvailable() return await ImageEditorNativeModule.rotate(angle) }, /** * Flip the image horizontally */ async flipHorizontal(): Promise<{ success: boolean }> { ensureModuleAvailable() return await ImageEditorNativeModule.flipHorizontal() }, /** * Flip the image vertically */ async flipVertical(): Promise<{ success: boolean }> { ensureModuleAvailable() return await ImageEditorNativeModule.flipVertical() }, /** * Resize the image to the specified dimensions */ async resize(params: ResizeParams): Promise<{ success: boolean }> { ensureModuleAvailable() return await ImageEditorNativeModule.resize(params.width, params.height) }, /** * Undo the last operation * @returns true if undo was successful */ undo(): boolean { ensureModuleAvailable() return ImageEditorNativeModule.undo() }, /** * Redo the last undone operation * @returns true if redo was successful */ redo(): boolean { ensureModuleAvailable() return ImageEditorNativeModule.redo() }, /** * Check if undo is available */ canUndo(): boolean { ensureModuleAvailable() return ImageEditorNativeModule.canUndo() }, /** * Check if redo is available */ canRedo(): boolean { ensureModuleAvailable() return ImageEditorNativeModule.canRedo() }, /** * Export the current image */ async export(options: ExportOptions = {}): Promise { ensureModuleAvailable() return await ImageEditorNativeModule.export(options) }, /** * Get list of available filter presets */ getAvailableFilters(): FilterPreset[] { ensureModuleAvailable() return ImageEditorNativeModule.getAvailableFilters() }, /** * Load a LUT (Look-Up Table) from a .cube file for later use * @param options LUT loading options */ async loadLUT(options: LUTOptions): Promise<{ success: boolean; name: string }> { ensureModuleAvailable() return await ImageEditorNativeModule.loadLUT(options.name, options.filePath) }, /** * Apply a previously loaded LUT to the current image * @param options LUT application options */ async applyLUT(options: LUTApplyOptions): Promise<{ success: boolean; intensity: number }> { ensureModuleAvailable() const intensity = options.intensity ?? 1.0 return await ImageEditorNativeModule.applyLUT(options.name, intensity) }, /** * Clear all cached LUTs from memory */ clearLUTCache(): void { ensureModuleAvailable() ImageEditorNativeModule.clearLUTCache() }, /** * Reset the editor and clear the current image */ reset(): void { ensureModuleAvailable() ImageEditorNativeModule.reset() }, // Event listeners (create emitter per subscription to avoid init errors) addImageLoadedListener(listener: (event: ImageLoadedEvent) => void) { return (createEventEmitter() as any).addListener('onImageLoaded', listener) }, addImageUpdatedListener(listener: () => void) { return (createEventEmitter() as any).addListener('onImageUpdated', listener) }, addExportProgressListener(listener: (progress: number) => void) { return (createEventEmitter() as any).addListener('onExportProgress', listener) }, addErrorListener(listener: (event: { error: string }) => void) { return (createEventEmitter() as any).addListener('onError', listener) }, } // MARK: - View Component export interface ImageEditorViewProps extends ViewProps { /** * Image source to load */ source?: ImageSource /** * Adjustments to apply */ adjustments?: ImageAdjustments /** * Filter to apply */ filter?: FilterOptions /** * Enable drawing mode */ drawingEnabled?: boolean /** * Drawing brush size */ drawingBrushSize?: number /** * Drawing color (hex string) */ drawingColor?: string /** * Called when the view is ready */ onReady?: () => void /** * Called when an image is loaded */ onImageLoaded?: (event: { nativeEvent: ImageLoadedEvent }) => void /** * Called when the image changes */ onImageChanged?: (event: { nativeEvent: ImageLoadedEvent }) => void /** * Called when an error occurs */ onError?: (event: { nativeEvent: { error: string } }) => void } // Get the native view manager const NativeView = requireNativeViewManager('ImageEditorView') /** * Native image editor view component */ export class ImageEditorView extends React.Component { private nativeRef = React.createRef(); /** * Crop the image */ async crop(params: CropParams): Promise { if (this.nativeRef.current) { await this.nativeRef.current.crop(params.x, params.y, params.width, params.height) } } /** * Rotate the image */ async rotate(angle: number): Promise { if (this.nativeRef.current) { await this.nativeRef.current.rotate(angle) } } /** * Flip horizontally */ async flipHorizontal(): Promise { if (this.nativeRef.current) { await this.nativeRef.current.flipHorizontal() } } /** * Flip vertically */ async flipVertical(): Promise { if (this.nativeRef.current) { await this.nativeRef.current.flipVertical() } } /** * Undo last operation */ async undo(): Promise { if (this.nativeRef.current) { await this.nativeRef.current.undo() } } /** * Redo last operation */ async redo(): Promise { if (this.nativeRef.current) { await this.nativeRef.current.redo() } } /** * Export the current image */ async export(options: ExportOptions = {}): Promise { if (this.nativeRef.current) { return await this.nativeRef.current.export(options) } throw new Error('View not ready') } render() { return ( ) } } // Export UI component export { ImageEditorUI, ImageEditorUIPresets } from './ImageEditorUI' export type { ImageEditorUIProps, ImageEditorUIConfig, ImageEditorUIRef } from './ImageEditorUI' // Default export export default { ImageEditorModule, ImageEditorView, }