expo-background-remover 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +5 -0
- package/README.md +35 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/expo/modules/backgroundremover/BackgroundRemoverProcessor.kt +104 -0
- package/android/src/main/java/expo/modules/backgroundremover/ExpoBackgroundRemoverModule.kt +57 -0
- package/android/src/main/java/expo/modules/backgroundremover/ExpoBackgroundRemoverView.kt +30 -0
- package/build/ExpoBackgroundRemover.types.d.ts +21 -0
- package/build/ExpoBackgroundRemover.types.d.ts.map +1 -0
- package/build/ExpoBackgroundRemover.types.js +8 -0
- package/build/ExpoBackgroundRemover.types.js.map +1 -0
- package/build/ExpoBackgroundRemoverModule.d.ts +11 -0
- package/build/ExpoBackgroundRemoverModule.d.ts.map +1 -0
- package/build/ExpoBackgroundRemoverModule.js +4 -0
- package/build/ExpoBackgroundRemoverModule.js.map +1 -0
- package/build/ExpoBackgroundRemoverModule.web.d.ts +10 -0
- package/build/ExpoBackgroundRemoverModule.web.d.ts.map +1 -0
- package/build/ExpoBackgroundRemoverModule.web.js +12 -0
- package/build/ExpoBackgroundRemoverModule.web.js.map +1 -0
- package/build/ExpoBackgroundRemoverView.d.ts +4 -0
- package/build/ExpoBackgroundRemoverView.d.ts.map +1 -0
- package/build/ExpoBackgroundRemoverView.js +7 -0
- package/build/ExpoBackgroundRemoverView.js.map +1 -0
- package/build/ExpoBackgroundRemoverView.web.d.ts +4 -0
- package/build/ExpoBackgroundRemoverView.web.d.ts.map +1 -0
- package/build/ExpoBackgroundRemoverView.web.js +7 -0
- package/build/ExpoBackgroundRemoverView.web.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +28 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoBackgroundRemover.podspec +32 -0
- package/ios/ExpoBackgroundRemoverModule.swift +176 -0
- package/ios/ExpoBackgroundRemoverView.swift +38 -0
- package/package.json +43 -0
- package/src/ExpoBackgroundRemover.types.ts +27 -0
- package/src/ExpoBackgroundRemoverModule.ts +13 -0
- package/src/ExpoBackgroundRemoverModule.web.ts +15 -0
- package/src/ExpoBackgroundRemoverView.tsx +11 -0
- package/src/ExpoBackgroundRemoverView.web.tsx +15 -0
- package/src/index.ts +35 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# expo-background-remover
|
|
2
|
+
|
|
3
|
+
Expo Module For High Quality Background Remover
|
|
4
|
+
|
|
5
|
+
# API documentation
|
|
6
|
+
|
|
7
|
+
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/background-remover/)
|
|
8
|
+
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/background-remover/)
|
|
9
|
+
|
|
10
|
+
# Installation in managed Expo projects
|
|
11
|
+
|
|
12
|
+
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](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
|
|
13
|
+
|
|
14
|
+
# Installation in bare React Native projects
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
### Add the package to your npm dependencies
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npm install expo-background-remover
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Configure for Android
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Configure for iOS
|
|
30
|
+
|
|
31
|
+
Run `npx pod-install` after installing the npm package.
|
|
32
|
+
|
|
33
|
+
# Contributing
|
|
34
|
+
|
|
35
|
+
Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
|
|
4
|
+
group = 'expo.modules.backgroundremover'
|
|
5
|
+
version = '0.1.0'
|
|
6
|
+
|
|
7
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
8
|
+
apply from: expoModulesCorePlugin
|
|
9
|
+
applyKotlinExpoModulesCorePlugin()
|
|
10
|
+
useCoreDependencies()
|
|
11
|
+
useExpoPublishing()
|
|
12
|
+
|
|
13
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
14
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
15
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
16
|
+
def useManagedAndroidSdkVersions = false
|
|
17
|
+
if (useManagedAndroidSdkVersions) {
|
|
18
|
+
useDefaultAndroidSdkVersions()
|
|
19
|
+
} else {
|
|
20
|
+
buildscript {
|
|
21
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
22
|
+
ext.safeExtGet = { prop, fallback ->
|
|
23
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
project.android {
|
|
27
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 35)
|
|
28
|
+
defaultConfig {
|
|
29
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
30
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
android {
|
|
37
|
+
namespace "expo.modules.backgroundremover"
|
|
38
|
+
defaultConfig {
|
|
39
|
+
versionCode 1
|
|
40
|
+
versionName "0.1.0"
|
|
41
|
+
}
|
|
42
|
+
lintOptions {
|
|
43
|
+
abortOnError false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
dependencies {
|
|
49
|
+
// 2. Kotlin Standard Library (SDK 54 uses Kotlin 1.9+)
|
|
50
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
|
51
|
+
|
|
52
|
+
// 3. Android KTX for cleaner Kotlin code
|
|
53
|
+
implementation "androidx.core:core-ktx:1.12.0"
|
|
54
|
+
|
|
55
|
+
// 4. Coroutines for background processing (Critical for VideoEncoder)
|
|
56
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2"
|
|
57
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
|
|
58
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// 5. EXIF support for image rotation handling
|
|
62
|
+
implementation "androidx.exifinterface:exifinterface:1.3.6"
|
|
63
|
+
|
|
64
|
+
// 6. Lifecycle KTX (Useful if you want to scope encoding to the app lifecycle)
|
|
65
|
+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
|
|
66
|
+
|
|
67
|
+
implementation "com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0"
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package expo.modules.backgroundremover
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.*
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import com.google.mlkit.vision.common.InputImage
|
|
7
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
|
|
8
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
|
|
9
|
+
import kotlinx.coroutines.tasks.await
|
|
10
|
+
import java.io.File
|
|
11
|
+
import java.io.FileOutputStream
|
|
12
|
+
import java.util.UUID
|
|
13
|
+
import android.graphics.PorterDuff
|
|
14
|
+
import android.graphics.PorterDuffXfermode
|
|
15
|
+
|
|
16
|
+
class BackgroundRemoverProcessor(private val context: Context) {
|
|
17
|
+
|
|
18
|
+
private val segmenter = SubjectSegmentation.getClient(
|
|
19
|
+
SubjectSegmenterOptions.Builder()
|
|
20
|
+
.enableForegroundConfidenceMask() // High quality edges
|
|
21
|
+
.build()
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
suspend fun processImage(uriString: String): String {
|
|
25
|
+
// 1. Efficiently load and downscale to max 2048px
|
|
26
|
+
val bitmap = loadAndResizeBitmap(uriString, 2048)
|
|
27
|
+
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
28
|
+
|
|
29
|
+
// 2. Perform Segmentation
|
|
30
|
+
val result = segmenter.process(inputImage).await()
|
|
31
|
+
|
|
32
|
+
// 3. Retrieve global mask (Includes People + Objects automatically)
|
|
33
|
+
val maskBuffer = result.foregroundConfidenceMask
|
|
34
|
+
?: throw Exception("Could not detect subjects")
|
|
35
|
+
|
|
36
|
+
// 4. Create Mask Bitmap and Blend
|
|
37
|
+
val maskBitmap = createMaskFromBuffer(maskBuffer, bitmap.width, bitmap.height)
|
|
38
|
+
val outputBitmap = applyMaskToBitmap(bitmap, maskBitmap)
|
|
39
|
+
|
|
40
|
+
return saveResult(outputBitmap)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun loadAndResizeBitmap(uriString: String, maxDimension: Int): Bitmap {
|
|
44
|
+
val uri = Uri.parse(uriString)
|
|
45
|
+
|
|
46
|
+
// Stage A: Get dimensions only
|
|
47
|
+
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
48
|
+
context.contentResolver.openInputStream(uri)?.use { stream ->
|
|
49
|
+
BitmapFactory.decodeStream(stream, null, loadOptions)
|
|
50
|
+
}?: throw Exception("Failed to open input stream")
|
|
51
|
+
|
|
52
|
+
// Stage B: Calculate optimal inSampleSize (power of 2)
|
|
53
|
+
var sampleSize = 1
|
|
54
|
+
while ((options.outWidth / (sampleSize * 2)) >= maxDimension &&
|
|
55
|
+
(options.outHeight / (sampleSize * 2)) >= maxDimension) {
|
|
56
|
+
sampleSize *= 2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Stage C: Load the subsampled bitmap
|
|
60
|
+
val loadOptions = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
|
61
|
+
val subsampledBitmap = context.contentResolver.openInputStream(uri).use {
|
|
62
|
+
BitmapFactory.decodeStream(it, null, loadOptions)
|
|
63
|
+
} ?: throw Exception("Failed to decode image")
|
|
64
|
+
|
|
65
|
+
// Stage D: Precise scaling to exactly fit within 2048px while preserving aspect ratio
|
|
66
|
+
val scale = minOf(maxDimension.toFloat() / subsampledBitmap.width, maxDimension.toFloat() / subsampledBitmap.height)
|
|
67
|
+
if (scale >= 1f) return subsampledBitmap
|
|
68
|
+
|
|
69
|
+
val targetWidth = (subsampledBitmap.width * scale).toInt()
|
|
70
|
+
val targetHeight = (subsampledBitmap.height * scale).toInt()
|
|
71
|
+
|
|
72
|
+
return Bitmap.createScaledBitmap(subsampledBitmap, targetWidth, targetHeight, true)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun createMaskFromBuffer(buffer: java.nio.FloatBuffer, width: Int, height: Int): Bitmap {
|
|
76
|
+
val mask = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
77
|
+
buffer.rewind()
|
|
78
|
+
val pixels = IntArray(width * height)
|
|
79
|
+
for (i in 0 until width * height) {
|
|
80
|
+
val alpha = (buffer.get() * 255).toInt()
|
|
81
|
+
pixels[i] = Color.argb(alpha, 0, 0, 0)
|
|
82
|
+
}
|
|
83
|
+
mask.setPixels(pixels, 0, width, 0, 0, width, height)
|
|
84
|
+
return mask
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun applyMaskToBitmap(source: Bitmap, mask: Bitmap): Bitmap {
|
|
88
|
+
val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
|
|
89
|
+
val canvas = Canvas(result)
|
|
90
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
91
|
+
|
|
92
|
+
canvas.drawBitmap(source, 0f, 0f, null)
|
|
93
|
+
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
|
|
94
|
+
canvas.drawBitmap(mask, 0f, 0f, paint)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun saveResult(bitmap: Bitmap): String {
|
|
100
|
+
val file = File(context.cacheDir, "${UUID.randomUUID()}.png")
|
|
101
|
+
FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
|
102
|
+
return Uri.fromFile(file).toString()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package expo.modules.backgroundremover
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
import java.net.URL
|
|
6
|
+
|
|
7
|
+
class ExpoBackgroundRemoverModule : Module() {
|
|
8
|
+
// Each module class must implement the definition function. The definition consists of components
|
|
9
|
+
// that describes the module's functionality and behavior.
|
|
10
|
+
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
|
11
|
+
override fun definition() = ModuleDefinition {
|
|
12
|
+
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
|
|
13
|
+
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
|
|
14
|
+
// The module will be accessible from `requireNativeModule('ExpoBackgroundRemover')` in JavaScript.
|
|
15
|
+
Name("ExpoBackgroundRemover")
|
|
16
|
+
|
|
17
|
+
// Defines constant property on the module.
|
|
18
|
+
Constant("PI") {
|
|
19
|
+
Math.PI
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Defines event names that the module can send to JavaScript.
|
|
23
|
+
Events("onChange")
|
|
24
|
+
|
|
25
|
+
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
|
|
26
|
+
Function("hello") {
|
|
27
|
+
"Hello world! 👋"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
AsyncFunction("removeBackgroundAsync") { imageUri: String ->
|
|
31
|
+
// Pass the internal context to the processor
|
|
32
|
+
val processor = BackgroundRemoverProcessor(appContext.reactContext!!)
|
|
33
|
+
return@AsyncFunction processor.processImage(imageUri)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// Defines a JavaScript function that always returns a Promise and whose native code
|
|
38
|
+
// is by default dispatched on the different thread than the JavaScript runtime runs on.
|
|
39
|
+
AsyncFunction("setValueAsync") { value: String ->
|
|
40
|
+
// Send an event to JavaScript.
|
|
41
|
+
sendEvent("onChange", mapOf(
|
|
42
|
+
"value" to value
|
|
43
|
+
))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Enables the module to be used as a native view. Definition components that are accepted as part of
|
|
47
|
+
// the view definition: Prop, Events.
|
|
48
|
+
View(ExpoBackgroundRemoverView::class) {
|
|
49
|
+
// Defines a setter for the `url` prop.
|
|
50
|
+
Prop("url") { view: ExpoBackgroundRemoverView, url: URL ->
|
|
51
|
+
view.webView.loadUrl(url.toString())
|
|
52
|
+
}
|
|
53
|
+
// Defines an event that the view can send to JavaScript.
|
|
54
|
+
Events("onLoad")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package expo.modules.backgroundremover
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.webkit.WebView
|
|
5
|
+
import android.webkit.WebViewClient
|
|
6
|
+
import expo.modules.kotlin.AppContext
|
|
7
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
8
|
+
import expo.modules.kotlin.views.ExpoView
|
|
9
|
+
|
|
10
|
+
class ExpoBackgroundRemoverView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
11
|
+
// Creates and initializes an event dispatcher for the `onLoad` event.
|
|
12
|
+
// The name of the event is inferred from the value and needs to match the event name defined in the module.
|
|
13
|
+
private val onLoad by EventDispatcher()
|
|
14
|
+
|
|
15
|
+
// Defines a WebView that will be used as the root subview.
|
|
16
|
+
internal val webView = WebView(context).apply {
|
|
17
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
18
|
+
webViewClient = object : WebViewClient() {
|
|
19
|
+
override fun onPageFinished(view: WebView, url: String) {
|
|
20
|
+
// Sends an event to JavaScript. Triggers a callback defined on the view component in JavaScript.
|
|
21
|
+
onLoad(mapOf("url" to url))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init {
|
|
27
|
+
// Adds the WebView to the view hierarchy.
|
|
28
|
+
addView(webView)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
export type OnLoadEventPayload = {
|
|
3
|
+
url: string;
|
|
4
|
+
};
|
|
5
|
+
export type ExpoBackgroundRemoverModuleEvents = {
|
|
6
|
+
onChange: (params: ChangeEventPayload) => void;
|
|
7
|
+
};
|
|
8
|
+
export type ChangeEventPayload = {
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
export type ExpoBackgroundRemoverViewProps = {
|
|
12
|
+
url: string;
|
|
13
|
+
onLoad: (event: {
|
|
14
|
+
nativeEvent: OnLoadEventPayload;
|
|
15
|
+
}) => void;
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
};
|
|
18
|
+
export declare class BackgroundRemoverError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=ExpoBackgroundRemover.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemover.types.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundRemover.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemover.types.js","sourceRoot":"","sources":["../src/ExpoBackgroundRemover.types.ts"],"names":[],"mappings":"AAoBA,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAAA,CAAC","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\nexport type OnLoadEventPayload = {\n url: string;\n};\n\nexport type ExpoBackgroundRemoverModuleEvents = {\n onChange: (params: ChangeEventPayload) => void;\n};\n\nexport type ChangeEventPayload = {\n value: string;\n};\n\nexport type ExpoBackgroundRemoverViewProps = {\n url: string;\n onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;\n style?: StyleProp<ViewStyle>;\n};\n\nexport class BackgroundRemoverError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BackgroundRemoverError';\n }\n};\n\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
import { ExpoBackgroundRemoverModuleEvents } from './ExpoBackgroundRemover.types';
|
|
3
|
+
declare class ExpoBackgroundRemoverModule extends NativeModule<ExpoBackgroundRemoverModuleEvents> {
|
|
4
|
+
PI: number;
|
|
5
|
+
hello(): string;
|
|
6
|
+
setValueAsync(value: string): Promise<void>;
|
|
7
|
+
removeBackgroundAsync(imageUri: string): Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
declare const _default: ExpoBackgroundRemoverModule;
|
|
10
|
+
export default _default;
|
|
11
|
+
//# sourceMappingURL=ExpoBackgroundRemoverModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverModule.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,iCAAiC,EAAE,MAAM,+BAA+B,CAAC;AAElF,OAAO,OAAO,2BAA4B,SAAQ,YAAY,CAAC,iCAAiC,CAAC;IAC/F,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,IAAI,MAAM;IACf,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CACzD;;AAGD,wBAAyF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverModule.js","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAWzD,yDAAyD;AACzD,eAAe,mBAAmB,CAA8B,uBAAuB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ExpoBackgroundRemoverModuleEvents } from './ExpoBackgroundRemover.types';\n\ndeclare class ExpoBackgroundRemoverModule extends NativeModule<ExpoBackgroundRemoverModuleEvents> {\n PI: number;\n hello(): string;\n setValueAsync(value: string): Promise<void>;\n removeBackgroundAsync(imageUri: string): Promise<string> ;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<ExpoBackgroundRemoverModule>('ExpoBackgroundRemover');\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
import { ExpoBackgroundRemoverModuleEvents } from './ExpoBackgroundRemover.types';
|
|
3
|
+
declare class ExpoBackgroundRemoverModule extends NativeModule<ExpoBackgroundRemoverModuleEvents> {
|
|
4
|
+
PI: number;
|
|
5
|
+
setValueAsync(value: string): Promise<void>;
|
|
6
|
+
hello(): string;
|
|
7
|
+
}
|
|
8
|
+
declare const _default: typeof ExpoBackgroundRemoverModule;
|
|
9
|
+
export default _default;
|
|
10
|
+
//# sourceMappingURL=ExpoBackgroundRemoverModule.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EAAE,iCAAiC,EAAE,MAAM,+BAA+B,CAAC;AAElF,cAAM,2BAA4B,SAAQ,YAAY,CAAC,iCAAiC,CAAC;IACvF,EAAE,SAAW;IACP,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAGjD,KAAK;CAGN;;AAED,wBAA6F"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerWebModule, NativeModule } from 'expo';
|
|
2
|
+
class ExpoBackgroundRemoverModule extends NativeModule {
|
|
3
|
+
PI = Math.PI;
|
|
4
|
+
async setValueAsync(value) {
|
|
5
|
+
this.emit('onChange', { value });
|
|
6
|
+
}
|
|
7
|
+
hello() {
|
|
8
|
+
return 'Hello world! 👋';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export default registerWebModule(ExpoBackgroundRemoverModule, 'ExpoBackgroundRemoverModule');
|
|
12
|
+
//# sourceMappingURL=ExpoBackgroundRemoverModule.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverModule.web.js","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAIvD,MAAM,2BAA4B,SAAQ,YAA+C;IACvF,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;IACb,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACnC,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAC;IAC3B,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,2BAA2B,EAAE,6BAA6B,CAAC,CAAC","sourcesContent":["import { registerWebModule, NativeModule } from 'expo';\n\nimport { ExpoBackgroundRemoverModuleEvents } from './ExpoBackgroundRemover.types';\n\nclass ExpoBackgroundRemoverModule extends NativeModule<ExpoBackgroundRemoverModuleEvents> {\n PI = Math.PI;\n async setValueAsync(value: string): Promise<void> {\n this.emit('onChange', { value });\n }\n hello() {\n return 'Hello world! 👋';\n }\n}\n\nexport default registerWebModule(ExpoBackgroundRemoverModule, 'ExpoBackgroundRemoverModule');\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ExpoBackgroundRemoverViewProps } from './ExpoBackgroundRemover.types';
|
|
3
|
+
export default function ExpoBackgroundRemoverView(props: ExpoBackgroundRemoverViewProps): React.JSX.Element;
|
|
4
|
+
//# sourceMappingURL=ExpoBackgroundRemoverView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverView.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAK/E,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,KAAK,EAAE,8BAA8B,qBAEtF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
const NativeView = requireNativeView('ExpoBackgroundRemover');
|
|
4
|
+
export default function ExpoBackgroundRemoverView(props) {
|
|
5
|
+
return <NativeView {...props}/>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=ExpoBackgroundRemoverView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverView.js","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,UAAU,GACd,iBAAiB,CAAC,uBAAuB,CAAC,CAAC;AAE7C,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,KAAqC;IACrF,OAAO,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACnC,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\n\nimport { ExpoBackgroundRemoverViewProps } from './ExpoBackgroundRemover.types';\n\nconst NativeView: React.ComponentType<ExpoBackgroundRemoverViewProps> =\n requireNativeView('ExpoBackgroundRemover');\n\nexport default function ExpoBackgroundRemoverView(props: ExpoBackgroundRemoverViewProps) {\n return <NativeView {...props} />;\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ExpoBackgroundRemoverViewProps } from './ExpoBackgroundRemover.types';
|
|
3
|
+
export default function ExpoBackgroundRemoverView(props: ExpoBackgroundRemoverViewProps): React.JSX.Element;
|
|
4
|
+
//# sourceMappingURL=ExpoBackgroundRemoverView.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverView.web.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAE/E,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,KAAK,EAAE,8BAA8B,qBAUtF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export default function ExpoBackgroundRemoverView(props) {
|
|
3
|
+
return (<div>
|
|
4
|
+
<iframe style={{ flex: 1 }} src={props.url} onLoad={() => props.onLoad({ nativeEvent: { url: props.url } })}/>
|
|
5
|
+
</div>);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=ExpoBackgroundRemoverView.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoBackgroundRemoverView.web.js","sourceRoot":"","sources":["../src/ExpoBackgroundRemoverView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,KAAqC;IACrF,OAAO,CACL,CAAC,GAAG,CACF;MAAA,CAAC,MAAM,CACL,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CACnB,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CACf,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAEpE;IAAA,EAAE,GAAG,CAAC,CACP,CAAC;AACJ,CAAC","sourcesContent":["import * as React from 'react';\n\nimport { ExpoBackgroundRemoverViewProps } from './ExpoBackgroundRemover.types';\n\nexport default function ExpoBackgroundRemoverView(props: ExpoBackgroundRemoverViewProps) {\n return (\n <div>\n <iframe\n style={{ flex: 1 }}\n src={props.url}\n onLoad={() => props.onLoad({ nativeEvent: { url: props.url } })}\n />\n </div>\n );\n}\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the background from an image using on-device ML.
|
|
3
|
+
* * @param imageUri - The local file URI of the source image.
|
|
4
|
+
* @returns A promise that resolves to the local file URI of the transparent PNG.
|
|
5
|
+
* @throws Will throw an error if the image cannot be loaded or no subjects are found.
|
|
6
|
+
*/
|
|
7
|
+
export declare function removeBackgroundAsync(imageUri: string): Promise<string>;
|
|
8
|
+
export { default } from './ExpoBackgroundRemoverModule';
|
|
9
|
+
export { default as ExpoBackgroundRemoverView } from './ExpoBackgroundRemoverView';
|
|
10
|
+
export * from './ExpoBackgroundRemover.types';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa7E;AAMD,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACnF,cAAe,+BAA+B,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BackgroundRemoverError } from './ExpoBackgroundRemover.types';
|
|
2
|
+
import ExpoBackgroundRemoverModule from './ExpoBackgroundRemoverModule';
|
|
3
|
+
/**
|
|
4
|
+
* Removes the background from an image using on-device ML.
|
|
5
|
+
* * @param imageUri - The local file URI of the source image.
|
|
6
|
+
* @returns A promise that resolves to the local file URI of the transparent PNG.
|
|
7
|
+
* @throws Will throw an error if the image cannot be loaded or no subjects are found.
|
|
8
|
+
*/
|
|
9
|
+
export async function removeBackgroundAsync(imageUri) {
|
|
10
|
+
if (!imageUri || typeof imageUri !== 'string') {
|
|
11
|
+
throw new BackgroundRemoverError('A valid image URI string is required.');
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const resultUri = await ExpoBackgroundRemoverModule.removeBackgroundAsync(imageUri);
|
|
15
|
+
return resultUri;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
// Catch native exceptions (e.g., NoSubjectDetectedException from Swift)
|
|
19
|
+
// and format them nicely for the JS thread.
|
|
20
|
+
throw new BackgroundRemoverError(error?.message || 'Failed to process image background.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Reexport the native module. On web, it will be resolved to ExpoBackgroundRemoverModule.web.ts
|
|
24
|
+
// and on native platforms to ExpoBackgroundRemoverModule.ts
|
|
25
|
+
export { default } from './ExpoBackgroundRemoverModule';
|
|
26
|
+
export { default as ExpoBackgroundRemoverView } from './ExpoBackgroundRemoverView';
|
|
27
|
+
export * from './ExpoBackgroundRemover.types';
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,2BAA2B,MAAM,+BAA+B,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,QAAgB;IAC1D,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC9C,MAAM,IAAI,sBAAsB,CAAC,uCAAuC,CAAC,CAAC;IAC5E,CAAC;IAED,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,2BAA2B,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC;QACpF,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,yEAAyE;QACzE,4CAA4C;QAC5C,MAAM,IAAI,sBAAsB,CAAC,KAAK,EAAE,OAAO,IAAI,qCAAqC,CAAC,CAAC;IAC5F,CAAC;AACH,CAAC;AAID,gGAAgG;AAChG,4DAA4D;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACnF,cAAe,+BAA+B,CAAC","sourcesContent":["\nimport { BackgroundRemoverError } from './ExpoBackgroundRemover.types';\nimport ExpoBackgroundRemoverModule from './ExpoBackgroundRemoverModule';\n \n\n/**\n * Removes the background from an image using on-device ML.\n * * @param imageUri - The local file URI of the source image.\n * @returns A promise that resolves to the local file URI of the transparent PNG.\n * @throws Will throw an error if the image cannot be loaded or no subjects are found.\n */\nexport async function removeBackgroundAsync(imageUri: string): Promise<string> {\n if (!imageUri || typeof imageUri !== 'string') {\n throw new BackgroundRemoverError('A valid image URI string is required.');\n }\n\n try {\n const resultUri = await ExpoBackgroundRemoverModule.removeBackgroundAsync(imageUri);\n return resultUri;\n } catch (error: any) {\n // Catch native exceptions (e.g., NoSubjectDetectedException from Swift) \n // and format them nicely for the JS thread.\n throw new BackgroundRemoverError(error?.message || 'Failed to process image background.');\n }\n}\n\n\n\n// Reexport the native module. On web, it will be resolved to ExpoBackgroundRemoverModule.web.ts\n// and on native platforms to ExpoBackgroundRemoverModule.ts\nexport { default } from './ExpoBackgroundRemoverModule';\nexport { default as ExpoBackgroundRemoverView } from './ExpoBackgroundRemoverView';\nexport * from './ExpoBackgroundRemover.types';\n\n\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoBackgroundRemover'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '15.1',
|
|
15
|
+
:tvos => '15.1'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
s.source = { git: 'https://github.com/tsnguyenducphuong/background-remover' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
s.frameworks = 'Vision', 'CoreImage','CoreMedia', 'UIKit', 'CoreGraphics'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Swift/Objective-C compatibility
|
|
27
|
+
s.pod_target_xcconfig = {
|
|
28
|
+
'DEFINES_MODULE' => 'YES',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
32
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Vision
|
|
3
|
+
import CoreImage
|
|
4
|
+
import CoreImage.CIFilterBuiltins
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
public class ExpoBackgroundRemoverModule: Module {
|
|
8
|
+
private let context = CIContext()
|
|
9
|
+
// Each module class must implement the definition function. The definition consists of components
|
|
10
|
+
// that describes the module's functionality and behavior.
|
|
11
|
+
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
|
12
|
+
public func definition() -> ModuleDefinition {
|
|
13
|
+
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
|
|
14
|
+
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
|
|
15
|
+
// The module will be accessible from `requireNativeModule('ExpoBackgroundRemover')` in JavaScript.
|
|
16
|
+
Name("ExpoBackgroundRemover")
|
|
17
|
+
|
|
18
|
+
// Defines constant property on the module.
|
|
19
|
+
Constant("PI") {
|
|
20
|
+
Double.pi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Defines event names that the module can send to JavaScript.
|
|
24
|
+
Events("onChange")
|
|
25
|
+
|
|
26
|
+
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
|
|
27
|
+
Function("hello") {
|
|
28
|
+
return "Hello world! 👋"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Defines a JavaScript function that always returns a Promise and whose native code
|
|
32
|
+
// is by default dispatched on the different thread than the JavaScript runtime runs on.
|
|
33
|
+
AsyncFunction("setValueAsync") { (value: String) in
|
|
34
|
+
// Send an event to JavaScript.
|
|
35
|
+
self.sendEvent("onChange", [
|
|
36
|
+
"value": value
|
|
37
|
+
])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
AsyncFunction("removeBackgroundAsync") { (imageUri: String) async throws -> String in
|
|
41
|
+
guard let url = URL(string: imageUri),
|
|
42
|
+
let imageData = try? Data(contentsOf: url),
|
|
43
|
+
let uiImage = UIImage(data: imageData) else {
|
|
44
|
+
throw ImageLoadingException()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 1. Thread Safety: Explicitly move to a background thread for ML/Vision
|
|
48
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
49
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
50
|
+
do {
|
|
51
|
+
// 2. Downscale to max 2048px to prevent OOM
|
|
52
|
+
let processedImage = self.downscaleImage(image: uiImage, maxDimension: 2048)
|
|
53
|
+
|
|
54
|
+
// 3. API Availability Check (iOS 17.0+ for Instance Masking)
|
|
55
|
+
if #available(iOS 17.0, *) {
|
|
56
|
+
let resultUri = try self.performModernSegmentation(image: processedImage)
|
|
57
|
+
continuation.resume(returning: resultUri)
|
|
58
|
+
} else {
|
|
59
|
+
// 4. Fallback for older iOS versions (using Person Segmentation)
|
|
60
|
+
let resultUri = try self.performLegacySegmentation(image: processedImage)
|
|
61
|
+
continuation.resume(returning: resultUri)
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
continuation.resume(throwing: error)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Enables the module to be used as a native view. Definition components that are accepted as part of the
|
|
71
|
+
// view definition: Prop, Events.
|
|
72
|
+
View(ExpoBackgroundRemoverView.self) {
|
|
73
|
+
// Defines a setter for the `url` prop.
|
|
74
|
+
Prop("url") { (view: ExpoBackgroundRemoverView, url: URL) in
|
|
75
|
+
if view.webView.url != url {
|
|
76
|
+
view.webView.load(URLRequest(url: url))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Events("onLoad")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - iOS 17+ Logic
|
|
85
|
+
@available(iOS 17.0, *)
|
|
86
|
+
private func performModernSegmentation(image: UIImage) throws -> String {
|
|
87
|
+
guard let cgImage = image.cgImage else { throw ImageProcessingException() }
|
|
88
|
+
|
|
89
|
+
let request = VNGenerateForegroundInstanceMaskRequest()
|
|
90
|
+
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
|
91
|
+
|
|
92
|
+
try handler.perform([request])
|
|
93
|
+
|
|
94
|
+
guard let result = request.results?.first else { throw NoSubjectDetectedException() }
|
|
95
|
+
|
|
96
|
+
// Generate combined mask for all subjects
|
|
97
|
+
let maskBuffer = try result.generateMask(forInstances: result.allInstances)
|
|
98
|
+
return try applyMaskAndSave(image: image, maskBuffer: maskBuffer)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - iOS 15/16 Fallback
|
|
102
|
+
private func performLegacySegmentation(image: UIImage) throws -> String {
|
|
103
|
+
// Falls back to Person Segmentation if iOS < 17
|
|
104
|
+
guard let cgImage = image.cgImage else { throw ImageProcessingException() }
|
|
105
|
+
let request = VNGeneratePersonSegmentationRequest()
|
|
106
|
+
request.qualityLevel = .accurate
|
|
107
|
+
|
|
108
|
+
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
|
109
|
+
try handler.perform([request])
|
|
110
|
+
|
|
111
|
+
guard let result = request.results?.first else { throw NoSubjectDetectedException() }
|
|
112
|
+
return try applyMaskAndSave(image: image, maskBuffer: result.pixelBuffer)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Core Image Pipeline
|
|
116
|
+
private func applyMaskAndSave(image: UIImage, maskBuffer: CVPixelBuffer) throws -> String {
|
|
117
|
+
guard let ciImage = CIImage(image: image) else {
|
|
118
|
+
throw ImageProcessingException()
|
|
119
|
+
}
|
|
120
|
+
let maskImage = CIImage(cvPixelBuffer: maskBuffer)
|
|
121
|
+
|
|
122
|
+
// Scale mask to match image
|
|
123
|
+
let scaleX = ciImage.extent.width / maskImage.extent.width
|
|
124
|
+
let scaleY = ciImage.extent.height / maskImage.extent.height
|
|
125
|
+
let scaledMask = maskImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
|
|
126
|
+
|
|
127
|
+
// Valid CIBlendWithMask Setup:
|
|
128
|
+
// Requires: inputImage (foreground), inputBackgroundImage (background), inputMaskImage
|
|
129
|
+
guard let filter = CIFilter(name: "CIBlendWithMask") else {
|
|
130
|
+
throw ImageProcessingException()
|
|
131
|
+
}
|
|
132
|
+
filter.inputImage = ciImage
|
|
133
|
+
let transparentBG = CIImage(color: .clear).cropped(to: ciImage.extent)
|
|
134
|
+
filter.backgroundImage = transparentBG // Ensures transparency where mask is 0
|
|
135
|
+
filter.maskImage = scaledMask
|
|
136
|
+
|
|
137
|
+
guard let outputImage = filter.outputImage,
|
|
138
|
+
let cgImage = context.createCGImage(outputImage, from: ciImage.extent) else {
|
|
139
|
+
throw ImageProcessingException()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let finalImage = UIImage(cgImage: cgImage)
|
|
143
|
+
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).png")
|
|
144
|
+
|
|
145
|
+
guard let data = finalImage.pngData() else { throw ImageProcessingException() }
|
|
146
|
+
try data.write(to: fileURL)
|
|
147
|
+
|
|
148
|
+
return fileURL.absoluteString
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func downscaleImage(image: UIImage, maxDimension: CGFloat) -> UIImage {
|
|
152
|
+
let size = image.size
|
|
153
|
+
if size.width <= maxDimension && size.height <= maxDimension { return image }
|
|
154
|
+
let ratio = min(maxDimension / size.width, maxDimension / size.height)
|
|
155
|
+
let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
|
|
156
|
+
return UIGraphicsImageRenderer(size: newSize).image { _ in
|
|
157
|
+
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
// MARK: - Exceptions
|
|
163
|
+
internal class ImageLoadingException: Exception {
|
|
164
|
+
override var reason: String { "Could not load image from the provided URI" }
|
|
165
|
+
}
|
|
166
|
+
internal class ImageProcessingException: Exception {
|
|
167
|
+
override var reason: String { "Internal error during image masking or saving" }
|
|
168
|
+
}
|
|
169
|
+
internal class NoSubjectDetectedException: Exception {
|
|
170
|
+
override var reason: String { "Vision could not detect any subjects in this image" }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
// This view will be used as a native component. Make sure to inherit from `ExpoView`
|
|
5
|
+
// to apply the proper styling (e.g. border radius and shadows).
|
|
6
|
+
class ExpoBackgroundRemoverView: ExpoView {
|
|
7
|
+
let webView = WKWebView()
|
|
8
|
+
let onLoad = EventDispatcher()
|
|
9
|
+
var delegate: WebViewDelegate?
|
|
10
|
+
|
|
11
|
+
required init(appContext: AppContext? = nil) {
|
|
12
|
+
super.init(appContext: appContext)
|
|
13
|
+
clipsToBounds = true
|
|
14
|
+
delegate = WebViewDelegate { url in
|
|
15
|
+
self.onLoad(["url": url])
|
|
16
|
+
}
|
|
17
|
+
webView.navigationDelegate = delegate
|
|
18
|
+
addSubview(webView)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override func layoutSubviews() {
|
|
22
|
+
webView.frame = bounds
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class WebViewDelegate: NSObject, WKNavigationDelegate {
|
|
27
|
+
let onUrlChange: (String) -> Void
|
|
28
|
+
|
|
29
|
+
init(onUrlChange: @escaping (String) -> Void) {
|
|
30
|
+
self.onUrlChange = onUrlChange
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) {
|
|
34
|
+
if let url = webView.url {
|
|
35
|
+
onUrlChange(url.absoluteString)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-background-remover",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Expo Module For High Quality Background Remover",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepare": "expo-module prepare",
|
|
13
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
+
"expo-module": "expo-module",
|
|
15
|
+
"open:ios": "xed example/ios",
|
|
16
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"expo",
|
|
21
|
+
"expo-background-remover",
|
|
22
|
+
"ExpoBackgroundRemover"
|
|
23
|
+
],
|
|
24
|
+
"repository": "https://github.com/tsnguyenducphuong/background-remover",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/tsnguyenducphuong/background-remover/issues"
|
|
27
|
+
},
|
|
28
|
+
"author": "Phuong Nguyen <ts.nguyenducphuong@gmail.com> (https://github.com/tsnguyenducphuong)",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://github.com/tsnguyenducphuong/background-remover#readme",
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "~19.1.1",
|
|
34
|
+
"expo-module-scripts": "^5.0.8",
|
|
35
|
+
"expo": "^54.0.13",
|
|
36
|
+
"react-native": "0.81.4"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"expo": "*",
|
|
40
|
+
"react": "*",
|
|
41
|
+
"react-native": "*"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type OnLoadEventPayload = {
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type ExpoBackgroundRemoverModuleEvents = {
|
|
8
|
+
onChange: (params: ChangeEventPayload) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ChangeEventPayload = {
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ExpoBackgroundRemoverViewProps = {
|
|
16
|
+
url: string;
|
|
17
|
+
onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class BackgroundRemoverError extends Error {
|
|
22
|
+
constructor(message: string) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'BackgroundRemoverError';
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NativeModule, requireNativeModule } from 'expo';
|
|
2
|
+
|
|
3
|
+
import { ExpoBackgroundRemoverModuleEvents } from './ExpoBackgroundRemover.types';
|
|
4
|
+
|
|
5
|
+
declare class ExpoBackgroundRemoverModule extends NativeModule<ExpoBackgroundRemoverModuleEvents> {
|
|
6
|
+
PI: number;
|
|
7
|
+
hello(): string;
|
|
8
|
+
setValueAsync(value: string): Promise<void>;
|
|
9
|
+
removeBackgroundAsync(imageUri: string): Promise<string> ;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// This call loads the native module object from the JSI.
|
|
13
|
+
export default requireNativeModule<ExpoBackgroundRemoverModule>('ExpoBackgroundRemover');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registerWebModule, NativeModule } from 'expo';
|
|
2
|
+
|
|
3
|
+
import { ExpoBackgroundRemoverModuleEvents } from './ExpoBackgroundRemover.types';
|
|
4
|
+
|
|
5
|
+
class ExpoBackgroundRemoverModule extends NativeModule<ExpoBackgroundRemoverModuleEvents> {
|
|
6
|
+
PI = Math.PI;
|
|
7
|
+
async setValueAsync(value: string): Promise<void> {
|
|
8
|
+
this.emit('onChange', { value });
|
|
9
|
+
}
|
|
10
|
+
hello() {
|
|
11
|
+
return 'Hello world! 👋';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default registerWebModule(ExpoBackgroundRemoverModule, 'ExpoBackgroundRemoverModule');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { ExpoBackgroundRemoverViewProps } from './ExpoBackgroundRemover.types';
|
|
5
|
+
|
|
6
|
+
const NativeView: React.ComponentType<ExpoBackgroundRemoverViewProps> =
|
|
7
|
+
requireNativeView('ExpoBackgroundRemover');
|
|
8
|
+
|
|
9
|
+
export default function ExpoBackgroundRemoverView(props: ExpoBackgroundRemoverViewProps) {
|
|
10
|
+
return <NativeView {...props} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ExpoBackgroundRemoverViewProps } from './ExpoBackgroundRemover.types';
|
|
4
|
+
|
|
5
|
+
export default function ExpoBackgroundRemoverView(props: ExpoBackgroundRemoverViewProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
<iframe
|
|
9
|
+
style={{ flex: 1 }}
|
|
10
|
+
src={props.url}
|
|
11
|
+
onLoad={() => props.onLoad({ nativeEvent: { url: props.url } })}
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
import { BackgroundRemoverError } from './ExpoBackgroundRemover.types';
|
|
3
|
+
import ExpoBackgroundRemoverModule from './ExpoBackgroundRemoverModule';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Removes the background from an image using on-device ML.
|
|
8
|
+
* * @param imageUri - The local file URI of the source image.
|
|
9
|
+
* @returns A promise that resolves to the local file URI of the transparent PNG.
|
|
10
|
+
* @throws Will throw an error if the image cannot be loaded or no subjects are found.
|
|
11
|
+
*/
|
|
12
|
+
export async function removeBackgroundAsync(imageUri: string): Promise<string> {
|
|
13
|
+
if (!imageUri || typeof imageUri !== 'string') {
|
|
14
|
+
throw new BackgroundRemoverError('A valid image URI string is required.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const resultUri = await ExpoBackgroundRemoverModule.removeBackgroundAsync(imageUri);
|
|
19
|
+
return resultUri;
|
|
20
|
+
} catch (error: any) {
|
|
21
|
+
// Catch native exceptions (e.g., NoSubjectDetectedException from Swift)
|
|
22
|
+
// and format them nicely for the JS thread.
|
|
23
|
+
throw new BackgroundRemoverError(error?.message || 'Failed to process image background.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// Reexport the native module. On web, it will be resolved to ExpoBackgroundRemoverModule.web.ts
|
|
30
|
+
// and on native platforms to ExpoBackgroundRemoverModule.ts
|
|
31
|
+
export { default } from './ExpoBackgroundRemoverModule';
|
|
32
|
+
export { default as ExpoBackgroundRemoverView } from './ExpoBackgroundRemoverView';
|
|
33
|
+
export * from './ExpoBackgroundRemover.types';
|
|
34
|
+
|
|
35
|
+
|
package/tsconfig.json
ADDED