rn-remove-image-bg 0.0.10
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/NitroRnRemoveImageBg.podspec +33 -0
- package/README.md +386 -0
- package/android/CMakeLists.txt +28 -0
- package/android/build.gradle +142 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt +31 -0
- package/app.plugin.js +12 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridImageBackgroundRemover.swift +224 -0
- package/ios/NitroRnRemoveImageBgOnLoad.mm +22 -0
- package/lib/ImageProcessing.d.ts +167 -0
- package/lib/ImageProcessing.js +323 -0
- package/lib/ImageProcessing.web.d.ts +80 -0
- package/lib/ImageProcessing.web.js +248 -0
- package/lib/__tests__/cache.test.d.ts +1 -0
- package/lib/__tests__/cache.test.js +87 -0
- package/lib/__tests__/errors.test.d.ts +1 -0
- package/lib/__tests__/errors.test.js +82 -0
- package/lib/cache.d.ts +72 -0
- package/lib/cache.js +228 -0
- package/lib/errors.d.ts +20 -0
- package/lib/errors.js +64 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +9 -0
- package/lib/specs/Example.nitro.d.ts +0 -0
- package/lib/specs/Example.nitro.js +2 -0
- package/lib/specs/ImageBackgroundRemover.nitro.d.ts +41 -0
- package/lib/specs/ImageBackgroundRemover.nitro.js +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.cpp +44 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.cpp +72 -0
- package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.hpp +65 -0
- package/nitrogen/generated/android/c++/JNativeRemoveBackgroundOptions.hpp +66 -0
- package/nitrogen/generated/android/c++/JOutputFormat.hpp +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemoverSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NativeRemoveBackgroundOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/OutputFormat.kt +21 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp +111 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.hpp +82 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec_cxx.swift +138 -0
- package/nitrogen/generated/ios/swift/NativeRemoveBackgroundOptions.swift +58 -0
- package/nitrogen/generated/ios/swift/OutputFormat.swift +40 -0
- package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/NativeRemoveBackgroundOptions.hpp +84 -0
- package/nitrogen/generated/shared/c++/OutputFormat.hpp +76 -0
- package/package.json +104 -0
- package/react-native.config.js +16 -0
- package/src/ImageProcessing.ts +532 -0
- package/src/ImageProcessing.web.ts +342 -0
- package/src/__tests__/ImageProcessing.test.ts +278 -0
- package/src/__tests__/cache.test.ts +110 -0
- package/src/__tests__/errors.test.ts +117 -0
- package/src/cache.ts +305 -0
- package/src/errors.ts +93 -0
- package/src/index.ts +49 -0
- package/src/specs/Example.nitro.ts +1 -0
- package/src/specs/ImageBackgroundRemover.nitro.ts +49 -0
package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package com.margelo.nitro.rnremoveimagebg
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.os.Build
|
|
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 com.margelo.nitro.rnremoveimagebg.HybridImageBackgroundRemoverSpec
|
|
10
|
+
import com.margelo.nitro.rnremoveimagebg.NativeRemoveBackgroundOptions
|
|
11
|
+
import com.margelo.nitro.rnremoveimagebg.OutputFormat
|
|
12
|
+
import com.margelo.nitro.core.Promise
|
|
13
|
+
import com.margelo.nitro.NitroModules
|
|
14
|
+
import java.io.File
|
|
15
|
+
import java.io.FileOutputStream
|
|
16
|
+
import java.util.UUID
|
|
17
|
+
import kotlin.coroutines.resume
|
|
18
|
+
import kotlin.coroutines.resumeWithException
|
|
19
|
+
import kotlin.coroutines.suspendCoroutine
|
|
20
|
+
import kotlin.math.max
|
|
21
|
+
import kotlinx.coroutines.Dispatchers
|
|
22
|
+
import kotlinx.coroutines.withContext
|
|
23
|
+
|
|
24
|
+
class HybridImageBackgroundRemover : HybridImageBackgroundRemoverSpec() {
|
|
25
|
+
override val memorySize: Long
|
|
26
|
+
get() = 0L
|
|
27
|
+
|
|
28
|
+
private val segmenter by lazy {
|
|
29
|
+
val options = SubjectSegmenterOptions.Builder()
|
|
30
|
+
.enableForegroundBitmap()
|
|
31
|
+
.build()
|
|
32
|
+
SubjectSegmentation.getClient(options)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun removeBackground(imagePath: String, options: NativeRemoveBackgroundOptions): Promise<String> {
|
|
36
|
+
return Promise.async {
|
|
37
|
+
withContext(Dispatchers.Default) {
|
|
38
|
+
processImage(imagePath, options)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private suspend fun processImage(imagePath: String, options: NativeRemoveBackgroundOptions): String {
|
|
44
|
+
val cleanPath = if (imagePath.startsWith("file://")) imagePath.substring(7) else imagePath
|
|
45
|
+
val file = File(cleanPath)
|
|
46
|
+
|
|
47
|
+
if (!file.exists() || !file.canRead()) {
|
|
48
|
+
throw Exception("File does not exist or is not readable: $imagePath")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Decode with downsampling for large images
|
|
52
|
+
val bitmap = decodeBitmapEfficiently(cleanPath, options.maxDimension.toInt())
|
|
53
|
+
?: throw Exception("Could not decode image at path: $imagePath")
|
|
54
|
+
|
|
55
|
+
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
56
|
+
|
|
57
|
+
return suspendCoroutine { continuation ->
|
|
58
|
+
segmenter.process(inputImage)
|
|
59
|
+
.addOnSuccessListener { result ->
|
|
60
|
+
val foregroundBitmap = result.foregroundBitmap
|
|
61
|
+
if (foregroundBitmap != null) {
|
|
62
|
+
try {
|
|
63
|
+
val context = NitroModules.applicationContext
|
|
64
|
+
if (context == null) {
|
|
65
|
+
bitmap.recycle()
|
|
66
|
+
foregroundBitmap.recycle()
|
|
67
|
+
continuation.resumeWithException(Exception("Application Context is null"))
|
|
68
|
+
return@addOnSuccessListener
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
val outputPath = saveImage(
|
|
72
|
+
foregroundBitmap,
|
|
73
|
+
context.cacheDir,
|
|
74
|
+
options.format,
|
|
75
|
+
options.quality.toInt()
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
bitmap.recycle()
|
|
79
|
+
foregroundBitmap.recycle()
|
|
80
|
+
continuation.resume(outputPath)
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
bitmap.recycle()
|
|
83
|
+
foregroundBitmap.recycle()
|
|
84
|
+
continuation.resumeWithException(e)
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
bitmap.recycle()
|
|
88
|
+
continuation.resumeWithException(Exception("Could not generate foreground bitmap"))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
.addOnFailureListener { e ->
|
|
92
|
+
bitmap.recycle()
|
|
93
|
+
continuation.resumeWithException(e)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decode bitmap with efficient sampling for large images
|
|
100
|
+
*/
|
|
101
|
+
private fun decodeBitmapEfficiently(path: String, maxDimension: Int): Bitmap? {
|
|
102
|
+
// First, get image dimensions without loading into memory
|
|
103
|
+
val boundsOptions = BitmapFactory.Options().apply {
|
|
104
|
+
inJustDecodeBounds = true
|
|
105
|
+
}
|
|
106
|
+
BitmapFactory.decodeFile(path, boundsOptions)
|
|
107
|
+
|
|
108
|
+
val imageWidth = boundsOptions.outWidth
|
|
109
|
+
val imageHeight = boundsOptions.outHeight
|
|
110
|
+
|
|
111
|
+
if (imageWidth <= 0 || imageHeight <= 0) {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Calculate sample size
|
|
116
|
+
val sampleSize = calculateInSampleSize(imageWidth, imageHeight, maxDimension)
|
|
117
|
+
|
|
118
|
+
// Decode with sample size
|
|
119
|
+
val decodeOptions = BitmapFactory.Options().apply {
|
|
120
|
+
inSampleSize = sampleSize
|
|
121
|
+
inPreferredConfig = Bitmap.Config.ARGB_8888
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return BitmapFactory.decodeFile(path, decodeOptions)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Calculate optimal inSampleSize for downsampling
|
|
129
|
+
*/
|
|
130
|
+
private fun calculateInSampleSize(width: Int, height: Int, maxDimension: Int): Int {
|
|
131
|
+
var sampleSize = 1
|
|
132
|
+
val maxSize = max(width, height)
|
|
133
|
+
|
|
134
|
+
if (maxSize > maxDimension) {
|
|
135
|
+
val halfWidth = width / 2
|
|
136
|
+
val halfHeight = height / 2
|
|
137
|
+
|
|
138
|
+
// Calculate the largest inSampleSize value that is a power of 2
|
|
139
|
+
// and keeps both dimensions above the requested max dimension
|
|
140
|
+
while ((halfWidth / sampleSize) >= maxDimension &&
|
|
141
|
+
(halfHeight / sampleSize) >= maxDimension) {
|
|
142
|
+
sampleSize *= 2
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return sampleSize
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Save bitmap to cache directory with specified format and quality
|
|
151
|
+
*/
|
|
152
|
+
@Suppress("DEPRECATION")
|
|
153
|
+
private fun saveImage(
|
|
154
|
+
bitmap: Bitmap,
|
|
155
|
+
outputDir: File,
|
|
156
|
+
format: OutputFormat,
|
|
157
|
+
quality: Int
|
|
158
|
+
): String {
|
|
159
|
+
val compressFormat = when (format) {
|
|
160
|
+
OutputFormat.WEBP -> {
|
|
161
|
+
// Use lossy for quality < 100 (smaller files), lossless for 100
|
|
162
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
163
|
+
if (quality < 100) Bitmap.CompressFormat.WEBP_LOSSY
|
|
164
|
+
else Bitmap.CompressFormat.WEBP_LOSSLESS
|
|
165
|
+
} else {
|
|
166
|
+
// Pre-Android 11 uses deprecated WEBP format
|
|
167
|
+
Bitmap.CompressFormat.WEBP
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
OutputFormat.PNG -> Bitmap.CompressFormat.PNG
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
val extension = when (format) {
|
|
174
|
+
OutputFormat.WEBP -> "webp"
|
|
175
|
+
OutputFormat.PNG -> "png"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
val outputFile = File(outputDir, "bg_removed_${UUID.randomUUID()}.$extension")
|
|
179
|
+
|
|
180
|
+
FileOutputStream(outputFile).use { outStream ->
|
|
181
|
+
// For PNG, quality is ignored (always lossless)
|
|
182
|
+
// For WEBP, use the provided quality
|
|
183
|
+
val finalQuality = if (format == OutputFormat.PNG) 100 else quality
|
|
184
|
+
bitmap.compress(compressFormat, finalQuality, outStream)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return outputFile.absolutePath
|
|
188
|
+
}
|
|
189
|
+
}
|
package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.margelo.nitro.rnremoveimagebg
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
import com.facebook.react.uimanager.ViewManager
|
|
8
|
+
|
|
9
|
+
class NitroRnRemoveImageBgPackage : TurboReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
15
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
init {
|
|
22
|
+
try {
|
|
23
|
+
System.loadLibrary("NitroRnRemoveImageBg")
|
|
24
|
+
println("🔥 Loaded native library: NitroRnRemoveImageBg")
|
|
25
|
+
} catch (e: Throwable) {
|
|
26
|
+
println("❌ Failed to load NitroRnRemoveImageBg: $e")
|
|
27
|
+
}
|
|
28
|
+
NitroRnRemoveImageBgOnLoad.initializeNative()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const { createRunOncePlugin } = require('@expo/config-plugins');
|
|
2
|
+
const pkg = require('./package.json');
|
|
3
|
+
|
|
4
|
+
const withRnRemoveImageBg = (config) => {
|
|
5
|
+
return config;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
module.exports = createRunOncePlugin(
|
|
9
|
+
withRnRemoveImageBg,
|
|
10
|
+
pkg.name,
|
|
11
|
+
pkg.version
|
|
12
|
+
);
|
package/ios/Bridge.h
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Vision
|
|
3
|
+
import CoreImage
|
|
4
|
+
import UIKit
|
|
5
|
+
import CoreML
|
|
6
|
+
import UniformTypeIdentifiers
|
|
7
|
+
|
|
8
|
+
class HybridImageBackgroundRemover: HybridImageBackgroundRemoverSpec {
|
|
9
|
+
var memorySize: Int {
|
|
10
|
+
return 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Reuse CIContext for better performance
|
|
14
|
+
private let ciContext: CIContext = {
|
|
15
|
+
// Use Metal for hardware acceleration if available
|
|
16
|
+
if let metalDevice = MTLCreateSystemDefaultDevice() {
|
|
17
|
+
return CIContext(mtlDevice: metalDevice, options: [.cacheIntermediates: false])
|
|
18
|
+
}
|
|
19
|
+
return CIContext(options: [.useSoftwareRenderer: false, .cacheIntermediates: false])
|
|
20
|
+
}()
|
|
21
|
+
|
|
22
|
+
// Lazy load CoreML model for iOS < 17
|
|
23
|
+
private lazy var coreMLModel: VNCoreMLModel? = {
|
|
24
|
+
guard let modelURL = Bundle(for: HybridImageBackgroundRemover.self).url(forResource: "U2Netp", withExtension: "mlmodelc") else {
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
guard let model = try? MLModel(contentsOf: modelURL) else {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
return try? VNCoreMLModel(for: model)
|
|
31
|
+
}()
|
|
32
|
+
|
|
33
|
+
func removeBackground(imagePath: String, options: NativeRemoveBackgroundOptions) throws -> Promise<String> {
|
|
34
|
+
return Promise.async { [self] in
|
|
35
|
+
let maxDimension = Int(options.maxDimension)
|
|
36
|
+
let format = options.format
|
|
37
|
+
let quality = Int(options.quality)
|
|
38
|
+
|
|
39
|
+
// Handle both absolute paths and file:// URLs
|
|
40
|
+
let cleanPath = imagePath.hasPrefix("file://") ? String(imagePath.dropFirst(7)) : imagePath
|
|
41
|
+
let fileUrl = URL(fileURLWithPath: cleanPath)
|
|
42
|
+
|
|
43
|
+
// Validate file exists
|
|
44
|
+
guard FileManager.default.fileExists(atPath: fileUrl.path) else {
|
|
45
|
+
throw RuntimeError.error(withMessage: "File does not exist: \(imagePath)")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
guard var inputImage = CIImage(contentsOf: fileUrl) else {
|
|
49
|
+
throw RuntimeError.error(withMessage: "Could not load image from path: \(imagePath)")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Downsample large images for better performance
|
|
53
|
+
inputImage = self.downsampleIfNeeded(inputImage, maxDimension: maxDimension)
|
|
54
|
+
|
|
55
|
+
var maskImage: CIImage?
|
|
56
|
+
|
|
57
|
+
if #available(iOS 17.0, *) {
|
|
58
|
+
// Use Vision's native foreground instance mask
|
|
59
|
+
let request = VNGenerateForegroundInstanceMaskRequest()
|
|
60
|
+
let handler = VNImageRequestHandler(ciImage: inputImage, options: [:])
|
|
61
|
+
try handler.perform([request])
|
|
62
|
+
|
|
63
|
+
if let result = request.results?.first {
|
|
64
|
+
let maskPixelBuffer = try result.generateScaledMaskForImage(forInstances: result.allInstances, from: handler)
|
|
65
|
+
maskImage = CIImage(cvPixelBuffer: maskPixelBuffer)
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Fallback to CoreML (U2Netp) for iOS < 17
|
|
69
|
+
maskImage = try self.processWithCoreML(inputImage: inputImage)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
guard let mask = maskImage else {
|
|
73
|
+
throw RuntimeError.error(withMessage: "Failed to generate mask")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Apply the mask to the original image
|
|
77
|
+
let maskedImage = inputImage.applyingFilter("CIBlendWithMask", parameters: [
|
|
78
|
+
kCIInputMaskImageKey: mask
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
// Render and save
|
|
82
|
+
return try self.saveImage(maskedImage, format: format, quality: quality)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Downsample image if it exceeds maxDimension
|
|
87
|
+
private func downsampleIfNeeded(_ image: CIImage, maxDimension: Int) -> CIImage {
|
|
88
|
+
let width = image.extent.width
|
|
89
|
+
let height = image.extent.height
|
|
90
|
+
let maxDim = CGFloat(maxDimension)
|
|
91
|
+
|
|
92
|
+
guard width > maxDim || height > maxDim else {
|
|
93
|
+
return image
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let scale: CGFloat
|
|
97
|
+
if width > height {
|
|
98
|
+
scale = maxDim / width
|
|
99
|
+
} else {
|
|
100
|
+
scale = maxDim / height
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return image.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Save processed image to disk
|
|
107
|
+
private func saveImage(_ image: CIImage, format: OutputFormat, quality: Int) throws -> String {
|
|
108
|
+
guard let cgImage = ciContext.createCGImage(image, from: image.extent) else {
|
|
109
|
+
throw RuntimeError.error(withMessage: "Failed to render masked image")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let uiImage = UIImage(cgImage: cgImage)
|
|
113
|
+
let tempDir = FileManager.default.temporaryDirectory
|
|
114
|
+
let uuid = UUID().uuidString
|
|
115
|
+
|
|
116
|
+
let data: Data?
|
|
117
|
+
let fileName: String
|
|
118
|
+
|
|
119
|
+
switch format {
|
|
120
|
+
case .png:
|
|
121
|
+
data = uiImage.pngData()
|
|
122
|
+
fileName = "bg_removed_\(uuid).png"
|
|
123
|
+
case .webp:
|
|
124
|
+
// iOS doesn't natively support WEBP encoding
|
|
125
|
+
// Use HEIC on iOS 17+ (supports alpha), otherwise fall back to PNG
|
|
126
|
+
if #available(iOS 17.0, *) {
|
|
127
|
+
data = uiImage.heicData()
|
|
128
|
+
fileName = "bg_removed_\(uuid).heic"
|
|
129
|
+
} else {
|
|
130
|
+
data = uiImage.pngData()
|
|
131
|
+
fileName = "bg_removed_\(uuid).png"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
guard let imageData = data else {
|
|
136
|
+
throw RuntimeError.error(withMessage: "Failed to encode image")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let fileURL = tempDir.appendingPathComponent(fileName)
|
|
140
|
+
try imageData.write(to: fileURL)
|
|
141
|
+
|
|
142
|
+
return fileURL.path
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func processWithCoreML(inputImage: CIImage) throws -> CIImage? {
|
|
146
|
+
guard let vnModel = coreMLModel else {
|
|
147
|
+
throw RuntimeError.error(withMessage: "Could not load U2Netp model")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let request = VNCoreMLRequest(model: vnModel)
|
|
151
|
+
request.imageCropAndScaleOption = .scaleFill
|
|
152
|
+
|
|
153
|
+
let handler = VNImageRequestHandler(ciImage: inputImage, options: [:])
|
|
154
|
+
try handler.perform([request])
|
|
155
|
+
|
|
156
|
+
// Try VNPixelBufferObservation first
|
|
157
|
+
if let results = request.results as? [VNPixelBufferObservation], let observation = results.first {
|
|
158
|
+
return CIImage(cvPixelBuffer: observation.pixelBuffer)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fallback to VNCoreMLFeatureValueObservation (MultiArray)
|
|
162
|
+
if let results = request.results as? [VNCoreMLFeatureValueObservation],
|
|
163
|
+
let feature = results.first?.featureValue.multiArrayValue {
|
|
164
|
+
return try self.convertMultiArrayToImage(multiArray: feature, originalSize: inputImage.extent.size)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw RuntimeError.error(withMessage: "No valid result from CoreML model")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func convertMultiArrayToImage(multiArray: MLMultiArray, originalSize: CGSize) throws -> CIImage? {
|
|
171
|
+
// U2Net output is typically [1, 1, H, W] where H,W are usually 320x320
|
|
172
|
+
guard multiArray.shape.count >= 3 else {
|
|
173
|
+
throw RuntimeError.error(withMessage: "Unexpected MultiArray shape: \(multiArray.shape)")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let height = multiArray.shape[multiArray.shape.count - 2].intValue
|
|
177
|
+
let width = multiArray.shape[multiArray.shape.count - 1].intValue
|
|
178
|
+
|
|
179
|
+
// Create grayscale bitmap from MultiArray
|
|
180
|
+
var pixelBuffer: CVPixelBuffer?
|
|
181
|
+
let status = CVPixelBufferCreate(
|
|
182
|
+
kCFAllocatorDefault,
|
|
183
|
+
width,
|
|
184
|
+
height,
|
|
185
|
+
kCVPixelFormatType_OneComponent8,
|
|
186
|
+
nil,
|
|
187
|
+
&pixelBuffer
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
|
|
191
|
+
throw RuntimeError.error(withMessage: "Failed to create pixel buffer")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
CVPixelBufferLockBaseAddress(buffer, [])
|
|
195
|
+
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
|
|
196
|
+
|
|
197
|
+
guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
|
|
198
|
+
throw RuntimeError.error(withMessage: "Failed to get pixel buffer base address")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
|
202
|
+
|
|
203
|
+
// Copy and normalize values from MultiArray (0.0-1.0) to UInt8 (0-255)
|
|
204
|
+
for y in 0..<height {
|
|
205
|
+
for x in 0..<width {
|
|
206
|
+
let index = y * width + x
|
|
207
|
+
let value = multiArray[[0, 0, y as NSNumber, x as NSNumber]].floatValue
|
|
208
|
+
pointer[index] = UInt8(min(max(value * 255.0, 0), 255))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Convert to CIImage and scale to original size
|
|
213
|
+
let maskImage = CIImage(cvPixelBuffer: buffer)
|
|
214
|
+
let scaleX = originalSize.width / CGFloat(width)
|
|
215
|
+
let scaleY = originalSize.height / CGFloat(height)
|
|
216
|
+
return maskImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@_cdecl("createHybridImageBackgroundRemover")
|
|
221
|
+
public func createHybridImageBackgroundRemover() -> UnsafeMutableRawPointer {
|
|
222
|
+
let hybridObject = HybridImageBackgroundRemover()
|
|
223
|
+
return hybridObject.getCxxWrapper().toUnsafe()
|
|
224
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <NitroModules/HybridObjectRegistry.hpp>
|
|
3
|
+
#import "NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp"
|
|
4
|
+
|
|
5
|
+
extern "C" void* createHybridImageBackgroundRemover();
|
|
6
|
+
|
|
7
|
+
@interface NitroRnRemoveImageBgOnLoad : NSObject
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation NitroRnRemoveImageBgOnLoad
|
|
11
|
+
|
|
12
|
+
+ (void)load {
|
|
13
|
+
margelo::nitro::HybridObjectRegistry::registerHybridObjectConstructor(
|
|
14
|
+
"ImageBackgroundRemover",
|
|
15
|
+
[]() -> std::shared_ptr<margelo::nitro::HybridObject> {
|
|
16
|
+
void* unsafe = createHybridImageBackgroundRemover();
|
|
17
|
+
return margelo::nitro::rnremoveimagebg::bridge::swift::create_std__shared_ptr_HybridImageBackgroundRemoverSpec_(unsafe);
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
2
|
+
import type { OutputFormat, NativeRemoveBackgroundOptions } from './specs/ImageBackgroundRemover.nitro';
|
|
3
|
+
export type { OutputFormat, NativeRemoveBackgroundOptions };
|
|
4
|
+
export interface CompressImageOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Maximum file size in KB (default: 250)
|
|
7
|
+
*/
|
|
8
|
+
maxSizeKB?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Initial image width (default: 1024)
|
|
11
|
+
*/
|
|
12
|
+
width?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Initial image height (default: 1024)
|
|
15
|
+
*/
|
|
16
|
+
height?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Initial compression quality (0-1, default: 0.85)
|
|
19
|
+
*/
|
|
20
|
+
quality?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Image format (default: WEBP)
|
|
23
|
+
*/
|
|
24
|
+
format?: ImageManipulator.SaveFormat;
|
|
25
|
+
}
|
|
26
|
+
export interface GenerateThumbhashOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Thumbhash size (default: 32)
|
|
29
|
+
*/
|
|
30
|
+
size?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Options for background removal
|
|
34
|
+
*/
|
|
35
|
+
export interface RemoveBgImageOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Maximum dimension (width or height) for processing
|
|
38
|
+
* Larger images will be downsampled for better performance
|
|
39
|
+
* @default 2048
|
|
40
|
+
*/
|
|
41
|
+
maxDimension?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Output image format
|
|
44
|
+
* - PNG: Lossless, larger file size, best for transparency
|
|
45
|
+
* - WEBP: Smaller file size, good quality
|
|
46
|
+
* @default 'PNG'
|
|
47
|
+
*/
|
|
48
|
+
format?: OutputFormat;
|
|
49
|
+
/**
|
|
50
|
+
* Quality for WEBP format (0-100)
|
|
51
|
+
* Ignored when format is PNG
|
|
52
|
+
* @default 100
|
|
53
|
+
*/
|
|
54
|
+
quality?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Progress callback (0-100)
|
|
57
|
+
* Note: Progress is approximate and may not be linear
|
|
58
|
+
*/
|
|
59
|
+
onProgress?: (progress: number) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Use cached result if available
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
useCache?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Enable debug logging
|
|
67
|
+
* @default false
|
|
68
|
+
*/
|
|
69
|
+
debug?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compress image to WebP format with configurable options
|
|
73
|
+
*/
|
|
74
|
+
export declare function compressImage(uri: string, options?: CompressImageOptions): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Generate thumbhash from image URI (Native/Mobile)
|
|
77
|
+
*/
|
|
78
|
+
export declare function generateThumbhash(imageUri: string, options?: GenerateThumbhashOptions): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Remove background from image using native ML models
|
|
81
|
+
*
|
|
82
|
+
* @param uri - File path or file:// URI to the source image
|
|
83
|
+
* @param options - Processing options
|
|
84
|
+
* @returns Promise resolving to a URI suitable for use with `<Image>` component.
|
|
85
|
+
* - **iOS/Android**: File path (`file:///path/to/cache/bg_removed_xxx.png`)
|
|
86
|
+
* - **Web**: Data URL (`data:image/png;base64,...`)
|
|
87
|
+
*
|
|
88
|
+
* @throws {BackgroundRemovalError} When image cannot be processed
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const result = await removeBgImage('file:///path/to/photo.jpg')
|
|
93
|
+
* // Use directly in Image component
|
|
94
|
+
* <Image source={{ uri: result }} />
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // With options
|
|
100
|
+
* const result = await removeBgImage('file:///path/to/photo.jpg', {
|
|
101
|
+
* maxDimension: 1024,
|
|
102
|
+
* format: 'WEBP',
|
|
103
|
+
* quality: 90,
|
|
104
|
+
* onProgress: (p) => console.log(`Progress: ${p}%`)
|
|
105
|
+
* })
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
|
|
109
|
+
/**
|
|
110
|
+
* Backward compatibility alias for removeBgImage
|
|
111
|
+
* @deprecated Use removeBgImage instead
|
|
112
|
+
*/
|
|
113
|
+
export declare const removeBackground: typeof removeBgImage;
|
|
114
|
+
/**
|
|
115
|
+
* Clear the background removal cache
|
|
116
|
+
* @param deleteFiles - Also delete cached files from disk (default: false)
|
|
117
|
+
*/
|
|
118
|
+
export declare function clearCache(deleteFiles?: boolean): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Get the current cache size
|
|
121
|
+
*/
|
|
122
|
+
export declare function getCacheSize(): number;
|
|
123
|
+
/**
|
|
124
|
+
* Handle low memory conditions by clearing the cache
|
|
125
|
+
* Call this when your app receives memory warnings
|
|
126
|
+
*
|
|
127
|
+
* @param deleteFiles - Also delete cached files from disk (default: true)
|
|
128
|
+
* @returns Number of entries that were cleared
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import { AppState } from 'react-native'
|
|
133
|
+
* import { onLowMemory } from 'rn-remove-image-bg'
|
|
134
|
+
*
|
|
135
|
+
* // In your app initialization
|
|
136
|
+
* AppState.addEventListener('memoryWarning', () => {
|
|
137
|
+
* onLowMemory()
|
|
138
|
+
* })
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export declare function onLowMemory(deleteFiles?: boolean): Promise<number>;
|
|
142
|
+
/**
|
|
143
|
+
* Configure the background removal cache
|
|
144
|
+
* Call this early in your app lifecycle to customize cache behavior
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* import { configureCache } from 'rn-remove-image-bg'
|
|
149
|
+
*
|
|
150
|
+
* configureCache({
|
|
151
|
+
* maxEntries: 100,
|
|
152
|
+
* maxAgeMinutes: 60,
|
|
153
|
+
* persistToDisk: true
|
|
154
|
+
* })
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export declare function configureCache(config: {
|
|
158
|
+
maxEntries?: number;
|
|
159
|
+
maxAgeMinutes?: number;
|
|
160
|
+
persistToDisk?: boolean;
|
|
161
|
+
cacheDirectory?: string;
|
|
162
|
+
}): void;
|
|
163
|
+
/**
|
|
164
|
+
* Get the cache directory path
|
|
165
|
+
* Useful for debugging or manual cache management
|
|
166
|
+
*/
|
|
167
|
+
export declare function getCacheDirectory(): string;
|