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

[New architecture] Replace @oguzhnatly/react-native-image-manipulator with expo-image-manipulator #36019

Merged
Merged
22 changes: 15 additions & 7 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ PODS:
- EXAV (13.10.4):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXImageLoader (4.6.0):
- ExpoModulesCore
- React-Core
- Expo (50.0.4):
- ExpoModulesCore
- ExpoImage (1.10.1):
Expand All @@ -43,6 +46,9 @@ PODS:
- SDWebImageAVIFCoder (~> 0.10.1)
- SDWebImageSVGCoder (~> 1.7.0)
- SDWebImageWebPCoder (~> 0.13.0)
- ExpoImageManipulator (11.8.0):
- EXImageLoader
- ExpoModulesCore
- ExpoModulesCore (1.11.8):
- glog
- RCT-Folly (= 2022.05.16.00)
Expand Down Expand Up @@ -1173,8 +1179,6 @@ PODS:
- React-Core
- react-native-geolocation (3.0.6):
- React-Core
- react-native-image-manipulator (1.0.5):
- React
- react-native-image-picker (7.0.3):
- React-Core
- react-native-key-command (1.0.6):
Expand Down Expand Up @@ -1473,8 +1477,10 @@ DEPENDENCIES:
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAV (from `../node_modules/expo-av/ios`)
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
- Expo (from `../node_modules/expo`)
- ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
Expand Down Expand Up @@ -1534,7 +1540,6 @@ DEPENDENCIES:
- react-native-config (from `../node_modules/react-native-config`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- "react-native-image-manipulator (from `../node_modules/@oguzhnatly/react-native-image-manipulator`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-key-command (from `../node_modules/react-native-key-command`)
- react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`)
Expand Down Expand Up @@ -1656,10 +1661,14 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EXAV:
:path: "../node_modules/expo-av/ios"
EXImageLoader:
:path: "../node_modules/expo-image-loader/ios"
Expo:
:path: "../node_modules/expo"
ExpoImage:
:path: "../node_modules/expo-image/ios"
ExpoImageManipulator:
:path: "../node_modules/expo-image-manipulator/ios"
ExpoModulesCore:
:path: "../node_modules/expo-modules-core"
FBLazyVector:
Expand Down Expand Up @@ -1729,8 +1738,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-document-picker"
react-native-geolocation:
:path: "../node_modules/@react-native-community/geolocation"
react-native-image-manipulator:
:path: "../node_modules/@oguzhnatly/react-native-image-manipulator"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-key-command:
Expand Down Expand Up @@ -1858,8 +1865,10 @@ SPEC CHECKSUMS:
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
EXAV: 09a4d87fa6b113fbb0ada3aade6799f78271cb44
EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b
Expo: 1e3bcf9dd99de57a636127057f6b488f0609681a
ExpoImage: 1cdaa65a6a70bb01067e21ad1347ff2d973885f5
ExpoImageManipulator: c1d7cb865eacd620a35659f3da34c70531f10b59
ExpoModulesCore: 96d1751929ad10622773bb729ab28a8423f0dd0c
FBLazyVector: fbc4957d9aa695250b55d879c1d86f79d7e69ab4
FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88
Expand Down Expand Up @@ -1933,7 +1942,6 @@ SPEC CHECKSUMS:
react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e
react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56
react-native-image-picker: 2381c008bbb09e72395a2d043c147b11bd1523d9
react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5
react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d
Expand Down Expand Up @@ -1997,7 +2005,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a
Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047
Yoga: 13c8ef87792450193e117976337b8527b49e8c03

PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2

Expand Down
27 changes: 20 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
"@invertase/react-native-apple-authentication": "^2.2.2",
"@kie/act-js": "^2.6.0",
"@kie/mock-github": "^1.0.0",
"@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52",
"@onfido/react-native-sdk": "10.6.0",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-native-camera-roll/camera-roll": "5.4.0",
Expand Down Expand Up @@ -105,6 +104,7 @@
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.10.1",
"expo-image-manipulator": "11.8.0",
"fbjs": "^3.0.2",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
Expand Down
19 changes: 0 additions & 19 deletions patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch

This file was deleted.

6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,12 @@ const CONST = {
VIDEO: 'video',
},

IMAGE_FILE_FORMAT: {
PNG: 'image/png',
WEBP: 'image/webp',
JPEG: 'image/jpeg',
},

FILE_TYPE_REGEX: {
// Image MimeTypes allowed by iOS photos app.
IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/,
Expand Down
17 changes: 17 additions & 0 deletions src/libs/cropOrRotateImage/getSaveFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {SaveFormat} from 'expo-image-manipulator';
import CONST from '@src/CONST';

function getSaveFormat(type: string) {
switch (type) {
case CONST.IMAGE_FILE_FORMAT.PNG:
return SaveFormat.PNG;
case CONST.IMAGE_FILE_FORMAT.WEBP:
return SaveFormat.WEBP;
case CONST.IMAGE_FILE_FORMAT.JPEG:
return SaveFormat.JPEG;
default:
return SaveFormat.JPEG;
}
}

export default getSaveFormat;
6 changes: 4 additions & 2 deletions src/libs/cropOrRotateImage/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import RNImageManipulator from '@oguzhnatly/react-native-image-manipulator';
import {manipulateAsync} from 'expo-image-manipulator';
import RNFetchBlob from 'react-native-blob-util';
import getSaveFormat from './getSaveFormat';
import type {CropOrRotateImage} from './types';

/**
* Crops and rotates the image on ios/android
*/
const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) =>
new Promise((resolve) => {
RNImageManipulator.manipulate(uri, actions, options).then((result) => {
const format = getSaveFormat(options.type);
manipulateAsync(uri, actions, {compress: options.compress, format}).then((result) => {
RNFetchBlob.fs.stat(result.uri.replace('file://', '')).then(({size}) => {
resolve({
...result,
Expand Down
136 changes: 14 additions & 122 deletions src/libs/cropOrRotateImage/index.ts
Original file line number Diff line number Diff line change
@@ -1,127 +1,19 @@
import type {CropOptions, CropOrRotateImage, CropOrRotateImageOptions} from './types';

type SizeFromAngle = {
width: number;
height: number;
};

/**
* Calculates a size of canvas after rotation
*/
function sizeFromAngle(width: number, height: number, angle: number): SizeFromAngle {
const radians = (angle * Math.PI) / 180;
let sine = Math.cos(radians);
let cosine = Math.sin(radians);
if (cosine < 0) {
cosine = -cosine;
}
if (sine < 0) {
sine = -sine;
}
return {width: height * cosine + width * sine, height: height * sine + width * cosine};
}

/**
* Creates a new rotated canvas
*/
function rotateCanvas(canvas: HTMLCanvasElement, degrees: number): HTMLCanvasElement {
const {width, height} = sizeFromAngle(canvas.width, canvas.height, degrees);

// We have to create a new canvas because it is not possible to change already drawn
// elements. Transformations such as rotation have to be applied before drawing
const result = document.createElement('canvas');
result.width = width;
result.height = height;

const context = result.getContext('2d');
if (context) {
// In order to rotate image along its center we have to apply next transformation
context.translate(result.width / 2, result.height / 2);

const radians = (degrees * Math.PI) / 180;
context.rotate(radians);

context.drawImage(canvas, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height);
}
return result;
}

/**
* Creates new cropped canvas and returns it
*/
function cropCanvas(canvas: HTMLCanvasElement, options: CropOptions) {
let {originX = 0, originY = 0, width = 0, height = 0} = options;
const clamp = (value: number, max: number) => Math.max(0, Math.min(max, value));

width = clamp(width, canvas.width);
height = clamp(height, canvas.height);
originX = clamp(originX, canvas.width);
originY = clamp(originY, canvas.height);

width = Math.min(originX + width, canvas.width) - originX;
height = Math.min(originY + height, canvas.height) - originY;

const result = document.createElement('canvas');
result.width = width;
result.height = height;

const context = result.getContext('2d');
context?.drawImage(canvas, originX, originY, width, height, 0, 0, width, height);

return result;
}

function convertCanvasToFile(canvas: HTMLCanvasElement, options: CropOrRotateImageOptions): Promise<File> {
return new Promise((resolve) => {
canvas.toBlob((blob) => {
if (!blob) {
return;
}
const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'});
file.uri = URL.createObjectURL(file);
resolve(file);
});
});
}

/**
* Loads image from specified url
*/
function loadImageAsync(uri: string): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
const imageSource = new Image();
imageSource.crossOrigin = 'anonymous';
const canvas = document.createElement('canvas');
imageSource.onload = () => {
canvas.width = imageSource.naturalWidth;
canvas.height = imageSource.naturalHeight;

const context = canvas.getContext('2d');
context?.drawImage(imageSource, 0, 0, imageSource.naturalWidth, imageSource.naturalHeight);

resolve(canvas);
};
imageSource.onerror = () => reject(canvas);
imageSource.src = uri;
});
}

/**
* Crops and rotates the image on web
*/
import {manipulateAsync} from 'expo-image-manipulator';
import getSaveFormat from './getSaveFormat';
import type {CropOrRotateImage} from './types';

const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) =>
loadImageAsync(uri).then((originalCanvas) => {
const resultCanvas = actions.reduce((canvas, action) => {
if (action.crop) {
return cropCanvas(canvas, action.crop);
}
if (action.rotate) {
return rotateCanvas(canvas, action.rotate);
}
return canvas;
}, originalCanvas);
return convertCanvasToFile(resultCanvas, options);
new Promise((resolve) => {
const format = getSaveFormat(options.type);
manipulateAsync(uri, actions, {compress: options.compress, format}).then((result) => {
fetch(result.uri)
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'});
file.uri = URL.createObjectURL(file);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am wondering in which case will options.name will not be set and we'll have to fallback for fileName.jpeg? Can we use any other fallback? instead of hardcoding?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resolve(file);
});
});
});

export default cropOrRotateImage;
Loading
Loading