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.
Files changed (74) hide show
  1. package/NitroRnRemoveImageBg.podspec +33 -0
  2. package/README.md +386 -0
  3. package/android/CMakeLists.txt +28 -0
  4. package/android/build.gradle +142 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +189 -0
  10. package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt +31 -0
  11. package/app.plugin.js +12 -0
  12. package/ios/Bridge.h +8 -0
  13. package/ios/HybridImageBackgroundRemover.swift +224 -0
  14. package/ios/NitroRnRemoveImageBgOnLoad.mm +22 -0
  15. package/lib/ImageProcessing.d.ts +167 -0
  16. package/lib/ImageProcessing.js +323 -0
  17. package/lib/ImageProcessing.web.d.ts +80 -0
  18. package/lib/ImageProcessing.web.js +248 -0
  19. package/lib/__tests__/cache.test.d.ts +1 -0
  20. package/lib/__tests__/cache.test.js +87 -0
  21. package/lib/__tests__/errors.test.d.ts +1 -0
  22. package/lib/__tests__/errors.test.js +82 -0
  23. package/lib/cache.d.ts +72 -0
  24. package/lib/cache.js +228 -0
  25. package/lib/errors.d.ts +20 -0
  26. package/lib/errors.js +64 -0
  27. package/lib/index.d.ts +6 -0
  28. package/lib/index.js +9 -0
  29. package/lib/specs/Example.nitro.d.ts +0 -0
  30. package/lib/specs/Example.nitro.js +2 -0
  31. package/lib/specs/ImageBackgroundRemover.nitro.d.ts +41 -0
  32. package/lib/specs/ImageBackgroundRemover.nitro.js +1 -0
  33. package/nitro.json +17 -0
  34. package/nitrogen/generated/.gitattributes +1 -0
  35. package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.cmake +81 -0
  36. package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.gradle +27 -0
  37. package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.cpp +44 -0
  38. package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.hpp +25 -0
  39. package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.cpp +72 -0
  40. package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.hpp +65 -0
  41. package/nitrogen/generated/android/c++/JNativeRemoveBackgroundOptions.hpp +66 -0
  42. package/nitrogen/generated/android/c++/JOutputFormat.hpp +59 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemoverSpec.kt +58 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NativeRemoveBackgroundOptions.kt +44 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgOnLoad.kt +35 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/OutputFormat.kt +21 -0
  47. package/nitrogen/generated/ios/NitroRnRemoveImageBg+autolinking.rb +60 -0
  48. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.cpp +49 -0
  49. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp +111 -0
  50. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Umbrella.hpp +51 -0
  51. package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.cpp +11 -0
  52. package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.hpp +82 -0
  53. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  54. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  55. package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec.swift +56 -0
  56. package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec_cxx.swift +138 -0
  57. package/nitrogen/generated/ios/swift/NativeRemoveBackgroundOptions.swift +58 -0
  58. package/nitrogen/generated/ios/swift/OutputFormat.swift +40 -0
  59. package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.cpp +21 -0
  60. package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.hpp +65 -0
  61. package/nitrogen/generated/shared/c++/NativeRemoveBackgroundOptions.hpp +84 -0
  62. package/nitrogen/generated/shared/c++/OutputFormat.hpp +76 -0
  63. package/package.json +104 -0
  64. package/react-native.config.js +16 -0
  65. package/src/ImageProcessing.ts +532 -0
  66. package/src/ImageProcessing.web.ts +342 -0
  67. package/src/__tests__/ImageProcessing.test.ts +278 -0
  68. package/src/__tests__/cache.test.ts +110 -0
  69. package/src/__tests__/errors.test.ts +117 -0
  70. package/src/cache.ts +305 -0
  71. package/src/errors.ts +93 -0
  72. package/src/index.ts +49 -0
  73. package/src/specs/Example.nitro.ts +1 -0
  74. package/src/specs/ImageBackgroundRemover.nitro.ts +49 -0
@@ -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
+ }
@@ -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,8 @@
1
+ //
2
+ // Bridge.h
3
+ // NitroRnRemoveImageBg
4
+ //
5
+ // Created by Marc Rousavy on 22.07.24.
6
+ //
7
+
8
+ #pragma once
@@ -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;