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: Add format on Android & iOS #135

Merged
merged 5 commits into from
Feb 11, 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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ ImageEditor.cropImage(uri, cropData).then((url) => {
| `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**: 'contain' |
| `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**: (iOS: `1`), (Android: `0.9`) |
| `format` | No | **(WEB ONLY)** The format of the resulting image, possible values are `jpeg`, `png`, `webp`, **Default value**: `jpeg` |
| `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. |

```ts
cropData: ImageCropData = {
offset: {x: number, y: number},
size: {width: number, height: number},
displaySize: {width: number, height: number},
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' // web only
format: 'jpeg' | 'png' | 'webp',
};
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

object MimeType {
const val JPEG = "image/jpeg"
const val PNG = "image/png"
const val WEBP = "image/webp"
}

class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
private val moduleCoroutineScope = CoroutineScope(Dispatchers.Default)

Expand Down Expand Up @@ -91,6 +97,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
* is passed to this is the file:// URI of the new image
*/
fun cropImage(uri: String?, options: ReadableMap, promise: Promise) {
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 quality =
Expand Down Expand Up @@ -149,14 +156,10 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
if (cropped == null) {
throw IOException("Cannot decode bitmap: $uri")
}
val mimeType = outOptions.outMimeType
if (mimeType.isNullOrEmpty()) {
throw IOException("Could not determine MIME type")
}

val mimeType = getMimeType(outOptions, format)
val tempFile = createTempFile(reactContext, mimeType)
writeCompressedBitmapToFile(cropped, mimeType, tempFile, quality)
if (mimeType == "image/jpeg") {
if (mimeType == MimeType.JPEG) {
copyExif(reactContext, Uri.parse(uri), tempFile)
}
promise.resolve(Uri.fromFile(tempFile).toString())
Expand Down Expand Up @@ -434,6 +437,20 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
)

// Utils
private fun getMimeType(outOptions: BitmapFactory.Options, format: String?): String {
val mimeType =
when (format) {
"webp" -> MimeType.WEBP
"png" -> MimeType.PNG
"jpeg" -> MimeType.JPEG
else -> outOptions.outMimeType
}
if (mimeType.isNullOrEmpty()) {
return MimeType.JPEG
}
return mimeType
}

private fun getOrientation(context: Context, uri: Uri): Int {
val file = getFileFromUri(context, uri)
if (file == null) {
Expand Down Expand Up @@ -501,8 +518,8 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {

private fun getFileExtensionForType(mimeType: String?): String {
return when (mimeType) {
"image/png" -> ".png"
"image/webp" -> ".webp"
MimeType.PNG -> ".png"
MimeType.WEBP -> ".webp"
else -> ".jpg"
}
}
Expand All @@ -515,8 +532,8 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
@Suppress("DEPRECATION") CompressFormat.WEBP
}
return when (mimeType) {
"image/png" -> CompressFormat.PNG
"image/webp" -> webpCompressFormat
MimeType.PNG -> CompressFormat.PNG
MimeType.WEBP -> webpCompressFormat
else -> CompressFormat.JPEG
}
}
Expand Down
5 changes: 5 additions & 0 deletions ios/RNCImageEditor.mm
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ - (void) cropImage:(NSString *)uri
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
NSString *format = data.format();
CGSize size = [RCTConvert CGSize:@{ @"width": @(data.size().width()), @"height": @(data.size().height()) }];
CGPoint offset = [RCTConvert CGPoint:@{ @"x": @(data.offset().x()), @"y": @(data.offset().y()) }];
CGSize targetSize = size;
Expand All @@ -66,6 +67,7 @@ - (void) cropImage:(NSString *)uri
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSString *format = cropData[@"format"];
CGSize size = [RCTConvert CGSize:cropData[@"size"]];
CGPoint offset = [RCTConvert CGPoint:cropData[@"offset"]];
CGSize targetSize = size;
Expand All @@ -82,6 +84,9 @@ - (void) cropImage:(NSString *)uri
NSURL *url = [imageRequest URL];
NSString *urlPath = [url path];
NSString *extension = [urlPath pathExtension];
if([format isEqualToString:@"png"] || [format isEqualToString:@"jpeg"]){
extension = format;
}

[[_bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES] loadImageWithURLRequest:imageRequest callback:^(NSError *error, UIImage *image) {
if (error) {
Expand Down
5 changes: 5 additions & 0 deletions src/NativeRNCImageEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface Spec extends TurboModule {
* (Optional) Compression quality jpg images (number from 0 to 1).
*/
quality?: Float;

/**
* (Optional) The format of the resulting image. Default auto-detection based on given image
*/
format?: string;
}
): Promise<string>;
}
Expand Down
11 changes: 1 addition & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Platform } from 'react-native';
import NativeRNCImageEditor from './NativeRNCImageEditor';
import type { Spec } from './NativeRNCImageEditor';
import type { ImageCropData } from './types.ts';

const LINKING_ERROR =
`The package '@react-native-community/image-editor' doesn't seem to be linked. Make sure: \n\n` +
Expand All @@ -23,16 +24,6 @@ const RNCImageEditor: Spec = NativeRNCImageEditor
},
});

type ImageCropDataFromSpec = Parameters<Spec['cropImage']>[1];

export interface ImageCropData
extends Omit<ImageCropDataFromSpec, 'resizeMode'> {
resizeMode?: 'contain' | 'cover' | 'stretch';
// ^^^ codegen doesn't support union types yet
// so to provide more type safety we override the type here
format?: 'png' | 'jpeg' | 'webp'; // web only
}

class ImageEditor {
/**
* Crop the image specified by the URI param. If URI points to a remote
Expand Down
12 changes: 1 addition & 11 deletions src/index.web.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
import type { Spec } from './NativeRNCImageEditor';

type ImageCropDataFromSpec = Parameters<Spec['cropImage']>[1];

export interface ImageCropData
extends Omit<ImageCropDataFromSpec, 'resizeMode'> {
resizeMode?: 'contain' | 'cover' | 'stretch';
// ^^^ codegen doesn't support union types yet
// so to provide more type safety we override the type here
format?: 'png' | 'jpeg' | 'webp'; // web only
}
import type { ImageCropData } from './types.ts';

function drawImage(
img: HTMLImageElement,
Expand Down Expand Up @@ -63,7 +53,7 @@
/**
* 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 56 in src/index.web.ts

View workflow job for this annotation

GitHub Actions / Code Quality

Prefer await to then()/catch()/finally()
const canvas = drawImage(image, cropData);
return canvas.toDataURL(
`image/${cropData.format ?? 'jpeg'}`,
Expand Down
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Spec } from './NativeRNCImageEditor.ts';

type ImageCropDataFromSpec = Parameters<Spec['cropImage']>[1];

export interface ImageCropData
extends Omit<ImageCropDataFromSpec, 'resizeMode' | 'format'> {
format?: 'png' | 'jpeg' | 'webp';
resizeMode?: 'contain' | 'cover' | 'stretch';
// ^^^ codegen doesn't support union types yet
// so to provide more type safety we override the type here
}
Loading