Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: [web/android/ios] Upon success, return more image information #140

Merged
merged 2 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
mateusz1913 marked this conversation as resolved.
Show resolved Hide resolved
}
);
```

### `cropData: ImageCropData`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions example/src/SquareImageCropper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@
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) {
Expand All @@ -151,7 +151,7 @@
}

const styles = StyleSheet.create({
container: {

Check warning on line 154 in example/src/SquareImageCropper.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

Color literal: { backgroundColor: 'white' }
flex: 1,
backgroundColor: 'white',
paddingTop: 20,
Expand All @@ -160,7 +160,7 @@
alignSelf: 'center',
marginTop: 12,
},
cropButtonTouchable: {

Check warning on line 163 in example/src/SquareImageCropper.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

Color literal: { backgroundColor: 'royalblue' }
alignSelf: 'center',
marginBottom: 10,
marginTop: 'auto',
Expand All @@ -170,17 +170,17 @@
cropButton: {
padding: 12,
},
cropButtonLabel: {

Check warning on line 173 in example/src/SquareImageCropper.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

Color literal: { color: 'white' }
color: 'white',
fontSize: 18,
fontWeight: '500',
},
text: {

Check warning on line 178 in example/src/SquareImageCropper.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

Color literal: { color: 'black' }
color: 'black',
textAlign: 'center',
fontSize: 16,
},
errorText: {

Check warning on line 183 in example/src/SquareImageCropper.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

Color literal: { color: 'red' }
color: 'red',
textAlign: 'center',
fontSize: 16,
Expand Down
17 changes: 15 additions & 2 deletions ios/RNCImageEditor.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
}
Expand All @@ -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);
}];
}

Expand Down
33 changes: 31 additions & 2 deletions src/NativeRNCImageEditor.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -46,7 +50,32 @@ export interface Spec extends TurboModule {
*/
format?: string;
}
): Promise<string>;
): 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<Spec>('RNCImageEditor');
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` +
Expand Down Expand Up @@ -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<string> {
static cropImage(uri: string, cropData: ImageCropData): CropResult {
return RNCImageEditor.cropImage(uri, cropData);
}
}
Expand Down
43 changes: 37 additions & 6 deletions src/index.web.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ImageCropData } from './types.ts';
import type { ImageCropData, CropResult } from './types.ts';

function drawImage(
img: HTMLImageElement,
Expand Down Expand Up @@ -51,16 +51,47 @@
const DEFAULT_COMPRESSION_QUALITY = 0.9;

class ImageEditor {
static cropImage(imgSrc: string, cropData: ImageCropData): Promise<string> {
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) {

Check warning on line 58 in src/index.web.ts

View workflow job for this annotation

GitHub Actions / Code Quality

Prefer await to then()/catch()/finally()
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<Blob | null>(function onfulfilledCanvasToBlob(
resolve
) {
canvas.toBlob(resolve, type, quality);
}).then((blob) => {

Check warning on line 68 in src/index.web.ts

View workflow job for this annotation

GitHub Actions / Code Quality

Prefer await to then()/catch()/finally()
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;
},
};
});
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Spec['cropImage']>;
Loading