expo-image-manipulator 10.2.1 → 10.3.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/CHANGELOG.md +16 -1
- package/README.md +3 -3
- package/android/build.gradle +24 -9
- package/build/ExpoImageManipulator.d.ts +1 -0
- package/build/ExpoImageManipulator.d.ts.map +1 -0
- package/build/ExpoImageManipulator.web.d.ts +1 -0
- package/build/ExpoImageManipulator.web.d.ts.map +1 -0
- package/build/ExpoImageManipulator.web.js +5 -5
- package/build/ExpoImageManipulator.web.js.map +1 -1
- package/build/ImageManipulator.d.ts +1 -0
- package/build/ImageManipulator.d.ts.map +1 -0
- package/build/ImageManipulator.types.d.ts +1 -0
- package/build/ImageManipulator.types.d.ts.map +1 -0
- package/build/actions/CropAction.web.d.ts +1 -0
- package/build/actions/CropAction.web.d.ts.map +1 -0
- package/build/actions/FlipAction.web.d.ts +1 -0
- package/build/actions/FlipAction.web.d.ts.map +1 -0
- package/build/actions/ResizeAction.web.d.ts +1 -0
- package/build/actions/ResizeAction.web.d.ts.map +1 -0
- package/build/actions/RotateAction.web.d.ts +1 -0
- package/build/actions/RotateAction.web.d.ts.map +1 -0
- package/build/actions/index.web.d.ts +1 -0
- package/build/actions/index.web.d.ts.map +1 -0
- package/build/utils/getContext.web.d.ts +1 -0
- package/build/utils/getContext.web.d.ts.map +1 -0
- package/build/validators.d.ts +1 -0
- package/build/validators.d.ts.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/{EXImageManipulator.podspec → ExpoImageManipulator.podspec} +10 -3
- package/ios/ImageManipulations.swift +209 -0
- package/ios/ImageManipulatorArguments.swift +93 -0
- package/ios/ImageManipulatorExceptions.swift +82 -0
- package/ios/ImageManipulatorModule.swift +133 -0
- package/package.json +3 -3
- package/src/ExpoImageManipulator.web.ts +5 -5
- package/ios/EXImageManipulator/EXImageManipulatorModule.h +0 -7
- package/ios/EXImageManipulator/EXImageManipulatorModule.m +0 -373
- package/unimodule.json +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
-
## 10.
|
|
13
|
+
## 10.3.0 — 2022-04-18
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- Native module on iOS is now written in Swift using the new API. ([#15277](https://github.com/expo/expo/pull/15277) by [@tsapeta](https://github.com/tsapeta))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- Remove `data:image` part in web base64 result. ([#16191](https://github.com/expo/expo/pull/16191) by [@AllanChain](https://github.com/AllanChain))
|
|
22
|
+
- On iOS fix rotation causing extra image borders. ([#16669](https://github.com/expo/expo/pull/16669) by [@mnightingale](https://github.com/mnightingale))
|
|
23
|
+
|
|
24
|
+
### ⚠️ Notices
|
|
25
|
+
|
|
26
|
+
- On Android bump `compileSdkVersion` to `31`, `targetSdkVersion` to `31` and `Java` version to `11`. ([#16941](https://github.com/expo/expo/pull/16941) by [@bbarthec](https://github.com/bbarthec))
|
|
27
|
+
|
|
28
|
+
## 10.2.1 - 2022-02-01
|
|
14
29
|
|
|
15
30
|
### 🐛 Bug fixes
|
|
16
31
|
|
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@ Provides functions that let you manipulation images on the local file system, eg
|
|
|
4
4
|
|
|
5
5
|
# API documentation
|
|
6
6
|
|
|
7
|
-
- [Documentation for the
|
|
8
|
-
- [Documentation for the latest stable release](https://docs.expo.
|
|
7
|
+
- [Documentation for the main branch](https://github.com/expo/expo/blob/main/docs/pages/versions/unversioned/sdk/imagemanipulator.md)
|
|
8
|
+
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/)
|
|
9
9
|
|
|
10
10
|
# Installation in managed Expo projects
|
|
11
11
|
|
|
12
|
-
For
|
|
12
|
+
For [managed](https://docs.expo.dev/versions/latest/introduction/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/).
|
|
13
13
|
|
|
14
14
|
# Installation in bare React Native projects
|
|
15
15
|
|
package/android/build.gradle
CHANGED
|
@@ -3,20 +3,35 @@ apply plugin: 'kotlin-android'
|
|
|
3
3
|
apply plugin: 'maven-publish'
|
|
4
4
|
|
|
5
5
|
group = 'host.exp.exponent'
|
|
6
|
-
version = '10.
|
|
6
|
+
version = '10.3.0'
|
|
7
7
|
|
|
8
8
|
buildscript {
|
|
9
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
10
|
+
if (expoModulesCorePlugin.exists()) {
|
|
11
|
+
apply from: expoModulesCorePlugin
|
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
// Simple helper that allows the root project to override versions declared by this library.
|
|
10
16
|
ext.safeExtGet = { prop, fallback ->
|
|
11
17
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
// Ensures backward compatibility
|
|
21
|
+
ext.getKotlinVersion = {
|
|
22
|
+
if (ext.has("kotlinVersion")) {
|
|
23
|
+
ext.kotlinVersion()
|
|
24
|
+
} else {
|
|
25
|
+
ext.safeExtGet("kotlinVersion", "1.6.10")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
repositories {
|
|
15
30
|
mavenCentral()
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
dependencies {
|
|
19
|
-
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${
|
|
34
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
|
|
@@ -44,22 +59,22 @@ afterEvaluate {
|
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
android {
|
|
47
|
-
compileSdkVersion safeExtGet("compileSdkVersion",
|
|
62
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 31)
|
|
48
63
|
|
|
49
64
|
compileOptions {
|
|
50
|
-
sourceCompatibility JavaVersion.
|
|
51
|
-
targetCompatibility JavaVersion.
|
|
65
|
+
sourceCompatibility JavaVersion.VERSION_11
|
|
66
|
+
targetCompatibility JavaVersion.VERSION_11
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
kotlinOptions {
|
|
55
|
-
jvmTarget = JavaVersion.
|
|
70
|
+
jvmTarget = JavaVersion.VERSION_11.majorVersion
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
defaultConfig {
|
|
59
74
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
60
|
-
targetSdkVersion safeExtGet("targetSdkVersion",
|
|
75
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 31)
|
|
61
76
|
versionCode 23
|
|
62
|
-
versionName "10.
|
|
77
|
+
versionName "10.3.0"
|
|
63
78
|
}
|
|
64
79
|
lintOptions {
|
|
65
80
|
abortOnError false
|
|
@@ -71,5 +86,5 @@ dependencies {
|
|
|
71
86
|
|
|
72
87
|
api "androidx.annotation:annotation:1.0.0"
|
|
73
88
|
|
|
74
|
-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${
|
|
89
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
75
90
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoImageManipulator.d.ts","sourceRoot":"","sources":["../src/ExpoImageManipulator.ts"],"names":[],"mappings":";AACA,wBAA6D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoImageManipulator.web.d.ts","sourceRoot":"","sources":["../src/ExpoImageManipulator.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;;;yBAiDnE,MAAM,0CAEF,WAAW,GACnB,QAAQ,WAAW,CAAC;;AARzB,wBA2BE"}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { crop, flip, resize, rotate } from './actions/index.web';
|
|
2
2
|
import { getContext } from './utils/getContext.web';
|
|
3
3
|
function getResults(canvas, options) {
|
|
4
|
-
let
|
|
4
|
+
let uri;
|
|
5
5
|
if (options) {
|
|
6
6
|
const { format = 'png' } = options;
|
|
7
7
|
if (options.format === 'png' && options.compress !== undefined) {
|
|
8
8
|
console.warn('compress is not supported with png format.');
|
|
9
9
|
}
|
|
10
10
|
const quality = Math.min(1, Math.max(0, options.compress ?? 1));
|
|
11
|
-
|
|
11
|
+
uri = canvas.toDataURL('image/' + format, quality);
|
|
12
12
|
}
|
|
13
13
|
else {
|
|
14
14
|
// defaults to PNG with no loss
|
|
15
|
-
|
|
15
|
+
uri = canvas.toDataURL();
|
|
16
16
|
}
|
|
17
17
|
return {
|
|
18
|
-
uri
|
|
18
|
+
uri,
|
|
19
19
|
width: canvas.width,
|
|
20
20
|
height: canvas.height,
|
|
21
|
-
base64,
|
|
21
|
+
base64: uri.replace(/^data:image\/\w+;base64,/, ''),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
function loadImageAsync(uri) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoImageManipulator.web.js","sourceRoot":"","sources":["../src/ExpoImageManipulator.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD,SAAS,UAAU,CAAC,MAAyB,EAAE,OAAqB;IAClE,IAAI,
|
|
1
|
+
{"version":3,"file":"ExpoImageManipulator.web.js","sourceRoot":"","sources":["../src/ExpoImageManipulator.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD,SAAS,UAAU,CAAC,MAAyB,EAAE,OAAqB;IAClE,IAAI,GAAW,CAAC;IAChB,IAAI,OAAO,EAAE;QACX,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;QACnC,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE;YAC9D,OAAO,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;SAC5D;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC;QAChE,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,GAAG,MAAM,EAAE,OAAO,CAAC,CAAC;KACpD;SAAM;QACL,+BAA+B;QAC/B,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;KAC1B;IACD,OAAO;QACL,GAAG;QACH,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,WAAW,GAAG,IAAI,KAAK,EAAE,CAAC;QAChC,WAAW,CAAC,WAAW,GAAG,WAAW,CAAC;QACtC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,WAAW,CAAC,MAAM,GAAG,GAAG,EAAE;YACxB,MAAM,CAAC,KAAK,GAAG,WAAW,CAAC,YAAY,CAAC;YACxC,MAAM,CAAC,MAAM,GAAG,WAAW,CAAC,aAAa,CAAC;YAE1C,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;YACnC,OAAO,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,YAAY,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;YAE1F,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,WAAW,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,WAAW,CAAC,GAAG,GAAG,GAAG,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,eAAe;IACb,IAAI,IAAI;QACN,OAAO,sBAAsB,CAAC;IAChC,CAAC;IACD,KAAK,CAAC,eAAe,CACnB,GAAW,EACX,UAAoB,EAAE,EACtB,OAAoB;QAEpB,MAAM,cAAc,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QAEjD,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;YACrD,IAAI,MAAM,IAAI,MAAM,EAAE;gBACpB,OAAO,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;aAClC;iBAAM,IAAI,QAAQ,IAAI,MAAM,EAAE;gBAC7B,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;aACtC;iBAAM,IAAI,MAAM,IAAI,MAAM,EAAE;gBAC3B,OAAO,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;aAClC;iBAAM,IAAI,QAAQ,IAAI,MAAM,EAAE;gBAC7B,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;aACtC;iBAAM;gBACL,OAAO,MAAM,CAAC;aACf;QACH,CAAC,EAAE,cAAc,CAAC,CAAC;QAEnB,OAAO,UAAU,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;CACF,CAAC","sourcesContent":["import { ImageResult, SaveOptions, Action } from './ImageManipulator.types';\nimport { crop, flip, resize, rotate } from './actions/index.web';\nimport { getContext } from './utils/getContext.web';\n\nfunction getResults(canvas: HTMLCanvasElement, options?: SaveOptions): ImageResult {\n let uri: string;\n if (options) {\n const { format = 'png' } = options;\n if (options.format === 'png' && options.compress !== undefined) {\n console.warn('compress is not supported with png format.');\n }\n const quality = Math.min(1, Math.max(0, options.compress ?? 1));\n uri = canvas.toDataURL('image/' + format, quality);\n } else {\n // defaults to PNG with no loss\n uri = canvas.toDataURL();\n }\n return {\n uri,\n width: canvas.width,\n height: canvas.height,\n base64: uri.replace(/^data:image\\/\\w+;base64,/, ''),\n };\n}\n\nfunction loadImageAsync(uri: string): Promise<HTMLCanvasElement> {\n return new Promise((resolve, reject) => {\n const imageSource = new Image();\n imageSource.crossOrigin = 'anonymous';\n const canvas = document.createElement('canvas');\n imageSource.onload = () => {\n canvas.width = imageSource.naturalWidth;\n canvas.height = imageSource.naturalHeight;\n\n const context = getContext(canvas);\n context.drawImage(imageSource, 0, 0, imageSource.naturalWidth, imageSource.naturalHeight);\n\n resolve(canvas);\n };\n imageSource.onerror = () => reject(canvas);\n imageSource.src = uri;\n });\n}\n\nexport default {\n get name(): string {\n return 'ExpoImageManipulator';\n },\n async manipulateAsync(\n uri: string,\n actions: Action[] = [],\n options: SaveOptions\n ): Promise<ImageResult> {\n const originalCanvas = await loadImageAsync(uri);\n\n const resultCanvas = actions.reduce((canvas, action) => {\n if ('crop' in action) {\n return crop(canvas, action.crop);\n } else if ('resize' in action) {\n return resize(canvas, action.resize);\n } else if ('flip' in action) {\n return flip(canvas, action.flip);\n } else if ('rotate' in action) {\n return rotate(canvas, action.rotate);\n } else {\n return canvas;\n }\n }, originalCanvas);\n\n return getResults(resultCanvas, options);\n },\n};\n"]}
|
|
@@ -12,3 +12,4 @@ import { Action, ImageResult, SaveOptions } from './ImageManipulator.types';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function manipulateAsync(uri: string, actions?: Action[], saveOptions?: SaveOptions): Promise<ImageResult>;
|
|
14
14
|
export * from './ImageManipulator.types';
|
|
15
|
+
//# sourceMappingURL=ImageManipulator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageManipulator.d.ts","sourceRoot":"","sources":["../src/ImageManipulator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,WAAW,EAAc,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIxF;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,EAAO,EACtB,WAAW,GAAE,WAAgB,GAC5B,OAAO,CAAC,WAAW,CAAC,CAStB;AAED,cAAc,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageManipulator.types.d.ts","sourceRoot":"","sources":["../src/ImageManipulator.types.ts"],"names":[],"mappings":"AACA,oBAAY,WAAW,GAAG;IACxB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAGF,oBAAY,YAAY,GAAG;IACzB;;;OAGG;IACH,MAAM,EAAE;QACN,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAGF,oBAAY,YAAY,GAAG;IACzB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,oBAAY,QAAQ;IAClB,QAAQ,aAAa;IACrB,UAAU,eAAe;CAC1B;AAGD,oBAAY,UAAU,GAAG;IACvB;;;OAGG;IACH,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAGF,oBAAY,UAAU,GAAG;IACvB;;OAEG;IACH,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAAC;AAGF,oBAAY,MAAM,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,GAAG,UAAU,CAAC;AAG3E,oBAAY,UAAU;IACpB,IAAI,SAAS;IACb,GAAG,QAAQ;IACX;;OAEG;IACH,IAAI,SAAS;CACd;AAGD;;GAEG;AACH,oBAAY,WAAW,GAAG;IACxB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CropAction.web.d.ts","sourceRoot":"","sources":["../../src/actions/CropAction.web.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;iCAG/B,iBAAiB,WAAW,UAAU,CAAC,MAAM,CAAC;AAAtE,wBA6BE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlipAction.web.d.ts","sourceRoot":"","sources":["../../src/actions/FlipAction.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAY,MAAM,2BAA2B,CAAC;iCAGzC,iBAAiB,QAAQ,UAAU,CAAC,MAAM,CAAC;AAAnE,wBAsBE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResizeAction.web.d.ts","sourceRoot":"","sources":["../../src/actions/ResizeAction.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;iCAqGjC,iBAAiB,qBAAqB,YAAY,CAAC,QAAQ,CAAC;AAApF,wBAiBE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RotateAction.web.d.ts","sourceRoot":"","sources":["../../src/actions/RotateAction.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;iCAoBjC,iBAAiB,WAAW,YAAY,CAAC,QAAQ,CAAC;AAA1E,wBAoBE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../src/actions/index.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getContext.web.d.ts","sourceRoot":"","sources":["../../src/utils/getContext.web.ts"],"names":[],"mappings":"AAEA,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,wBAAwB,CAM9E"}
|
package/build/validators.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export declare function validateArguments(uri: string, actions: Action[], saveOp
|
|
|
3
3
|
export declare function validateUri(uri: string): void;
|
|
4
4
|
export declare function validateActions(actions: Action[]): void;
|
|
5
5
|
export declare function validateSaveOptions({ base64, compress, format }: SaveOptions): void;
|
|
6
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EAON,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,WAAW,QAIzF;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI7C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CA8BvD;AA6CD,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,WAAW,GAAG,IAAI,CAgBnF"}
|
|
@@ -3,7 +3,7 @@ require 'json'
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
4
|
|
|
5
5
|
Pod::Spec.new do |s|
|
|
6
|
-
s.name = '
|
|
6
|
+
s.name = 'ExpoImageManipulator'
|
|
7
7
|
s.version = package['version']
|
|
8
8
|
s.summary = package['description']
|
|
9
9
|
s.description = package['description']
|
|
@@ -11,16 +11,23 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.homepage = package['homepage']
|
|
13
13
|
s.platform = :ios, '12.0'
|
|
14
|
+
s.swift_version = '5.4'
|
|
14
15
|
s.source = { git: 'https://github.com/expo/expo.git' }
|
|
15
16
|
s.static_framework = true
|
|
16
17
|
|
|
17
18
|
s.dependency 'ExpoModulesCore'
|
|
18
19
|
s.dependency 'EXImageLoader'
|
|
19
20
|
|
|
21
|
+
# Swift/Objective-C compatibility
|
|
22
|
+
s.pod_target_xcconfig = {
|
|
23
|
+
'DEFINES_MODULE' => 'YES',
|
|
24
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
|
|
21
|
-
s.source_files = "
|
|
28
|
+
s.source_files = "**/*.h"
|
|
22
29
|
s.vendored_frameworks = "#{s.name}.xcframework"
|
|
23
30
|
else
|
|
24
|
-
s.source_files = "
|
|
31
|
+
s.source_files = "**/*.{h,m,swift}"
|
|
25
32
|
end
|
|
26
33
|
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Copyright 2021-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Main `manipulate` function that takes an array of any supported actions to apply.
|
|
7
|
+
*/
|
|
8
|
+
internal func manipulate(image initialImage: UIImage, actions: [ManipulateAction]) throws -> UIImage {
|
|
9
|
+
var image: UIImage = initialImage
|
|
10
|
+
|
|
11
|
+
image = try fixImageOrientation(image)
|
|
12
|
+
|
|
13
|
+
for action in actions {
|
|
14
|
+
if let resize = action.resize {
|
|
15
|
+
image = try manipulate(image: image, resize: resize)
|
|
16
|
+
} else if let rotate = action.rotate {
|
|
17
|
+
image = try manipulate(image: image, rotate: rotate)
|
|
18
|
+
} else if let flip = action.flip {
|
|
19
|
+
image = try manipulate(image: image, flip: flip)
|
|
20
|
+
} else if let crop = action.crop {
|
|
21
|
+
image = try manipulate(image: image, crop: crop)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return image
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
Draws a new image by resizing given image to specified size.
|
|
29
|
+
*/
|
|
30
|
+
internal func manipulate(image: UIImage, resize: ResizeOptions) throws -> UIImage {
|
|
31
|
+
let imageWidth = image.size.width
|
|
32
|
+
let imageHeight = image.size.height
|
|
33
|
+
let imageRatio = imageWidth / imageHeight
|
|
34
|
+
|
|
35
|
+
var targetSize = CGSize.zero
|
|
36
|
+
|
|
37
|
+
if let width = resize.width {
|
|
38
|
+
targetSize.width = width
|
|
39
|
+
targetSize.height = width / imageRatio
|
|
40
|
+
}
|
|
41
|
+
if let height = resize.height {
|
|
42
|
+
targetSize.height = height
|
|
43
|
+
targetSize.width = targetSize.width == 0 ? imageRatio * targetSize.height : targetSize.width
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
UIGraphicsBeginImageContextWithOptions(targetSize, false, 1.0)
|
|
47
|
+
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
|
48
|
+
|
|
49
|
+
guard let newImage = UIGraphicsGetImageFromCurrentImageContext() else {
|
|
50
|
+
UIGraphicsEndImageContext()
|
|
51
|
+
throw NoImageInContextException()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
UIGraphicsEndImageContext()
|
|
55
|
+
return newImage
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
Creates a new image by rotating given image by the rotate angle.
|
|
60
|
+
*/
|
|
61
|
+
internal func manipulate(image: UIImage, rotate: Double) throws -> UIImage {
|
|
62
|
+
guard let cgImage = image.cgImage else {
|
|
63
|
+
throw ImageNotFoundException()
|
|
64
|
+
}
|
|
65
|
+
let rads = rotate * Double.pi / 180
|
|
66
|
+
let rotatedView = UIView(frame: CGRect(origin: .zero, size: image.size))
|
|
67
|
+
|
|
68
|
+
rotatedView.transform = CGAffineTransform(rotationAngle: rads)
|
|
69
|
+
|
|
70
|
+
let rotatedSize = CGSize(width: rotatedView.frame.size.width.rounded(.down), height: rotatedView.frame.size.height.rounded(.down))
|
|
71
|
+
let origin = CGPoint(x: -image.size.width / 2, y: -image.size.height / 2)
|
|
72
|
+
|
|
73
|
+
return try drawInNewContext(size: rotatedSize) { context in
|
|
74
|
+
context.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
|
|
75
|
+
context.rotate(by: rads)
|
|
76
|
+
context.scaleBy(x: 1.0, y: -1.0)
|
|
77
|
+
context.draw(cgImage, in: CGRect(origin: origin, size: image.size))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
Creates a new image by flipping given image vertically or horizontally.
|
|
83
|
+
*/
|
|
84
|
+
internal func manipulate(image: UIImage, flip: FlipType) throws -> UIImage {
|
|
85
|
+
let imageView = UIImageView(image: image)
|
|
86
|
+
|
|
87
|
+
return try drawInNewContext(size: imageView.frame.size) { context in
|
|
88
|
+
switch flip {
|
|
89
|
+
case .vertical:
|
|
90
|
+
let transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: imageView.frame.size.height)
|
|
91
|
+
context.concatenate(transform)
|
|
92
|
+
case .horizontal:
|
|
93
|
+
let transform = CGAffineTransform(a: -1, b: 0, c: 0, d: 1, tx: imageView.frame.size.width, ty: 0)
|
|
94
|
+
context.concatenate(transform)
|
|
95
|
+
}
|
|
96
|
+
imageView.layer.render(in: context)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
Creates a new image by cropping given image to specified width and height.
|
|
102
|
+
*/
|
|
103
|
+
internal func manipulate(image: UIImage, crop: CropRect) throws -> UIImage {
|
|
104
|
+
let rect = crop.toRect()
|
|
105
|
+
let isOutOfBounds = rect.origin.x > image.size.width
|
|
106
|
+
|| rect.origin.y > image.size.height
|
|
107
|
+
|| rect.width > image.size.width
|
|
108
|
+
|| rect.height > image.size.height
|
|
109
|
+
|
|
110
|
+
guard !isOutOfBounds else {
|
|
111
|
+
throw ImageInvalidCropException()
|
|
112
|
+
}
|
|
113
|
+
guard let cgImage = image.cgImage?.cropping(to: rect) else {
|
|
114
|
+
throw ImageCropFailedException(rect)
|
|
115
|
+
}
|
|
116
|
+
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
Makes sure the image is oriented up and not mirrored.
|
|
121
|
+
Guarantees that the original pixel data matches the displayed orientation.
|
|
122
|
+
*/
|
|
123
|
+
internal func fixImageOrientation(_ image: UIImage) throws -> UIImage {
|
|
124
|
+
guard let cgImage = image.cgImage else {
|
|
125
|
+
throw ImageNotFoundException()
|
|
126
|
+
}
|
|
127
|
+
guard let colorSpace = cgImage.colorSpace else {
|
|
128
|
+
// That should never happen as `colorSpace` is empty only when the image is a mask.
|
|
129
|
+
throw ImageColorSpaceNotFoundException()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var transform = CGAffineTransform.identity
|
|
133
|
+
|
|
134
|
+
switch image.imageOrientation {
|
|
135
|
+
case .down, .downMirrored:
|
|
136
|
+
transform = transform.translatedBy(x: image.size.width, y: image.size.height)
|
|
137
|
+
transform = transform.rotated(by: Double.pi)
|
|
138
|
+
case .left, .leftMirrored:
|
|
139
|
+
transform = transform.translatedBy(x: image.size.width, y: 0)
|
|
140
|
+
transform = transform.rotated(by: Double.pi / 2)
|
|
141
|
+
case .right, .rightMirrored:
|
|
142
|
+
transform = transform.translatedBy(x: 0, y: image.size.height)
|
|
143
|
+
transform = transform.rotated(by: -Double.pi / 2)
|
|
144
|
+
default:
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
switch image.imageOrientation {
|
|
149
|
+
case .upMirrored, .downMirrored:
|
|
150
|
+
transform = transform.translatedBy(x: image.size.width, y: 0)
|
|
151
|
+
transform = transform.scaledBy(x: -1, y: 1)
|
|
152
|
+
case .leftMirrored, .rightMirrored:
|
|
153
|
+
transform = transform.translatedBy(x: image.size.height, y: 0)
|
|
154
|
+
transform = transform.scaledBy(x: -1, y: 1)
|
|
155
|
+
default:
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let context = CGContext(
|
|
160
|
+
data: nil,
|
|
161
|
+
width: Int(image.size.width),
|
|
162
|
+
height: Int(image.size.height),
|
|
163
|
+
bitsPerComponent: cgImage.bitsPerComponent,
|
|
164
|
+
bytesPerRow: 0,
|
|
165
|
+
space: colorSpace,
|
|
166
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
guard let context = context else {
|
|
170
|
+
throw ImageContextLostException()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
context.concatenate(transform)
|
|
174
|
+
|
|
175
|
+
switch image.imageOrientation {
|
|
176
|
+
case .left, .leftMirrored, .right, .rightMirrored:
|
|
177
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width))
|
|
178
|
+
default:
|
|
179
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
guard let newCGImage = context.makeImage() else {
|
|
183
|
+
throw ImageDrawingFailedException()
|
|
184
|
+
}
|
|
185
|
+
return UIImage(cgImage: newCGImage)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
Helper function for drawing the image in graphics context.
|
|
190
|
+
Throws appropriate exceptions when the context is missing or the image couldn't be rendered.
|
|
191
|
+
*/
|
|
192
|
+
private func drawInNewContext(size: CGSize, drawing: (CGContext) -> Void) throws -> UIImage {
|
|
193
|
+
UIGraphicsBeginImageContext(size)
|
|
194
|
+
|
|
195
|
+
guard let context = UIGraphicsGetCurrentContext() else {
|
|
196
|
+
UIGraphicsEndImageContext()
|
|
197
|
+
throw ImageContextLostException()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
drawing(context)
|
|
201
|
+
|
|
202
|
+
guard let newImage = UIGraphicsGetImageFromCurrentImageContext() else {
|
|
203
|
+
UIGraphicsEndImageContext()
|
|
204
|
+
throw NoImageInContextException()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
UIGraphicsEndImageContext()
|
|
208
|
+
return newImage
|
|
209
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright 2021-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
Represents a single manipulate action. Only one field can be set.
|
|
8
|
+
*/
|
|
9
|
+
internal struct ManipulateAction: Record {
|
|
10
|
+
@Field
|
|
11
|
+
var resize: ResizeOptions?
|
|
12
|
+
|
|
13
|
+
@Field
|
|
14
|
+
var rotate: Double?
|
|
15
|
+
|
|
16
|
+
@Field
|
|
17
|
+
var flip: FlipType?
|
|
18
|
+
|
|
19
|
+
@Field
|
|
20
|
+
var crop: CropRect?
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
Options provided for resize action.
|
|
25
|
+
*/
|
|
26
|
+
internal struct ResizeOptions: Record {
|
|
27
|
+
@Field
|
|
28
|
+
var width: CGFloat?
|
|
29
|
+
|
|
30
|
+
@Field
|
|
31
|
+
var height: CGFloat?
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
Cropping rect for crop action.
|
|
36
|
+
*/
|
|
37
|
+
internal struct CropRect: Record {
|
|
38
|
+
@Field
|
|
39
|
+
var originX: Double = 0.0
|
|
40
|
+
|
|
41
|
+
@Field
|
|
42
|
+
var originY: Double = 0.0
|
|
43
|
+
|
|
44
|
+
@Field
|
|
45
|
+
var width: Double = 0.0
|
|
46
|
+
|
|
47
|
+
@Field
|
|
48
|
+
var height: Double = 0.0
|
|
49
|
+
|
|
50
|
+
func toRect() -> CGRect {
|
|
51
|
+
return CGRect(x: originX, y: originY, width: width, height: height)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
Options to use when saving the resulted image.
|
|
57
|
+
*/
|
|
58
|
+
internal struct ManipulateOptions: Record {
|
|
59
|
+
@Field
|
|
60
|
+
var base64: Bool = false
|
|
61
|
+
|
|
62
|
+
@Field
|
|
63
|
+
var compress: Double = 1.0
|
|
64
|
+
|
|
65
|
+
@Field
|
|
66
|
+
var format: ImageFormat = .jpeg
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
Possible options for flip action.
|
|
71
|
+
*/
|
|
72
|
+
internal enum FlipType: String, EnumArgument {
|
|
73
|
+
case vertical
|
|
74
|
+
case horizontal
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
Enum with supported image formats.
|
|
79
|
+
*/
|
|
80
|
+
internal enum ImageFormat: String, EnumArgument {
|
|
81
|
+
case jpeg
|
|
82
|
+
case jpg
|
|
83
|
+
case png
|
|
84
|
+
|
|
85
|
+
var fileExtension: String {
|
|
86
|
+
switch self {
|
|
87
|
+
case .jpeg, .jpg:
|
|
88
|
+
return ".jpg"
|
|
89
|
+
case .png:
|
|
90
|
+
return ".png"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Copyright 2021-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
internal class ImageContextLostException: Exception {
|
|
7
|
+
override var reason: String {
|
|
8
|
+
"Image context has been lost"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
internal class ImageDrawingFailedException: Exception {
|
|
13
|
+
override var reason: String {
|
|
14
|
+
"Drawing the new image failed"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
internal class ImageNotFoundException: Exception {
|
|
19
|
+
override var reason: String {
|
|
20
|
+
"Image cannot be found"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
internal class ImageColorSpaceNotFoundException: Exception {
|
|
25
|
+
override var reason: String {
|
|
26
|
+
"The image does not specify any color space"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
internal class ImageInvalidCropException: Exception {
|
|
31
|
+
override var reason: String {
|
|
32
|
+
"Invalid crop options has been passed. Please make sure the requested crop rectangle is inside source image"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal class ImageCropFailedException: GenericException<CGRect> {
|
|
37
|
+
override var reason: String {
|
|
38
|
+
"Cropping the image to rectangle (x: \(param.origin.x), y: \(param.origin.y), width: \(param.width), height: \(param.height)) has failed"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
internal class NoImageInContextException: Exception {
|
|
43
|
+
override var reason: String {
|
|
44
|
+
"Could not read the image from the drawing context"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
internal class ImageLoaderNotFoundException: Exception {
|
|
49
|
+
override var reason: String {
|
|
50
|
+
"ImageLoader module not found, make sure 'expo-image-loader' is linked correctly"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
internal class FileSystemNotFoundException: Exception {
|
|
55
|
+
override var reason: String {
|
|
56
|
+
"FileSystem module not found, make sure 'expo-file-system' is linked correctly"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
internal class FileSystemReadPermissionException: GenericException<String> {
|
|
61
|
+
override var reason: String {
|
|
62
|
+
"File '\(param)' is not readable"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
internal class ImageLoadingFailedException: GenericException<String> {
|
|
67
|
+
override var reason: String {
|
|
68
|
+
"Could not load the image: \(param)"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
internal class CorruptedImageDataException: Exception {
|
|
73
|
+
override var reason: String {
|
|
74
|
+
"Cannot create image data for given image format"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
internal class ImageWriteFailedException: GenericException<String> {
|
|
79
|
+
override var reason: String {
|
|
80
|
+
"Writing image data to the file has failed: \(param)"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Copyright 2021-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
import Photos
|
|
5
|
+
import UIKit
|
|
6
|
+
import ExpoModulesCore
|
|
7
|
+
|
|
8
|
+
public class ImageManipulatorModule: Module {
|
|
9
|
+
typealias LoadImageCallback = (Result<UIImage, Error>) -> Void
|
|
10
|
+
typealias SaveImageResult = (url: URL, data: Data)
|
|
11
|
+
|
|
12
|
+
public func definition() -> ModuleDefinition {
|
|
13
|
+
name("ExpoImageManipulator")
|
|
14
|
+
|
|
15
|
+
function("manipulateAsync", manipulateImage)
|
|
16
|
+
.runOnQueue(.main)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
internal func manipulateImage(url: URL, actions: [ManipulateAction], options: ManipulateOptions, promise: Promise) {
|
|
20
|
+
loadImage(atUrl: url) { result in
|
|
21
|
+
switch result {
|
|
22
|
+
case .failure(let error):
|
|
23
|
+
return promise.reject(error)
|
|
24
|
+
case .success(let image):
|
|
25
|
+
do {
|
|
26
|
+
let newImage = try manipulate(image: image, actions: actions)
|
|
27
|
+
let saveResult = try self.saveImage(newImage, options: options)
|
|
28
|
+
|
|
29
|
+
promise.resolve([
|
|
30
|
+
"uri": saveResult.url.absoluteString,
|
|
31
|
+
"width": newImage.cgImage?.width ?? 0,
|
|
32
|
+
"height": newImage.cgImage?.height ?? 0,
|
|
33
|
+
"base64": options.base64 ? saveResult.data.base64EncodedData() : nil
|
|
34
|
+
])
|
|
35
|
+
} catch {
|
|
36
|
+
promise.reject(error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
Loads the image from given URL.
|
|
44
|
+
*/
|
|
45
|
+
internal func loadImage(atUrl url: URL, callback: @escaping LoadImageCallback) {
|
|
46
|
+
if url.scheme == "data" {
|
|
47
|
+
guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
|
|
48
|
+
return callback(.failure(CorruptedImageDataException()))
|
|
49
|
+
}
|
|
50
|
+
return callback(.success(image))
|
|
51
|
+
}
|
|
52
|
+
if url.scheme == "assets-library" {
|
|
53
|
+
// TODO: ALAsset URLs are deprecated as of iOS 11, we should migrate to `ph://` soon.
|
|
54
|
+
return loadImageFromPhotoLibrary(url: url, callback: callback)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
guard let imageLoader = self.appContext?.imageLoader else {
|
|
58
|
+
return callback(.failure(ImageLoaderNotFoundException()))
|
|
59
|
+
}
|
|
60
|
+
guard let fileSystem = self.appContext?.fileSystem else {
|
|
61
|
+
return callback(.failure(FileSystemNotFoundException()))
|
|
62
|
+
}
|
|
63
|
+
guard fileSystem.permissions(forURI: url).contains(.read) else {
|
|
64
|
+
return callback(.failure(FileSystemReadPermissionException(url.absoluteString)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
imageLoader.loadImage(for: url) { error, image in
|
|
68
|
+
guard let image = image, error == nil else {
|
|
69
|
+
return callback(.failure(ImageLoadingFailedException(error.debugDescription)))
|
|
70
|
+
}
|
|
71
|
+
callback(.success(image))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
Loads the image from user's photo library.
|
|
77
|
+
*/
|
|
78
|
+
internal func loadImageFromPhotoLibrary(url: URL, callback: @escaping LoadImageCallback) {
|
|
79
|
+
guard let asset = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject else {
|
|
80
|
+
return callback(.failure(ImageNotFoundException()))
|
|
81
|
+
}
|
|
82
|
+
let size = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
|
|
83
|
+
let options = PHImageRequestOptions()
|
|
84
|
+
|
|
85
|
+
options.resizeMode = .exact
|
|
86
|
+
options.isNetworkAccessAllowed = true
|
|
87
|
+
options.isSynchronous = true
|
|
88
|
+
options.deliveryMode = .highQualityFormat
|
|
89
|
+
|
|
90
|
+
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: options) { image, _ in
|
|
91
|
+
guard let image = image else {
|
|
92
|
+
return callback(.failure(ImageNotFoundException()))
|
|
93
|
+
}
|
|
94
|
+
return callback(.success(image))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
Saves the image as a file.
|
|
100
|
+
*/
|
|
101
|
+
internal func saveImage(_ image: UIImage, options: ManipulateOptions) throws -> SaveImageResult {
|
|
102
|
+
guard let fileSystem = self.appContext?.fileSystem else {
|
|
103
|
+
throw FileSystemNotFoundException()
|
|
104
|
+
}
|
|
105
|
+
let directory = URL(fileURLWithPath: fileSystem.cachesDirectory).appendingPathComponent("ImageManipulator")
|
|
106
|
+
let filename = UUID().uuidString.appending(options.format.fileExtension)
|
|
107
|
+
let fileUrl = directory.appendingPathComponent(filename)
|
|
108
|
+
|
|
109
|
+
fileSystem.ensureDirExists(withPath: directory.path)
|
|
110
|
+
|
|
111
|
+
guard let data = imageData(from: image, format: options.format, compression: options.compress) else {
|
|
112
|
+
throw CorruptedImageDataException()
|
|
113
|
+
}
|
|
114
|
+
do {
|
|
115
|
+
try data.write(to: fileUrl, options: .atomic)
|
|
116
|
+
} catch let error {
|
|
117
|
+
throw ImageWriteFailedException(error.localizedDescription)
|
|
118
|
+
}
|
|
119
|
+
return (url: fileUrl, data: data)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
Returns pixel data representation of the image.
|
|
125
|
+
*/
|
|
126
|
+
func imageData(from image: UIImage, format: ImageFormat, compression: Double) -> Data? {
|
|
127
|
+
switch format {
|
|
128
|
+
case .jpeg, .jpg:
|
|
129
|
+
return image.jpegData(compressionQuality: compression)
|
|
130
|
+
case .png:
|
|
131
|
+
return image.pngData()
|
|
132
|
+
}
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-image-manipulator",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0",
|
|
4
4
|
"description": "Provides functions that let you manipulation images on the local file system, eg: resize, crop.",
|
|
5
5
|
"main": "build/ImageManipulator.js",
|
|
6
6
|
"types": "build/ImageManipulator.d.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"homepage": "https://docs.expo.dev/versions/latest/sdk/imagemanipulator/",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"expo-image-loader": "~3.
|
|
34
|
+
"expo-image-loader": "~3.2.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"expo-module-scripts": "^2.0.0"
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"expo": "*"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "22dce752354bb429c84851bc4389abe47a766b1f"
|
|
43
43
|
}
|
|
@@ -3,23 +3,23 @@ import { crop, flip, resize, rotate } from './actions/index.web';
|
|
|
3
3
|
import { getContext } from './utils/getContext.web';
|
|
4
4
|
|
|
5
5
|
function getResults(canvas: HTMLCanvasElement, options?: SaveOptions): ImageResult {
|
|
6
|
-
let
|
|
6
|
+
let uri: string;
|
|
7
7
|
if (options) {
|
|
8
8
|
const { format = 'png' } = options;
|
|
9
9
|
if (options.format === 'png' && options.compress !== undefined) {
|
|
10
10
|
console.warn('compress is not supported with png format.');
|
|
11
11
|
}
|
|
12
12
|
const quality = Math.min(1, Math.max(0, options.compress ?? 1));
|
|
13
|
-
|
|
13
|
+
uri = canvas.toDataURL('image/' + format, quality);
|
|
14
14
|
} else {
|
|
15
15
|
// defaults to PNG with no loss
|
|
16
|
-
|
|
16
|
+
uri = canvas.toDataURL();
|
|
17
17
|
}
|
|
18
18
|
return {
|
|
19
|
-
uri
|
|
19
|
+
uri,
|
|
20
20
|
width: canvas.width,
|
|
21
21
|
height: canvas.height,
|
|
22
|
-
base64,
|
|
22
|
+
base64: uri.replace(/^data:image\/\w+;base64,/, ''),
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
// Copyright 2018-present 650 Industries. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <EXImageManipulator/EXImageManipulatorModule.h>
|
|
4
|
-
#import <ExpoModulesCore/EXFileSystemInterface.h>
|
|
5
|
-
#import <ExpoModulesCore/EXImageLoaderInterface.h>
|
|
6
|
-
#import <Photos/Photos.h>
|
|
7
|
-
|
|
8
|
-
@interface EXImageManipulatorModule ()
|
|
9
|
-
|
|
10
|
-
@property (nonatomic, weak) EXModuleRegistry *moduleRegistry;
|
|
11
|
-
@property (nonatomic, weak) id<EXFileSystemInterface> fileSystem;
|
|
12
|
-
@property (nonatomic, weak) id<EXImageLoaderInterface> imageLoader;
|
|
13
|
-
|
|
14
|
-
@end
|
|
15
|
-
|
|
16
|
-
static NSString* const ACTION_KEY_RESIZE = @"resize";
|
|
17
|
-
static NSString* const ACTION_KEY_ROTATE = @"rotate";
|
|
18
|
-
static NSString* const ACTION_KEY_FLIP = @"flip";
|
|
19
|
-
static NSString* const ACTION_KEY_CROP = @"crop";
|
|
20
|
-
|
|
21
|
-
static NSString* const SAVE_OPTIONS_KEY_FORMAT = @"format";
|
|
22
|
-
static NSString* const SAVE_OPTIONS_KEY_COMPRESS = @"compress";
|
|
23
|
-
static NSString* const SAVE_OPTIONS_KEY_BASE64 = @"base64";
|
|
24
|
-
|
|
25
|
-
@implementation EXImageManipulatorModule
|
|
26
|
-
|
|
27
|
-
EX_EXPORT_MODULE(ExpoImageManipulator);
|
|
28
|
-
|
|
29
|
-
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
|
|
30
|
-
{
|
|
31
|
-
_moduleRegistry = moduleRegistry;
|
|
32
|
-
_fileSystem = [moduleRegistry getModuleImplementingProtocol:@protocol(EXFileSystemInterface)];
|
|
33
|
-
_imageLoader = [moduleRegistry getModuleImplementingProtocol:@protocol(EXImageLoaderInterface)];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
EX_EXPORT_METHOD_AS(manipulateAsync,
|
|
37
|
-
uri:(NSString *)uri
|
|
38
|
-
actions:(NSArray *)actions
|
|
39
|
-
saveOptions:(NSDictionary *)saveOptions
|
|
40
|
-
resolve:(EXPromiseResolveBlock)resolve
|
|
41
|
-
reject:(EXPromiseRejectBlock)reject)
|
|
42
|
-
{
|
|
43
|
-
NSURL *url = [NSURL URLWithString:uri];
|
|
44
|
-
// no scheme provided in uri, handle as a local path and add 'file://' scheme
|
|
45
|
-
if (!url.scheme) {
|
|
46
|
-
url = [NSURL fileURLWithPath:uri isDirectory:false];
|
|
47
|
-
}
|
|
48
|
-
NSString *path = [url.path stringByStandardizingPath];
|
|
49
|
-
|
|
50
|
-
if ([[url scheme] isEqualToString:@"data"]) {
|
|
51
|
-
NSData *data = [[NSData alloc] initWithContentsOfURL:url];
|
|
52
|
-
if (data != nil) {
|
|
53
|
-
UIImage *image = [UIImage imageWithData:data];
|
|
54
|
-
image = [self fixOrientation:image];
|
|
55
|
-
[self manipulateImage:image actions:actions saveOptions:saveOptions resolver:resolve rejecter:reject];
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!_fileSystem) {
|
|
61
|
-
return reject(@"E_MISSING_MODULE", @"No FileSystem module.", nil);
|
|
62
|
-
}
|
|
63
|
-
if (!([_fileSystem permissionsForURI:url] & EXFileSystemPermissionRead)) {
|
|
64
|
-
return reject(@"E_FILESYSTEM_PERMISSIONS", [NSString stringWithFormat:@"File '%@' isn't readable.", uri], nil);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
NSString *errorMessage;
|
|
68
|
-
if (![self areActionsValid:actions errorMessage:&errorMessage] || ![self areSaveOptionsValid:saveOptions errorMessage:&errorMessage]) {
|
|
69
|
-
return reject(@"E_IMAGE_MANIPULATOR_INVALID_ARG", errorMessage, nil);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if ([[url scheme] isEqualToString:@"assets-library"]) {
|
|
73
|
-
PHFetchResult<PHAsset *> *fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil];
|
|
74
|
-
if (fetchResult.count > 0) {
|
|
75
|
-
PHAsset *asset = fetchResult[0];
|
|
76
|
-
CGSize size = CGSizeMake([asset pixelWidth], [asset pixelHeight]);
|
|
77
|
-
PHImageRequestOptions *options = [PHImageRequestOptions new];
|
|
78
|
-
[options setResizeMode:PHImageRequestOptionsResizeModeExact];
|
|
79
|
-
[options setNetworkAccessAllowed:YES];
|
|
80
|
-
[options setSynchronous:NO];
|
|
81
|
-
[options setDeliveryMode:PHImageRequestOptionsDeliveryModeHighQualityFormat];
|
|
82
|
-
|
|
83
|
-
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFit options:options resultHandler:^(UIImage * _Nullable image, NSDictionary * _Nullable info) {
|
|
84
|
-
if (!image) {
|
|
85
|
-
reject(@"E_IMAGE_MANIPULATION_FAILED", [NSString stringWithFormat:@"The file isn't convertable to image. Given path: `%@`.", path], nil);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
image = [self fixOrientation:image];
|
|
89
|
-
[self manipulateImage:image actions:actions saveOptions:saveOptions resolver:resolve rejecter:reject];
|
|
90
|
-
}];
|
|
91
|
-
return;
|
|
92
|
-
} else {
|
|
93
|
-
return reject(@"E_IMAGE_MANIPULATION_FAILED", [NSString stringWithFormat:@"The file does not exist. Given path: `%@`.", path], nil);
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
[_imageLoader loadImageForURL:url completionHandler:^(NSError *error, UIImage *loadedImage) {
|
|
97
|
-
if (error != nil) {
|
|
98
|
-
return reject(@"E_IMAGE_MANIPULATION_FAILED", @"Could not get the image", error);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
UIImage *image = [self fixOrientation:loadedImage];
|
|
102
|
-
[self manipulateImage:image actions:actions saveOptions:saveOptions resolver:resolve rejecter:reject];
|
|
103
|
-
}];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
-(BOOL)areActionsValid:(NSArray *)actions errorMessage:(NSString **)errorMessage
|
|
108
|
-
{
|
|
109
|
-
for (NSDictionary *action in actions) {
|
|
110
|
-
int actionsCounter = 0;
|
|
111
|
-
if (action[ACTION_KEY_RESIZE]) {
|
|
112
|
-
actionsCounter += 1;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (action[ACTION_KEY_ROTATE]) {
|
|
116
|
-
actionsCounter += 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (action[ACTION_KEY_FLIP]) {
|
|
120
|
-
actionsCounter += 1;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (action[ACTION_KEY_CROP]) {
|
|
124
|
-
actionsCounter += 1;
|
|
125
|
-
|
|
126
|
-
if (action[ACTION_KEY_CROP][@"originX"] == nil
|
|
127
|
-
|| action[ACTION_KEY_CROP][@"originY"] == nil
|
|
128
|
-
|| action[ACTION_KEY_CROP][@"width"] == nil
|
|
129
|
-
|| action[ACTION_KEY_CROP][@"height"] == nil
|
|
130
|
-
) {
|
|
131
|
-
*errorMessage = @"Invalid crop options has been passed. Please make sure the object contains originX, originY, width and height.";
|
|
132
|
-
return NO;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (actionsCounter != 1) {
|
|
138
|
-
*errorMessage = [NSString stringWithFormat:@"Single action must contain exactly one transformation from list: ['%@', '%@', '%@', '%@']",
|
|
139
|
-
ACTION_KEY_RESIZE,
|
|
140
|
-
ACTION_KEY_ROTATE,
|
|
141
|
-
ACTION_KEY_FLIP,
|
|
142
|
-
ACTION_KEY_CROP];
|
|
143
|
-
return NO;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return YES;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
-(BOOL)areSaveOptionsValid:(NSDictionary *)saveOptions errorMessage:(NSString **)errorMessage
|
|
150
|
-
{
|
|
151
|
-
NSString* format = saveOptions[SAVE_OPTIONS_KEY_FORMAT];
|
|
152
|
-
if (![format isEqualToString:@"jpeg"] && ![format isEqualToString:@"png"]) {
|
|
153
|
-
*errorMessage = [NSString stringWithFormat:@"SaveOption 'format' must be one of ['png', 'jpeg']. Obtained '%@'.", format];
|
|
154
|
-
return NO;
|
|
155
|
-
}
|
|
156
|
-
return YES;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
-(UIImage *)fixOrientation:(UIImage *)image
|
|
160
|
-
{
|
|
161
|
-
if (image.imageOrientation == UIImageOrientationUp) {
|
|
162
|
-
return image;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
CGAffineTransform transform = CGAffineTransformIdentity;
|
|
166
|
-
switch (image.imageOrientation) {
|
|
167
|
-
case UIImageOrientationDown:
|
|
168
|
-
case UIImageOrientationDownMirrored:
|
|
169
|
-
transform = CGAffineTransformTranslate(transform, image.size.width, image.size.height);
|
|
170
|
-
transform = CGAffineTransformRotate(transform, M_PI);
|
|
171
|
-
break;
|
|
172
|
-
|
|
173
|
-
case UIImageOrientationLeft:
|
|
174
|
-
case UIImageOrientationLeftMirrored:
|
|
175
|
-
transform = CGAffineTransformTranslate(transform, image.size.width, 0);
|
|
176
|
-
transform = CGAffineTransformRotate(transform, M_PI_2);
|
|
177
|
-
break;
|
|
178
|
-
|
|
179
|
-
case UIImageOrientationRight:
|
|
180
|
-
case UIImageOrientationRightMirrored:
|
|
181
|
-
transform = CGAffineTransformTranslate(transform, 0, image.size.height);
|
|
182
|
-
transform = CGAffineTransformRotate(transform, -M_PI_2);
|
|
183
|
-
break;
|
|
184
|
-
|
|
185
|
-
default:
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
switch (image.imageOrientation) {
|
|
190
|
-
case UIImageOrientationUpMirrored:
|
|
191
|
-
case UIImageOrientationDownMirrored:
|
|
192
|
-
transform = CGAffineTransformTranslate(transform, image.size.width, 0);
|
|
193
|
-
transform = CGAffineTransformScale(transform, -1, 1);
|
|
194
|
-
break;
|
|
195
|
-
|
|
196
|
-
case UIImageOrientationLeftMirrored:
|
|
197
|
-
case UIImageOrientationRightMirrored:
|
|
198
|
-
transform = CGAffineTransformTranslate(transform, image.size.height, 0);
|
|
199
|
-
transform = CGAffineTransformScale(transform, -1, 1);
|
|
200
|
-
break;
|
|
201
|
-
|
|
202
|
-
default:
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
CGContextRef ctx = CGBitmapContextCreate(NULL, image.size.width, image.size.height, CGImageGetBitsPerComponent(image.CGImage), 0, CGImageGetColorSpace(image.CGImage), CGImageGetBitmapInfo(image.CGImage));
|
|
207
|
-
CGContextConcatCTM(ctx, transform);
|
|
208
|
-
switch (image.imageOrientation) {
|
|
209
|
-
case UIImageOrientationLeft:
|
|
210
|
-
case UIImageOrientationLeftMirrored:
|
|
211
|
-
case UIImageOrientationRight:
|
|
212
|
-
case UIImageOrientationRightMirrored:
|
|
213
|
-
CGContextDrawImage(ctx, CGRectMake(0, 0, image.size.height, image.size.width), image.CGImage);
|
|
214
|
-
break;
|
|
215
|
-
|
|
216
|
-
default:
|
|
217
|
-
CGContextDrawImage(ctx, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage);
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
|
|
222
|
-
UIImage *img = [UIImage imageWithCGImage:cgimg];
|
|
223
|
-
CGContextRelease(ctx);
|
|
224
|
-
CGImageRelease(cgimg);
|
|
225
|
-
return img;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
-(void)manipulateImage:(UIImage *)image
|
|
229
|
-
actions:(NSArray *)actions
|
|
230
|
-
saveOptions:(NSDictionary *)saveOptions
|
|
231
|
-
resolver:(EXPromiseResolveBlock)resolve
|
|
232
|
-
rejecter:(EXPromiseRejectBlock)reject
|
|
233
|
-
{
|
|
234
|
-
for (NSDictionary *action in actions) {
|
|
235
|
-
if (action[ACTION_KEY_RESIZE]) {
|
|
236
|
-
image = [self resizeImage:image options:action[ACTION_KEY_RESIZE]];
|
|
237
|
-
} else if (action[ACTION_KEY_ROTATE]) {
|
|
238
|
-
image = [self rotateImage:image rotation:(NSNumber *)action[ACTION_KEY_ROTATE]];
|
|
239
|
-
} else if (action[ACTION_KEY_FLIP]) {
|
|
240
|
-
image = [self flipImage:image flipType:action[ACTION_KEY_FLIP]];
|
|
241
|
-
} else if (action[ACTION_KEY_CROP]) {
|
|
242
|
-
NSString *errorMessage;
|
|
243
|
-
image = [self cropImage:image options:action[ACTION_KEY_CROP] didFailWithErrorMessage:&errorMessage];
|
|
244
|
-
if (errorMessage != nil) {
|
|
245
|
-
return reject(@"E_INVALID_CROP_DATA", errorMessage, nil);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
float compressionValue = saveOptions[SAVE_OPTIONS_KEY_COMPRESS] != nil ? [(NSNumber *)saveOptions[SAVE_OPTIONS_KEY_COMPRESS] floatValue] : 1.0;
|
|
251
|
-
|
|
252
|
-
NSString *format = saveOptions[SAVE_OPTIONS_KEY_FORMAT];
|
|
253
|
-
NSData *imageData = nil;
|
|
254
|
-
NSString *extension;
|
|
255
|
-
if ([format isEqualToString:@"jpeg"]) {
|
|
256
|
-
imageData = UIImageJPEGRepresentation(image, compressionValue);
|
|
257
|
-
extension = @".jpg";
|
|
258
|
-
} else if ([format isEqualToString:@"png"]) {
|
|
259
|
-
imageData = UIImagePNGRepresentation(image);
|
|
260
|
-
extension = @".png";
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
NSString *directory = [_fileSystem.cachesDirectory stringByAppendingPathComponent:@"ImageManipulator"];
|
|
264
|
-
[_fileSystem ensureDirExistsWithPath:directory];
|
|
265
|
-
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
|
|
266
|
-
NSString *newPath = [directory stringByAppendingPathComponent:fileName];
|
|
267
|
-
[imageData writeToFile:newPath atomically:YES];
|
|
268
|
-
NSURL *fileURL = [NSURL fileURLWithPath:newPath];
|
|
269
|
-
NSString *filePath = [fileURL absoluteString];
|
|
270
|
-
|
|
271
|
-
NSMutableDictionary *response = [NSMutableDictionary new];
|
|
272
|
-
response[@"uri"] = filePath;
|
|
273
|
-
response[@"width"] = @(CGImageGetWidth(image.CGImage));
|
|
274
|
-
response[@"height"] = @(CGImageGetHeight(image.CGImage));
|
|
275
|
-
if (saveOptions[SAVE_OPTIONS_KEY_BASE64] && [saveOptions[SAVE_OPTIONS_KEY_BASE64] boolValue]) {
|
|
276
|
-
response[@"base64"] = [imageData base64EncodedStringWithOptions:0];
|
|
277
|
-
}
|
|
278
|
-
resolve(response);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
- (UIImage *)resizeImage:(UIImage *)image options:(NSDictionary *)resize
|
|
282
|
-
{
|
|
283
|
-
float imageWidth = image.size.width;
|
|
284
|
-
float imageHeight = image.size.height;
|
|
285
|
-
float imageRatio = imageWidth / imageHeight;
|
|
286
|
-
|
|
287
|
-
NSInteger requestedWidth = 0;
|
|
288
|
-
NSInteger requestedHeight = 0;
|
|
289
|
-
|
|
290
|
-
if (resize[@"width"]) {
|
|
291
|
-
requestedWidth = [(NSNumber *)resize[@"width"] integerValue];
|
|
292
|
-
requestedHeight = requestedWidth / imageRatio;
|
|
293
|
-
}
|
|
294
|
-
if (resize[@"height"]) {
|
|
295
|
-
requestedHeight = [(NSNumber *)resize[@"height"] integerValue];
|
|
296
|
-
requestedWidth = requestedWidth == 0 ? imageRatio * requestedHeight : requestedWidth;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
CGSize requestedSize = CGSizeMake(requestedWidth, requestedHeight);
|
|
300
|
-
UIGraphicsBeginImageContextWithOptions(requestedSize, NO, 1.0);
|
|
301
|
-
[image drawInRect:CGRectMake(0, 0, requestedWidth, requestedHeight)];
|
|
302
|
-
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
303
|
-
UIGraphicsEndImageContext();
|
|
304
|
-
|
|
305
|
-
return image;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
- (UIImage *)rotateImage:(UIImage *)image rotation:(NSNumber *)rotation
|
|
309
|
-
{
|
|
310
|
-
float rads = [rotation integerValue] * M_PI / 180;
|
|
311
|
-
CGSize size = image.size;
|
|
312
|
-
UIView *rotatedViewBox = [[UIView alloc] initWithFrame:CGRectMake(0,0,size.width, size.height)];
|
|
313
|
-
CGAffineTransform t = CGAffineTransformMakeRotation(rads);
|
|
314
|
-
rotatedViewBox.transform = t;
|
|
315
|
-
CGSize rotatedSize = rotatedViewBox.frame.size;
|
|
316
|
-
|
|
317
|
-
UIGraphicsBeginImageContext(rotatedSize);
|
|
318
|
-
CGContextRef bitmap = UIGraphicsGetCurrentContext();
|
|
319
|
-
CGContextTranslateCTM(bitmap, rotatedSize.width/2, rotatedSize.height/2);
|
|
320
|
-
CGContextRotateCTM(bitmap, rads);
|
|
321
|
-
CGContextScaleCTM(bitmap, 1.0, -1.0);
|
|
322
|
-
CGContextDrawImage(bitmap, CGRectMake(-size.width / 2, -size.height / 2, size.width, size.height), image.CGImage);
|
|
323
|
-
|
|
324
|
-
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
325
|
-
UIGraphicsEndImageContext();
|
|
326
|
-
return image;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
- (UIImage *)flipImage:(UIImage *)image flipType:(NSString *)flip
|
|
330
|
-
{
|
|
331
|
-
UIImageView *tempImageView = [[UIImageView alloc] initWithImage:image];
|
|
332
|
-
UIGraphicsBeginImageContext(tempImageView.frame.size);
|
|
333
|
-
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
334
|
-
CGAffineTransform transform;
|
|
335
|
-
|
|
336
|
-
if ([flip isEqualToString:@"vertical"]) {
|
|
337
|
-
transform = CGAffineTransformMake(1, 0, 0, -1, 0, tempImageView.frame.size.height);
|
|
338
|
-
CGContextConcatCTM(context, transform);
|
|
339
|
-
} else if ([flip isEqualToString:@"horizontal"]) {
|
|
340
|
-
transform = CGAffineTransformMake(-1, 0, 0, 1, tempImageView.frame.size.width, 0);
|
|
341
|
-
CGContextConcatCTM(context, transform);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
[tempImageView.layer renderInContext:context];
|
|
345
|
-
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
346
|
-
UIGraphicsEndImageContext();
|
|
347
|
-
return image;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
- (UIImage *)cropImage:(UIImage *)image options:(NSDictionary *)cropData didFailWithErrorMessage:(NSString **)errorMessage
|
|
351
|
-
{
|
|
352
|
-
float originX = [(NSNumber *)cropData[@"originX"] floatValue];
|
|
353
|
-
float originY = [(NSNumber *)cropData[@"originY"] floatValue];
|
|
354
|
-
float requestedWidth = [(NSNumber *)cropData[@"width"] floatValue];
|
|
355
|
-
float requestedHeight = [(NSNumber *)cropData[@"height"] floatValue];
|
|
356
|
-
|
|
357
|
-
if (originX > image.size.width
|
|
358
|
-
|| originY > image.size.height
|
|
359
|
-
|| requestedWidth > image.size.width
|
|
360
|
-
|| requestedHeight > image.size.height
|
|
361
|
-
) {
|
|
362
|
-
*errorMessage = @"Invalid crop options has been passed. Please make sure the requested crop rectangle is inside source image.";
|
|
363
|
-
return nil;
|
|
364
|
-
}
|
|
365
|
-
CGRect cropDimensions = CGRectMake(originX, originY, requestedWidth, requestedHeight);
|
|
366
|
-
CGImageRef takenCGImage = image.CGImage;
|
|
367
|
-
CGImageRef cropCGImage = CGImageCreateWithImageInRect(takenCGImage, cropDimensions);
|
|
368
|
-
image = [UIImage imageWithCGImage:cropCGImage scale:image.scale orientation:image.imageOrientation];
|
|
369
|
-
CGImageRelease(cropCGImage);
|
|
370
|
-
return image;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
@end
|
package/unimodule.json
DELETED