diff --git a/README.md b/README.md
index e5f8d15..b0ac36e 100755
--- a/README.md
+++ b/README.md
@@ -36,47 +36,38 @@ import ImageEditor from '@react-native-community/image-editor';
Crop the image specified by the URI param. If URI points to a remote image, it will be downloaded automatically. If the image cannot be loaded/downloaded, the promise will be rejected.
-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.
+If the cropping process is successful, the resultant cropped image will be stored in the cache path, and the [`CropResult`](#result-cropresult) 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(
- ({
- 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`)
- }
-);
+ImageEditor.cropImage(uri, cropData).then((result) => {
+ console.log('Cropped image uri:', result.uri);
+});
```
### `cropData: ImageCropData`
-| Property | Required | Description |
-| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `offset` | Yes | The top-left corner of the cropped image, specified in the original image's coordinate space |
-| `size` | Yes | Size (dimensions) of the cropped image |
-| `displaySize` | No | Size to which you want to scale the cropped image |
-| `resizeMode` | No | Resizing mode to use when scaling the image (iOS only, Android resize mode is always `'cover'`, Web - no support) **Default value**: `'cover'` |
-| `quality` | No | The quality of the resulting image, expressed as a value from `0.0` to `1.0`.
The value `0.0` represents the maximum compression (or lowest quality) while the value `1.0` represents the least compression (or best quality).
iOS supports only `JPEG` format, while Android/Web supports both `JPEG`, `WEBP` and `PNG` formats.
**Default value**: `0.9` |
-| `format` | No | The format of the resulting image, possible values are `jpeg`, `png`, `webp`.
**Default value**: based on the provided image; if value determination is not possible, `jpeg` will be used as a fallback.
`webp` isn't supported by iOS. |
-
-```ts
-cropData: ImageCropData = {
- offset: { x: number, y: number },
- size: { width: number, height: number },
- displaySize: { width: number, height: number },
- resizeMode: 'contain' | 'cover' | 'stretch',
- quality: number, // 0...1
- format: 'jpeg' | 'png' | 'webp',
-};
-```
+| Name | Type | Description |
+| ------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `offset` | `{ x: number, y: number }` | The top-left corner of the cropped image, specified in the original image's coordinate space |
+| `size` | `{ width: number, height: number }` | Size (dimensions) of the cropped image |
+| `displaySize`
_(optional)_ | `{ width: number, height: number }` | Size to which you want to scale the cropped image |
+| `resizeMode`
_(optional)_ | `'contain' \| 'cover' \| 'stretch' \| 'center'` | Resizing mode to use when scaling the image (iOS only, Android resize mode is always `'cover'`, Web - no support)
**Default value**: `'cover'` |
+| `quality`
_(optional)_ | `number` | A value in range `0.0` - `1.0` specifying compression level of the result image. `1` means no compression (highest quality) and `0` the highest compression (lowest quality)
**Default value**: `0.9` |
+| `format`
_(optional)_ | `'jpeg' \| 'png' \| 'webp'` | The format of the resulting image.
**Default value**: based on the provided image;
if value determination is not possible, `'jpeg'` will be used as a fallback.
`'webp'` isn't supported by iOS. |
+| `includeBase64`
_(optional)_ | `boolean` | Indicates if Base64 formatted picture data should also be included in the [`CropResult`](#result-cropresult).
**Default value**: `false` |
+
+### `result: CropResult`
+
+| Name | Type | Description |
+| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `uri` | `string` | The path to the image file (example: `'file:///data/user/0/.../image.jpg'`)
**WEB:** `uri` is the data URI string (example `'data:image/jpeg;base64,/4AAQ...AQABAA'`) |
+| `path` | `string` | The URI of the image (example: `'/data/user/0/.../image.jpg'`)
**WEB:** `path` is the blob URL (example `'blob:https://example.com/43ff7a16...e46b1'`) |
+| `name` | `string` | The name of the image file. (example: `'image.jpg'`) |
+| `width` | `number` | The width of the image in pixels |
+| `height` | `number` | Height of the image in pixels |
+| `size` | `number` | The size of the image in bytes |
+| `type` | `string` | The MIME type of the image (`'image/jpeg'`, `'image/png'`, `'image/webp'`) |
+| `base64`
_(optional)_ | `string` | The base64-encoded image data example: `'/9j/4AAQSkZJRgABAQAAAQABAAD'`
if you need data URI as the `source` for an `Image` element for example, you can use `data:${type};base64,${base64}` |
For more advanced usage check our [example app](/example/src/App.tsx).
diff --git a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt
index 4ffc191..ff78c15 100644
--- a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt
+++ b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt
@@ -19,7 +19,7 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.text.TextUtils
-import android.util.Base64 as AndroidUtilBase64
+import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import com.facebook.common.logging.FLog
import com.facebook.infer.annotation.Assertions
@@ -32,11 +32,11 @@ import com.facebook.react.bridge.WritableMap
import com.facebook.react.common.ReactConstants
import java.io.ByteArrayInputStream
import java.io.File
+import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.URL
-import java.util.Base64
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -102,6 +102,8 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
val format = if (options.hasKey("format")) options.getString("format") else null
val offset = if (options.hasKey("offset")) options.getMap("offset") else null
val size = if (options.hasKey("size")) options.getMap("size") else null
+ val includeBase64 =
+ if (options.hasKey("includeBase64")) options.getBoolean("includeBase64") else false
val quality =
if (options.hasKey("quality")) (options.getDouble("quality") * 100).toInt() else 90
if (
@@ -164,7 +166,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
if (mimeType == MimeType.JPEG) {
copyExif(reactContext, Uri.parse(uri), tempFile)
}
- promise.resolve(getResultMap(tempFile, cropped))
+ promise.resolve(getResultMap(tempFile, cropped, mimeType, includeBase64))
} catch (e: Exception) {
promise.reject(e)
}
@@ -319,11 +321,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
private fun openBitmapInputStream(uri: String): InputStream? {
return if (uri.startsWith("data:")) {
val src = uri.substring(uri.indexOf(",") + 1)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- ByteArrayInputStream(Base64.getMimeDecoder().decode(src))
- } else {
- ByteArrayInputStream(AndroidUtilBase64.decode(src, AndroidUtilBase64.DEFAULT))
- }
+ ByteArrayInputStream(Base64.decode(src, Base64.DEFAULT))
} else if (isLocalUri(uri)) {
reactContext.contentResolver.openInputStream(Uri.parse(uri))
} else {
@@ -439,7 +437,12 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
)
// Utils
- private fun getResultMap(resizedImage: File, image: Bitmap): WritableMap {
+ private fun getResultMap(
+ resizedImage: File,
+ image: Bitmap,
+ mimeType: String,
+ includeBase64: Boolean
+ ): WritableMap {
val response = Arguments.createMap()
response.putString("path", resizedImage.absolutePath)
response.putString("uri", Uri.fromFile(resizedImage).toString())
@@ -447,9 +450,23 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
response.putInt("size", resizedImage.length().toInt())
response.putInt("width", image.width)
response.putInt("height", image.height)
+ response.putString("type", mimeType)
+
+ if (includeBase64) {
+ response.putString("base64", getBase64String(resizedImage))
+ }
+
return response
}
+ private fun getBase64String(file: File): String {
+ val inputStream = FileInputStream(file)
+ val buffer = ByteArray(file.length().toInt())
+ inputStream.read(buffer)
+ inputStream.close()
+ return Base64.encodeToString(buffer, Base64.NO_WRAP)
+ }
+
private fun getMimeType(outOptions: BitmapFactory.Options, format: String?): String {
val mimeType =
when (format) {
diff --git a/ios/RNCImageEditor.mm b/ios/RNCImageEditor.mm
index caf59e7..440ea6b 100644
--- a/ios/RNCImageEditor.mm
+++ b/ios/RNCImageEditor.mm
@@ -35,6 +35,7 @@
RCTResizeMode resizeMode;
CGFloat quality;
NSString *format;
+ BOOL includeBase64;
};
@implementation RNCImageEditor
@@ -52,6 +53,7 @@ - (Params)adaptParamsWithFormat:(id)format
displayWidth:(id)displayWidth
displayHeight:(id)displayHeight
quality:(id)quality
+ includeBase64:(id)includeBase64
{
return Params{
.offset = {[RCTConvert double:offsetX], [RCTConvert double:offsetY]},
@@ -59,7 +61,8 @@ - (Params)adaptParamsWithFormat:(id)format
.displaySize = {[RCTConvert double:displayWidth], [RCTConvert double:displayHeight]},
.resizeMode = [RCTConvert RCTResizeMode:resizeMode ?: @(DEFAULT_RESIZE_MODE)],
.quality = [RCTConvert CGFloat:quality],
- .format = [RCTConvert NSString:format]
+ .format = [RCTConvert NSString:format],
+ .includeBase64 = [RCTConvert BOOL:includeBase64]
};
}
@@ -89,7 +92,8 @@ - (void) cropImage:(NSString *)uri
resizeMode:data.resizeMode()
displayWidth:@(data.displaySize().has_value() ? data.displaySize()->width() : DEFAULT_DISPLAY_SIZE)
displayHeight:@(data.displaySize().has_value() ? data.displaySize()->height() : DEFAULT_DISPLAY_SIZE)
- quality:@(data.quality().has_value() ? *data.quality() : DEFAULT_COMPRESSION_QUALITY)];
+ quality:@(data.quality().has_value() ? *data.quality() : DEFAULT_COMPRESSION_QUALITY)
+ includeBase64:@(data.includeBase64().has_value() ? *data.includeBase64() : NO)];
#else
RCT_EXPORT_METHOD(cropImage:(NSURLRequest *)imageRequest
cropData:(NSDictionary *)cropData
@@ -104,7 +108,9 @@ - (void) cropImage:(NSString *)uri
resizeMode:cropData[@"resizeMode"]
displayWidth:cropData[@"displaySize"] ? cropData[@"displaySize"][@"width"] : @(DEFAULT_DISPLAY_SIZE)
displayHeight:cropData[@"displaySize"] ? cropData[@"displaySize"][@"height"] : @(DEFAULT_DISPLAY_SIZE)
- quality:cropData[@"quality"] ? cropData[@"quality"] : @(DEFAULT_COMPRESSION_QUALITY)];
+ quality:cropData[@"quality"] ? cropData[@"quality"] : @(DEFAULT_COMPRESSION_QUALITY)
+ includeBase64:cropData[@"includeBase64"]
+ ];
#endif
NSURL *url = [imageRequest URL];
@@ -139,14 +145,15 @@ - (void) cropImage:(NSString *)uri
}
// Store image
+ NSString *type = @"image/jpeg";
NSString *path = NULL;
NSData *imageData = NULL;
if([extension isEqualToString:@"png"]){
+ type = @"image/png";
imageData = UIImagePNGRepresentation(croppedImage);
path = [RNCFileSystem generatePathInDirectory:[[RNCFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"ReactNative_cropped_image_"] withExtension:@".png"];
- }
- else{
+ } else{
imageData = UIImageJPEGRepresentation(croppedImage, params.quality);
path = [RNCFileSystem generatePathInDirectory:[[RNCFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"ReactNative_cropped_image_"] withExtension:@".jpg"];
}
@@ -164,14 +171,18 @@ - (void) cropImage:(NSString *)uri
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),
- };
+
+ NSMutableDictionary *response = [[NSMutableDictionary alloc] init];
+ response[@"path"] = path;
+ response[@"uri"] = uri;
+ response[@"name"] = filename;
+ response[@"type"] = type;
+ response[@"size"] = fileSize ?: @(0);
+ response[@"width"] = @(croppedImage.size.width);
+ response[@"height"] = @(croppedImage.size.height);
+ if (params.includeBase64) {
+ response[@"base64"] = [imageData base64EncodedStringWithOptions:0];
+ }
resolve(response);
}];
diff --git a/package.json b/package.json
index fc91e97..4497987 100755
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"!android/gradlew",
"!android/gradlew.bat",
"!android/local.properties",
+ "!**/__typetests__",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__",
diff --git a/src/NativeRNCImageEditor.ts b/src/NativeRNCImageEditor.ts
index f911a0b..c3c9a07 100644
--- a/src/NativeRNCImageEditor.ts
+++ b/src/NativeRNCImageEditor.ts
@@ -49,6 +49,11 @@ export interface Spec extends TurboModule {
* (Optional) The format of the resulting image. Default auto-detection based on given image
*/
format?: string;
+
+ /**
+ * (Optional) Indicates if Base64 formatted picture data should also be included in the result.
+ */
+ includeBase64?: boolean;
}
): Promise<{
/**
@@ -75,6 +80,16 @@ export interface Spec extends TurboModule {
* The size of the image in bytes
*/
size: Int32;
+
+ /**
+ * MIME type of the image (example: 'image/jpeg')
+ */
+ type: string;
+
+ /**
+ * The base64 string of the image if the `base64` param is true
+ */
+ base64?: string;
}>;
}
diff --git a/src/__typetests__/index.ts b/src/__typetests__/index.ts
new file mode 100644
index 0000000..98fbc86
--- /dev/null
+++ b/src/__typetests__/index.ts
@@ -0,0 +1,35 @@
+/* eslint-disable */
+import ImageEditorNative from '../index.ts';
+
+const requiredParams = {
+ size: { width: 100, height: 100 },
+ offset: { x: 0, y: 0 },
+};
+
+ImageEditorNative.cropImage('', {
+ ...requiredParams,
+ includeBase64: true,
+ // ^^^: if `true` then result has `base64` property as string
+}).then((e) => {
+ const a: string = e.base64;
+ // @ts-expect-error - base64 is a string
+ const b: number = e.base64;
+});
+
+ImageEditorNative.cropImage('', {
+ ...requiredParams,
+ includeBase64: false,
+ // ^^^: if `false` then result doesn't have `base64` property
+}).then((e) => {
+ // @ts-expect-error - base64 doesn't exist
+ const a: string = e.base64;
+});
+
+ImageEditorNative.cropImage('', {
+ ...requiredParams,
+ // includeBase64: false,
+ // ^^^: if `undefined` then result doesn't have `base64` property
+}).then((e) => {
+ // @ts-expect-error - base64 doesn't exist
+ const a: string = e.base64;
+});
diff --git a/src/index.ts b/src/index.ts
index bbb918f..acdafd3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -24,6 +24,9 @@ const RNCImageEditor: Spec = NativeRNCImageEditor
},
});
+type CropResultWithoutBase64 = Omit;
+type ImageCropDataWithoutBase64 = Omit;
+
class ImageEditor {
/**
* Crop the image specified by the URI param. If URI points to a remote
@@ -37,8 +40,23 @@ 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): CropResult {
- return RNCImageEditor.cropImage(uri, cropData);
+
+ // TS overload for better `base64` type inference (see: `src/__typetests__/index.ts`)
+ static cropImage(
+ uri: string,
+ cropData: ImageCropDataWithoutBase64
+ ): Promise;
+ static cropImage(
+ uri: string,
+ cropData: ImageCropDataWithoutBase64 & { includeBase64: false }
+ ): Promise;
+ static cropImage(
+ uri: string,
+ cropData: ImageCropDataWithoutBase64 & { includeBase64: true }
+ ): Promise;
+
+ static cropImage(uri: string, cropData: ImageCropData): Promise {
+ return RNCImageEditor.cropImage(uri, cropData) as Promise;
}
}
diff --git a/src/index.web.ts b/src/index.web.ts
index c74360d..960233f 100644
--- a/src/index.web.ts
+++ b/src/index.web.ts
@@ -51,7 +51,10 @@ function fetchImage(imgSrc: string): Promise {
const DEFAULT_COMPRESSION_QUALITY = 0.9;
class ImageEditor {
- static cropImage(imgSrc: string, cropData: ImageCropData): CropResult {
+ static cropImage(
+ imgSrc: string,
+ cropData: ImageCropData
+ ): Promise {
/**
* Returns a promise that resolves with the base64 encoded string of the cropped image
*/
@@ -72,10 +75,11 @@ class ImageEditor {
let _path: string, _uri: string;
- return {
+ const result: CropResult = {
width: canvas.width,
height: canvas.height,
name: 'ReactNative_cropped_image.' + ext,
+ type: ('image/' + ext) as CropResult['type'],
size: blob.size,
// Lazy getters to avoid unnecessary memory usage
get path() {
@@ -85,12 +89,18 @@ class ImageEditor {
return _path;
},
get uri() {
+ return result.base64 as string;
+ },
+ get base64() {
if (!_uri) {
_uri = canvas.toDataURL(type, quality);
}
- return _uri;
+ return _uri.split(',')[1];
+ // ^^^ remove `data:image/xxx;base64,` prefix (to align with iOS/Android platform behavior)
},
};
+
+ return result;
});
});
}
diff --git a/src/types.ts b/src/types.ts
index baac5fe..40f1f05 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -10,4 +10,13 @@ export interface ImageCropData
// so to provide more type safety we override the type here
}
-export type CropResult = ReturnType;
+export interface CropResult
+ extends Omit, 'type'> {
+ type: 'image/jpeg' | 'image/png' | 'image/webp';
+ // ^^^ codegen doesn't support union types yet
+}
+
+// Utils
+type AsyncReturnType = T extends (...args: any[]) => Promise
+ ? R
+ : never;