expo-image-manipulator 10.2.1 → 10.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +3 -3
  3. package/android/build.gradle +24 -9
  4. package/build/ExpoImageManipulator.d.ts +1 -0
  5. package/build/ExpoImageManipulator.d.ts.map +1 -0
  6. package/build/ExpoImageManipulator.web.d.ts +1 -0
  7. package/build/ExpoImageManipulator.web.d.ts.map +1 -0
  8. package/build/ExpoImageManipulator.web.js +5 -5
  9. package/build/ExpoImageManipulator.web.js.map +1 -1
  10. package/build/ImageManipulator.d.ts +1 -0
  11. package/build/ImageManipulator.d.ts.map +1 -0
  12. package/build/ImageManipulator.types.d.ts +2 -1
  13. package/build/ImageManipulator.types.d.ts.map +1 -0
  14. package/build/ImageManipulator.types.js +1 -1
  15. package/build/ImageManipulator.types.js.map +1 -1
  16. package/build/actions/CropAction.web.d.ts +1 -0
  17. package/build/actions/CropAction.web.d.ts.map +1 -0
  18. package/build/actions/FlipAction.web.d.ts +1 -0
  19. package/build/actions/FlipAction.web.d.ts.map +1 -0
  20. package/build/actions/ResizeAction.web.d.ts +1 -0
  21. package/build/actions/ResizeAction.web.d.ts.map +1 -0
  22. package/build/actions/RotateAction.web.d.ts +1 -0
  23. package/build/actions/RotateAction.web.d.ts.map +1 -0
  24. package/build/actions/index.web.d.ts +1 -0
  25. package/build/actions/index.web.d.ts.map +1 -0
  26. package/build/utils/getContext.web.d.ts +1 -0
  27. package/build/utils/getContext.web.d.ts.map +1 -0
  28. package/build/validators.d.ts +1 -0
  29. package/build/validators.d.ts.map +1 -0
  30. package/expo-module.config.json +7 -0
  31. package/ios/{EXImageManipulator.podspec → ExpoImageManipulator.podspec} +10 -3
  32. package/ios/ImageManipulations.swift +209 -0
  33. package/ios/ImageManipulatorArguments.swift +93 -0
  34. package/ios/ImageManipulatorExceptions.swift +82 -0
  35. package/ios/ImageManipulatorModule.swift +133 -0
  36. package/package.json +3 -3
  37. package/src/ExpoImageManipulator.web.ts +5 -5
  38. package/src/ImageManipulator.types.ts +1 -1
  39. package/tsconfig.json +1 -1
  40. package/ios/EXImageManipulator/EXImageManipulatorModule.h +0 -7
  41. package/ios/EXImageManipulator/EXImageManipulatorModule.m +0 -373
  42. package/unimodule.json +0 -4
package/CHANGELOG.md CHANGED
@@ -10,7 +10,34 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
- ## 10.2.1 — 2022-02-01
13
+ ## 10.4.0 — 2022-07-07
14
+
15
+ ### 💡 Others
16
+
17
+ - Migrated Expo modules definitions to the new naming convention. ([#17193](https://github.com/expo/expo/pull/17193) by [@tsapeta](https://github.com/tsapeta))
18
+
19
+ ## 10.3.1 — 2022-04-20
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - Fix `base64` result of `manipulateAsync` being always `null` on iOS. ([#17122](https://github.com/expo/expo/pull/17122) by [@barthap](https://github.com/barthap))
24
+
25
+ ## 10.3.0 — 2022-04-18
26
+
27
+ ### 🎉 New features
28
+
29
+ - 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))
30
+
31
+ ### 🐛 Bug fixes
32
+
33
+ - Remove `data:image` part in web base64 result. ([#16191](https://github.com/expo/expo/pull/16191) by [@AllanChain](https://github.com/AllanChain))
34
+ - On iOS fix rotation causing extra image borders. ([#16669](https://github.com/expo/expo/pull/16669) by [@mnightingale](https://github.com/mnightingale))
35
+
36
+ ### ⚠️ Notices
37
+
38
+ - 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))
39
+
40
+ ## 10.2.1 - 2022-02-01
14
41
 
15
42
  ### 🐛 Bug fixes
16
43
 
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 master branch](https://github.com/expo/expo/blob/master/docs/pages/versions/unversioned/sdk/imagemanipulator.md)
8
- - [Documentation for the latest stable release](https://docs.expo.io/versions/latest/sdk/imagemanipulator/)
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 managed [managed](https://docs.expo.io/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.io/versions/latest/sdk/imagemanipulator/).
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
 
@@ -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.2.1'
6
+ version = '10.4.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:${safeExtGet('kotlinVersion', '1.4.21')}")
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", 30)
62
+ compileSdkVersion safeExtGet("compileSdkVersion", 31)
48
63
 
49
64
  compileOptions {
50
- sourceCompatibility JavaVersion.VERSION_1_8
51
- targetCompatibility JavaVersion.VERSION_1_8
65
+ sourceCompatibility JavaVersion.VERSION_11
66
+ targetCompatibility JavaVersion.VERSION_11
52
67
  }
53
68
 
54
69
  kotlinOptions {
55
- jvmTarget = JavaVersion.VERSION_1_8
70
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
56
71
  }
57
72
 
58
73
  defaultConfig {
59
74
  minSdkVersion safeExtGet("minSdkVersion", 21)
60
- targetSdkVersion safeExtGet("targetSdkVersion", 30)
75
+ targetSdkVersion safeExtGet("targetSdkVersion", 31)
61
76
  versionCode 23
62
- versionName "10.2.1"
77
+ versionName "10.4.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:${safeExtGet('kotlinVersion', '1.4.21')}"
89
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
75
90
  }
@@ -1,2 +1,3 @@
1
1
  declare const _default: import("expo-modules-core").ProxyNativeModule;
2
2
  export default _default;
3
+ //# sourceMappingURL=ExpoImageManipulator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoImageManipulator.d.ts","sourceRoot":"","sources":["../src/ExpoImageManipulator.ts"],"names":[],"mappings":";AACA,wBAA6D"}
@@ -4,3 +4,4 @@ declare const _default: {
4
4
  manipulateAsync(uri: string, actions: Action[] | undefined, options: SaveOptions): Promise<ImageResult>;
5
5
  };
6
6
  export default _default;
7
+ //# sourceMappingURL=ExpoImageManipulator.web.d.ts.map
@@ -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 base64;
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
- base64 = canvas.toDataURL('image/' + format, quality);
11
+ uri = canvas.toDataURL('image/' + format, quality);
12
12
  }
13
13
  else {
14
14
  // defaults to PNG with no loss
15
- base64 = canvas.toDataURL();
15
+ uri = canvas.toDataURL();
16
16
  }
17
17
  return {
18
- uri: base64,
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":"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","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 base64;\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 base64 = canvas.toDataURL('image/' + format, quality);\n } else {\n // defaults to PNG with no loss\n base64 = canvas.toDataURL();\n }\n return {\n uri: base64,\n width: canvas.width,\n height: canvas.height,\n 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"]}
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"}
@@ -63,7 +63,7 @@ export declare enum SaveFormat {
63
63
  JPEG = "jpeg",
64
64
  PNG = "png",
65
65
  /**
66
- * __Web Only__
66
+ * @platform web
67
67
  */
68
68
  WEBP = "webp"
69
69
  }
@@ -87,3 +87,4 @@ export declare type SaveOptions = {
87
87
  */
88
88
  format?: SaveFormat;
89
89
  };
90
+ //# sourceMappingURL=ImageManipulator.types.d.ts.map
@@ -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"}
@@ -10,7 +10,7 @@ export var SaveFormat;
10
10
  SaveFormat["JPEG"] = "jpeg";
11
11
  SaveFormat["PNG"] = "png";
12
12
  /**
13
- * __Web Only__
13
+ * @platform web
14
14
  */
15
15
  SaveFormat["WEBP"] = "webp";
16
16
  })(SaveFormat || (SaveFormat = {}));
@@ -1 +1 @@
1
- {"version":3,"file":"ImageManipulator.types.js","sourceRoot":"","sources":["../src/ImageManipulator.types.ts"],"names":[],"mappings":"AA4CA,eAAe;AACf,MAAM,CAAN,IAAY,QAGX;AAHD,WAAY,QAAQ;IAClB,iCAAqB,CAAA;IACrB,qCAAyB,CAAA;AAC3B,CAAC,EAHW,QAAQ,KAAR,QAAQ,QAGnB;AA2BD,eAAe;AACf,MAAM,CAAN,IAAY,UAOX;AAPD,WAAY,UAAU;IACpB,2BAAa,CAAA;IACb,yBAAW,CAAA;IACX;;OAEG;IACH,2BAAa,CAAA;AACf,CAAC,EAPW,UAAU,KAAV,UAAU,QAOrB","sourcesContent":["// @needsAudit\nexport type ImageResult = {\n /**\n * An URI to the modified image (usable as the source for an `Image` or `Video` element).\n */\n uri: string;\n /**\n * Width of the image or video.\n */\n width: number;\n /**\n * Height of the image or video.\n */\n height: number;\n /**\n * It is included if the `base64` save option was truthy, and is a string containing the\n * JPEG/PNG (depending on `format`) data of the image in Base64. Prepend that with `'data:image/xxx;base64,'`\n * to get a data URI, which you can use as the source for an `Image` element for example\n * (where `xxx` is `jpeg` or `png`).\n */\n base64?: string;\n};\n\n// @needsAudit\nexport type ActionResize = {\n /**\n * Values correspond to the result image dimensions. If you specify only one value, the other will\n * be calculated automatically to preserve image ratio.\n */\n resize: {\n width?: number;\n height?: number;\n };\n};\n\n// @needsAudit\nexport type ActionRotate = {\n /**\n * Degrees to rotate the image. Rotation is clockwise when the value is positive and\n * counter-clockwise when negative.\n */\n rotate: number;\n};\n\n// @docsMissing\nexport enum FlipType {\n Vertical = 'vertical',\n Horizontal = 'horizontal',\n}\n\n// @needsAudit\nexport type ActionFlip = {\n /**\n * An axis on which image will be flipped. Only one flip per transformation is available. If you\n * want to flip according to both axes then provide two separate transformations.\n */\n flip: FlipType;\n};\n\n// @needsAudit\nexport type ActionCrop = {\n /**\n * Fields specify top-left corner and dimensions of a crop rectangle.\n */\n crop: {\n originX: number;\n originY: number;\n width: number;\n height: number;\n };\n};\n\n// @docsMissing\nexport type Action = ActionResize | ActionRotate | ActionFlip | ActionCrop;\n\n// @docsMissing\nexport enum SaveFormat {\n JPEG = 'jpeg',\n PNG = 'png',\n /**\n * __Web Only__\n */\n WEBP = 'webp',\n}\n\n// @needsAudit\n/**\n * A map defining how modified image should be saved.\n */\nexport type SaveOptions = {\n /**\n * Whether to also include the image data in Base64 format.\n */\n base64?: boolean;\n /**\n * A value in range `0.0` - `1.0` specifying compression level of the result image. `1` means\n * no compression (highest quality) and `0` the highest compression (lowest quality).\n */\n compress?: number;\n /**\n * Specifies what type of compression should be used and what is the result file extension.\n * `SaveFormat.PNG` compression is lossless but slower, `SaveFormat.JPEG` is faster but the image\n * has visible artifacts. Defaults to `SaveFormat.JPEG`\n */\n format?: SaveFormat;\n};\n"]}
1
+ {"version":3,"file":"ImageManipulator.types.js","sourceRoot":"","sources":["../src/ImageManipulator.types.ts"],"names":[],"mappings":"AA4CA,eAAe;AACf,MAAM,CAAN,IAAY,QAGX;AAHD,WAAY,QAAQ;IAClB,iCAAqB,CAAA;IACrB,qCAAyB,CAAA;AAC3B,CAAC,EAHW,QAAQ,KAAR,QAAQ,QAGnB;AA2BD,eAAe;AACf,MAAM,CAAN,IAAY,UAOX;AAPD,WAAY,UAAU;IACpB,2BAAa,CAAA;IACb,yBAAW,CAAA;IACX;;OAEG;IACH,2BAAa,CAAA;AACf,CAAC,EAPW,UAAU,KAAV,UAAU,QAOrB","sourcesContent":["// @needsAudit\nexport type ImageResult = {\n /**\n * An URI to the modified image (usable as the source for an `Image` or `Video` element).\n */\n uri: string;\n /**\n * Width of the image or video.\n */\n width: number;\n /**\n * Height of the image or video.\n */\n height: number;\n /**\n * It is included if the `base64` save option was truthy, and is a string containing the\n * JPEG/PNG (depending on `format`) data of the image in Base64. Prepend that with `'data:image/xxx;base64,'`\n * to get a data URI, which you can use as the source for an `Image` element for example\n * (where `xxx` is `jpeg` or `png`).\n */\n base64?: string;\n};\n\n// @needsAudit\nexport type ActionResize = {\n /**\n * Values correspond to the result image dimensions. If you specify only one value, the other will\n * be calculated automatically to preserve image ratio.\n */\n resize: {\n width?: number;\n height?: number;\n };\n};\n\n// @needsAudit\nexport type ActionRotate = {\n /**\n * Degrees to rotate the image. Rotation is clockwise when the value is positive and\n * counter-clockwise when negative.\n */\n rotate: number;\n};\n\n// @docsMissing\nexport enum FlipType {\n Vertical = 'vertical',\n Horizontal = 'horizontal',\n}\n\n// @needsAudit\nexport type ActionFlip = {\n /**\n * An axis on which image will be flipped. Only one flip per transformation is available. If you\n * want to flip according to both axes then provide two separate transformations.\n */\n flip: FlipType;\n};\n\n// @needsAudit\nexport type ActionCrop = {\n /**\n * Fields specify top-left corner and dimensions of a crop rectangle.\n */\n crop: {\n originX: number;\n originY: number;\n width: number;\n height: number;\n };\n};\n\n// @docsMissing\nexport type Action = ActionResize | ActionRotate | ActionFlip | ActionCrop;\n\n// @docsMissing\nexport enum SaveFormat {\n JPEG = 'jpeg',\n PNG = 'png',\n /**\n * @platform web\n */\n WEBP = 'webp',\n}\n\n// @needsAudit\n/**\n * A map defining how modified image should be saved.\n */\nexport type SaveOptions = {\n /**\n * Whether to also include the image data in Base64 format.\n */\n base64?: boolean;\n /**\n * A value in range `0.0` - `1.0` specifying compression level of the result image. `1` means\n * no compression (highest quality) and `0` the highest compression (lowest quality).\n */\n compress?: number;\n /**\n * Specifies what type of compression should be used and what is the result file extension.\n * `SaveFormat.PNG` compression is lossless but slower, `SaveFormat.JPEG` is faster but the image\n * has visible artifacts. Defaults to `SaveFormat.JPEG`\n */\n format?: SaveFormat;\n};\n"]}
@@ -1,3 +1,4 @@
1
1
  import { ActionCrop } from '../ImageManipulator.types';
2
2
  declare const _default: (canvas: HTMLCanvasElement, options: ActionCrop['crop']) => HTMLCanvasElement;
3
3
  export default _default;
4
+ //# sourceMappingURL=CropAction.web.d.ts.map
@@ -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"}
@@ -1,3 +1,4 @@
1
1
  import { ActionFlip } from '../ImageManipulator.types';
2
2
  declare const _default: (canvas: HTMLCanvasElement, flip: ActionFlip['flip']) => HTMLCanvasElement;
3
3
  export default _default;
4
+ //# sourceMappingURL=FlipAction.web.d.ts.map
@@ -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"}
@@ -1,3 +1,4 @@
1
1
  import { ActionResize } from '../ImageManipulator.types';
2
2
  declare const _default: (canvas: HTMLCanvasElement, { width, height }: ActionResize['resize']) => HTMLCanvasElement;
3
3
  export default _default;
4
+ //# sourceMappingURL=ResizeAction.web.d.ts.map
@@ -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"}
@@ -1,3 +1,4 @@
1
1
  import { ActionRotate } from '../ImageManipulator.types';
2
2
  declare const _default: (canvas: HTMLCanvasElement, degrees: ActionRotate['rotate']) => HTMLCanvasElement;
3
3
  export default _default;
4
+ //# sourceMappingURL=RotateAction.web.d.ts.map
@@ -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"}
@@ -2,3 +2,4 @@ export { default as crop } from './CropAction.web';
2
2
  export { default as flip } from './FlipAction.web';
3
3
  export { default as resize } from './ResizeAction.web';
4
4
  export { default as rotate } from './RotateAction.web';
5
+ //# sourceMappingURL=index.web.d.ts.map
@@ -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"}
@@ -1 +1,2 @@
1
1
  export declare function getContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D;
2
+ //# sourceMappingURL=getContext.web.d.ts.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "expo-image-manipulator",
3
+ "platforms": ["ios", "android"],
4
+ "ios": {
5
+ "modulesClassNames": ["ImageManipulatorModule"]
6
+ }
7
+ }
@@ -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 = 'EXImageManipulator'
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 = "#{s.name}/**/*.h"
28
+ s.source_files = "**/*.h"
22
29
  s.vendored_frameworks = "#{s.name}.xcframework"
23
30
  else
24
- s.source_files = "#{s.name}/**/*.{h,m}"
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
+ }