diff --git a/README.md b/README.md index c3e4ae8..4e3c8a3 100755 --- a/README.md +++ b/README.md @@ -39,10 +39,21 @@ Crop the image specified by the URI param. If URI points to a remote image, it w If the cropping process is successful, the resultant cropped image will be stored in the cache path, and the URI returned in the promise will point to the image in the cache path. Remember to delete the cropped image from the cache path when you are done with it. ```ts -ImageEditor.cropImage(uri, cropData).then((url) => { - console.log('Cropped image uri', url); - // In case of Web, the `url` is the base64 string -}); +ImageEditor.cropImage(uri, cropData).then( + ({ + uri, // the path to the image file (example: 'file:///data/user/0/.../image.jpg') + path, // the URI of the image (example: '/data/user/0/.../image.jpg') + name, // the name of the image file. (example: 'image.jpg') + width, // the width of the image in pixels + height, // height of the image in pixels + size, // the size of the image in bytes + }) => { + console.log('Cropped image uri:', uri); + // WEB has different response: + // - `uri` is the base64 string (example `data:image/jpeg;base64,/4AAQ...AQABAA`) + // - `path` is the blob URL (example `blob:https://example.com/43ff7a16...e46b1`) + } +); ``` ### `cropData: ImageCropData` diff --git a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt index 453c2d9..4ffc191 100644 --- a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt +++ b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt @@ -23,10 +23,12 @@ import android.util.Base64 as AndroidUtilBase64 import androidx.exifinterface.media.ExifInterface import com.facebook.common.logging.FLog import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.JSApplicationIllegalArgumentException import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap import com.facebook.react.common.ReactConstants import java.io.ByteArrayInputStream import java.io.File @@ -162,7 +164,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { if (mimeType == MimeType.JPEG) { copyExif(reactContext, Uri.parse(uri), tempFile) } - promise.resolve(Uri.fromFile(tempFile).toString()) + promise.resolve(getResultMap(tempFile, cropped)) } catch (e: Exception) { promise.reject(e) } @@ -437,6 +439,17 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { ) // Utils + private fun getResultMap(resizedImage: File, image: Bitmap): WritableMap { + val response = Arguments.createMap() + response.putString("path", resizedImage.absolutePath) + response.putString("uri", Uri.fromFile(resizedImage).toString()) + response.putString("name", resizedImage.name) + response.putInt("size", resizedImage.length().toInt()) + response.putInt("width", image.width) + response.putInt("height", image.height) + return response + } + private fun getMimeType(outOptions: BitmapFactory.Options, format: String?): String { val mimeType = when (format) { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 009fae9..43b0005 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -807,7 +807,7 @@ PODS: - React-jsinspector (0.72.6) - React-logger (0.72.6): - glog - - react-native-image-editor (3.1.0): + - react-native-image-editor (3.2.0): - RCT-Folly (= 2021.07.22.00) - RCTRequired - RCTTypeSafety @@ -1129,7 +1129,7 @@ SPEC CHECKSUMS: React-jsiexecutor: faca9c368233f59ed24601aca0185870466a96e9 React-jsinspector: 194e32c6aab382d88713ad3dd0025c5f5c4ee072 React-logger: cebf22b6cf43434e471dc561e5911b40ac01d289 - react-native-image-editor: a58ef0223f36bd9b8aa5c2b9d45926ce51946e50 + react-native-image-editor: ed27495b9a98482d6f4642c42b165dfe23523b8d React-NativeModulesApple: 63505fb94b71e2469cab35bdaf36cca813cb5bfd React-perflogger: e3596db7e753f51766bceadc061936ef1472edc3 React-RCTActionSheet: 17ab132c748b4471012abbcdcf5befe860660485 diff --git a/example/src/SquareImageCropper.tsx b/example/src/SquareImageCropper.tsx index ab4abf4..253cb02 100644 --- a/example/src/SquareImageCropper.tsx +++ b/example/src/SquareImageCropper.tsx @@ -131,12 +131,12 @@ export class SquareImageCropper extends Component { if (!this._transformData) { return; } - const croppedImageURI = await ImageEditor.cropImage( + const { uri } = await ImageEditor.cropImage( this.state.photo.uri, this._transformData ); - if (croppedImageURI) { - this.setState({ croppedImageURI }); + if (uri) { + this.setState({ croppedImageURI: uri }); } } catch (cropError) { if (cropError instanceof Error) { diff --git a/ios/RNCImageEditor.mm b/ios/RNCImageEditor.mm index 4102a4f..3ae3b63 100644 --- a/ios/RNCImageEditor.mm +++ b/ios/RNCImageEditor.mm @@ -122,7 +122,6 @@ - (void) cropImage:(NSString *)uri path = [RNCFileSystem generatePathInDirectory:[[RNCFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"ReactNative_cropped_image_"] withExtension:@".png"]; } else{ - imageData = UIImageJPEGRepresentation(croppedImage, compressionQuality); path = [RNCFileSystem generatePathInDirectory:[[RNCFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"ReactNative_cropped_image_"] withExtension:@".jpg"]; } @@ -135,7 +134,21 @@ - (void) cropImage:(NSString *)uri return; } - resolve(uri); + NSURL *fileurl = [[NSURL alloc] initFileURLWithPath:path]; + NSString *filename = fileurl.lastPathComponent; + NSError *attributesError = nil; + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&attributesError]; + NSNumber *fileSize = fileAttributes == nil ? 0 : [fileAttributes objectForKey:NSFileSize]; + NSDictionary *response = @{ + @"path": path, + @"uri": uri, + @"name": filename, + @"size": fileSize ?: @(0), + @"width": @(croppedImage.size.width), + @"height": @(croppedImage.size.height), + }; + + resolve(response); }]; } diff --git a/src/NativeRNCImageEditor.ts b/src/NativeRNCImageEditor.ts index 896063a..f911a0b 100644 --- a/src/NativeRNCImageEditor.ts +++ b/src/NativeRNCImageEditor.ts @@ -1,5 +1,9 @@ import type { TurboModule } from 'react-native'; -import type { Double, Float } from 'react-native/Libraries/Types/CodegenTypes'; +import type { + Double, + Float, + Int32, +} from 'react-native/Libraries/Types/CodegenTypes'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { @@ -46,7 +50,32 @@ export interface Spec extends TurboModule { */ format?: string; } - ): Promise; + ): Promise<{ + /** + * The path to the image file (example: '/data/user/0/.../image.jpg') + */ + path: string; + /** + * The URI of the image (example: 'file:///data/user/0/.../image.jpg') + */ + uri: string; + /** + * The name of the image file. (example: 'image.jpg') + */ + name: string; + /** + * The width of the image in pixels + */ + width: Int32; + /** + * The height of the image in pixels + */ + height: Int32; + /** + * The size of the image in bytes + */ + size: Int32; + }>; } export default TurboModuleRegistry.getEnforcing('RNCImageEditor'); diff --git a/src/index.ts b/src/index.ts index 88ba526..bbb918f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import { Platform } from 'react-native'; import NativeRNCImageEditor from './NativeRNCImageEditor'; import type { Spec } from './NativeRNCImageEditor'; -import type { ImageCropData } from './types.ts'; +import type { ImageCropData, CropResult } from './types.ts'; const LINKING_ERROR = `The package '@react-native-community/image-editor' doesn't seem to be linked. Make sure: \n\n` + @@ -37,7 +37,7 @@ class ImageEditor { * will point to the image in the cache path. Remember to delete the * cropped image from the cache path when you are done with it. */ - static cropImage(uri: string, cropData: ImageCropData): Promise { + static cropImage(uri: string, cropData: ImageCropData): CropResult { return RNCImageEditor.cropImage(uri, cropData); } } diff --git a/src/index.web.ts b/src/index.web.ts index 066870e..c74360d 100644 --- a/src/index.web.ts +++ b/src/index.web.ts @@ -1,4 +1,4 @@ -import type { ImageCropData } from './types.ts'; +import type { ImageCropData, CropResult } from './types.ts'; function drawImage( img: HTMLImageElement, @@ -51,16 +51,47 @@ function fetchImage(imgSrc: string): Promise { const DEFAULT_COMPRESSION_QUALITY = 0.9; class ImageEditor { - static cropImage(imgSrc: string, cropData: ImageCropData): Promise { + static cropImage(imgSrc: string, cropData: ImageCropData): CropResult { /** * Returns a promise that resolves with the base64 encoded string of the cropped image */ return fetchImage(imgSrc).then(function onfulfilledImgToCanvas(image) { + const ext = cropData.format ?? 'jpeg'; + const type = `image/${ext}`; + const quality = cropData.quality ?? DEFAULT_COMPRESSION_QUALITY; const canvas = drawImage(image, cropData); - return canvas.toDataURL( - `image/${cropData.format ?? 'jpeg'}`, - cropData.quality ?? DEFAULT_COMPRESSION_QUALITY - ); + + return new Promise(function onfulfilledCanvasToBlob( + resolve + ) { + canvas.toBlob(resolve, type, quality); + }).then((blob) => { + if (!blob) { + throw new Error('Image cannot be created from canvas'); + } + + let _path: string, _uri: string; + + return { + width: canvas.width, + height: canvas.height, + name: 'ReactNative_cropped_image.' + ext, + size: blob.size, + // Lazy getters to avoid unnecessary memory usage + get path() { + if (!_path) { + _path = URL.createObjectURL(blob); + } + return _path; + }, + get uri() { + if (!_uri) { + _uri = canvas.toDataURL(type, quality); + } + return _uri; + }, + }; + }); }); } } diff --git a/src/types.ts b/src/types.ts index 8647b38..e2a8387 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,3 +9,5 @@ export interface ImageCropData // ^^^ codegen doesn't support union types yet // so to provide more type safety we override the type here } + +export type CropResult = ReturnType;