@oguzhnatly/react-native-image-manipulator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/Example/.babelrc +3 -0
- package/Example/.buckconfig +6 -0
- package/Example/.flowconfig +70 -0
- package/Example/.gitattributes +1 -0
- package/Example/.watchmanconfig +1 -0
- package/Example/App.js +49 -0
- package/Example/android/app/BUCK +65 -0
- package/Example/android/app/build.gradle +150 -0
- package/Example/android/app/proguard-rules.pro +17 -0
- package/Example/android/app/src/main/AndroidManifest.xml +26 -0
- package/Example/android/app/src/main/java/com/example/MainActivity.java +15 -0
- package/Example/android/app/src/main/java/com/example/MainApplication.java +45 -0
- package/Example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/Example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/Example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/Example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/Example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/Example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/Example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/Example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/Example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/Example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/Example/android/app/src/main/res/values/strings.xml +3 -0
- package/Example/android/app/src/main/res/values/styles.xml +8 -0
- package/Example/android/build.gradle +39 -0
- package/Example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/Example/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/Example/android/gradle.properties +18 -0
- package/Example/android/gradlew +172 -0
- package/Example/android/gradlew.bat +84 -0
- package/Example/android/keystores/BUCK +8 -0
- package/Example/android/keystores/debug.keystore.properties +4 -0
- package/Example/android/settings.gradle +3 -0
- package/Example/app.json +4 -0
- package/Example/index.js +7 -0
- package/Example/ios/Example/AppDelegate.h +14 -0
- package/Example/ios/Example/AppDelegate.m +35 -0
- package/Example/ios/Example/Base.lproj/LaunchScreen.xib +42 -0
- package/Example/ios/Example/Images.xcassets/AppIcon.appiconset/Contents.json +38 -0
- package/Example/ios/Example/Images.xcassets/Contents.json +6 -0
- package/Example/ios/Example/Info.plist +60 -0
- package/Example/ios/Example/main.m +16 -0
- package/Example/ios/Example-tvOS/Info.plist +54 -0
- package/Example/ios/Example-tvOSTests/Info.plist +24 -0
- package/Example/ios/Example.xcodeproj/project.pbxproj +1494 -0
- package/Example/ios/Example.xcodeproj/xcshareddata/xcschemes/Example-tvOS.xcscheme +129 -0
- package/Example/ios/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme +129 -0
- package/Example/ios/ExampleTests/ExampleTests.m +68 -0
- package/Example/ios/ExampleTests/Info.plist +24 -0
- package/Example/package.json +17 -0
- package/Example/yarn.lock +4046 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/android/build.gradle +36 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/reactnativeimagemanipulator/RNImageManipulatorModule.java +208 -0
- package/android/src/main/java/com/reactnativeimagemanipulator/RNImageManipulatorPackage.java +29 -0
- package/index.js +5 -0
- package/ios/ImageUtils.h +9 -0
- package/ios/ImageUtils.m +14 -0
- package/ios/RNImageManipulator.h +11 -0
- package/ios/RNImageManipulator.m +200 -0
- package/ios/RNImageManipulator.xcodeproj/project.pbxproj +265 -0
- package/ios/RNImageManipulator.xcworkspace/contents.xcworkspacedata +9 -0
- package/package.json +9 -0
- package/react-native-image-manipulator.podspec +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Oğuzhan Atalay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
### `ImageManipulator.manipulate(uri, actions, saveOptions)`
|
|
2
|
+
|
|
3
|
+
Manipulate the image provided via `uri`. Available modifications are rotating, flipping (mirroring), resizing and cropping. Each invocation results in a new file. With one invocation you can provide a set of actions to perform over the image. Overwriting the source file would not have an effect in displaying the result as images are cached.
|
|
4
|
+
|
|
5
|
+
#### Arguments
|
|
6
|
+
|
|
7
|
+
- **uri (_string_)** -- URI of the file to manipulate. Should be in the app's scope.
|
|
8
|
+
- **actions (_array_)** --
|
|
9
|
+
|
|
10
|
+
An array of objects representing manipulation options. Each object should have one of the following keys:
|
|
11
|
+
|
|
12
|
+
- **resize (_object_)** -- An object of shape `{ width, height }`. Values correspond to the result image dimensions. If you specify only one value, the other will be set automatically to preserve image ratio.
|
|
13
|
+
- **rotate (_number_)** -- Degrees to rotate the image. Rotation is clockwise when the value is positive and counter-clockwise when negative.
|
|
14
|
+
- **flip (_object_)** -- An object of shape `{ vertical, horizontal }`. Having a field set to true, flips the image in specified axis.
|
|
15
|
+
- **crop (_object_)** -- An object of shape `{ originX, originY, width, height }`. Fields specify top-left corner and dimensions of a crop rectangle.
|
|
16
|
+
|
|
17
|
+
- **saveOptions (_object_)** -- A map defining how modified image should be saved:
|
|
18
|
+
- **compress (_number_)** -- A value in range 0 - 1 specifying compression level of the result image. 1 means no compression and 0 the highest compression.
|
|
19
|
+
- **format (_string_)** -- Either `'jpeg'` or `'png'`. Specifies what type of compression should be used and what is the result file extension. PNG compression is lossless but slower, JPEG is faster but the image has visible artifacts. Defaults to `'jpeg'`.
|
|
20
|
+
- **base64 (_boolean_)** -- Whether to also include the image data in Base64 format.
|
|
21
|
+
|
|
22
|
+
#### Returns
|
|
23
|
+
|
|
24
|
+
Returns `{ uri, width, height }` where `uri` is a URI to the modified image (useable as the source for an `Image`/`Video` element), `width, height` specify the dimensions of the image. It can contain also `base64` - it is included if the `base64` saveOption was truthy, and is a string containing the JPEG/PNG (depending on `format`) data of the image in Base64--prepend that with `'data:image/xxx;base64,'` to get a data URI, which you can use as the source for an `Image` element for example (where `xxx` is 'jpeg' or 'png').
|
|
25
|
+
|
|
26
|
+
### Basic Example
|
|
27
|
+
|
|
28
|
+
This will first rotate the image 90 degrees clockwise, then flip the rotated image vertically and save it as a PNG.
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
import React from "react";
|
|
32
|
+
import { Button, TouchableOpacity, Text, View, Image } from "react-native";
|
|
33
|
+
import ImageManipulator from "react-native-image-manipulator";
|
|
34
|
+
|
|
35
|
+
import Colors from "../constants/Colors";
|
|
36
|
+
|
|
37
|
+
export default class ImageManipulatorSample extends React.Component {
|
|
38
|
+
state = {
|
|
39
|
+
ready: false,
|
|
40
|
+
image: null
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
componentWillMount() {
|
|
44
|
+
(async () => {
|
|
45
|
+
const image = Asset.fromModule(require("../path/to/image.jpg"));
|
|
46
|
+
await image.downloadAsync();
|
|
47
|
+
this.setState({
|
|
48
|
+
ready: true,
|
|
49
|
+
image
|
|
50
|
+
});
|
|
51
|
+
})();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render() {
|
|
55
|
+
return (
|
|
56
|
+
<View style={{ flex: 1 }}>
|
|
57
|
+
<View style={{ padding: 10 }}>
|
|
58
|
+
<Button onPress={this._rotate90andFlip} />
|
|
59
|
+
{this.state.ready && this._renderImage()}
|
|
60
|
+
</View>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_rotate90andFlip = async () => {
|
|
66
|
+
const manipResult = await ImageManipulator.manipulate(
|
|
67
|
+
this.state.image.localUri || this.state.image.uri,
|
|
68
|
+
[{ rotate: 90 }, { flip: { vertical: true } }],
|
|
69
|
+
{ format: "png" }
|
|
70
|
+
);
|
|
71
|
+
this.setState({ image: manipResult });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
_renderImage = () => {
|
|
75
|
+
return (
|
|
76
|
+
<View
|
|
77
|
+
style={{
|
|
78
|
+
marginVertical: 10,
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
justifyContent: "center"
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Image
|
|
84
|
+
source={{ uri: this.state.image.localUri || this.state.image.uri }}
|
|
85
|
+
style={{ width: 300, height: 300, resizeMode: "contain" }}
|
|
86
|
+
/>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Getting started
|
|
94
|
+
|
|
95
|
+
`$ npm install @oguzhnatly/react-native-image-manipulator --save`
|
|
96
|
+
### OR
|
|
97
|
+
`$ yarn add @oguzhnatly/react-native-image-manipulator`
|
|
98
|
+
|
|
99
|
+
### Mostly automatic installation
|
|
100
|
+
|
|
101
|
+
`$ react-native link react-native-image-manipulator`
|
|
102
|
+
|
|
103
|
+
### Manual installation
|
|
104
|
+
|
|
105
|
+
#### iOS
|
|
106
|
+
|
|
107
|
+
1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]`
|
|
108
|
+
2. Go to `node_modules` ➜ `react-native-image-manipulator` and add `RNImageManipulator.xcodeproj`
|
|
109
|
+
3. In XCode, in the project navigator, select your project. Add `libRNImageManipulator.a` to your project's `Build Phases` ➜ `Link Binary With Libraries`
|
|
110
|
+
4. Run your project (`Cmd+R`)<
|
|
111
|
+
|
|
112
|
+
#### Android
|
|
113
|
+
|
|
114
|
+
1. Open up `android/app/src/main/java/[...]/MainActivity.java`
|
|
115
|
+
|
|
116
|
+
- Add `import com.reactnativeimagemanipulator.RNImageManipulatorPackage;` to the imports at the top of the file
|
|
117
|
+
- Add `new RNImageManipulatorPackage()` to the list returned by the `getPackages()` method
|
|
118
|
+
|
|
119
|
+
2. Append the following lines to `android/settings.gradle`:
|
|
120
|
+
```
|
|
121
|
+
include ':react-native-image-manipulator'
|
|
122
|
+
project(':react-native-image-manipulator').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-manipulator/android')
|
|
123
|
+
```
|
|
124
|
+
3. Insert the following lines inside the dependencies block in `android/app/build.gradle`:
|
|
125
|
+
```
|
|
126
|
+
compile project(':react-native-image-manipulator')
|
|
127
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
buildscript {
|
|
3
|
+
repositories {
|
|
4
|
+
google()
|
|
5
|
+
jcenter()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
dependencies {
|
|
9
|
+
classpath 'com.android.tools.build:gradle:3.3.1'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
apply plugin: 'com.android.library'
|
|
14
|
+
|
|
15
|
+
android {
|
|
16
|
+
compileSdkVersion 28
|
|
17
|
+
buildToolsVersion "28.0.3"
|
|
18
|
+
|
|
19
|
+
defaultConfig {
|
|
20
|
+
minSdkVersion 16
|
|
21
|
+
targetSdkVersion 28
|
|
22
|
+
versionCode 1
|
|
23
|
+
versionName "1.0"
|
|
24
|
+
}
|
|
25
|
+
lintOptions {
|
|
26
|
+
abortOnError false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
repositories {
|
|
31
|
+
mavenCentral()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dependencies {
|
|
35
|
+
implementation 'com.facebook.react:react-native:+'
|
|
36
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
package com.reactnativeimagemanipulator;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.Matrix;
|
|
5
|
+
import android.net.Uri;
|
|
6
|
+
import android.util.Base64;
|
|
7
|
+
|
|
8
|
+
import com.facebook.common.executors.CallerThreadExecutor;
|
|
9
|
+
import com.facebook.common.executors.UiThreadImmediateExecutorService;
|
|
10
|
+
import com.facebook.common.references.CloseableReference;
|
|
11
|
+
import com.facebook.datasource.DataSource;
|
|
12
|
+
import com.facebook.drawee.backends.pipeline.Fresco;
|
|
13
|
+
import com.facebook.imagepipeline.common.RotationOptions;
|
|
14
|
+
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
|
15
|
+
import com.facebook.imagepipeline.image.CloseableImage;
|
|
16
|
+
import com.facebook.imagepipeline.request.ImageRequest;
|
|
17
|
+
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
|
18
|
+
import com.facebook.react.bridge.Arguments;
|
|
19
|
+
import com.facebook.react.bridge.Promise;
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
21
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
22
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
23
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
24
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
25
|
+
import com.facebook.react.bridge.WritableMap;
|
|
26
|
+
|
|
27
|
+
import java.io.ByteArrayOutputStream;
|
|
28
|
+
import java.io.File;
|
|
29
|
+
import java.io.FileOutputStream;
|
|
30
|
+
import java.io.IOException;
|
|
31
|
+
import java.util.UUID;
|
|
32
|
+
|
|
33
|
+
public class RNImageManipulatorModule extends ReactContextBaseJavaModule {
|
|
34
|
+
private static final String DECODE_ERROR_TAG = "E_DECODE_ERR";
|
|
35
|
+
private static final String ARGS_ERROR_TAG = "E_ARGS_ERR";
|
|
36
|
+
|
|
37
|
+
public RNImageManipulatorModule(ReactApplicationContext reactContext) {
|
|
38
|
+
super(reactContext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Override
|
|
42
|
+
public String getName() {
|
|
43
|
+
return "RNImageManipulator";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@ReactMethod
|
|
47
|
+
public void manipulate(final String uriString, final ReadableArray actions, final ReadableMap saveOptions, final Promise promise) {
|
|
48
|
+
if (uriString == null || uriString.length() == 0) {
|
|
49
|
+
promise.reject(ARGS_ERROR_TAG, "Uri passed to ImageManipulator cannot be empty!");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
ImageRequest imageRequest =
|
|
53
|
+
ImageRequestBuilder
|
|
54
|
+
.newBuilderWithSource(Uri.parse(uriString))
|
|
55
|
+
.setRotationOptions(RotationOptions.autoRotate())
|
|
56
|
+
.build();
|
|
57
|
+
final DataSource<CloseableReference<CloseableImage>> dataSource
|
|
58
|
+
= Fresco.getImagePipeline().fetchDecodedImage(imageRequest, getReactApplicationContext());
|
|
59
|
+
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
|
60
|
+
@Override
|
|
61
|
+
public void onNewResultImpl(Bitmap bitmap) {
|
|
62
|
+
if (bitmap != null) {
|
|
63
|
+
processBitmapWithActions(bitmap, actions, saveOptions, promise);
|
|
64
|
+
} else {
|
|
65
|
+
onFailureImpl(dataSource);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Override
|
|
70
|
+
public void onFailureImpl(DataSource dataSource) {
|
|
71
|
+
// No cleanup required here.
|
|
72
|
+
String basicMessage = "Could not get decoded bitmap of " + uriString;
|
|
73
|
+
if (dataSource.getFailureCause() != null) {
|
|
74
|
+
promise.reject(DECODE_ERROR_TAG,
|
|
75
|
+
basicMessage + ": " + dataSource.getFailureCause().toString(), dataSource.getFailureCause());
|
|
76
|
+
} else {
|
|
77
|
+
promise.reject(DECODE_ERROR_TAG, basicMessage + ".");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
CallerThreadExecutor.getInstance()
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private void processBitmapWithActions(Bitmap bmp, ReadableArray actions, ReadableMap saveOptions, Promise promise) {
|
|
86
|
+
int imageWidth, imageHeight;
|
|
87
|
+
|
|
88
|
+
for (int idx = 0; idx < actions.size(); idx ++) {
|
|
89
|
+
ReadableMap options = actions.getMap(idx);
|
|
90
|
+
|
|
91
|
+
imageWidth = bmp.getWidth();
|
|
92
|
+
imageHeight = bmp.getHeight();
|
|
93
|
+
|
|
94
|
+
if (options.hasKey("resize")) {
|
|
95
|
+
ReadableMap resize = options.getMap("resize");
|
|
96
|
+
int requestedWidth = 0;
|
|
97
|
+
int requestedHeight = 0;
|
|
98
|
+
float imageRatio = 1.0f * imageWidth / imageHeight;
|
|
99
|
+
|
|
100
|
+
if (resize.hasKey("width")) {
|
|
101
|
+
requestedWidth = (int) resize.getDouble("width");
|
|
102
|
+
requestedHeight = (int) (requestedWidth / imageRatio);
|
|
103
|
+
}
|
|
104
|
+
if (resize.hasKey("height")) {
|
|
105
|
+
requestedHeight = (int) resize.getDouble("height");
|
|
106
|
+
requestedWidth = requestedWidth == 0 ? (int) (imageRatio * requestedHeight) : requestedWidth;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bmp = Bitmap.createScaledBitmap(bmp, requestedWidth, requestedHeight, true);
|
|
110
|
+
} else if (options.hasKey("rotate")) {
|
|
111
|
+
int requestedRotation = options.getInt("rotate");
|
|
112
|
+
Matrix rotationMatrix = new Matrix();
|
|
113
|
+
rotationMatrix.postRotate(requestedRotation);
|
|
114
|
+
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), rotationMatrix, true);
|
|
115
|
+
} else if (options.hasKey("flip")) {
|
|
116
|
+
Matrix rotationMatrix = new Matrix();
|
|
117
|
+
ReadableMap flip = options.getMap("flip");
|
|
118
|
+
if (flip.hasKey("horizontal") && flip.getBoolean("horizontal")) {
|
|
119
|
+
rotationMatrix.postScale(-1, 1);
|
|
120
|
+
}
|
|
121
|
+
if (flip.hasKey("vertical") && flip.getBoolean("vertical")) {
|
|
122
|
+
rotationMatrix.postScale(1, -1);
|
|
123
|
+
}
|
|
124
|
+
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), rotationMatrix, true);
|
|
125
|
+
} else if (options.hasKey("crop")) {
|
|
126
|
+
ReadableMap crop = options.getMap("crop");
|
|
127
|
+
if (!crop.hasKey("originX") || !crop.hasKey("originY") || !crop.hasKey("width") || !crop.hasKey("height")) {
|
|
128
|
+
promise.reject("E_INVALID_CROP_DATA", "Invalid crop options has been passed. Please make sure the object contains originX, originY, width and height.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
int originX, originY, requestedWidth, requestedHeight;
|
|
132
|
+
originX = (int) crop.getDouble("originX");
|
|
133
|
+
originY = (int) crop.getDouble("originY");
|
|
134
|
+
requestedWidth = (int) crop.getDouble("width");
|
|
135
|
+
requestedHeight = (int) crop.getDouble("height");
|
|
136
|
+
if (originX > imageWidth || originY > imageHeight || requestedWidth > bmp.getWidth() || requestedHeight > bmp.getHeight()) {
|
|
137
|
+
promise.reject("E_INVALID_CROP_DATA", "Invalid crop options has been passed. Please make sure the requested crop rectangle is inside source image.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
bmp = Bitmap.createBitmap(bmp, originX, originY, requestedWidth, requestedHeight);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
int compressionQuality = 100;
|
|
145
|
+
if (saveOptions.hasKey("compress")) {
|
|
146
|
+
compressionQuality = (int) (100 * saveOptions.getDouble("compress"));
|
|
147
|
+
}
|
|
148
|
+
String format, extension;
|
|
149
|
+
Bitmap.CompressFormat compressFormat;
|
|
150
|
+
|
|
151
|
+
if (saveOptions.hasKey("format")) {
|
|
152
|
+
format = saveOptions.getString("format");
|
|
153
|
+
} else {
|
|
154
|
+
format = "jpeg";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (format.equals("png")) {
|
|
158
|
+
compressFormat = Bitmap.CompressFormat.PNG;
|
|
159
|
+
extension = ".png";
|
|
160
|
+
} else if (format.equals("jpeg")) {
|
|
161
|
+
compressFormat = Bitmap.CompressFormat.JPEG;
|
|
162
|
+
extension = ".jpg";
|
|
163
|
+
} else {
|
|
164
|
+
compressFormat = Bitmap.CompressFormat.JPEG;
|
|
165
|
+
extension = ".jpg";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
boolean base64 = saveOptions.hasKey("base64") && saveOptions.getBoolean("base64");
|
|
169
|
+
|
|
170
|
+
FileOutputStream out = null;
|
|
171
|
+
ByteArrayOutputStream byteOut = null;
|
|
172
|
+
String path = null;
|
|
173
|
+
String base64String = null;
|
|
174
|
+
try {
|
|
175
|
+
path = this.getReactApplicationContext().getFilesDir() + "/" + UUID.randomUUID() + extension;
|
|
176
|
+
out = new FileOutputStream(path);
|
|
177
|
+
bmp.compress(compressFormat, compressionQuality, out);
|
|
178
|
+
|
|
179
|
+
if (base64) {
|
|
180
|
+
byteOut = new ByteArrayOutputStream();
|
|
181
|
+
bmp.compress(compressFormat, compressionQuality, byteOut);
|
|
182
|
+
base64String = Base64.encodeToString(byteOut.toByteArray(), Base64.DEFAULT);
|
|
183
|
+
}
|
|
184
|
+
} catch (Exception e) {
|
|
185
|
+
e.printStackTrace();
|
|
186
|
+
} finally {
|
|
187
|
+
try {
|
|
188
|
+
if (out != null) {
|
|
189
|
+
out.close();
|
|
190
|
+
}
|
|
191
|
+
if (byteOut != null) {
|
|
192
|
+
byteOut.close();
|
|
193
|
+
}
|
|
194
|
+
} catch (IOException e) {
|
|
195
|
+
e.printStackTrace();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
WritableMap response = Arguments.createMap();
|
|
200
|
+
response.putString("uri", Uri.fromFile(new File(path)).toString());
|
|
201
|
+
response.putInt("width", bmp.getWidth());
|
|
202
|
+
response.putInt("height", bmp.getHeight());
|
|
203
|
+
if (base64) {
|
|
204
|
+
response.putString("base64", base64String);
|
|
205
|
+
}
|
|
206
|
+
promise.resolve(response);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
package com.reactnativeimagemanipulator;
|
|
3
|
+
|
|
4
|
+
import java.util.Arrays;
|
|
5
|
+
import java.util.Collections;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.ReactPackage;
|
|
9
|
+
import com.facebook.react.bridge.NativeModule;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
12
|
+
import com.facebook.react.bridge.JavaScriptModule;
|
|
13
|
+
|
|
14
|
+
public class RNImageManipulatorPackage implements ReactPackage {
|
|
15
|
+
@Override
|
|
16
|
+
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
|
17
|
+
return Arrays.<NativeModule>asList(new RNImageManipulatorModule(reactContext));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Deprecated from RN 0.47
|
|
21
|
+
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
|
22
|
+
return Collections.emptyList();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
27
|
+
return Collections.emptyList();
|
|
28
|
+
}
|
|
29
|
+
}
|
package/index.js
ADDED
package/ios/ImageUtils.h
ADDED
package/ios/ImageUtils.m
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import "ImageUtils.h"
|
|
2
|
+
|
|
3
|
+
@implementation ImageUtils
|
|
4
|
+
|
|
5
|
+
+ (UIImage *)cropImage:(UIImage *)image toRect:(CGRect)rect
|
|
6
|
+
{
|
|
7
|
+
CGImageRef takenCGImage = image.CGImage;
|
|
8
|
+
CGImageRef cropCGImage = CGImageCreateWithImageInRect(takenCGImage, rect);
|
|
9
|
+
image = [UIImage imageWithCGImage:cropCGImage scale:image.scale orientation:image.imageOrientation];
|
|
10
|
+
CGImageRelease(cropCGImage);
|
|
11
|
+
return image;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#import "RNImageManipulator.h"
|
|
2
|
+
#import "ImageUtils.h"
|
|
3
|
+
#import <React/RCTLog.h>
|
|
4
|
+
#import <Photos/Photos.h>
|
|
5
|
+
|
|
6
|
+
@implementation RNImageManipulator
|
|
7
|
+
|
|
8
|
+
RCT_EXPORT_MODULE(RNImageManipulator);
|
|
9
|
+
|
|
10
|
+
@synthesize bridge = _bridge;
|
|
11
|
+
|
|
12
|
+
- (void)setBridge:(RCTBridge *)bridge
|
|
13
|
+
{
|
|
14
|
+
_bridge = bridge;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
- (dispatch_queue_t)methodQueue
|
|
18
|
+
{
|
|
19
|
+
return dispatch_get_main_queue();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
RCT_EXPORT_METHOD(manipulate:(NSString *)uri
|
|
23
|
+
actions:(NSArray *)actions
|
|
24
|
+
saveOptions:(NSDictionary *)saveOptions
|
|
25
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
27
|
+
{
|
|
28
|
+
NSURL *url = [NSURL URLWithString:uri];
|
|
29
|
+
NSString *path = [url.path stringByStandardizingPath];
|
|
30
|
+
|
|
31
|
+
if ([[url scheme] isEqualToString:@"assets-library"]) {
|
|
32
|
+
PHFetchResult<PHAsset *> *fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil];
|
|
33
|
+
if (fetchResult.count > 0) {
|
|
34
|
+
PHAsset *asset = fetchResult[0];
|
|
35
|
+
CGSize size = CGSizeMake([asset pixelWidth], [asset pixelHeight]);
|
|
36
|
+
PHImageRequestOptions *options = [PHImageRequestOptions new];
|
|
37
|
+
[options setResizeMode:PHImageRequestOptionsResizeModeExact];
|
|
38
|
+
[options setNetworkAccessAllowed:YES];
|
|
39
|
+
[options setSynchronous:NO];
|
|
40
|
+
[options setDeliveryMode:PHImageRequestOptionsDeliveryModeHighQualityFormat];
|
|
41
|
+
|
|
42
|
+
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFit options:options resultHandler:^(UIImage * _Nullable image, NSDictionary * _Nullable info) {
|
|
43
|
+
if (!image) {
|
|
44
|
+
reject(@"E_IMAGE_MANIPULATION_FAILED", [NSString stringWithFormat:@"The file isn't convertable to image. Given path: `%@`.", path], nil);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
[self manipulateImage:image actions:actions saveOptions:saveOptions resolver:resolve rejecter:reject];
|
|
48
|
+
}];
|
|
49
|
+
return;
|
|
50
|
+
} else {
|
|
51
|
+
reject(@"E_IMAGE_MANIPULATION_FAILED", [NSString stringWithFormat:@"The file does not exist. Given path: `%@`.", path], nil);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
56
|
+
reject(@"E_IMAGE_MANIPULATION_FAILED", [NSString stringWithFormat:@"The file does not exist. Given path: `%@`.", path], nil);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
|
|
61
|
+
if (image == nil) {
|
|
62
|
+
reject(@"E_CANNOT_OPEN", @"Could not open provided image", nil);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[self manipulateImage:image actions:actions saveOptions:saveOptions resolver:resolve rejecter:reject];
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
-(void)manipulateImage:(UIImage *)image
|
|
72
|
+
actions:(NSArray *)actions
|
|
73
|
+
saveOptions:(NSDictionary *)saveOptions
|
|
74
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
75
|
+
rejecter:(RCTPromiseRejectBlock)reject
|
|
76
|
+
{
|
|
77
|
+
for (NSDictionary *options in actions) {
|
|
78
|
+
if (options[@"resize"]) {
|
|
79
|
+
float imageWidth = image.size.width;
|
|
80
|
+
float imageHeight = image.size.height;
|
|
81
|
+
float imageRatio = imageWidth / imageHeight;
|
|
82
|
+
|
|
83
|
+
NSInteger requestedWidth = 0;
|
|
84
|
+
NSInteger requestedHeight = 0;
|
|
85
|
+
NSDictionary *resize = options[@"resize"];
|
|
86
|
+
if (resize[@"width"]) {
|
|
87
|
+
requestedWidth = [(NSNumber *)resize[@"width"] integerValue];
|
|
88
|
+
requestedHeight = requestedWidth/imageRatio;
|
|
89
|
+
}
|
|
90
|
+
if (resize[@"height"]) {
|
|
91
|
+
requestedHeight = [(NSNumber *)resize[@"height"] integerValue];
|
|
92
|
+
requestedWidth = requestedWidth == 0 ? imageRatio * requestedHeight : requestedWidth;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
CGSize requestedSize = CGSizeMake(requestedWidth, requestedHeight);
|
|
96
|
+
UIGraphicsBeginImageContextWithOptions(requestedSize, NO, 1.0);
|
|
97
|
+
[image drawInRect:CGRectMake(0, 0, requestedWidth, requestedHeight)];
|
|
98
|
+
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
99
|
+
UIGraphicsEndImageContext();
|
|
100
|
+
} else if (options[@"rotate"]) {
|
|
101
|
+
float rads = [(NSNumber *)options[@"rotate"] integerValue] * M_PI/180;
|
|
102
|
+
CGSize size = image.size;
|
|
103
|
+
UIView *rotatedViewBox = [[UIView alloc] initWithFrame:CGRectMake(0,0,size.width, size.height)];
|
|
104
|
+
CGAffineTransform t = CGAffineTransformMakeRotation(rads);
|
|
105
|
+
rotatedViewBox.transform = t;
|
|
106
|
+
CGSize rotatedSize = rotatedViewBox.frame.size;
|
|
107
|
+
|
|
108
|
+
UIGraphicsBeginImageContext(rotatedSize);
|
|
109
|
+
CGContextRef bitmap = UIGraphicsGetCurrentContext();
|
|
110
|
+
CGContextTranslateCTM(bitmap, rotatedSize.width/2, rotatedSize.height/2);
|
|
111
|
+
CGContextRotateCTM(bitmap, rads);
|
|
112
|
+
CGContextScaleCTM(bitmap, 1.0, -1.0);
|
|
113
|
+
CGContextDrawImage(bitmap, CGRectMake(-size.width / 2, -size.height / 2, size.width, size.height), image.CGImage);
|
|
114
|
+
|
|
115
|
+
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
116
|
+
UIGraphicsEndImageContext();
|
|
117
|
+
} else if (options[@"flip"]) {
|
|
118
|
+
NSDictionary *flip = options[@"flip"];
|
|
119
|
+
UIImageView *tempImageView = [[UIImageView alloc] initWithImage:image];
|
|
120
|
+
|
|
121
|
+
UIGraphicsBeginImageContext(tempImageView.frame.size);
|
|
122
|
+
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
123
|
+
CGAffineTransform transform;
|
|
124
|
+
if (flip[@"vertical"]) {
|
|
125
|
+
transform = CGAffineTransformMake(1, 0, 0, -1, 0, tempImageView.frame.size.height);
|
|
126
|
+
CGContextConcatCTM(context, transform);
|
|
127
|
+
} else if (flip[@"horizontal"]) {
|
|
128
|
+
transform = CGAffineTransformMake(-1, 0, 0, 1, tempImageView.frame.size.width, 0);
|
|
129
|
+
CGContextConcatCTM(context, transform);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[tempImageView.layer renderInContext:context];
|
|
133
|
+
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
134
|
+
UIGraphicsEndImageContext();
|
|
135
|
+
} else if (options[@"crop"]) {
|
|
136
|
+
NSDictionary *cropData = options[@"crop"];
|
|
137
|
+
if (cropData[@"originX"] == nil || cropData[@"originY"] == nil || cropData[@"width"] == nil || cropData[@"height"]== nil) {
|
|
138
|
+
reject(@"E_INVALID_CROP_DATA", @"Invalid crop options has been passed. Please make sure the object contains originX, originY, width and height.", nil);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
float originX = [(NSNumber *)cropData[@"originX"] floatValue];
|
|
143
|
+
float originY = [(NSNumber *)cropData[@"originY"] floatValue];
|
|
144
|
+
float requestedWidth = [(NSNumber *)cropData[@"width"] floatValue];
|
|
145
|
+
float requestedHeight = [(NSNumber *)cropData[@"height"] floatValue];
|
|
146
|
+
|
|
147
|
+
if (originX > image.size.width || originY > image.size.height || requestedWidth > image.size.width || requestedHeight > image.size.height) {
|
|
148
|
+
reject(@"E_INVALID_CROP_DATA", @"Invalid crop options has been passed. Please make sure the requested crop rectangle is inside source image.", nil);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
CGRect cropDimensions = CGRectMake(originX, originY, requestedWidth, requestedHeight);
|
|
152
|
+
image = [ImageUtils cropImage:image toRect:cropDimensions];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
float compressionValue = 1.0;
|
|
157
|
+
if (saveOptions[@"compress"] != nil) {
|
|
158
|
+
compressionValue = [(NSNumber *)saveOptions[@"compress"] floatValue];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
NSData *imageData = nil;
|
|
162
|
+
NSString *format = saveOptions[@"format"];
|
|
163
|
+
NSString *extension;
|
|
164
|
+
if (format == nil) {
|
|
165
|
+
format = @"jpeg";
|
|
166
|
+
}
|
|
167
|
+
if ([format isEqualToString:@"jpeg"]) {
|
|
168
|
+
imageData = UIImageJPEGRepresentation(image, compressionValue);
|
|
169
|
+
extension = @".jpg";
|
|
170
|
+
} else if ([format isEqualToString:@"png"]) {
|
|
171
|
+
imageData = UIImagePNGRepresentation(image);
|
|
172
|
+
extension = @".png";
|
|
173
|
+
} else {
|
|
174
|
+
RCTLogWarn(@"Unsupported format: %@, using JPEG instead.", format);
|
|
175
|
+
imageData = UIImageJPEGRepresentation(image, compressionValue);
|
|
176
|
+
extension = @".jpg";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
|
181
|
+
NSString *directory = directory = [paths firstObject];
|
|
182
|
+
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
|
|
183
|
+
NSString *newPath = [directory stringByAppendingPathComponent:fileName];
|
|
184
|
+
|
|
185
|
+
[imageData writeToFile:newPath atomically:YES];
|
|
186
|
+
NSURL *fileURL = [NSURL fileURLWithPath:newPath];
|
|
187
|
+
NSString *filePath = [fileURL absoluteString];
|
|
188
|
+
NSMutableDictionary *response = [[NSMutableDictionary alloc] init];
|
|
189
|
+
response[@"uri"] = filePath;
|
|
190
|
+
response[@"filename"] = fileName;
|
|
191
|
+
response[@"width"] = @(CGImageGetWidth(image.CGImage));
|
|
192
|
+
response[@"height"] = @(CGImageGetHeight(image.CGImage));
|
|
193
|
+
if (saveOptions[@"base64"] && [saveOptions[@"base64"] boolValue]) {
|
|
194
|
+
response[@"base64"] = [imageData base64EncodedStringWithOptions:0];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
resolve(response);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@end
|