Skip to content

Commit 764f6ec

Browse files
authored
Merge pull request #144 from callstack/feature/retyui/base64
feature: Add base64 output
2 parents c59e226 + eeec3fe commit 764f6ec

File tree

9 files changed

+170
-63
lines changed

9 files changed

+170
-63
lines changed

README.md

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,47 +36,38 @@ import ImageEditor from '@react-native-community/image-editor';
3636

3737
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.
3838

39-
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.
39+
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.
4040

4141
```ts
42-
ImageEditor.cropImage(uri, cropData).then(
43-
({
44-
uri, // the path to the image file (example: 'file:///data/user/0/.../image.jpg')
45-
path, // the URI of the image (example: '/data/user/0/.../image.jpg')
46-
name, // the name of the image file. (example: 'image.jpg')
47-
width, // the width of the image in pixels
48-
height, // height of the image in pixels
49-
size, // the size of the image in bytes
50-
}) => {
51-
console.log('Cropped image uri:', uri);
52-
// WEB has different response:
53-
// - `uri` is the base64 string (example `data:image/jpeg;base64,/4AAQ...AQABAA`)
54-
// - `path` is the blob URL (example `blob:https://example.com/43ff7a16...e46b1`)
55-
}
56-
);
42+
ImageEditor.cropImage(uri, cropData).then((result) => {
43+
console.log('Cropped image uri:', result.uri);
44+
});
5745
```
5846

5947
### `cropData: ImageCropData`
6048

61-
| Property | Required | Description |
62-
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63-
| `offset` | Yes | The top-left corner of the cropped image, specified in the original image's coordinate space |
64-
| `size` | Yes | Size (dimensions) of the cropped image |
65-
| `displaySize` | No | Size to which you want to scale the cropped image |
66-
| `resizeMode` | No | Resizing mode to use when scaling the image (iOS only, Android resize mode is always `'cover'`, Web - no support) **Default value**: `'cover'` |
67-
| `quality` | No | The quality of the resulting image, expressed as a value from `0.0` to `1.0`. <br/>The value `0.0` represents the maximum compression (or lowest quality) while the value `1.0` represents the least compression (or best quality).<br/>iOS supports only `JPEG` format, while Android/Web supports both `JPEG`, `WEBP` and `PNG` formats.<br/>**Default value**: `0.9` |
68-
| `format` | No | The format of the resulting image, possible values are `jpeg`, `png`, `webp`. <br/> **Default value**: based on the provided image; if value determination is not possible, `jpeg` will be used as a fallback. <br/> `webp` isn't supported by iOS. |
69-
70-
```ts
71-
cropData: ImageCropData = {
72-
offset: { x: number, y: number },
73-
size: { width: number, height: number },
74-
displaySize: { width: number, height: number },
75-
resizeMode: 'contain' | 'cover' | 'stretch',
76-
quality: number, // 0...1
77-
format: 'jpeg' | 'png' | 'webp',
78-
};
79-
```
49+
| Name | Type | Description |
50+
| ------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
51+
| `offset` | `{ x: number, y: number }` | The top-left corner of the cropped image, specified in the original image's coordinate space |
52+
| `size` | `{ width: number, height: number }` | Size (dimensions) of the cropped image |
53+
| `displaySize`<br>_(optional)_ | `{ width: number, height: number }` | Size to which you want to scale the cropped image |
54+
| `resizeMode`<br>_(optional)_ | `'contain' \| 'cover' \| 'stretch' \| 'center'` | Resizing mode to use when scaling the image (iOS only, Android resize mode is always `'cover'`, Web - no support) <br/>**Default value**: `'cover'` |
55+
| `quality`<br>_(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) <br/>**Default value**: `0.9` |
56+
| `format`<br>_(optional)_ | `'jpeg' \| 'png' \| 'webp'` | The format of the resulting image.<br/> **Default value**: based on the provided image;<br>if value determination is not possible, `'jpeg'` will be used as a fallback.<br/>`'webp'` isn't supported by iOS. |
57+
| `includeBase64`<br>_(optional)_ | `boolean` | Indicates if Base64 formatted picture data should also be included in the [`CropResult`](#result-cropresult). <br/>**Default value**: `false` |
58+
59+
### `result: CropResult`
60+
61+
| Name | Type | Description |
62+
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63+
| `uri` | `string` | The path to the image file (example: `'file:///data/user/0/.../image.jpg'`)<br> **WEB:** `uri` is the data URI string (example `'data:image/jpeg;base64,/4AAQ...AQABAA'`) |
64+
| `path` | `string` | The URI of the image (example: `'/data/user/0/.../image.jpg'`)<br> **WEB:** `path` is the blob URL (example `'blob:https://example.com/43ff7a16...e46b1'`) |
65+
| `name` | `string` | The name of the image file. (example: `'image.jpg'`) |
66+
| `width` | `number` | The width of the image in pixels |
67+
| `height` | `number` | Height of the image in pixels |
68+
| `size` | `number` | The size of the image in bytes |
69+
| `type` | `string` | The MIME type of the image (`'image/jpeg'`, `'image/png'`, `'image/webp'`) |
70+
| `base64`<br>_(optional)_ | `string` | The base64-encoded image data example: `'/9j/4AAQSkZJRgABAQAAAQABAAD'`<br>if you need data URI as the `source` for an `Image` element for example, you can use `data:${type};base64,${base64}` |
8071

8172
For more advanced usage check our [example app](/example/src/App.tsx).
8273

android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import android.net.Uri
1919
import android.os.Build
2020
import android.provider.MediaStore
2121
import android.text.TextUtils
22-
import android.util.Base64 as AndroidUtilBase64
22+
import android.util.Base64
2323
import androidx.exifinterface.media.ExifInterface
2424
import com.facebook.common.logging.FLog
2525
import com.facebook.infer.annotation.Assertions
@@ -32,11 +32,11 @@ import com.facebook.react.bridge.WritableMap
3232
import com.facebook.react.common.ReactConstants
3333
import java.io.ByteArrayInputStream
3434
import java.io.File
35+
import java.io.FileInputStream
3536
import java.io.FileOutputStream
3637
import java.io.IOException
3738
import java.io.InputStream
3839
import java.net.URL
39-
import java.util.Base64
4040
import kotlin.math.roundToInt
4141
import kotlinx.coroutines.CoroutineScope
4242
import kotlinx.coroutines.Dispatchers
@@ -102,6 +102,8 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
102102
val format = if (options.hasKey("format")) options.getString("format") else null
103103
val offset = if (options.hasKey("offset")) options.getMap("offset") else null
104104
val size = if (options.hasKey("size")) options.getMap("size") else null
105+
val includeBase64 =
106+
if (options.hasKey("includeBase64")) options.getBoolean("includeBase64") else false
105107
val quality =
106108
if (options.hasKey("quality")) (options.getDouble("quality") * 100).toInt() else 90
107109
if (
@@ -164,7 +166,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
164166
if (mimeType == MimeType.JPEG) {
165167
copyExif(reactContext, Uri.parse(uri), tempFile)
166168
}
167-
promise.resolve(getResultMap(tempFile, cropped))
169+
promise.resolve(getResultMap(tempFile, cropped, mimeType, includeBase64))
168170
} catch (e: Exception) {
169171
promise.reject(e)
170172
}
@@ -319,11 +321,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
319321
private fun openBitmapInputStream(uri: String): InputStream? {
320322
return if (uri.startsWith("data:")) {
321323
val src = uri.substring(uri.indexOf(",") + 1)
322-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
323-
ByteArrayInputStream(Base64.getMimeDecoder().decode(src))
324-
} else {
325-
ByteArrayInputStream(AndroidUtilBase64.decode(src, AndroidUtilBase64.DEFAULT))
326-
}
324+
ByteArrayInputStream(Base64.decode(src, Base64.DEFAULT))
327325
} else if (isLocalUri(uri)) {
328326
reactContext.contentResolver.openInputStream(Uri.parse(uri))
329327
} else {
@@ -439,17 +437,36 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
439437
)
440438

441439
// Utils
442-
private fun getResultMap(resizedImage: File, image: Bitmap): WritableMap {
440+
private fun getResultMap(
441+
resizedImage: File,
442+
image: Bitmap,
443+
mimeType: String,
444+
includeBase64: Boolean
445+
): WritableMap {
443446
val response = Arguments.createMap()
444447
response.putString("path", resizedImage.absolutePath)
445448
response.putString("uri", Uri.fromFile(resizedImage).toString())
446449
response.putString("name", resizedImage.name)
447450
response.putInt("size", resizedImage.length().toInt())
448451
response.putInt("width", image.width)
449452
response.putInt("height", image.height)
453+
response.putString("type", mimeType)
454+
455+
if (includeBase64) {
456+
response.putString("base64", getBase64String(resizedImage))
457+
}
458+
450459
return response
451460
}
452461

462+
private fun getBase64String(file: File): String {
463+
val inputStream = FileInputStream(file)
464+
val buffer = ByteArray(file.length().toInt())
465+
inputStream.read(buffer)
466+
inputStream.close()
467+
return Base64.encodeToString(buffer, Base64.NO_WRAP)
468+
}
469+
453470
private fun getMimeType(outOptions: BitmapFactory.Options, format: String?): String {
454471
val mimeType =
455472
when (format) {

ios/RNCImageEditor.mm

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
RCTResizeMode resizeMode;
3636
CGFloat quality;
3737
NSString *format;
38+
BOOL includeBase64;
3839
};
3940

4041
@implementation RNCImageEditor
@@ -52,14 +53,16 @@ - (Params)adaptParamsWithFormat:(id)format
5253
displayWidth:(id)displayWidth
5354
displayHeight:(id)displayHeight
5455
quality:(id)quality
56+
includeBase64:(id)includeBase64
5557
{
5658
return Params{
5759
.offset = {[RCTConvert double:offsetX], [RCTConvert double:offsetY]},
5860
.size = {[RCTConvert double:width], [RCTConvert double:height]},
5961
.displaySize = {[RCTConvert double:displayWidth], [RCTConvert double:displayHeight]},
6062
.resizeMode = [RCTConvert RCTResizeMode:resizeMode ?: @(DEFAULT_RESIZE_MODE)],
6163
.quality = [RCTConvert CGFloat:quality],
62-
.format = [RCTConvert NSString:format]
64+
.format = [RCTConvert NSString:format],
65+
.includeBase64 = [RCTConvert BOOL:includeBase64]
6366
};
6467
}
6568

@@ -89,7 +92,8 @@ - (void) cropImage:(NSString *)uri
8992
resizeMode:data.resizeMode()
9093
displayWidth:@(data.displaySize().has_value() ? data.displaySize()->width() : DEFAULT_DISPLAY_SIZE)
9194
displayHeight:@(data.displaySize().has_value() ? data.displaySize()->height() : DEFAULT_DISPLAY_SIZE)
92-
quality:@(data.quality().has_value() ? *data.quality() : DEFAULT_COMPRESSION_QUALITY)];
95+
quality:@(data.quality().has_value() ? *data.quality() : DEFAULT_COMPRESSION_QUALITY)
96+
includeBase64:@(data.includeBase64().has_value() ? *data.includeBase64() : NO)];
9397
#else
9498
RCT_EXPORT_METHOD(cropImage:(NSURLRequest *)imageRequest
9599
cropData:(NSDictionary *)cropData
@@ -104,7 +108,9 @@ - (void) cropImage:(NSString *)uri
104108
resizeMode:cropData[@"resizeMode"]
105109
displayWidth:cropData[@"displaySize"] ? cropData[@"displaySize"][@"width"] : @(DEFAULT_DISPLAY_SIZE)
106110
displayHeight:cropData[@"displaySize"] ? cropData[@"displaySize"][@"height"] : @(DEFAULT_DISPLAY_SIZE)
107-
quality:cropData[@"quality"] ? cropData[@"quality"] : @(DEFAULT_COMPRESSION_QUALITY)];
111+
quality:cropData[@"quality"] ? cropData[@"quality"] : @(DEFAULT_COMPRESSION_QUALITY)
112+
includeBase64:cropData[@"includeBase64"]
113+
];
108114

109115
#endif
110116
NSURL *url = [imageRequest URL];
@@ -139,14 +145,15 @@ - (void) cropImage:(NSString *)uri
139145
}
140146

141147
// Store image
148+
NSString *type = @"image/jpeg";
142149
NSString *path = NULL;
143150
NSData *imageData = NULL;
144151

145152
if([extension isEqualToString:@"png"]){
153+
type = @"image/png";
146154
imageData = UIImagePNGRepresentation(croppedImage);
147155
path = [RNCFileSystem generatePathInDirectory:[[RNCFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"ReactNative_cropped_image_"] withExtension:@".png"];
148-
}
149-
else{
156+
} else{
150157
imageData = UIImageJPEGRepresentation(croppedImage, params.quality);
151158
path = [RNCFileSystem generatePathInDirectory:[[RNCFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"ReactNative_cropped_image_"] withExtension:@".jpg"];
152159
}
@@ -164,14 +171,18 @@ - (void) cropImage:(NSString *)uri
164171
NSError *attributesError = nil;
165172
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&attributesError];
166173
NSNumber *fileSize = fileAttributes == nil ? 0 : [fileAttributes objectForKey:NSFileSize];
167-
NSDictionary *response = @{
168-
@"path": path,
169-
@"uri": uri,
170-
@"name": filename,
171-
@"size": fileSize ?: @(0),
172-
@"width": @(croppedImage.size.width),
173-
@"height": @(croppedImage.size.height),
174-
};
174+
175+
NSMutableDictionary *response = [[NSMutableDictionary alloc] init];
176+
response[@"path"] = path;
177+
response[@"uri"] = uri;
178+
response[@"name"] = filename;
179+
response[@"type"] = type;
180+
response[@"size"] = fileSize ?: @(0);
181+
response[@"width"] = @(croppedImage.size.width);
182+
response[@"height"] = @(croppedImage.size.height);
183+
if (params.includeBase64) {
184+
response[@"base64"] = [imageData base64EncodedStringWithOptions:0];
185+
}
175186

176187
resolve(response);
177188
}];

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"!android/gradlew",
4141
"!android/gradlew.bat",
4242
"!android/local.properties",
43+
"!**/__typetests__",
4344
"!**/__tests__",
4445
"!**/__fixtures__",
4546
"!**/__mocks__",

src/NativeRNCImageEditor.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export interface Spec extends TurboModule {
4949
* (Optional) The format of the resulting image. Default auto-detection based on given image
5050
*/
5151
format?: string;
52+
53+
/**
54+
* (Optional) Indicates if Base64 formatted picture data should also be included in the result.
55+
*/
56+
includeBase64?: boolean;
5257
}
5358
): Promise<{
5459
/**
@@ -75,6 +80,16 @@ export interface Spec extends TurboModule {
7580
* The size of the image in bytes
7681
*/
7782
size: Int32;
83+
84+
/**
85+
* MIME type of the image (example: 'image/jpeg')
86+
*/
87+
type: string;
88+
89+
/**
90+
* The base64 string of the image if the `base64` param is true
91+
*/
92+
base64?: string;
7893
}>;
7994
}
8095

src/__typetests__/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* eslint-disable */
2+
import ImageEditorNative from '../index.ts';
3+
4+
const requiredParams = {
5+
size: { width: 100, height: 100 },
6+
offset: { x: 0, y: 0 },
7+
};
8+
9+
ImageEditorNative.cropImage('<test>', {
10+
...requiredParams,
11+
includeBase64: true,
12+
// ^^^: if `true` then result has `base64` property as string
13+
}).then((e) => {
14+
const a: string = e.base64;
15+
// @ts-expect-error - base64 is a string
16+
const b: number = e.base64;
17+
});
18+
19+
ImageEditorNative.cropImage('<test>', {
20+
...requiredParams,
21+
includeBase64: false,
22+
// ^^^: if `false` then result doesn't have `base64` property
23+
}).then((e) => {
24+
// @ts-expect-error - base64 doesn't exist
25+
const a: string = e.base64;
26+
});
27+
28+
ImageEditorNative.cropImage('<test>', {
29+
...requiredParams,
30+
// includeBase64: false,
31+
// ^^^: if `undefined` then result doesn't have `base64` property
32+
}).then((e) => {
33+
// @ts-expect-error - base64 doesn't exist
34+
const a: string = e.base64;
35+
});

0 commit comments

Comments
 (0)