build: compile release APK and update dependencies (expo-image-manipulator/loader)
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
// @generated by expo-module-scripts
|
||||
module.exports = require('expo-module-scripts/eslintrc.base.js');
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
# Changelog
|
||||
|
||||
## Unpublished
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 💡 Others
|
||||
|
||||
## 14.0.8 — 2025-12-05
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.7 — 2025-09-10
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- Extracted the read permission check from `FileSystemUtilities` ([#39210](https://github.com/expo/expo/pull/39210) by [@kosmydel](https://github.com/kosmydel))
|
||||
|
||||
## 14.0.6 — 2025-09-02
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.5 — 2025-08-31
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.4 — 2025-08-27
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.3 — 2025-08-25
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.2 — 2025-08-16
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.1 — 2025-08-15
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 14.0.0 — 2025-08-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [iOS] Add Apple TV support. ([#38513](https://github.com/expo/expo/pull/38513) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## 13.1.7 — 2025-05-08
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `ReferenceError` during export for web. ([#36703](https://github.com/expo/expo/pull/36703) by [@aleqsio](https://github.com/aleqsio))
|
||||
|
||||
## 13.1.6 — 2025-05-06
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.1.5 — 2025-04-30
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.1.4 — 2025-04-25
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.1.3 — 2025-04-14
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.1.2 — 2025-04-11
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.1.1 — 2025-04-09
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.1.0 — 2025-04-04
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Changed `export` to `export type` for TS type declarations. ([#33446](https://github.com/expo/expo/pull/33446) by [@j-piasecki](https://github.com/j-piasecki))
|
||||
- Fixed `saveAsync` not respecting the format and compression quality. ([#33690](https://github.com/expo/expo/pull/33690) by [@tsapeta](https://github.com/tsapeta))
|
||||
- [Web] Overwrite class `name` property in `ImageManipulator`. ([#35806](https://github.com/expo/expo/pull/35806) by [@alimek](https://github.com/alimek))
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- [Android] Started using expo modules gradle plugin. ([#34176](https://github.com/expo/expo/pull/34176) by [@lukmccall](https://github.com/lukmccall))
|
||||
- [apple] Migrate remaining `expo-module.config.json` to unified platform syntax. ([#34445](https://github.com/expo/expo/pull/34445) by [@reichhartd](https://github.com/reichhartd))
|
||||
- [iOS] Fix warnings which will become errors in Swift 6. ([#35288](https://github.com/expo/expo/pull/35288) by [@behenate](https://github.com/behenate))
|
||||
|
||||
## 13.0.6 - 2025-01-10
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.0.5 — 2024-10-29
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [Android] Added support for image refs in `ImageManipulator.manipulate` and `useImageManipulator`. ([#32398](https://github.com/expo/expo/pull/32398) by [@lukmccall](https://github.com/lukmccall))
|
||||
|
||||
## 13.0.4 — 2024-10-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [iOS][Web] Added support for image refs in `ImageManipulator.manipulate` and `useImageManipulator`. ([#32346](https://github.com/expo/expo/pull/32346), [#32354](https://github.com/expo/expo/pull/32354) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
## 13.0.3 — 2024-10-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [iOS] Added support for image refs in `ImageManipulator.manipulate` and `useImageManipulator`. ([#32346](https://github.com/expo/expo/pull/32346) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
## 13.0.2 — 2024-10-24
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- Exported missing types, removed unnecessary fallback and properly registered the module on Web. ([#32302](https://github.com/expo/expo/pull/32302) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
## 13.0.1 — 2024-10-22
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 13.0.0 — 2024-10-22
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Bumped iOS deployment target to 15.1. ([#30840](https://github.com/expo/expo/pull/30840) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [iOS][Web] Introduce a new API for image manipulation. ([#30135](https://github.com/expo/expo/pull/30135), [#30194](https://github.com/expo/expo/pull/30194) by [@tsapeta](https://github.com/tsapeta))
|
||||
- [Android] Introduce a new API for image manipulation. ([#30232](https://github.com/expo/expo/pull/30232) by [@lukmccall](https://github.com/lukmccall))
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- Use the `src` folder as the Metro target. ([#30079](https://github.com/expo/expo/pull/30079) by [@tsapeta](https://github.com/tsapeta))
|
||||
- Prefer `UIGraphicsImageRenderer` over `UIGraphicsBeginImageContext`. ([#30211](https://github.com/expo/expo/pull/30211) by [@alanjhughes](https://github.com/alanjhughes))
|
||||
- Provide image's memory footprint for better garbage collection. ([#31168](https://github.com/expo/expo/pull/31168) by [@tsapeta](https://github.com/tsapeta) & [#31784](https://github.com/expo/expo/pull/31784) by [@lukmccall](https://github.com/lukmccall))
|
||||
|
||||
## 12.0.5 — 2024-05-13
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 12.0.4 — 2024-05-13
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- On `iOS` correctly handle urls coming from the users photo library. ([#28777](https://github.com/expo/expo/pull/28777) by [@alanjhughes](https://github.com/alanjhughes))
|
||||
|
||||
## 12.0.3 — 2024-05-01
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 12.0.2 — 2024-04-23
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 12.0.1 — 2024-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added support for converting to WEBP on Android and iOS. ([#26379](https://github.com/expo/expo/pull/26379) by [@NikitaDudin](https://github.com/NikitaDudin))
|
||||
|
||||
## 12.0.0 — 2024-04-18
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- drop unused web `name` property. ([#27437](https://github.com/expo/expo/pull/27437) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Removed deprecated backward compatible Gradle settings. ([#28083](https://github.com/expo/expo/pull/28083) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## 11.8.0 — 2023-12-12
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [iOS] Fix an issue where the image is manipulated on a background thread. ([#25756](https://github.com/expo/expo/pull/25756) by [@alanjhughes](https://github.com/alanjhughes))
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- [iOS] Replace legacy `FileSystem` interfaces usage with core `FileSystemUtilities`. ([#25495](https://github.com/expo/expo/pull/25495) by [@alanhughes](https://github.com/alanjhughes))
|
||||
|
||||
## 11.7.0 — 2023-11-14
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Bumped iOS deployment target to 13.4. ([#25063](https://github.com/expo/expo/pull/25063) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
||||
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added `extent` action ([#25116](https://github.com/expo/expo/pull/25116) by [@LinusU](https://github.com/LinusU))
|
||||
|
||||
## 11.6.0 — 2023-10-17
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
|
||||
|
||||
## 11.5.0 — 2023-09-04
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## 11.4.1 — 2023-08-02
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 11.4.0 — 2023-07-28
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 11.3.0 — 2023-06-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## 11.2.0 — 2023-05-08
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Handle images with an unsupported color space ([#21757](https://github.com/expo/expo/pull/21757) by [@mmmulani](https://github.com/mmmulani))
|
||||
|
||||
## 11.1.1 — 2023-02-09
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 11.1.0 — 2023-02-03
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- On Android bump `compileSdkVersion` and `targetSdkVersion` to `33`. ([#20721](https://github.com/expo/expo/pull/20721) by [@lukmccall](https://github.com/lukmccall))
|
||||
|
||||
## 11.0.0 — 2022-10-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Bumped iOS deployment target to 13.0 and deprecated support for iOS 12. ([#18873](https://github.com/expo/expo/pull/18873) by [@tsapeta](https://github.com/tsapeta))
|
||||
- Migrated Android codebase to use the new Expo modules API. ([#20683](https://github.com/expo/expo/pull/20683) by [@alanhughes](https://github.com/alanjhughes))
|
||||
|
||||
## 10.4.0 — 2022-07-07
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- Migrated Expo modules definitions to the new naming convention. ([#17193](https://github.com/expo/expo/pull/17193) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
## 10.3.1 — 2022-04-20
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `base64` result of `manipulateAsync` being always `null` on iOS. ([#17122](https://github.com/expo/expo/pull/17122) by [@barthap](https://github.com/barthap))
|
||||
|
||||
## 10.3.0 — 2022-04-18
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- 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))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Remove `data:image` part in web base64 result. ([#16191](https://github.com/expo/expo/pull/16191) by [@AllanChain](https://github.com/AllanChain))
|
||||
- On iOS fix rotation causing extra image borders. ([#16669](https://github.com/expo/expo/pull/16669) by [@mnightingale](https://github.com/mnightingale))
|
||||
|
||||
### ⚠️ Notices
|
||||
|
||||
- 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))
|
||||
|
||||
## 10.2.1 - 2022-02-01
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## 10.2.0 — 2021-12-03
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `Tainted canvases may not be exported` CORS error on web. ([#14739](https://github.com/expo/expo/pull/14739) by [@IjzerenHein](https://github.com/IjzerenHein))
|
||||
|
||||
## 10.1.0 — 2021-10-01
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Added missing dependency on `expo-image-loader`. ([#14585](https://github.com/expo/expo/pull/14585) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
## 10.0.0 — 2021-09-28
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support loading base64 data URIs on iOS. ([#13725](https://github.com/expo/expo/pull/13725) by [@mnightingale](https://github.com/mnightingale))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix resize action validator to allow providing just one of `width` or `height`. ([#13369](https://github.com/expo/expo/pull/13369) by [@cruzach](https://github.com/cruzach))
|
||||
- Fix incorrect compression used when `0` is requested on web. ([#13728](https://github.com/expo/expo/pull/13728) by [@mnightingale](https://github.com/mnightingale))
|
||||
- Fix Android `manipulateAsync` returns incorrect height (original unmanipulated height). ([#13726](https://github.com/expo/expo/pull/13726) by [@mnightingale](https://github.com/mnightingale))
|
||||
- Fixed Android to use filename extension consistent with other platforms. ([#13726](https://github.com/expo/expo/pull/13726) by [@mnightingale](https://github.com/mnightingale))
|
||||
- Fixed rotation direction to be clockwise on web. ([#13760](https://github.com/expo/expo/pull/13760) by [@mnightingale](https://github.com/mnightingale))
|
||||
- Fixed web support for multiple actions. ([#14056](https://github.com/expo/expo/pull/14056) by [@mnightingale](https://github.com/mnightingale))
|
||||
- Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo))
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- Migrated from `@unimodules/core` to `expo-modules-core`. ([#13757](https://github.com/expo/expo/pull/13757) by [@tsapeta](https://github.com/tsapeta))
|
||||
- Refactored web to pass canvases to each action avoiding multiple calls to `toDataURL`. ([#14145](https://github.com/expo/expo/pull/14145) by [@mnightingale](https://github.com/mnightingale))
|
||||
|
||||
## 9.2.0 — 2021-06-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 💡 Others
|
||||
|
||||
- Migrated from `unimodules-file-system-interface` to `expo-modules-core`.
|
||||
- Build Android code using Java 8 to fix Android instrumented test build error. ([#12939](https://github.com/expo/expo/pull/12939) by [@kudo](https://github.com/kudo))
|
||||
- Converted Android code to Kotlin. ([#13231](https://github.com/expo/expo/pull/13231) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## 9.1.0 — 2021-03-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Updated Android build configuration to target Android 11 (added support for Android SDK 30). ([#11647](https://github.com/expo/expo/pull/11647) by [@bbarthec](https://github.com/bbarthec))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed incorrect image cropping on Web. ([#12021](https://github.com/expo/expo/pull/12021) by [@rSkogeby](https://github.com/rskogeby))
|
||||
|
||||
## 9.0.0 — 2021-01-15
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Dropped support for iOS 10.0 ([#11344](https://github.com/expo/expo/pull/11344) by [@tsapeta](https://github.com/tsapeta))
|
||||
|
||||
## 8.4.0 — 2020-11-17
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 8.3.0 — 2020-08-18
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 8.2.1 — 2020-05-29
|
||||
|
||||
_This version does not introduce any user-facing changes._
|
||||
|
||||
## 8.2.0 — 2020-05-27
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- The base64 output will no longer contain newline and special character (`\n`, `\r`) on Android. ([#7841](https://github.com/expo/expo/pull/7841) by [@jarvisluong](https://github.com/jarvisluong))
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<p>
|
||||
<a href="https://docs.expo.dev/versions/latest/sdk/imagemanipulator/">
|
||||
<img
|
||||
src="../../.github/resources/expo-image-manipulator.svg"
|
||||
alt="expo-image-manipulator"
|
||||
height="64" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Provides functions that let you manipulation images on the local file system, eg: resize, crop.
|
||||
|
||||
# API documentation
|
||||
|
||||
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/)
|
||||
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/imagemanipulator/)
|
||||
|
||||
# Installation in managed Expo projects
|
||||
|
||||
For [managed](https://docs.expo.dev/archive/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/).
|
||||
|
||||
# Installation in bare React Native projects
|
||||
|
||||
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
|
||||
|
||||
### Add the package to your npm dependencies
|
||||
|
||||
```
|
||||
npx expo install expo-image-manipulator
|
||||
```
|
||||
|
||||
### Configure for Android
|
||||
|
||||
No additional set up necessary.
|
||||
|
||||
### Configure for iOS
|
||||
|
||||
Run `npx pod-install` after installing the npm package.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'host.exp.exponent'
|
||||
version = '14.0.8'
|
||||
|
||||
android {
|
||||
namespace "expo.modules.imagemanipulator"
|
||||
defaultConfig {
|
||||
versionCode 23
|
||||
versionName "14.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "androidx.annotation:annotation:1.0.0"
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
node_modules/expo-image-manipulator/android/src/main/java/expo/modules/imagemanipulator/FileUtils.kt
Generated
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
package expo.modules.imagemanipulator
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
internal object FileUtils {
|
||||
@Throws(IOException::class)
|
||||
fun generateRandomOutputPath(context: Context, imageFormat: ImageFormat): String {
|
||||
val directory = File("${context.cacheDir}${File.separator}ImageManipulator")
|
||||
ensureDirExists(directory)
|
||||
return "${directory}${File.separator}${UUID.randomUUID()}${imageFormat.fileExtension}"
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun ensureDirExists(dir: File): File {
|
||||
if (!(dir.isDirectory || dir.mkdirs())) {
|
||||
throw ImageWriteFailedException(dir.path)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
package expo.modules.imagemanipulator
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import expo.modules.kotlin.records.Field
|
||||
import expo.modules.kotlin.records.Record
|
||||
import expo.modules.kotlin.types.Enumerable
|
||||
|
||||
/**
|
||||
* Options provided for resize action.
|
||||
*/
|
||||
class ResizeOptions : Record {
|
||||
@Field
|
||||
val width: Int? = null
|
||||
|
||||
@Field
|
||||
val height: Int? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Cropping rect for crop action.
|
||||
*/
|
||||
class CropRect : Record {
|
||||
@Field
|
||||
val originX: Double = 0.0
|
||||
|
||||
@Field
|
||||
val originY: Double = 0.0
|
||||
|
||||
@Field
|
||||
val width: Double = 0.0
|
||||
|
||||
@Field
|
||||
val height: Double = 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to use when saving the resulted image.
|
||||
*/
|
||||
class ManipulateOptions : Record {
|
||||
@Field
|
||||
val base64: Boolean = false
|
||||
|
||||
@Field
|
||||
val compress: Double = 1.0
|
||||
|
||||
@Field
|
||||
val format: ImageFormat = ImageFormat.JPEG
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible options for flip action.
|
||||
*/
|
||||
enum class FlipType(val value: String) : Enumerable {
|
||||
VERTICAL("vertical"),
|
||||
HORIZONTAL("horizontal")
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum with supported image formats.
|
||||
*/
|
||||
enum class ImageFormat(val value: String) : Enumerable {
|
||||
JPEG("jpeg"),
|
||||
JPG("jpg"),
|
||||
PNG("png"),
|
||||
WEBP("webp");
|
||||
|
||||
val fileExtension: String
|
||||
get() = when (this) {
|
||||
JPEG, JPG -> ".jpg"
|
||||
PNG -> ".png"
|
||||
WEBP -> ".webp"
|
||||
}
|
||||
|
||||
val compressFormat: Bitmap.CompressFormat
|
||||
get() = when (this) {
|
||||
JPEG, JPG -> Bitmap.CompressFormat.JPEG
|
||||
PNG -> Bitmap.CompressFormat.PNG
|
||||
WEBP -> Bitmap.CompressFormat.WEBP
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
package expo.modules.imagemanipulator
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import expo.modules.imagemanipulator.transformers.ImageTransformer
|
||||
import expo.modules.kotlin.RuntimeContext
|
||||
import expo.modules.kotlin.exception.CodedException
|
||||
import expo.modules.kotlin.exception.toCodedException
|
||||
import expo.modules.kotlin.sharedobjects.SharedObject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
data class ManipulatorResult(
|
||||
private val value: Bitmap?,
|
||||
private val error: CodedException?
|
||||
) {
|
||||
fun map(transformer: ImageTransformer): ManipulatorResult {
|
||||
if (error != null) {
|
||||
return ManipulatorResult(null, error)
|
||||
}
|
||||
|
||||
return try {
|
||||
ManipulatorResult(
|
||||
transformer.transform(
|
||||
requireNotNull(value) { "The result doesn't have a value or error" }
|
||||
),
|
||||
null
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
ManipulatorResult(null, e.toCodedException())
|
||||
}
|
||||
}
|
||||
|
||||
fun get(): Bitmap {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
return requireNotNull(value) { "The result doesn't have a value or error" }
|
||||
}
|
||||
}
|
||||
|
||||
class ManipulatorTask(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val loader: suspend () -> Bitmap
|
||||
) {
|
||||
private var task: Deferred<ManipulatorResult> = launchLoader()
|
||||
|
||||
private fun launchLoader(): Deferred<ManipulatorResult> = coroutineScope.async {
|
||||
try {
|
||||
ManipulatorResult(loader(), null)
|
||||
} catch (e: Throwable) {
|
||||
ManipulatorResult(null, e.toCodedException())
|
||||
}
|
||||
}
|
||||
|
||||
fun addTransformer(transformer: ImageTransformer) {
|
||||
val oldTask = task
|
||||
task = coroutineScope.async {
|
||||
val currentValue = oldTask.await()
|
||||
return@async currentValue.map(transformer)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun render(): Bitmap {
|
||||
return task.await().get()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
task.cancel()
|
||||
task = launchLoader()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
class ImageManipulatorContext(
|
||||
runtimeContext: RuntimeContext,
|
||||
private val task: ManipulatorTask
|
||||
) : SharedObject(runtimeContext) {
|
||||
fun addTransformer(transformer: ImageTransformer) = apply { task.addTransformer(transformer) }
|
||||
|
||||
fun reset() = apply { task.reset() }
|
||||
|
||||
suspend fun render() = task.render()
|
||||
|
||||
override fun sharedObjectDidRelease() {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
package expo.modules.imagemanipulator
|
||||
|
||||
import expo.modules.kotlin.exception.CodedException
|
||||
import expo.modules.kotlin.exception.DecoratedException
|
||||
|
||||
internal class ImageInvalidCropException :
|
||||
CodedException("Invalid crop options has been passed. Please make sure the requested crop rectangle is inside source image")
|
||||
|
||||
internal class ImageLoaderNotFoundException :
|
||||
CodedException(message = "ImageLoader module not found, make sure 'expo-image-loader' is linked correctly")
|
||||
|
||||
internal class ImageLoadingFailedException(image: String, cause: CodedException) :
|
||||
DecoratedException(message = "Could not load the image: $image", cause)
|
||||
|
||||
internal class ImageWriteFailedException(file: String) :
|
||||
CodedException(message = "Writing image data to the file has failed: $file")
|
||||
Generated
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
@file:OptIn(EitherType::class)
|
||||
|
||||
package expo.modules.imagemanipulator
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import expo.modules.imagemanipulator.transformers.CropTransformer
|
||||
import expo.modules.imagemanipulator.transformers.FlipTransformer
|
||||
import expo.modules.imagemanipulator.transformers.ResizeTransformer
|
||||
import expo.modules.imagemanipulator.transformers.RotateTransformer
|
||||
import expo.modules.interfaces.imageloader.ImageLoaderInterface.ResultListener
|
||||
import expo.modules.kotlin.apifeatures.EitherType
|
||||
import expo.modules.kotlin.exception.Exceptions
|
||||
import expo.modules.kotlin.exception.toCodedException
|
||||
import expo.modules.kotlin.functions.Coroutine
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import expo.modules.kotlin.sharedobjects.SharedRef
|
||||
import expo.modules.kotlin.types.EitherOfThree
|
||||
import expo.modules.kotlin.types.toKClass
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
class ImageManipulatorModule : Module() {
|
||||
private val context: Context
|
||||
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
||||
|
||||
private fun createManipulatorContext(url: Uri): ImageManipulatorContext {
|
||||
val loader = suspend {
|
||||
val imageLoader = appContext.imageLoader
|
||||
?: throw ImageLoaderNotFoundException()
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
imageLoader.loadImageForManipulationFromURL(
|
||||
url.toString(),
|
||||
object : ResultListener {
|
||||
override fun onSuccess(bitmap: Bitmap) {
|
||||
continuation.resume(bitmap)
|
||||
}
|
||||
|
||||
override fun onFailure(cause: Throwable?) {
|
||||
continuation.resumeWithException(ImageLoadingFailedException(url.toString(), cause.toCodedException()))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val task = ManipulatorTask(appContext.backgroundCoroutineScope, loader)
|
||||
return ImageManipulatorContext(runtimeContext, task)
|
||||
}
|
||||
|
||||
private fun createManipulatorContext(bitmap: Bitmap): ImageManipulatorContext {
|
||||
val task = ManipulatorTask(appContext.backgroundCoroutineScope) { bitmap }
|
||||
return ImageManipulatorContext(runtimeContext, task)
|
||||
}
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoImageManipulator")
|
||||
|
||||
Function("manipulate") { url: EitherOfThree<Uri, SharedRef<Bitmap>, SharedRef<Drawable>> ->
|
||||
return@Function if (url.`is`(Uri::class)) {
|
||||
createManipulatorContext(url.get(Uri::class))
|
||||
} else if (url.`is`(toKClass<SharedRef<Bitmap>>())) {
|
||||
val bitmap = url.get(toKClass<SharedRef<Bitmap>>()).ref
|
||||
createManipulatorContext(bitmap)
|
||||
} else {
|
||||
val drawable = url.get(toKClass<SharedRef<Drawable>>()).ref
|
||||
val bitmap = (drawable as? BitmapDrawable)?.bitmap
|
||||
?: throw Exceptions.IllegalArgument("The drawable cannot be converted to a bitmap")
|
||||
createManipulatorContext(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
Class<ImageManipulatorContext>("Context") {
|
||||
Constructor { url: Uri ->
|
||||
createManipulatorContext(url)
|
||||
}
|
||||
|
||||
Function("resize") { context: ImageManipulatorContext, options: ResizeOptions ->
|
||||
context.addTransformer(ResizeTransformer(options))
|
||||
}
|
||||
|
||||
Function("rotate") { context: ImageManipulatorContext, rotation: Float ->
|
||||
context.addTransformer(RotateTransformer(rotation))
|
||||
}
|
||||
|
||||
Function("flip") { context: ImageManipulatorContext, flipType: FlipType ->
|
||||
context.addTransformer(FlipTransformer(flipType))
|
||||
}
|
||||
|
||||
Function("crop") { context: ImageManipulatorContext, rect: CropRect ->
|
||||
context.addTransformer(CropTransformer(rect))
|
||||
}
|
||||
|
||||
Function("reset") { context: ImageManipulatorContext ->
|
||||
context.reset()
|
||||
}
|
||||
|
||||
AsyncFunction("renderAsync") Coroutine { context: ImageManipulatorContext ->
|
||||
val image = context.render()
|
||||
ImageRef(image, runtimeContext)
|
||||
}
|
||||
}
|
||||
|
||||
Class<ImageRef>("Image") {
|
||||
Property("width") { image: ImageRef -> image.ref.width }
|
||||
Property("height") { image: ImageRef -> image.ref.height }
|
||||
|
||||
AsyncFunction("saveAsync") Coroutine { image: ImageRef, options: ManipulateOptions? ->
|
||||
val options = options ?: ManipulateOptions()
|
||||
val path = FileUtils.generateRandomOutputPath(context, options.format)
|
||||
val compression = (options.compress * 100).toInt()
|
||||
val resultBitmap = image.ref
|
||||
|
||||
var base64String: String? = null
|
||||
appContext.backgroundCoroutineScope.async {
|
||||
FileOutputStream(path).use { fileOut ->
|
||||
val compressFormat = options.format.compressFormat
|
||||
resultBitmap.compress(compressFormat, compression, fileOut)
|
||||
if (options.base64) {
|
||||
ByteArrayOutputStream().use { byteOut ->
|
||||
resultBitmap.compress(compressFormat, compression, byteOut)
|
||||
base64String = Base64.encodeToString(byteOut.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
|
||||
mapOf(
|
||||
"uri" to Uri.fromFile(File(path)).toString(),
|
||||
"width" to resultBitmap.width,
|
||||
"height" to resultBitmap.height,
|
||||
"base64" to base64String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
package expo.modules.imagemanipulator
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import expo.modules.kotlin.RuntimeContext
|
||||
import expo.modules.kotlin.sharedobjects.SharedRef
|
||||
|
||||
class ImageRef(bitmap: Bitmap, runtimeContext: RuntimeContext) : SharedRef<Bitmap>(bitmap, runtimeContext) {
|
||||
override val nativeRefType: String = "image"
|
||||
|
||||
override fun getAdditionalMemoryPressure(): Int {
|
||||
return ref.allocationByteCount
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
package expo.modules.imagemanipulator.transformers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import expo.modules.imagemanipulator.CropRect
|
||||
import expo.modules.imagemanipulator.ImageInvalidCropException
|
||||
|
||||
class CropTransformer(
|
||||
private val rect: CropRect
|
||||
) : ImageTransformer {
|
||||
override fun transform(bitmap: Bitmap): Bitmap {
|
||||
val isInBounds = rect.originX <= bitmap.width &&
|
||||
rect.originY <= bitmap.height &&
|
||||
rect.width <= bitmap.width &&
|
||||
rect.height <= bitmap.height
|
||||
if (!isInBounds) {
|
||||
throw ImageInvalidCropException()
|
||||
}
|
||||
|
||||
return Bitmap.createBitmap(bitmap, rect.originX.toInt(), rect.originY.toInt(), rect.width.toInt(), rect.height.toInt())
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
package expo.modules.imagemanipulator.transformers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import expo.modules.imagemanipulator.FlipType
|
||||
|
||||
class FlipTransformer(
|
||||
private val flipType: FlipType
|
||||
) : ImageTransformer {
|
||||
override fun transform(bitmap: Bitmap): Bitmap {
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, rotationMatrix, true)
|
||||
}
|
||||
|
||||
private val rotationMatrix: Matrix
|
||||
get() = Matrix().apply {
|
||||
when (flipType) {
|
||||
FlipType.VERTICAL -> postScale(1f, -1f)
|
||||
FlipType.HORIZONTAL -> postScale(-1f, 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
package expo.modules.imagemanipulator.transformers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
@FunctionalInterface
|
||||
interface ImageTransformer {
|
||||
fun transform(bitmap: Bitmap): Bitmap
|
||||
}
|
||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
package expo.modules.imagemanipulator.transformers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import expo.modules.imagemanipulator.ResizeOptions
|
||||
|
||||
class ResizeTransformer(
|
||||
private val resizeOptions: ResizeOptions
|
||||
) : ImageTransformer {
|
||||
override fun transform(bitmap: Bitmap): Bitmap {
|
||||
var targetWidth = 0
|
||||
var targetHeight = 0
|
||||
|
||||
val imageRatio = bitmap.width.toDouble() / bitmap.height.toDouble()
|
||||
|
||||
if (resizeOptions.width != null) {
|
||||
targetWidth = resizeOptions.width
|
||||
targetHeight = (resizeOptions.width / imageRatio).toInt()
|
||||
}
|
||||
|
||||
if (resizeOptions.height != null) {
|
||||
targetHeight = resizeOptions.height
|
||||
targetWidth = if (targetWidth == 0) (resizeOptions.height * imageRatio).toInt() else targetWidth
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
package expo.modules.imagemanipulator.transformers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
|
||||
class RotateTransformer(private val rotation: Float) : ImageTransformer {
|
||||
override fun transform(bitmap: Bitmap): Bitmap {
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, rotationMatrix, true)
|
||||
}
|
||||
|
||||
private val rotationMatrix: Matrix
|
||||
get() = Matrix().apply { postRotate(rotation) }
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"platforms": ["apple", "android"],
|
||||
"apple": {
|
||||
"modules": ["ImageManipulatorModule"]
|
||||
},
|
||||
"android": {
|
||||
"modules": ["expo.modules.imagemanipulator.ImageManipulatorModule"],
|
||||
"publication": {
|
||||
"groupId": "host.exp.exponent",
|
||||
"artifactId": "expo.modules.imagemanipulator",
|
||||
"version": "14.0.8",
|
||||
"repository": "local-maven-repo"
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ExpoImageManipulator'
|
||||
s.version = package['version']
|
||||
s.summary = package['description']
|
||||
s.description = package['description']
|
||||
s.license = package['license']
|
||||
s.author = package['author']
|
||||
s.homepage = package['homepage']
|
||||
s.platforms = {
|
||||
:ios => '15.1',
|
||||
:tvos => '15.1'
|
||||
}
|
||||
s.swift_version = '5.9'
|
||||
s.source = { git: 'https://github.com/expo/expo.git' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.dependency 'EXImageLoader'
|
||||
s.dependency 'SDWebImageWebPCoder'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
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')
|
||||
s.source_files = "**/*.h"
|
||||
s.vendored_frameworks = "#{s.name}.xcframework"
|
||||
else
|
||||
s.source_files = "**/*.{h,m,swift}"
|
||||
end
|
||||
end
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Copyright 2021-present 650 Industries. All rights reserved.
|
||||
|
||||
import CoreGraphics
|
||||
import ExpoModulesCore
|
||||
|
||||
/**
|
||||
Options provided for resize action.
|
||||
*/
|
||||
internal struct ResizeOptions: Record {
|
||||
@Field
|
||||
var width: CGFloat?
|
||||
|
||||
@Field
|
||||
var height: CGFloat?
|
||||
}
|
||||
|
||||
/**
|
||||
Cropping rect for crop action.
|
||||
*/
|
||||
internal struct CropRect: Record {
|
||||
@Field
|
||||
var originX: Double = 0.0
|
||||
|
||||
@Field
|
||||
var originY: Double = 0.0
|
||||
|
||||
@Field
|
||||
var width: Double = 0.0
|
||||
|
||||
@Field
|
||||
var height: Double = 0.0
|
||||
|
||||
func toRect() -> CGRect {
|
||||
return CGRect(x: originX, y: originY, width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Options to use when saving the resulted image.
|
||||
*/
|
||||
internal struct ManipulateOptions: Record {
|
||||
@Field
|
||||
var base64: Bool = false
|
||||
|
||||
@Field
|
||||
var compress: Double = 1.0
|
||||
|
||||
@Field
|
||||
var format: ImageFormat = .jpeg
|
||||
}
|
||||
|
||||
/**
|
||||
Possible options for flip action.
|
||||
*/
|
||||
internal enum FlipType: String, Enumerable {
|
||||
case vertical
|
||||
case horizontal
|
||||
}
|
||||
|
||||
/**
|
||||
Enum with supported image formats.
|
||||
*/
|
||||
internal enum ImageFormat: String, Enumerable {
|
||||
case jpeg
|
||||
case jpg
|
||||
case png
|
||||
case webp
|
||||
|
||||
var fileExtension: String {
|
||||
switch self {
|
||||
case .jpeg, .jpg:
|
||||
return ".jpg"
|
||||
case .png:
|
||||
return ".png"
|
||||
case .webp:
|
||||
return ".webp"
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
import ExpoModulesCore
|
||||
|
||||
/**
|
||||
A context of the image manipulation.
|
||||
*/
|
||||
public final class ImageManipulatorContext: SharedObject {
|
||||
internal typealias Loader = () async throws -> UIImage
|
||||
|
||||
/**
|
||||
The last task added to the rendering pipeline.
|
||||
*/
|
||||
private var currentTask: Task<UIImage, Error>
|
||||
|
||||
/**
|
||||
A function that was used to load the original image.
|
||||
Can be used to reset context's state to the original image.
|
||||
*/
|
||||
private let loader: Loader
|
||||
|
||||
/**
|
||||
Initializes a manipulation context with the given loader that returns the original image.
|
||||
*/
|
||||
init(loader: @escaping Loader) {
|
||||
self.loader = loader
|
||||
currentTask = Task(priority: .background) {
|
||||
return try await loader()
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
Adds an image transformer to run on the rendering context in the background.
|
||||
*/
|
||||
@discardableResult
|
||||
internal func addTransformer(_ transformer: ImageTransformer) -> Self {
|
||||
currentTask = Task(priority: .background) { [currentTask] in
|
||||
// The task can be canceled in the meantime (e.g. by resetting to the original image).
|
||||
// In this case there is no reason to transform the image any further.
|
||||
try Task.checkCancellation()
|
||||
|
||||
let image = try await currentTask.value
|
||||
return try await transformer.transform(image: image)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Awaits for the last processing task to finish and returns its result.
|
||||
*/
|
||||
internal func render() async throws -> UIImage {
|
||||
return try await currentTask.value
|
||||
}
|
||||
|
||||
/**
|
||||
Resets the manipulator context to the originally loaded image.
|
||||
*/
|
||||
internal func reset() {
|
||||
// Firstly cancel currently running manipulations.
|
||||
currentTask.cancel()
|
||||
|
||||
currentTask = Task(priority: .background) {
|
||||
return try await loader()
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Copyright 2021-present 650 Industries. All rights reserved.
|
||||
|
||||
import CoreGraphics
|
||||
import ExpoModulesCore
|
||||
|
||||
internal final class ImageContextLostException: Exception {
|
||||
override var reason: String {
|
||||
"Image context has been lost"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageDrawingFailedException: Exception {
|
||||
override var reason: String {
|
||||
"Drawing the new image failed"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageNotFoundException: Exception {
|
||||
override var reason: String {
|
||||
"Image cannot be found"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageColorSpaceNotFoundException: Exception {
|
||||
override var reason: String {
|
||||
"The image does not specify any color space"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageInvalidCropException: Exception {
|
||||
override var reason: String {
|
||||
"Invalid crop options has been passed. Please make sure the requested crop rectangle is inside source image"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageCropFailedException: GenericException<CGRect> {
|
||||
override var reason: String {
|
||||
"Cropping the image to rectangle (x: \(param.origin.x), y: \(param.origin.y), width: \(param.width), height: \(param.height)) has failed"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class NoImageInContextException: Exception {
|
||||
override var reason: String {
|
||||
"Could not read the image from the drawing context"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageLoaderNotFoundException: Exception {
|
||||
override var reason: String {
|
||||
"ImageLoader module not found, make sure 'expo-image-loader' is linked correctly"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class FileSystemNotFoundException: Exception {
|
||||
override var reason: String {
|
||||
"FileSystem module not found, make sure 'expo-file-system' is linked correctly"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class FileSystemReadPermissionException: GenericException<String> {
|
||||
override var reason: String {
|
||||
"File '\(param)' is not readable"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageLoadingFailedException: GenericException<String> {
|
||||
override var reason: String {
|
||||
"Could not load the image: \(param)"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class CorruptedImageDataException: Exception {
|
||||
override var reason: String {
|
||||
"Cannot create image data for given image format"
|
||||
}
|
||||
}
|
||||
|
||||
internal final class ImageWriteFailedException: GenericException<String> {
|
||||
override var reason: String {
|
||||
"Writing image data to the file has failed: \(param)"
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright 2021-present 650 Industries. All rights reserved.
|
||||
|
||||
import CoreGraphics
|
||||
import Photos
|
||||
import UIKit
|
||||
import ExpoModulesCore
|
||||
import SDWebImageWebPCoder
|
||||
|
||||
public class ImageManipulatorModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoImageManipulator")
|
||||
|
||||
Function("manipulate") { (source: Either<URL, SharedRef<UIImage>>) -> ImageManipulatorContext in
|
||||
let context = ImageManipulatorContext { [weak appContext] in
|
||||
guard let appContext else {
|
||||
throw Exceptions.AppContextLost()
|
||||
}
|
||||
if let url: URL = source.get() {
|
||||
return try await loadImage(atUrl: url, appContext: appContext)
|
||||
}
|
||||
if let image: SharedRef<UIImage> = source.get() {
|
||||
return image.ref
|
||||
}
|
||||
throw Exceptions.RuntimeLost()
|
||||
}
|
||||
|
||||
// Immediately try to fix the orientation once the image is loaded
|
||||
context.addTransformer(ImageFixOrientationTransformer())
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
Class("Context", ImageManipulatorContext.self) {
|
||||
Function("resize") { (context: ImageManipulatorContext, options: ResizeOptions) -> ImageManipulatorContext in
|
||||
return context.addTransformer(ImageResizeTransformer(options: options))
|
||||
}
|
||||
|
||||
Function("rotate") { (context: ImageManipulatorContext, rotate: Double) -> ImageManipulatorContext in
|
||||
return context.addTransformer(ImageRotateTransformer(rotate: rotate))
|
||||
}
|
||||
|
||||
Function("flip") { (context: ImageManipulatorContext, flipType: FlipType) -> ImageManipulatorContext in
|
||||
return context.addTransformer(ImageFlipTransformer(flip: flipType))
|
||||
}
|
||||
|
||||
Function("crop") { (context: ImageManipulatorContext, rect: CropRect) -> ImageManipulatorContext in
|
||||
return context.addTransformer(ImageCropTransformer(options: rect))
|
||||
}
|
||||
|
||||
Function("reset") { (context: ImageManipulatorContext) -> ImageManipulatorContext in
|
||||
context.reset()
|
||||
return context
|
||||
}
|
||||
|
||||
AsyncFunction("renderAsync") { (context: ImageManipulatorContext) -> ImageRef in
|
||||
let image = try await context.render()
|
||||
return ImageRef(image)
|
||||
}
|
||||
}
|
||||
|
||||
Class("Image", ImageRef.self) {
|
||||
Property("width") { (image: ImageRef) -> Int in
|
||||
return image.ref.cgImage?.width ?? 0
|
||||
}
|
||||
|
||||
Property("height") { (image: ImageRef) -> Int in
|
||||
return image.ref.cgImage?.height ?? 0
|
||||
}
|
||||
|
||||
AsyncFunction("saveAsync") { (image: ImageRef, options: ManipulateOptions?) -> [String: Any?] in
|
||||
guard let appContext else {
|
||||
throw Exceptions.AppContextLost()
|
||||
}
|
||||
let options = options ?? ManipulateOptions()
|
||||
let result = try saveImage(image.ref, options: options, appContext: appContext)
|
||||
|
||||
// We're returning a dict instead of a path directly because in the future we'll replace it
|
||||
// with a shared ref to the file once this feature gets implemented in expo-file-system.
|
||||
// This should be fully backwards-compatible switch.
|
||||
return [
|
||||
"uri": result.url.absoluteString,
|
||||
"width": image.ref.cgImage?.width ?? 0,
|
||||
"height": image.ref.cgImage?.height ?? 0,
|
||||
"base64": options.base64 ? result.data.base64EncodedString() : nil
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import SDWebImageWebPCoder
|
||||
import Photos
|
||||
import ExpoModulesCore
|
||||
|
||||
internal typealias SaveImageResult = (url: URL, data: Data)
|
||||
|
||||
/**
|
||||
Loads the image from given URL.
|
||||
*/
|
||||
internal func loadImage(atUrl url: URL, appContext: AppContext) async throws -> UIImage {
|
||||
if url.scheme == "data" {
|
||||
guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
|
||||
throw CorruptedImageDataException()
|
||||
}
|
||||
return image
|
||||
}
|
||||
if url.scheme == "ph" || url.scheme == "assets-library" {
|
||||
return try await loadImageFromPhotoLibrary(url: url)
|
||||
}
|
||||
|
||||
guard let imageLoader = appContext.imageLoader else {
|
||||
throw ImageLoaderNotFoundException()
|
||||
}
|
||||
guard FileSystemUtilities.permissions(appContext, for: url).contains(.read) && FileManager.default.isReadableFile(atPath: url.path) else {
|
||||
throw FileSystemReadPermissionException(url.absoluteString)
|
||||
}
|
||||
|
||||
do {
|
||||
if let result = try await imageLoader.loadImage(for: url) {
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
throw ImageLoadingFailedException((error as NSError).debugDescription)
|
||||
}
|
||||
// TODO: throw something better
|
||||
throw ImageLoadingFailedException("")
|
||||
}
|
||||
|
||||
/**
|
||||
Loads the image from user's photo library.
|
||||
*/
|
||||
internal func loadImageFromPhotoLibrary(url: URL) async throws -> UIImage {
|
||||
guard let asset = retrieveAsset(from: url) else {
|
||||
throw ImageNotFoundException()
|
||||
}
|
||||
let size = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
|
||||
let options = PHImageRequestOptions()
|
||||
|
||||
options.resizeMode = .exact
|
||||
options.isNetworkAccessAllowed = true
|
||||
options.isSynchronous = true
|
||||
options.deliveryMode = .highQualityFormat
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: options) { image, _ in
|
||||
if let image {
|
||||
continuation.resume(returning: image)
|
||||
} else {
|
||||
continuation.resume(throwing: ImageNotFoundException())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns pixel data representation of the image.
|
||||
*/
|
||||
func imageData(from image: UIImage, format: ImageFormat, compression: Double) -> Data? {
|
||||
switch format {
|
||||
case .jpeg, .jpg:
|
||||
return image.jpegData(compressionQuality: compression)
|
||||
case .png:
|
||||
return image.pngData()
|
||||
case .webp:
|
||||
return SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: [.encodeCompressionQuality: compression])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if we are dealing with a ph asset URL and uses the correct method to fetch it.
|
||||
*/
|
||||
func retrieveAsset(from url: URL) -> PHAsset? {
|
||||
if url.scheme == "ph" {
|
||||
let identifier = String(url.absoluteString.dropFirst(5)) // removes ph://
|
||||
return PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject
|
||||
}
|
||||
return PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject
|
||||
}
|
||||
|
||||
/**
|
||||
Helper function for drawing the image in graphics context.
|
||||
Throws appropriate exceptions when the context is missing or the image couldn't be rendered.
|
||||
*/
|
||||
internal func drawInNewContext(size: CGSize, drawing: (UIGraphicsImageRendererContext) -> Void) -> UIImage {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = 1
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: size, format: format)
|
||||
|
||||
return renderer.image { context in
|
||||
drawing(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Saves the image as a file.
|
||||
*/
|
||||
internal func saveImage(_ image: UIImage, options: ManipulateOptions, appContext: AppContext) throws -> SaveImageResult {
|
||||
guard let cachesDirectory = appContext.config.cacheDirectory else {
|
||||
throw FileSystemNotFoundException()
|
||||
}
|
||||
|
||||
let directory = URL(fileURLWithPath: cachesDirectory.path).appendingPathComponent("ImageManipulator")
|
||||
let filename = UUID().uuidString.appending(options.format.fileExtension)
|
||||
let fileUrl = directory.appendingPathComponent(filename)
|
||||
|
||||
FileSystemUtilities.ensureDirExists(at: directory)
|
||||
|
||||
guard let data = imageData(from: image, format: options.format, compression: options.compress) else {
|
||||
throw CorruptedImageDataException()
|
||||
}
|
||||
do {
|
||||
try data.write(to: fileUrl, options: .atomic)
|
||||
} catch let error {
|
||||
throw ImageWriteFailedException(error.localizedDescription)
|
||||
}
|
||||
return (url: fileUrl, data: data)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
import ExpoModulesCore
|
||||
|
||||
/**
|
||||
Represents a shared reference to the `UIImage` instance.
|
||||
*/
|
||||
internal final class ImageRef: SharedRef<UIImage> {
|
||||
override var nativeRefType: String {
|
||||
"image"
|
||||
}
|
||||
|
||||
override func getAdditionalMemoryPressure() -> Int {
|
||||
guard let cgImage = ref.cgImage else {
|
||||
return 0
|
||||
}
|
||||
return cgImage.bytesPerRow * cgImage.height
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
internal protocol ImageTransformer {
|
||||
func transform(image: UIImage) async throws -> UIImage
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
/**
|
||||
Transformer that creates a new image by cropping given image to specified width and height.
|
||||
*/
|
||||
internal struct ImageCropTransformer: ImageTransformer {
|
||||
let options: CropRect
|
||||
|
||||
func transform(image: UIImage) async throws -> UIImage {
|
||||
let rect = options.toRect()
|
||||
let isOutOfBounds = rect.origin.x > image.size.width
|
||||
|| rect.origin.y > image.size.height
|
||||
|| rect.width > image.size.width
|
||||
|| rect.height > image.size.height
|
||||
|
||||
guard !isOutOfBounds else {
|
||||
throw ImageInvalidCropException()
|
||||
}
|
||||
guard let cgImage = image.cgImage?.cropping(to: rect) else {
|
||||
throw ImageCropFailedException(rect)
|
||||
}
|
||||
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
/**
|
||||
Transformer that makes sure the image is oriented up and not mirrored.
|
||||
Guarantees that the original pixel data matches the displayed orientation.
|
||||
*/
|
||||
internal struct ImageFixOrientationTransformer: ImageTransformer {
|
||||
func transform(image: UIImage) async throws -> UIImage {
|
||||
guard let cgImage = image.cgImage else {
|
||||
throw ImageNotFoundException()
|
||||
}
|
||||
guard var colorSpace = cgImage.colorSpace else {
|
||||
// That should never happen as `colorSpace` is empty only when the image is a mask.
|
||||
throw ImageColorSpaceNotFoundException()
|
||||
}
|
||||
if !colorSpace.supportsOutput {
|
||||
colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
}
|
||||
|
||||
var transform = CGAffineTransform.identity
|
||||
|
||||
switch image.imageOrientation {
|
||||
case .down, .downMirrored:
|
||||
transform = transform.translatedBy(x: image.size.width, y: image.size.height)
|
||||
transform = transform.rotated(by: Double.pi)
|
||||
case .left, .leftMirrored:
|
||||
transform = transform.translatedBy(x: image.size.width, y: 0)
|
||||
transform = transform.rotated(by: Double.pi / 2)
|
||||
case .right, .rightMirrored:
|
||||
transform = transform.translatedBy(x: 0, y: image.size.height)
|
||||
transform = transform.rotated(by: -Double.pi / 2)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch image.imageOrientation {
|
||||
case .upMirrored, .downMirrored:
|
||||
transform = transform.translatedBy(x: image.size.width, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
case .leftMirrored, .rightMirrored:
|
||||
transform = transform.translatedBy(x: image.size.height, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: Int(image.size.width),
|
||||
height: Int(image.size.height),
|
||||
bitsPerComponent: cgImage.bitsPerComponent,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
)
|
||||
|
||||
guard let context = context else {
|
||||
throw ImageContextLostException()
|
||||
}
|
||||
|
||||
context.concatenate(transform)
|
||||
|
||||
switch image.imageOrientation {
|
||||
case .left, .leftMirrored, .right, .rightMirrored:
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width))
|
||||
default:
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||
}
|
||||
|
||||
guard let newCGImage = context.makeImage() else {
|
||||
throw ImageDrawingFailedException()
|
||||
}
|
||||
return UIImage(cgImage: newCGImage)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
/**
|
||||
Transformer that creates a new image by flipping given image vertically or horizontally.
|
||||
*/
|
||||
internal struct ImageFlipTransformer: ImageTransformer {
|
||||
let flip: FlipType
|
||||
|
||||
@MainActor
|
||||
func transform(image: UIImage) async -> UIImage {
|
||||
let imageView = UIImageView(image: image)
|
||||
|
||||
return drawInNewContext(size: imageView.frame.size) { context in
|
||||
switch flip {
|
||||
case .vertical:
|
||||
let transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: imageView.frame.size.height)
|
||||
context.cgContext.concatenate(transform)
|
||||
case .horizontal:
|
||||
let transform = CGAffineTransform(a: -1, b: 0, c: 0, d: 1, tx: imageView.frame.size.width, ty: 0)
|
||||
context.cgContext.concatenate(transform)
|
||||
}
|
||||
imageView.layer.render(in: context.cgContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
/**
|
||||
Transformer that draws a new image by resizing given image to specified size.
|
||||
*/
|
||||
internal struct ImageResizeTransformer: ImageTransformer {
|
||||
let options: ResizeOptions
|
||||
|
||||
func transform(image: UIImage) async -> UIImage {
|
||||
let imageWidth = image.size.width
|
||||
let imageHeight = image.size.height
|
||||
let imageRatio = imageWidth / imageHeight
|
||||
|
||||
var targetSize = CGSize.zero
|
||||
|
||||
if let width = options.width {
|
||||
targetSize.width = width
|
||||
targetSize.height = width / imageRatio
|
||||
}
|
||||
if let height = options.height {
|
||||
targetSize.height = height
|
||||
targetSize.width = targetSize.width == 0 ? imageRatio * targetSize.height : targetSize.width
|
||||
}
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.opaque = false
|
||||
format.scale = 1
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright 2024-present 650 Industries. All rights reserved.
|
||||
|
||||
/**
|
||||
Transformer that creates a new image by rotating given image by the rotate angle.
|
||||
*/
|
||||
internal struct ImageRotateTransformer: ImageTransformer {
|
||||
let rotate: Double
|
||||
|
||||
@MainActor
|
||||
func transform(image: UIImage) async throws -> UIImage {
|
||||
guard let cgImage = image.cgImage else {
|
||||
throw ImageNotFoundException()
|
||||
}
|
||||
let rads = rotate * .pi / 180
|
||||
let rotatedView = UIView(frame: CGRect(origin: .zero, size: image.size))
|
||||
|
||||
rotatedView.transform = CGAffineTransform(rotationAngle: rads)
|
||||
|
||||
let rotatedSize = CGSize(width: rotatedView.frame.size.width.rounded(.down), height: rotatedView.frame.size.height.rounded(.down))
|
||||
let origin = CGPoint(x: -image.size.width / 2, y: -image.size.height / 2)
|
||||
|
||||
return drawInNewContext(size: rotatedSize) { context in
|
||||
let cgContext = context.cgContext
|
||||
cgContext.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
|
||||
cgContext.rotate(by: rads)
|
||||
cgContext.scaleBy(x: 1.0, y: -1.0)
|
||||
cgContext.draw(cgImage, in: CGRect(origin: origin, size: image.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
71622281255feb4dcaf797a74964698b
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
b9b08b0aca5c25507ec419236d8895a87df02585
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
83452b235a0ea4b9ed9dc693de4fdda6a7940c15c27b9033fb0e398e00b56a7b
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
08af6c95ebf4df88f09274f2dc172df230d57b8f3ca2d1774d44584282b25d9d93ba57a78a2cdfef9232145c67aa8765b8df86f6c79852ba8c1b6b3eb3c21350
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
3b556fa1d971cb956ac33c76aa2dd3f6
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
12f15ef6d33811a5cfe744a41afaa9b07fb90707
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
d17bd551626089157b2c6c4bf741ea463e2b1e592804f26495e774fd90a16b33
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
d8d435b830a1dbb2d6230a113b4605de27bd1fff1d6fbf37ebd770632f61656bf8f7b0d4f408713df1be79ffe44d183abda936ad8a5cf7580cc16078c04276c8
|
||||
Generated
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"formatVersion": "1.1",
|
||||
"component": {
|
||||
"group": "host.exp.exponent",
|
||||
"module": "expo.modules.imagemanipulator",
|
||||
"version": "14.0.8",
|
||||
"attributes": {
|
||||
"org.gradle.status": "release"
|
||||
}
|
||||
},
|
||||
"createdBy": {
|
||||
"gradle": {
|
||||
"version": "8.14.3"
|
||||
}
|
||||
},
|
||||
"variants": [
|
||||
{
|
||||
"name": "releaseVariantReleaseApiPublication",
|
||||
"attributes": {
|
||||
"org.gradle.category": "library",
|
||||
"org.gradle.dependency.bundling": "external",
|
||||
"org.gradle.libraryelements": "aar",
|
||||
"org.gradle.usage": "java-api"
|
||||
},
|
||||
"dependencies": [
|
||||
{
|
||||
"group": "androidx.annotation",
|
||||
"module": "annotation",
|
||||
"version": {
|
||||
"requires": "1.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"name": "expo.modules.imagemanipulator-14.0.8.aar",
|
||||
"url": "expo.modules.imagemanipulator-14.0.8.aar",
|
||||
"size": 74861,
|
||||
"sha512": "d8d435b830a1dbb2d6230a113b4605de27bd1fff1d6fbf37ebd770632f61656bf8f7b0d4f408713df1be79ffe44d183abda936ad8a5cf7580cc16078c04276c8",
|
||||
"sha256": "d17bd551626089157b2c6c4bf741ea463e2b1e592804f26495e774fd90a16b33",
|
||||
"sha1": "12f15ef6d33811a5cfe744a41afaa9b07fb90707",
|
||||
"md5": "3b556fa1d971cb956ac33c76aa2dd3f6"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "releaseVariantReleaseRuntimePublication",
|
||||
"attributes": {
|
||||
"org.gradle.category": "library",
|
||||
"org.gradle.dependency.bundling": "external",
|
||||
"org.gradle.libraryelements": "aar",
|
||||
"org.gradle.usage": "java-runtime"
|
||||
},
|
||||
"dependencies": [
|
||||
{
|
||||
"group": "org.jetbrains.kotlin",
|
||||
"module": "kotlin-stdlib-jdk7",
|
||||
"version": {
|
||||
"requires": "2.1.20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "androidx.annotation",
|
||||
"module": "annotation",
|
||||
"version": {
|
||||
"requires": "1.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"name": "expo.modules.imagemanipulator-14.0.8.aar",
|
||||
"url": "expo.modules.imagemanipulator-14.0.8.aar",
|
||||
"size": 74861,
|
||||
"sha512": "d8d435b830a1dbb2d6230a113b4605de27bd1fff1d6fbf37ebd770632f61656bf8f7b0d4f408713df1be79ffe44d183abda936ad8a5cf7580cc16078c04276c8",
|
||||
"sha256": "d17bd551626089157b2c6c4bf741ea463e2b1e592804f26495e774fd90a16b33",
|
||||
"sha1": "12f15ef6d33811a5cfe744a41afaa9b07fb90707",
|
||||
"md5": "3b556fa1d971cb956ac33c76aa2dd3f6"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "releaseVariantReleaseSourcePublication",
|
||||
"attributes": {
|
||||
"org.gradle.category": "documentation",
|
||||
"org.gradle.dependency.bundling": "external",
|
||||
"org.gradle.docstype": "sources",
|
||||
"org.gradle.usage": "java-runtime"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"name": "expo.modules.imagemanipulator-14.0.8-sources.jar",
|
||||
"url": "expo.modules.imagemanipulator-14.0.8-sources.jar",
|
||||
"size": 8159,
|
||||
"sha512": "08af6c95ebf4df88f09274f2dc172df230d57b8f3ca2d1774d44584282b25d9d93ba57a78a2cdfef9232145c67aa8765b8df86f6c79852ba8c1b6b3eb3c21350",
|
||||
"sha256": "83452b235a0ea4b9ed9dc693de4fdda6a7940c15c27b9033fb0e398e00b56a7b",
|
||||
"sha1": "b9b08b0aca5c25507ec419236d8895a87df02585",
|
||||
"md5": "71622281255feb4dcaf797a74964698b"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
ee29bc804b44da82fafb788ffb704332
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
257f693917ba997ae8ae82c5328c9cf84a7e83fc
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
13191f571bb064f7784e51d57461164c5139cb1dc9a104050d7357faff5ac52f
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
f80e71e63352cb0e927f01de38275cc7f2deae494fe20001cde2c9280366381d84fd2fbb25a268afbb1d0a49ef904ce71f85ac74f3a7ff791580f6524d63e097
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<!-- This module was also published with a richer model, Gradle metadata, -->
|
||||
<!-- which should be used instead. Do not delete the following line which -->
|
||||
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
|
||||
<!-- that they should prefer consuming it instead. -->
|
||||
<!-- do_not_remove: published-with-gradle-metadata -->
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>host.exp.exponent</groupId>
|
||||
<artifactId>expo.modules.imagemanipulator</artifactId>
|
||||
<version>14.0.8</version>
|
||||
<packaging>aar</packaging>
|
||||
<name>expo.modules.imagemanipulator</name>
|
||||
<url>https://github.com/expo/expo</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://github.com/expo/expo/blob/main/LICENSE</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<scm>
|
||||
<connection>https://github.com/expo/expo.git</connection>
|
||||
<developerConnection>https://github.com/expo/expo.git</developerConnection>
|
||||
<url>https://github.com/expo/expo</url>
|
||||
</scm>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>androidx.annotation</groupId>
|
||||
<artifactId>annotation</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk7</artifactId>
|
||||
<version>2.1.20</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
6255de747cd8d99e72b0a0628cc18b78
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
705f2a794cb817d58e5a1ffbec1db8889c4e4eb6
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
5e9b402133fe28bd9c9825a027628b163fea1dbcf86024e383177ac8b5698a09
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
85c72a628753a6b7e101ba9cabcc576579b44cb2ffa48dca4315b45c2c62c808d4ebdc6bff7aaecb0585ed7790b108ea2868e9deb22bcc31bbbde298062c09a3
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>host.exp.exponent</groupId>
|
||||
<artifactId>expo.modules.imagemanipulator</artifactId>
|
||||
<versioning>
|
||||
<latest>14.0.8</latest>
|
||||
<release>14.0.8</release>
|
||||
<versions>
|
||||
<version>14.0.8</version>
|
||||
</versions>
|
||||
<lastUpdated>20251205064553</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
92a5bfa319c4cadf0d23323dae58b89b
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
135c796c9b4ce76499159d56653cae1e0b534caa
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
c2d51d0f4dea25c64d26a365243a762deeeed576b0192ce03e173ed9f224dd63
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
b35852c351368ba1e90c3ace2a9a6e2d5453b56cdaad23406d75b63639c120e0d07a7852ede983a147401f900df8da0efffe80caa868c1221acd3a21c9673d2f
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "expo-image-manipulator",
|
||||
"version": "14.0.8",
|
||||
"description": "Provides functions that let you manipulation images on the local file system, eg: resize, crop.",
|
||||
"main": "src/index.ts",
|
||||
"types": "build/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "expo-module build",
|
||||
"clean": "expo-module clean",
|
||||
"lint": "expo-module lint",
|
||||
"test": "expo-module test",
|
||||
"prepare": "expo-module prepare",
|
||||
"prepublishOnly": "expo-module prepublishOnly",
|
||||
"expo-module": "expo-module"
|
||||
},
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"expo",
|
||||
"expo-image-manipulator"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/expo/expo.git",
|
||||
"directory": "packages/expo-image-manipulator"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/expo/expo/issues"
|
||||
},
|
||||
"author": "650 Industries, Inc.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://docs.expo.dev/versions/latest/sdk/imagemanipulator/",
|
||||
"dependencies": {
|
||||
"expo-image-loader": "~6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expo-module-scripts": "^5.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
},
|
||||
"gitHead": "172a69f5f70c1d0e043e1532f924de97210cabc3"
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useReleasingSharedObject } from 'expo-modules-core';
|
||||
import { SharedRef } from 'expo-modules-core/types';
|
||||
|
||||
import { Action, ImageResult, SaveFormat, SaveOptions } from './ImageManipulator.types';
|
||||
import { ImageManipulatorContext } from './ImageManipulatorContext';
|
||||
import ExpoImageManipulator from './NativeImageManipulatorModule';
|
||||
import { validateArguments } from './validators';
|
||||
|
||||
// @needsAudit
|
||||
/**
|
||||
* Manipulate the image provided via `uri`. Available modifications are rotating, flipping (mirroring),
|
||||
* resizing and cropping. Each invocation results in a new file. With one invocation you can provide
|
||||
* a set of actions to perform over the image. Overwriting the source file would not have an effect
|
||||
* in displaying the result as images are cached.
|
||||
* @param uri URI of the file to manipulate. Should be on the local file system or a base64 data URI.
|
||||
* @param actions An array of objects representing manipulation options. Each object should have
|
||||
* __only one__ of the keys that corresponds to specific transformation.
|
||||
* @param saveOptions A map defining how modified image should be saved.
|
||||
* @return Promise which fulfils with [`ImageResult`](#imageresult) object.
|
||||
* @deprecated It has been replaced by the new, contextual and object-oriented API.
|
||||
* Use [`ImageManipulator.manipulate`](#manipulatesource) or [`useImageManipulator`](#useimagemanipulatorsource) instead.
|
||||
*/
|
||||
export async function manipulateAsync(
|
||||
uri: string,
|
||||
actions: Action[] = [],
|
||||
saveOptions: SaveOptions = {}
|
||||
): Promise<ImageResult> {
|
||||
validateArguments(uri, actions, saveOptions);
|
||||
|
||||
const { format = SaveFormat.JPEG, ...rest } = saveOptions;
|
||||
const context = ExpoImageManipulator.manipulate(uri);
|
||||
|
||||
for (const action of actions) {
|
||||
if ('resize' in action) {
|
||||
context.resize(action.resize);
|
||||
} else if ('rotate' in action) {
|
||||
context.rotate(action.rotate);
|
||||
} else if ('flip' in action) {
|
||||
context.flip(action.flip);
|
||||
} else if ('crop' in action) {
|
||||
context.crop(action.crop);
|
||||
} else if ('extent' in action && context.extent) {
|
||||
context.extent(action.extent);
|
||||
}
|
||||
}
|
||||
const image = await context.renderAsync();
|
||||
const result = await image.saveAsync({ format, ...rest });
|
||||
|
||||
// These shared objects will not be used anymore, so free up some memory.
|
||||
context.release();
|
||||
image.release();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function useImageManipulator(source: string | SharedRef<'image'>): ImageManipulatorContext {
|
||||
return useReleasingSharedObject(() => ExpoImageManipulator.manipulate(source), [source]);
|
||||
}
|
||||
|
||||
export { ExpoImageManipulator as ImageManipulator };
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import type { NativeModule } from 'expo';
|
||||
import { SharedRef } from 'expo-modules-core/types';
|
||||
|
||||
import type { ImageManipulatorContext } from './ImageManipulatorContext';
|
||||
import ImageRef from './ImageRef';
|
||||
|
||||
// @needsAudit
|
||||
export type ImageResult = {
|
||||
/**
|
||||
* An URI to the modified image (usable as the source for an `Image` or `Video` element).
|
||||
*/
|
||||
uri: string;
|
||||
/**
|
||||
* Width of the image or video.
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* Height of the image or video.
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* It is included if the `base64` save option was truthy, and is a string containing the
|
||||
* JPEG/PNG (depending on `format`) data of the image in Base64. Prepend that with `'data:image/xxx;base64,'`
|
||||
* to get a data URI, which you can use as the source for an `Image` element for example
|
||||
* (where `xxx` is `jpeg` or `png`).
|
||||
*/
|
||||
base64?: string;
|
||||
};
|
||||
|
||||
// @needsAudit
|
||||
export type ActionResize = {
|
||||
/**
|
||||
* Values correspond to the result image dimensions. If you specify only one value, the other will
|
||||
* be calculated automatically to preserve image ratio.
|
||||
*/
|
||||
resize: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
};
|
||||
|
||||
// @needsAudit
|
||||
export type ActionRotate = {
|
||||
/**
|
||||
* Degrees to rotate the image. Rotation is clockwise when the value is positive and
|
||||
* counter-clockwise when negative.
|
||||
*/
|
||||
rotate: number;
|
||||
};
|
||||
|
||||
// @docsMissing
|
||||
export enum FlipType {
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
// @needsAudit
|
||||
export type ActionFlip = {
|
||||
/**
|
||||
* An axis on which image will be flipped. Only one flip per transformation is available. If you
|
||||
* want to flip according to both axes then provide two separate transformations.
|
||||
*/
|
||||
flip: FlipType;
|
||||
};
|
||||
|
||||
// @needsAudit
|
||||
export type ActionCrop = {
|
||||
/**
|
||||
* Fields specify top-left corner and dimensions of a crop rectangle.
|
||||
*/
|
||||
crop: {
|
||||
originX: number;
|
||||
originY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
|
||||
// @needsAudit
|
||||
export type ActionExtent = {
|
||||
/**
|
||||
* Set the image size and offset. If the image is enlarged, unfilled areas are set to the `backgroundColor`.
|
||||
* To position the image, use `originX` and `originY`.
|
||||
*
|
||||
* @platform web
|
||||
*/
|
||||
extent: {
|
||||
backgroundColor?: string | null;
|
||||
originX?: number;
|
||||
originY?: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
|
||||
// @docsMissing
|
||||
export type Action = ActionResize | ActionRotate | ActionFlip | ActionCrop | ActionExtent;
|
||||
|
||||
// @docsMissing
|
||||
export enum SaveFormat {
|
||||
JPEG = 'jpeg',
|
||||
PNG = 'png',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
// @needsAudit
|
||||
/**
|
||||
* A map defining how modified image should be saved.
|
||||
*/
|
||||
export type SaveOptions = {
|
||||
/**
|
||||
* Whether to also include the image data in Base64 format.
|
||||
*/
|
||||
base64?: boolean;
|
||||
/**
|
||||
* A value in range `0.0` - `1.0` specifying compression level of the result image. `1` means
|
||||
* no compression (highest quality) and `0` the highest compression (lowest quality).
|
||||
*/
|
||||
compress?: number;
|
||||
/**
|
||||
* Specifies what type of compression should be used and what is the result file extension.
|
||||
* `SaveFormat.PNG` compression is lossless but slower, `SaveFormat.JPEG` is faster but the image
|
||||
* has visible artifacts. Defaults to `SaveFormat.JPEG`
|
||||
*/
|
||||
format?: SaveFormat;
|
||||
};
|
||||
|
||||
export declare class ImageManipulator extends NativeModule {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
Context: typeof ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
Image: typeof ImageRef;
|
||||
|
||||
/**
|
||||
* Loads an image from the given URI and creates a new image manipulation context.
|
||||
*/
|
||||
manipulate(source: string | SharedRef<'image'>): ImageManipulatorContext;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { SharedObject } from 'expo';
|
||||
|
||||
import type { ActionCrop, ActionExtent } from './ImageManipulator.types';
|
||||
import type { ImageRef } from './ImageRef';
|
||||
import ExpoImageManipulator from './NativeImageManipulatorModule';
|
||||
|
||||
/**
|
||||
* A context for an image manipulation. It provides synchronous, chainable functions that schedule transformations on the original image to the background thread.
|
||||
* Use an asynchronous [`renderAsync`](#renderasync) to await for all transformations to finish and access the final image.
|
||||
*/
|
||||
export declare class ImageManipulatorContext extends SharedObject {
|
||||
/**
|
||||
* Resizes the image to the given size.
|
||||
* @param size Values correspond to the result image dimensions. If you specify only one value, the other will
|
||||
* be calculated automatically to preserve image ratio.
|
||||
*/
|
||||
resize(size: { width?: number | null; height?: number | null }): ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* Rotates the image by the given number of degrees.
|
||||
* @param degrees Degrees to rotate the image. Rotation is clockwise when the value is positive and
|
||||
* counter-clockwise when negative.
|
||||
*/
|
||||
rotate(degrees: number): ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* Flips the image vertically or horizontally.
|
||||
* @param flipType An axis on which image will be flipped. Only one flip per transformation is available. If you
|
||||
* want to flip according to both axes then provide two separate transformations.
|
||||
*/
|
||||
flip(flipType: 'vertical' | 'horizontal'): ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* Crops the image to the given rectangle's origin and size.
|
||||
* @param rect Fields specify top-left corner and dimensions of a crop rectangle.
|
||||
*/
|
||||
crop(rect: ActionCrop['crop']): ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* Set the image size and offset. If the image is enlarged, unfilled areas are set to the `backgroundColor`.
|
||||
* To position the image, use `originX` and `originY`.
|
||||
*
|
||||
* @platform web
|
||||
*/
|
||||
extent(options: ActionExtent['extent']): ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* Resets the manipulator context to the originally loaded image.
|
||||
*/
|
||||
reset(): ImageManipulatorContext;
|
||||
|
||||
/**
|
||||
* Awaits for all manipulation tasks to finish and resolves with a reference to the resulted native image.
|
||||
*/
|
||||
renderAsync(): Promise<ImageRef>;
|
||||
}
|
||||
|
||||
export default ExpoImageManipulator.Context as typeof ImageManipulatorContext;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { SharedRef } from 'expo';
|
||||
|
||||
import type { ImageResult, SaveOptions } from './ImageManipulator.types';
|
||||
import ExpoImageManipulator from './NativeImageManipulatorModule';
|
||||
|
||||
/**
|
||||
* A reference to a native instance of the image.
|
||||
*/
|
||||
export declare class ImageRef extends SharedRef<'image'> {
|
||||
/**
|
||||
* Width of the image.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* Height of the image.
|
||||
*/
|
||||
height: number;
|
||||
|
||||
/**
|
||||
* Saves the image to the file system in the cache directory.
|
||||
* @param options A map defining how modified image should be saved.
|
||||
*/
|
||||
saveAsync(options?: SaveOptions): Promise<ImageResult>;
|
||||
}
|
||||
|
||||
export default ExpoImageManipulator.ImageRef as typeof ImageRef;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { requireNativeModule } from 'expo';
|
||||
|
||||
import { ImageManipulator } from './ImageManipulator.types';
|
||||
|
||||
export default requireNativeModule<ImageManipulator>('ExpoImageManipulator');
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { NativeModule } from 'expo';
|
||||
import { registerWebModule } from 'expo-modules-core';
|
||||
import { SharedRef } from 'expo-modules-core/types';
|
||||
|
||||
import ImageManipulatorContext from './web/ImageManipulatorContext.web';
|
||||
import ImageManipulatorImageRef from './web/ImageManipulatorImageRef.web';
|
||||
import { loadImageAsync } from './web/utils.web';
|
||||
|
||||
class ImageManipulator extends NativeModule {
|
||||
Context = ImageManipulatorContext;
|
||||
Image = ImageManipulatorImageRef;
|
||||
|
||||
manipulate(source: string | SharedRef<'image'>): ImageManipulatorContext {
|
||||
return new ImageManipulatorContext(() => {
|
||||
if (typeof source === 'string') {
|
||||
return loadImageAsync(source);
|
||||
}
|
||||
// Image refs should provide the `uri` property on Web. It could be either remote url, blob or data url.
|
||||
if (typeof source === 'object' && 'uri' in source && typeof source.uri === 'string') {
|
||||
return loadImageAsync(source.uri);
|
||||
}
|
||||
throw new Error(`Source not supported: ${source}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default registerWebModule(ImageManipulator, 'ImageManipulator');
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export { ImageManipulator, manipulateAsync, useImageManipulator } from './ImageManipulator';
|
||||
|
||||
// SaveFormat and FlipType are enums
|
||||
export { SaveFormat, FlipType } from './ImageManipulator.types';
|
||||
|
||||
export type { SaveOptions, ImageResult } from './ImageManipulator.types';
|
||||
|
||||
// Export types that are deprecated as of SDK 52
|
||||
export type {
|
||||
ActionResize,
|
||||
ActionRotate,
|
||||
ActionFlip,
|
||||
ActionCrop,
|
||||
ActionExtent,
|
||||
Action,
|
||||
} from './ImageManipulator.types';
|
||||
|
||||
export type { ImageRef } from './ImageRef';
|
||||
export type { ImageManipulatorContext } from './ImageManipulatorContext';
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
Action,
|
||||
ActionCrop,
|
||||
ActionExtent,
|
||||
ActionFlip,
|
||||
ActionResize,
|
||||
ActionRotate,
|
||||
FlipType,
|
||||
SaveFormat,
|
||||
SaveOptions,
|
||||
} from './ImageManipulator.types';
|
||||
|
||||
export function validateArguments(uri: string, actions: Action[], saveOptions: SaveOptions) {
|
||||
validateUri(uri);
|
||||
validateActions(actions);
|
||||
validateSaveOptions(saveOptions);
|
||||
}
|
||||
|
||||
export function validateUri(uri: string): void {
|
||||
if (!(typeof uri === 'string')) {
|
||||
throw new TypeError('The "uri" argument must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
export function validateActions(actions: Action[]): void {
|
||||
if (!Array.isArray(actions)) {
|
||||
throw new TypeError('The "actions" argument must be an array');
|
||||
}
|
||||
for (const action of actions) {
|
||||
if (typeof action !== 'object' || action === null) {
|
||||
throw new TypeError('Action must be an object');
|
||||
}
|
||||
const supportedActionTypes = ['crop', 'extent', 'flip', 'rotate', 'resize'];
|
||||
const actionKeys = Object.keys(action);
|
||||
if (actionKeys.length !== 1) {
|
||||
throw new TypeError(
|
||||
`Single action must contain exactly one transformation: ${supportedActionTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
const actionType = actionKeys[0];
|
||||
if (!supportedActionTypes.includes(actionType)) {
|
||||
throw new TypeError(`Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
if (actionType === 'crop') {
|
||||
validateCropAction(action as ActionCrop);
|
||||
} else if (actionType === 'extent') {
|
||||
validateExtentAction(action as ActionExtent);
|
||||
} else if (actionType === 'flip') {
|
||||
validateFlipAction(action as ActionFlip);
|
||||
} else if (actionType === 'rotate') {
|
||||
validateRotateAction(action as ActionRotate);
|
||||
} else if (actionType === 'resize') {
|
||||
validateResizeAction(action as ActionResize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateCropAction(action: ActionCrop): void {
|
||||
const isValid =
|
||||
typeof action.crop === 'object' &&
|
||||
action.crop !== null &&
|
||||
typeof action.crop.originX === 'number' &&
|
||||
typeof action.crop.originY === 'number' &&
|
||||
typeof action.crop.width === 'number' &&
|
||||
typeof action.crop.height === 'number';
|
||||
if (!isValid) {
|
||||
throw new TypeError(
|
||||
'Crop action must be an object of shape { originX: number; originY: number; width: number; height: number }'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateExtentAction(action: ActionExtent): void {
|
||||
const isValid =
|
||||
typeof action.extent === 'object' &&
|
||||
action.extent !== null &&
|
||||
(action.extent.backgroundColor == null || typeof action.extent.backgroundColor === 'string') &&
|
||||
(action.extent.originX == null || typeof action.extent.originX === 'number') &&
|
||||
(action.extent.originY == null || typeof action.extent.originY === 'number') &&
|
||||
typeof action.extent.width === 'number' &&
|
||||
typeof action.extent.height === 'number';
|
||||
if (!isValid) {
|
||||
throw new TypeError(
|
||||
'Extent action must be an object of shape { backgroundColor?: string; originX?: number; originY?: number; width: number; height: number }'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFlipAction(action: ActionFlip): void {
|
||||
if (
|
||||
typeof action.flip !== 'string' ||
|
||||
![FlipType.Horizontal, FlipType.Vertical].includes(action.flip)
|
||||
) {
|
||||
throw new TypeError(`Unsupported flip type: ${action.flip}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRotateAction(action: ActionRotate): void {
|
||||
if (typeof action.rotate !== 'number') {
|
||||
throw new TypeError('Rotation must be a number');
|
||||
}
|
||||
}
|
||||
|
||||
function validateResizeAction(action: ActionResize): void {
|
||||
const isValid =
|
||||
typeof action.resize === 'object' &&
|
||||
action.resize !== null &&
|
||||
(typeof action.resize.width === 'number' || typeof action.resize.width === 'undefined') &&
|
||||
(typeof action.resize.height === 'number' || typeof action.resize.height === 'undefined');
|
||||
if (!isValid) {
|
||||
throw new TypeError(
|
||||
'Resize action must be an object of shape { width?: number; height?: number }'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSaveOptions({ base64, compress, format }: SaveOptions): void {
|
||||
if (base64 !== undefined && typeof base64 !== 'boolean') {
|
||||
throw new TypeError('The "base64" argument must be a boolean');
|
||||
}
|
||||
if (compress !== undefined) {
|
||||
if (typeof compress !== 'number') {
|
||||
throw new TypeError('The "compress" argument must be a number');
|
||||
}
|
||||
if (compress < 0 || compress > 1) {
|
||||
throw new TypeError('The "compress" argument must be a number between 0 and 1');
|
||||
}
|
||||
}
|
||||
const allowedFormats = [SaveFormat.JPEG, SaveFormat.PNG, SaveFormat.WEBP];
|
||||
if (format !== undefined && !allowedFormats.includes(format)) {
|
||||
throw new TypeError(`The "format" argument must be one of: ${allowedFormats.join(', ')}`);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { SharedObject } from 'expo';
|
||||
|
||||
import { ActionCrop, ActionExtent, FlipType } from '../ImageManipulator.types';
|
||||
import ImageManipulatorImageRef from './ImageManipulatorImageRef.web';
|
||||
import { crop, extent, flip, resize, rotate } from './actions/index.web';
|
||||
|
||||
type ContextLoader = () => HTMLCanvasElement | Promise<HTMLCanvasElement>;
|
||||
|
||||
export default class ImageManipulatorContext extends SharedObject {
|
||||
private loader: ContextLoader;
|
||||
|
||||
private _currentTask: Promise<HTMLCanvasElement> | undefined;
|
||||
get currentTask() {
|
||||
if (this._currentTask) {
|
||||
return this._currentTask;
|
||||
}
|
||||
this._currentTask = new Promise((resolve) => resolve(this.loader()));
|
||||
return this._currentTask;
|
||||
}
|
||||
set currentTask(task) {
|
||||
this._currentTask = task;
|
||||
}
|
||||
|
||||
constructor(loader?: ContextLoader) {
|
||||
super();
|
||||
this.loader = loader ?? (() => document.createElement('canvas'));
|
||||
}
|
||||
|
||||
resize(size: { width: number; height: number }): ImageManipulatorContext {
|
||||
return this.addTask((canvas) => resize(canvas, size));
|
||||
}
|
||||
|
||||
rotate(degrees: number): ImageManipulatorContext {
|
||||
return this.addTask((canvas) => rotate(canvas, degrees));
|
||||
}
|
||||
|
||||
flip(flipType: FlipType): ImageManipulatorContext {
|
||||
return this.addTask((canvas) => flip(canvas, flipType));
|
||||
}
|
||||
|
||||
crop(rect: ActionCrop['crop']): ImageManipulatorContext {
|
||||
return this.addTask((canvas) => crop(canvas, rect));
|
||||
}
|
||||
|
||||
extent(options: ActionExtent['extent']): ImageManipulatorContext {
|
||||
return this.addTask((canvas) => extent(canvas, options));
|
||||
}
|
||||
|
||||
reset(): ImageManipulatorContext {
|
||||
this.currentTask = new Promise((resolve) => resolve(this.loader()));
|
||||
return this;
|
||||
}
|
||||
|
||||
async renderAsync(): Promise<ImageManipulatorImageRef> {
|
||||
const canvas = await this.currentTask;
|
||||
|
||||
// We're copying the canvas so ref's `saveAsync` can safely use `toBlob` again with the desired format and quality.
|
||||
// The original canvas cannot be reused as the manipulator context may still draw on it.
|
||||
const clonedCanvas = document.createElement('canvas');
|
||||
const clonedCanvasCtx = clonedCanvas.getContext('2d');
|
||||
|
||||
clonedCanvas.width = canvas.width;
|
||||
clonedCanvas.height = canvas.height;
|
||||
clonedCanvasCtx?.drawImage(canvas, 0, 0);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Create a full-sized, full-quality blob from the original canvas.
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
const url = blob ? URL.createObjectURL(blob) : canvas.toDataURL();
|
||||
const image = new ImageManipulatorImageRef(url, clonedCanvas);
|
||||
|
||||
resolve(image);
|
||||
},
|
||||
// Use PNG format so the result is of the best quality.
|
||||
// If you need another format, see `saveAsync` function on the image ref.
|
||||
'image/png'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private addTask(
|
||||
task: (canvas: HTMLCanvasElement) => HTMLCanvasElement | Promise<HTMLCanvasElement>
|
||||
): ImageManipulatorContext {
|
||||
this.currentTask = this.currentTask.then((canvas) => {
|
||||
return task(canvas);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { SharedRef } from 'expo';
|
||||
|
||||
import { ImageResult, SaveFormat, SaveOptions } from '../ImageManipulator.types';
|
||||
import { blobToBase64String } from './utils.web';
|
||||
|
||||
export default class ImageManipulatorImageRef extends SharedRef<'image'> {
|
||||
readonly nativeRefType: string = 'image';
|
||||
|
||||
readonly uri: string;
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
|
||||
constructor(uri: string, canvas: HTMLCanvasElement) {
|
||||
super();
|
||||
this.uri = uri;
|
||||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.canvas.width;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.canvas.height;
|
||||
}
|
||||
|
||||
async saveAsync(options: SaveOptions = { base64: false }): Promise<ImageResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.canvas.toBlob(
|
||||
async (blob) => {
|
||||
if (!blob) {
|
||||
return reject(new Error(`Unable to save image: ${this.uri}`));
|
||||
}
|
||||
const base64 = options.base64 ? await blobToBase64String(blob) : undefined;
|
||||
const uri = URL.createObjectURL(blob);
|
||||
|
||||
resolve({
|
||||
uri,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
base64,
|
||||
});
|
||||
},
|
||||
`image/${options.format ?? SaveFormat.JPEG}`,
|
||||
options.compress
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { CodedError } from 'expo-modules-core';
|
||||
|
||||
import { ActionCrop } from '../../ImageManipulator.types';
|
||||
import { getContext } from '../utils.web';
|
||||
|
||||
const clamp = (value: number, max: number): number => Math.max(0, Math.min(max, value));
|
||||
|
||||
export default (canvas: HTMLCanvasElement, options: ActionCrop['crop']) => {
|
||||
// ensure values are defined.
|
||||
let { originX = 0, originY = 0, width = 0, height = 0 } = options;
|
||||
// lock within bounds.
|
||||
width = clamp(width, canvas.width);
|
||||
height = clamp(height, canvas.height);
|
||||
originX = clamp(originX, canvas.width);
|
||||
originY = clamp(originY, canvas.height);
|
||||
|
||||
// lock sum of crop.
|
||||
width = Math.min(originX + width, canvas.width) - originX;
|
||||
height = Math.min(originY + height, canvas.height) - originY;
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
throw new CodedError(
|
||||
'ERR_IMAGE_MANIPULATOR_CROP',
|
||||
'Crop size must be greater than 0: ' + JSON.stringify(options, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
const result = document.createElement('canvas');
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
|
||||
const context = getContext(result);
|
||||
context.drawImage(canvas, originX, originY, width, height, 0, 0, width, height);
|
||||
|
||||
return result;
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { CodedError } from 'expo-modules-core';
|
||||
|
||||
import { ActionExtent } from '../../ImageManipulator.types';
|
||||
import { getContext } from '../utils.web';
|
||||
|
||||
export default (canvas: HTMLCanvasElement, options: ActionExtent['extent']) => {
|
||||
// ensure values are defined.
|
||||
const { backgroundColor = null, originX = 0, originY = 0, width = 0, height = 0 } = options;
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
throw new CodedError(
|
||||
'ERR_IMAGE_MANIPULATOR_EXTENT',
|
||||
'Extent size must be greater than 0: ' + JSON.stringify(options, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
const result = document.createElement('canvas');
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
|
||||
const sx = originX < 0 ? 0 : originX;
|
||||
const sy = originY < 0 ? 0 : originY;
|
||||
const sw =
|
||||
originX < 0 ? Math.min(canvas.width, width + originX) : Math.min(canvas.width - originX, width);
|
||||
const sh =
|
||||
originY < 0
|
||||
? Math.min(canvas.height, height + originY)
|
||||
: Math.min(canvas.height - originY, height);
|
||||
|
||||
const dx = originX < 0 ? -originX : 0;
|
||||
const dy = originY < 0 ? -originY : 0;
|
||||
|
||||
const context = getContext(result);
|
||||
|
||||
if (backgroundColor != null) {
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
context.drawImage(canvas, sx, sy, sw, sh, dx, dy, sw, sh);
|
||||
|
||||
return result;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { ActionFlip, FlipType } from '../../ImageManipulator.types';
|
||||
import { getContext } from '../utils.web';
|
||||
|
||||
export default (canvas: HTMLCanvasElement, flip: ActionFlip['flip']) => {
|
||||
const xFlip = flip === FlipType.Horizontal;
|
||||
const yFlip = flip === FlipType.Vertical;
|
||||
|
||||
const result = document.createElement('canvas');
|
||||
result.width = canvas.width;
|
||||
result.height = canvas.height;
|
||||
|
||||
const context = getContext(result);
|
||||
|
||||
// Set the origin to the center of the image
|
||||
context.translate(canvas.width / 2, canvas.height / 2);
|
||||
|
||||
// Flip/flop the canvas
|
||||
const xScale = xFlip ? -1 : 1;
|
||||
const yScale = yFlip ? -1 : 1;
|
||||
context.scale(xScale, yScale);
|
||||
|
||||
// Draw the image
|
||||
context.drawImage(canvas, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height);
|
||||
|
||||
return result;
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { ActionResize } from '../../ImageManipulator.types';
|
||||
import { getContext } from '../utils.web';
|
||||
|
||||
/**
|
||||
* Hermite resize - fast image resize/resample using Hermite filter. 1 cpu version!
|
||||
* https://stackoverflow.com/a/18320662/4047926
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @param {int} width
|
||||
* @param {int} height
|
||||
* @param {boolean} resizeCanvas if true, canvas will be resized. Optional.
|
||||
*/
|
||||
function resampleSingle(
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeCanvas: boolean = false
|
||||
): HTMLCanvasElement {
|
||||
const result = document.createElement('canvas');
|
||||
result.width = canvas.width;
|
||||
result.height = canvas.height;
|
||||
|
||||
const widthSource = canvas.width;
|
||||
const heightSource = canvas.height;
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
|
||||
const wRatio = widthSource / width;
|
||||
const hRatio = heightSource / height;
|
||||
const wRatioHalf = Math.ceil(wRatio / 2);
|
||||
const hRatioHalf = Math.ceil(hRatio / 2);
|
||||
|
||||
const ctx = getContext(canvas);
|
||||
|
||||
const img = ctx.getImageData(0, 0, widthSource, heightSource);
|
||||
const img2 = ctx.createImageData(width, height);
|
||||
const data = img.data;
|
||||
const data2 = img2.data;
|
||||
|
||||
for (let j = 0; j < height; j++) {
|
||||
for (let i = 0; i < width; i++) {
|
||||
const x2 = (i + j * width) * 4;
|
||||
let weight = 0;
|
||||
let weights = 0;
|
||||
let weightsAlpha = 0;
|
||||
let gx_r = 0;
|
||||
let gx_g = 0;
|
||||
let gx_b = 0;
|
||||
let gx_a = 0;
|
||||
const yCenter = (j + 0.5) * hRatio;
|
||||
const yy_start = Math.floor(j * hRatio);
|
||||
const yy_stop = Math.ceil((j + 1) * hRatio);
|
||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
||||
const dy = Math.abs(yCenter - (yy + 0.5)) / hRatioHalf;
|
||||
const center_x = (i + 0.5) * wRatio;
|
||||
const w0 = dy * dy; //pre-calc part of w
|
||||
const xx_start = Math.floor(i * wRatio);
|
||||
const xx_stop = Math.ceil((i + 1) * wRatio);
|
||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
||||
const dx = Math.abs(center_x - (xx + 0.5)) / wRatioHalf;
|
||||
const w = Math.sqrt(w0 + dx * dx);
|
||||
if (w >= 1) {
|
||||
//pixel too far
|
||||
continue;
|
||||
}
|
||||
//hermite filter
|
||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
||||
const xPosition = 4 * (xx + yy * widthSource);
|
||||
//alpha
|
||||
gx_a += weight * data[xPosition + 3];
|
||||
weightsAlpha += weight;
|
||||
//colors
|
||||
if (data[xPosition + 3] < 255) {
|
||||
weight = (weight * data[xPosition + 3]) / 250;
|
||||
}
|
||||
gx_r += weight * data[xPosition];
|
||||
gx_g += weight * data[xPosition + 1];
|
||||
gx_b += weight * data[xPosition + 2];
|
||||
weights += weight;
|
||||
}
|
||||
}
|
||||
data2[x2] = gx_r / weights;
|
||||
data2[x2 + 1] = gx_g / weights;
|
||||
data2[x2 + 2] = gx_b / weights;
|
||||
data2[x2 + 3] = gx_a / weightsAlpha;
|
||||
}
|
||||
}
|
||||
|
||||
//resize canvas
|
||||
if (resizeCanvas) {
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
}
|
||||
|
||||
//draw
|
||||
const context = getContext(result);
|
||||
context.putImageData(img2, 0, 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default (canvas: HTMLCanvasElement, { width, height }: ActionResize['resize']) => {
|
||||
const imageRatio = canvas.width / canvas.height;
|
||||
|
||||
let requestedWidth: number = 0;
|
||||
let requestedHeight: number = 0;
|
||||
if (width !== undefined) {
|
||||
requestedWidth = width;
|
||||
requestedHeight = requestedWidth / imageRatio;
|
||||
}
|
||||
if (height !== undefined) {
|
||||
requestedHeight = height;
|
||||
if (requestedWidth === 0) {
|
||||
requestedWidth = requestedHeight * imageRatio;
|
||||
}
|
||||
}
|
||||
|
||||
return resampleSingle(canvas, requestedWidth, requestedHeight, true);
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { ActionRotate } from '../../ImageManipulator.types';
|
||||
import { getContext } from '../utils.web';
|
||||
|
||||
function sizeFromAngle(
|
||||
width: number,
|
||||
height: number,
|
||||
angle: number
|
||||
): { width: number; height: number } {
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
let c = Math.cos(radians);
|
||||
let s = Math.sin(radians);
|
||||
if (s < 0) {
|
||||
s = -s;
|
||||
}
|
||||
if (c < 0) {
|
||||
c = -c;
|
||||
}
|
||||
return { width: height * s + width * c, height: height * c + width * s };
|
||||
}
|
||||
|
||||
export default (canvas: HTMLCanvasElement, degrees: ActionRotate['rotate']) => {
|
||||
const { width, height } = sizeFromAngle(canvas.width, canvas.height, degrees);
|
||||
|
||||
const result = document.createElement('canvas');
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
|
||||
const context = getContext(result);
|
||||
|
||||
// Set the origin to the center of the image
|
||||
context.translate(result.width / 2, result.height / 2);
|
||||
|
||||
// Rotate the canvas around the origin
|
||||
const radians = (degrees * Math.PI) / 180;
|
||||
context.rotate(radians);
|
||||
|
||||
// Draw the image
|
||||
context.drawImage(canvas, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height);
|
||||
|
||||
return result;
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export { default as crop } from './CropAction.web';
|
||||
export { default as extent } from './ExtentAction.web';
|
||||
export { default as flip } from './FlipAction.web';
|
||||
export { default as resize } from './ResizeAction.web';
|
||||
export { default as rotate } from './RotateAction.web';
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { CodedError } from 'expo-modules-core';
|
||||
|
||||
export function getContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new CodedError('ERR_IMAGE_MANIPULATOR', 'Failed to create canvas context');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export async function blobToBase64String(blob: Blob): Promise<string> {
|
||||
const dataURL = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = () =>
|
||||
reject(new Error(`Unable to convert blob to base64 string: ${reader.error}`));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
return dataURL.replace(/^data:image\/\w+;base64,/, '');
|
||||
}
|
||||
|
||||
export function loadImageAsync(uri: string): Promise<HTMLCanvasElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const imageSource = new Image();
|
||||
imageSource.crossOrigin = 'anonymous';
|
||||
const canvas = document.createElement('canvas');
|
||||
imageSource.onload = () => {
|
||||
canvas.width = imageSource.naturalWidth;
|
||||
canvas.height = imageSource.naturalHeight;
|
||||
|
||||
const context = getContext(canvas);
|
||||
context.drawImage(imageSource, 0, 0, imageSource.naturalWidth, imageSource.naturalHeight);
|
||||
|
||||
resolve(canvas);
|
||||
};
|
||||
imageSource.onerror = () => reject(canvas);
|
||||
imageSource.src = uri;
|
||||
});
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "expo-module-scripts/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user