Skip to content

Commit ab88861

Browse files
authored
Merge pull request #131 from callstack/feature/retyui/web-support
feature: Add web support
2 parents 4be0c12 + 07a56b2 commit ab88861

File tree

4 files changed

+110
-12
lines changed

4 files changed

+110
-12
lines changed

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ npx pod-install
2929
Start by importing the library:
3030

3131
```ts
32-
import ImageEditor from "@react-native-community/image-editor";
32+
import ImageEditor from '@react-native-community/image-editor';
3333
```
3434

3535
### Crop image
@@ -39,33 +39,38 @@ Crop the image specified by the URI param. If URI points to a remote image, it w
3939
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.
4040

4141
```ts
42-
ImageEditor.cropImage(uri, cropData).then(url => {
43-
console.log("Cropped image uri", url);
44-
})
42+
ImageEditor.cropImage(uri, cropData).then((url) => {
43+
console.log('Cropped image uri', url);
44+
// In case of Web, the `url` is the base64 string
45+
});
4546
```
4647

4748
### `cropData: ImageCropData`
48-
| Property | Required | Description |
49-
|---------------|----------|----------------------------------------------------------------------------------------------------------------------------|
50-
| `offset` | Yes | The top-left corner of the cropped image, specified in the original image's coordinate space |
51-
| `size` | Yes | Size (dimensions) of the cropped image |
52-
| `displaySize` | No | Size to which you want to scale the cropped image |
53-
| `resizeMode` | No | Resizing mode to use when scaling the image (iOS only, android resize mode is always 'cover') **Default value**: 'contain' |
54-
| `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`) |
49+
50+
| Property | Required | Description |
51+
| ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52+
| `offset` | Yes | The top-left corner of the cropped image, specified in the original image's coordinate space |
53+
| `size` | Yes | Size (dimensions) of the cropped image |
54+
| `displaySize` | No | Size to which you want to scale the cropped image |
55+
| `resizeMode` | No | Resizing mode to use when scaling the image (iOS only, Android resize mode is always 'cover', Web - no support) **Default value**: 'contain' |
56+
| `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`) |
57+
| `format` | No | **(WEB ONLY)** The format of the resulting image, possible values are `jpeg`, `png`, `webp`, **Default value**: `jpeg` |
5558

5659
```ts
5760
cropData: ImageCropData = {
5861
offset: {x: number, y: number},
5962
size: {width: number, height: number},
6063
displaySize: {width: number, height: number},
6164
resizeMode: 'contain' | 'cover' | 'stretch',
62-
quality: number // 0...1
65+
quality: number, // 0...1
66+
format: 'jpeg' | 'png' | 'webp' // web only
6367
};
6468
```
6569

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

6872
<!-- badges -->
73+
6974
[build-badge]: https://github.com/callstack/react-native-image-editor/actions/workflows/main.yml/badge.svg
7075
[build]: https://github.com/callstack/react-native-image-editor/actions/workflows/main.yml
7176
[version-badge]: https://img.shields.io/npm/v/@react-native-community/image-editor.svg

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface ImageCropData
3030
resizeMode?: 'contain' | 'cover' | 'stretch';
3131
// ^^^ codegen doesn't support union types yet
3232
// so to provide more type safety we override the type here
33+
format?: 'png' | 'jpeg' | 'webp'; // web only
3334
}
3435

3536
class ImageEditor {

src/index.web.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { Spec } from './NativeRNCImageEditor';
2+
3+
type ImageCropDataFromSpec = Parameters<Spec['cropImage']>[1];
4+
5+
export interface ImageCropData
6+
extends Omit<ImageCropDataFromSpec, 'resizeMode'> {
7+
resizeMode?: 'contain' | 'cover' | 'stretch';
8+
// ^^^ codegen doesn't support union types yet
9+
// so to provide more type safety we override the type here
10+
format?: 'png' | 'jpeg' | 'webp'; // web only
11+
}
12+
13+
function drawImage(
14+
img: HTMLImageElement,
15+
{ offset, size, displaySize }: ImageCropData
16+
): HTMLCanvasElement {
17+
const canvas = document.createElement('canvas');
18+
const context = canvas.getContext('2d');
19+
20+
if (!context) {
21+
throw new Error('Failed to get canvas context');
22+
}
23+
24+
const sx = offset.x,
25+
sy = offset.y,
26+
sWidth = size.width,
27+
sHeight = size.height,
28+
dx = 0,
29+
dy = 0,
30+
dWidth = displaySize?.width ?? sWidth,
31+
dHeight = displaySize?.height ?? sHeight;
32+
33+
canvas.width = dWidth;
34+
canvas.height = dHeight;
35+
36+
context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
37+
38+
return canvas;
39+
}
40+
41+
function fetchImage(imgSrc: string): Promise<HTMLImageElement> {
42+
return new Promise<HTMLImageElement>((resolve, reject) => {
43+
const onceOptions = { once: true };
44+
const img = new Image();
45+
46+
function onImageError(event: ErrorEvent) {
47+
reject(event);
48+
}
49+
50+
function onLoad() {
51+
resolve(img);
52+
}
53+
54+
img.addEventListener('error', onImageError, onceOptions);
55+
img.addEventListener('load', onLoad, onceOptions);
56+
img.crossOrigin = 'anonymous';
57+
img.src = imgSrc;
58+
});
59+
}
60+
61+
class ImageEditor {
62+
static cropImage(imgSrc: string, cropData: ImageCropData): Promise<string> {
63+
/**
64+
* Returns a promise that resolves with the base64 encoded string of the cropped image
65+
*/
66+
return fetchImage(imgSrc).then(function onfulfilledImgToCanvas(image) {
67+
const canvas = drawImage(image, cropData);
68+
return canvas.toDataURL(
69+
`image/${cropData.format ?? 'jpeg'}`,
70+
cropData.quality ?? 1
71+
);
72+
});
73+
}
74+
}
75+
76+
export default ImageEditor;

tsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
"extends": "@react-native/typescript-config/tsconfig.json",
33
"compilerOptions": {
44
"types": ["react-native"],
5+
"lib": [
6+
"es2019",
7+
"es2020.bigint",
8+
"es2020.date",
9+
"es2020.number",
10+
"es2020.promise",
11+
"es2020.string",
12+
"es2020.symbol.wellknown",
13+
"es2021.promise",
14+
"es2021.string",
15+
"es2021.weakref",
16+
"es2022.array",
17+
"es2022.object",
18+
"es2022.string",
19+
"dom"
20+
],
521
"rootDir": "./",
622
"paths": {
723
"@react-native-community/image-editor": ["./src/index"]

0 commit comments

Comments
 (0)