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.
Files changed (51) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +35 -0
  3. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  6. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  7. package/android/.gradle/8.9/gc.properties +0 -0
  8. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  9. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  10. package/android/.gradle/vcs-1/gc.properties +0 -0
  11. package/android/build.gradle +69 -0
  12. package/android/src/main/AndroidManifest.xml +7 -0
  13. package/android/src/main/java/expo/modules/backgroundremover/BackgroundRemoverProcessor.kt +104 -0
  14. package/android/src/main/java/expo/modules/backgroundremover/ExpoBackgroundRemoverModule.kt +57 -0
  15. package/android/src/main/java/expo/modules/backgroundremover/ExpoBackgroundRemoverView.kt +30 -0
  16. package/build/ExpoBackgroundRemover.types.d.ts +21 -0
  17. package/build/ExpoBackgroundRemover.types.d.ts.map +1 -0
  18. package/build/ExpoBackgroundRemover.types.js +8 -0
  19. package/build/ExpoBackgroundRemover.types.js.map +1 -0
  20. package/build/ExpoBackgroundRemoverModule.d.ts +11 -0
  21. package/build/ExpoBackgroundRemoverModule.d.ts.map +1 -0
  22. package/build/ExpoBackgroundRemoverModule.js +4 -0
  23. package/build/ExpoBackgroundRemoverModule.js.map +1 -0
  24. package/build/ExpoBackgroundRemoverModule.web.d.ts +10 -0
  25. package/build/ExpoBackgroundRemoverModule.web.d.ts.map +1 -0
  26. package/build/ExpoBackgroundRemoverModule.web.js +12 -0
  27. package/build/ExpoBackgroundRemoverModule.web.js.map +1 -0
  28. package/build/ExpoBackgroundRemoverView.d.ts +4 -0
  29. package/build/ExpoBackgroundRemoverView.d.ts.map +1 -0
  30. package/build/ExpoBackgroundRemoverView.js +7 -0
  31. package/build/ExpoBackgroundRemoverView.js.map +1 -0
  32. package/build/ExpoBackgroundRemoverView.web.d.ts +4 -0
  33. package/build/ExpoBackgroundRemoverView.web.d.ts.map +1 -0
  34. package/build/ExpoBackgroundRemoverView.web.js +7 -0
  35. package/build/ExpoBackgroundRemoverView.web.js.map +1 -0
  36. package/build/index.d.ts +11 -0
  37. package/build/index.d.ts.map +1 -0
  38. package/build/index.js +28 -0
  39. package/build/index.js.map +1 -0
  40. package/expo-module.config.json +9 -0
  41. package/ios/ExpoBackgroundRemover.podspec +32 -0
  42. package/ios/ExpoBackgroundRemoverModule.swift +176 -0
  43. package/ios/ExpoBackgroundRemoverView.swift +38 -0
  44. package/package.json +43 -0
  45. package/src/ExpoBackgroundRemover.types.ts +27 -0
  46. package/src/ExpoBackgroundRemoverModule.ts +13 -0
  47. package/src/ExpoBackgroundRemoverModule.web.ts +15 -0
  48. package/src/ExpoBackgroundRemoverView.tsx +11 -0
  49. package/src/ExpoBackgroundRemoverView.web.tsx +15 -0
  50. package/src/index.ts +35 -0
  51. package/tsconfig.json +9 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/native', 'universe/web'],
4
+ ignorePatterns: ['build'],
5
+ };
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).
File without changes
@@ -0,0 +1,2 @@
1
+ #Fri Apr 10 09:33:17 ICT 2026
2
+ gradle.version=8.9
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,7 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <application>
3
+ <meta-data
4
+ android:name="com.google.mlkit.vision.DEPENDENCIES"
5
+ android:value="subject_segment" />
6
+ </application>
7
+ </manifest>
@@ -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,8 @@
1
+ export class BackgroundRemoverError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'BackgroundRemoverError';
5
+ }
6
+ }
7
+ ;
8
+ //# sourceMappingURL=ExpoBackgroundRemover.types.js.map
@@ -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,4 @@
1
+ import { requireNativeModule } from 'expo';
2
+ // This call loads the native module object from the JSI.
3
+ export default requireNativeModule('ExpoBackgroundRemover');
4
+ //# sourceMappingURL=ExpoBackgroundRemoverModule.js.map
@@ -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"]}
@@ -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,9 @@
1
+ {
2
+ "platforms": ["apple", "android", "web"],
3
+ "apple": {
4
+ "modules": ["ExpoBackgroundRemoverModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.backgroundremover.ExpoBackgroundRemoverModule"]
8
+ }
9
+ }
@@ -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
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
9
+ }