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 web support #131

Merged
merged 1 commit into from
Feb 5, 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
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ npx pod-install
Start by importing the library:

```ts
import ImageEditor from "@react-native-community/image-editor";
import ImageEditor from '@react-native-community/image-editor';
```

### Crop image
Expand All @@ -39,33 +39,38 @@ 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);
})
ImageEditor.cropImage(uri, cropData).then((url) => {
console.log('Cropped image uri', url);
// In case of Web, the `url` is the base64 string
});
```

### `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') **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 supports both `JPEG`, `WEBP` and `PNG` formats.<br/>**Default value**: (iOS: `1`), (Android: `0.9`) |

| 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**: '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`) |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can update that iOS supports PNG as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PNG uses lossless compression, that's why UIImagePNGRepresentation does not accept compressionQuality parameter like UIImageJPEGRepresentation does, see:

if([extension isEqualToString:@"png"]){
imageData = UIImagePNGRepresentation(croppedImage);
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"];
}

| `format` | No | **(WEB ONLY)** The format of the resulting image, possible values are `jpeg`, `png`, `webp`, **Default value**: `jpeg` |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool to support it in Android and iOS implementation as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I going to add it after fixing one important issue on Android


```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
quality: number, // 0...1
format: 'jpeg' | 'png' | 'webp' // web only
};
```

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

<!-- badges -->

[build-badge]: https://github.com/callstack/react-native-image-editor/actions/workflows/main.yml/badge.svg
[build]: https://github.com/callstack/react-native-image-editor/actions/workflows/main.yml
[version-badge]: https://img.shields.io/npm/v/@react-native-community/image-editor.svg
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ImageCropData
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 {
Expand Down
76 changes: 76 additions & 0 deletions src/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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
}

function drawImage(
img: HTMLImageElement,
{ offset, size, displaySize }: ImageCropData
): HTMLCanvasElement {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

if (!context) {
throw new Error('Failed to get canvas context');
}

const sx = offset.x,
sy = offset.y,
sWidth = size.width,
sHeight = size.height,
dx = 0,
dy = 0,
dWidth = displaySize?.width ?? sWidth,
dHeight = displaySize?.height ?? sHeight;

canvas.width = dWidth;
canvas.height = dHeight;

context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

return canvas;
}

function fetchImage(imgSrc: string): Promise<HTMLImageElement> {
return new Promise<HTMLImageElement>((resolve, reject) => {
const onceOptions = { once: true };
const img = new Image();

function onImageError(event: ErrorEvent) {
reject(event);
}

function onLoad() {
resolve(img);
}

img.addEventListener('error', onImageError, onceOptions);
img.addEventListener('load', onLoad, onceOptions);
img.crossOrigin = 'anonymous';
img.src = imgSrc;
});
}

class ImageEditor {
static cropImage(imgSrc: string, cropData: ImageCropData): Promise<string> {
/**
* 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 66 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'}`,
cropData.quality ?? 1
);
});
}
}

export default ImageEditor;
16 changes: 16 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"types": ["react-native"],
"lib": [
"es2019",
"es2020.bigint",
"es2020.date",
"es2020.number",
"es2020.promise",
"es2020.string",
"es2020.symbol.wellknown",
"es2021.promise",
"es2021.string",
"es2021.weakref",
"es2022.array",
"es2022.object",
"es2022.string",
"dom"
],
"rootDir": "./",
"paths": {
"@react-native-community/image-editor": ["./src/index"]
Expand Down
Loading