expo-background-remover 1.2.0 → 1.2.2

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.
@@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
2
2
  apply plugin: 'kotlin-android'
3
3
 
4
4
  group = 'expo.modules.backgroundremover'
5
- version = '1.2.0'
5
+ version = '1.2.2'
6
6
 
7
7
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
8
8
  apply from: expoModulesCorePlugin
@@ -37,7 +37,7 @@ android {
37
37
  namespace "expo.modules.backgroundremover"
38
38
  defaultConfig {
39
39
  versionCode 1
40
- versionName "1.2.0"
40
+ versionName "1.2.2"
41
41
  }
42
42
  lintOptions {
43
43
  abortOnError false
@@ -12,6 +12,8 @@ import kotlinx.coroutines.tasks.await
12
12
  import kotlinx.coroutines.withContext
13
13
  import java.io.File
14
14
  import java.io.FileOutputStream
15
+ import java.io.InputStream
16
+ import java.io.FileInputStream
15
17
  import java.util.UUID
16
18
  import android.graphics.PorterDuff
17
19
  import android.graphics.PorterDuffXfermode
@@ -25,7 +27,9 @@ class BackgroundRemoverProcessor(private val context: Context) {
25
27
  )
26
28
 
27
29
  suspend fun processImage(uriString: String): String = withContext(Dispatchers.IO) {
28
- val bitmap = loadAndResizeBitmap(uriString, 2048)
30
+ var bitmap: Bitmap? = null
31
+ try{
32
+ bitmap = loadAndResizeBitmap(uriString, 2048)
29
33
 
30
34
  // FIX 1: Defined getRotation helper
31
35
  val inputImage = InputImage.fromBitmap(bitmap, getRotation(uriString))
@@ -42,32 +46,47 @@ class BackgroundRemoverProcessor(private val context: Context) {
42
46
  val maskBitmap = createMaskFromBuffer(maskBuffer, maskWidth, maskHeight)
43
47
  val outputBitmap = applyMaskToBitmap(bitmap, maskBitmap)
44
48
 
45
- saveResult(outputBitmap)
49
+ // mask no longer needed
50
+ maskBitmap.recycle()
51
+
52
+ val resultPath = saveResult(outputBitmap)
53
+
54
+ // cleanup
55
+ outputBitmap.recycle()
56
+ return@withContext resultPath
57
+ }finally {
58
+ // ALWAYS recycle original bitmap
59
+ bitmap?.recycle()
60
+ }
61
+
46
62
  }
47
63
 
48
64
  // Helper to handle image rotation logic
49
- private fun getRotation(uriString: String): Int {
50
- return try {
51
- val inputStream = context.contentResolver.openInputStream(Uri.parse(uriString))
52
- val exifInterface = inputStream?.use { ExifInterface(it) }
53
- when (exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
54
- ExifInterface.ORIENTATION_ROTATE_90 -> 90
55
- ExifInterface.ORIENTATION_ROTATE_180 -> 180
56
- ExifInterface.ORIENTATION_ROTATE_270 -> 270
57
- else -> 0
58
- }
59
- } catch (e: Exception) {
60
- 0
65
+ private fun getRotation(uriString: String): Int {
66
+ return try {
67
+ // Use the helper and the .use block for automatic closing
68
+ val exifInterface = openStream(uriString)?.use { ExifInterface(it) }
69
+
70
+ when (exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
71
+ ExifInterface.ORIENTATION_ROTATE_90 -> 90
72
+ ExifInterface.ORIENTATION_ROTATE_180 -> 180
73
+ ExifInterface.ORIENTATION_ROTATE_270 -> 270
74
+ else -> 0
61
75
  }
76
+ } catch (e: Exception) {
77
+ e.printStackTrace() // Logs the specific exception (e.g., FileNotFound, SecurityException)
78
+ 0
62
79
  }
80
+ }
63
81
 
64
82
  private fun loadAndResizeBitmap(uriString: String, maxDimension: Int): Bitmap {
65
83
  val uri = Uri.parse(uriString)
66
- val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
67
-
68
- context.contentResolver.openInputStream(uri)?.use {
69
- BitmapFactory.decodeStream(it, null, options)
70
- } ?: throw Exception("Failed to open input stream")
84
+ val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
85
+
86
+ // Using the helper with the .use extension
87
+ openStream(uriString)?.use { stream ->
88
+ BitmapFactory.decodeStream(stream, null, options)
89
+ } ?: throw Exception("Could not open stream for image")
71
90
 
72
91
  var sampleSize = 1
73
92
  while ((options.outWidth / (sampleSize * 2)) >= maxDimension &&
@@ -76,9 +95,9 @@ class BackgroundRemoverProcessor(private val context: Context) {
76
95
  }
77
96
 
78
97
  val loadOptions = BitmapFactory.Options().apply { inSampleSize = sampleSize }
79
- val subsampledBitmap = context.contentResolver.openInputStream(uri)?.use {
80
- BitmapFactory.decodeStream(it, null, loadOptions)
81
- } ?: throw Exception("Failed to decode image")
98
+ val subsampledBitmap = openStream(uriString)?.use { stream ->
99
+ BitmapFactory.decodeStream(stream, null, loadOptions)
100
+ } ?: throw Exception("Failed to decode subsampled image")
82
101
 
83
102
  val scale = minOf(maxDimension.toFloat() / subsampledBitmap.width, maxDimension.toFloat() / subsampledBitmap.height)
84
103
  if (scale >= 1f) return subsampledBitmap
@@ -125,6 +144,24 @@ class BackgroundRemoverProcessor(private val context: Context) {
125
144
  FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
126
145
  return Uri.fromFile(file).toString()
127
146
  }
147
+
148
+ private fun openStream(uriString: String): java.io.InputStream? {
149
+ val uri = Uri.parse(uriString)
150
+ return when (uri.scheme) {
151
+ "content" -> {
152
+ // Asks the Android System to resolve the virtual URI
153
+ context.contentResolver.openInputStream(uri)
154
+ }
155
+ "file" -> {
156
+ // Opens a direct pipe to the file on the storage
157
+ uri.path?.let { java.io.FileInputStream(it) }
158
+ }
159
+ else -> {
160
+ // Handles raw paths that might not have a scheme prefix
161
+ java.io.FileInputStream(uriString)
162
+ }
163
+ }
164
+ }
128
165
 
129
166
  fun close() {
130
167
  segmenter.close()
@@ -3,6 +3,7 @@ import Vision
3
3
  import CoreImage
4
4
  import CoreImage.CIFilterBuiltins
5
5
  import UIKit
6
+ import Foundation
6
7
 
7
8
  public class ExpoBackgroundRemoverModule: Module {
8
9
  private let context = CIContext()
@@ -94,7 +95,9 @@ public class ExpoBackgroundRemoverModule: Module {
94
95
  guard let result = request.results?.first else { throw NoSubjectDetectedException() }
95
96
 
96
97
  // Generate combined mask for all subjects
97
- let maskBuffer = try result.generateMask(forInstances: result.allInstances)
98
+ guard let maskBuffer = try result.generateMask(forInstances: result.allInstances) else {
99
+ throw ImageProcessingException()
100
+ }
98
101
  return try applyMaskAndSave(image: image, maskBuffer: maskBuffer)
99
102
  }
100
103
 
@@ -129,10 +132,10 @@ public class ExpoBackgroundRemoverModule: Module {
129
132
  guard let filter = CIFilter(name: "CIBlendWithMask") else {
130
133
  throw ImageProcessingException()
131
134
  }
132
- filter.inputImage = ciImage
135
+ filter.setValue(ciImage, forKey: kCIInputImageKey)
133
136
  let transparentBG = CIImage(color: .clear).cropped(to: ciImage.extent)
134
- filter.backgroundImage = transparentBG // Ensures transparency where mask is 0
135
- filter.maskImage = scaledMask
137
+ filter.setValue(transparentBG, forKey: kCIInputBackgroundImageKey)
138
+ filter.setValue(scaledMask, forKey: kCIInputMaskImageKey)
136
139
 
137
140
  guard let outputImage = filter.outputImage,
138
141
  let cgImage = context.createCGImage(outputImage, from: ciImage.extent) else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-background-remover",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Expo Module For High Quality Background Remover",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",