react-native-nitro-image-pipeline 0.3.4 → 1.0.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 (25) hide show
  1. package/README.md +60 -6
  2. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +68 -38
  3. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt +50 -24
  4. package/ios/GaussianBlur.swift +135 -0
  5. package/ios/GaussianBlurProcessor.swift +41 -0
  6. package/ios/HybridNitroImagePipeline.swift +70 -25
  7. package/lib/commonjs/index.js +32 -11
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/module/index.js +33 -12
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/typescript/src/index.d.ts +9 -3
  12. package/lib/typescript/src/index.d.ts.map +1 -1
  13. package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +17 -4
  14. package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
  15. package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +14 -3
  16. package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.hpp +1 -1
  17. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipelineSpec.kt +1 -1
  18. package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +0 -9
  19. package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +3 -1
  20. package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec.swift +1 -1
  21. package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec_cxx.swift +12 -4
  22. package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.hpp +1 -1
  23. package/package.json +21 -13
  24. package/src/index.ts +37 -5
  25. package/src/specs/nitro-image-toolkit.nitro.ts +25 -5
package/README.md CHANGED
@@ -48,7 +48,7 @@ import { useImage } from 'react-native-nitro-image-pipeline';
48
48
  function MyComponent() {
49
49
  const { image, error } = useImage({
50
50
  url: 'https://example.com/photo.jpg',
51
- blur: 4,
51
+ blur: 4, // Gaussian sigma in source pixels — same result on iOS and Android
52
52
  cornerRadius: 12,
53
53
  });
54
54
 
@@ -67,7 +67,7 @@ import { NitroImagePipeline } from 'react-native-nitro-image-pipeline';
67
67
 
68
68
  // Load an image with options
69
69
  const image = await NitroImagePipeline.loadImage('https://example.com/photo.jpg', {
70
- blur: 4,
70
+ blur: 4, // Gaussian sigma in source pixels — see "Blur units"
71
71
  cornerRadius: 12,
72
72
  cache: 'disk',
73
73
  });
@@ -85,7 +85,7 @@ await NitroImagePipeline.preLoadImages([
85
85
  const blurred = await NitroImagePipeline.gaussianBlur(image, 10);
86
86
 
87
87
  // Clear the image cache
88
- NitroImagePipeline.clearCache();
88
+ await NitroImagePipeline.clearCache();
89
89
  ```
90
90
 
91
91
  ## API Reference
@@ -96,7 +96,7 @@ Loads an image from a URL and returns a `Promise<Image>`.
96
96
 
97
97
  | Option | Type | Default | Description |
98
98
  |---|---|---|---|
99
- | `blur` | `number` | `0` | Gaussian blur radius applied at load time |
99
+ | `blur` | `number` | `0` | Gaussian blur strength applied at load time — see [Blur units](#blur-units) |
100
100
  | `cornerRadius` | `number` | `0` | Corner radius in points |
101
101
  | `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
102
102
 
@@ -110,11 +110,65 @@ Prefetches multiple images into the cache. Returns `Promise<void>`.
110
110
 
111
111
  ### `gaussianBlur(image, radius)`
112
112
 
113
- Applies a Gaussian blur to an existing `Image` object. Returns `Promise<Image>`.
113
+ Applies a Gaussian blur to an existing `Image` object. Returns `Promise<Image>`. `radius` uses the
114
+ same unit as the `blur` option — see [Blur units](#blur-units).
115
+
116
+ ### Blur units
117
+
118
+ `blur` (and `gaussianBlur`'s `radius`) is the **standard deviation (sigma) of the Gaussian, in
119
+ source-image pixels**. The same value on the same source file produces the same result on iOS and
120
+ Android — the platforms are calibrated against each other rather than each exposing its native
121
+ backend's own idea of "radius".
122
+
123
+ ```ts
124
+ // ~11px of blur on both platforms, whatever the device
125
+ await NitroImagePipeline.loadImage(url, { blur: 11 });
126
+ ```
127
+
128
+ Two things follow from the unit being *source* pixels:
129
+
130
+ - Blur is measured against the image's own resolution, not the size it is displayed at. A 4000px
131
+ photo at `blur: 11` looks subtler than a 400px thumbnail at `blur: 11`. To keep a feed visually
132
+ consistent, scale the value with the source width.
133
+ - Coming from React Native's `<Image blurRadius={n} />`? That halves its input internally, so
134
+ `blurRadius={n}` ≈ `blur: n / 2`. RN's value is also density-scaled on Android and not on iOS,
135
+ which is why the two never quite matched there.
136
+
137
+ Values below ~1 are smaller than the smallest kernel either backend can build and are effectively a
138
+ no-op. There is no upper bound.
139
+
140
+ Implementation: iOS runs three Accelerate box-convolution passes sized to hit the requested sigma
141
+ (the standard three-box Gaussian approximation, accurate to a few percent); Android uses
142
+ RenderScript's true Gaussian, downscaling first when sigma exceeds the single-pass ceiling of
143
+ ~10.6px and compensating the radius so the result is unchanged. Both clamp at the edges, so blurred
144
+ images keep their borders instead of fading out.
114
145
 
115
146
  ### `clearCache()`
116
147
 
117
- Removes all cached images from memory and disk.
148
+ Removes all cached images from memory and disk. Returns `Promise<void>` that resolves once both caches are cleared.
149
+
150
+ ## Upgrading from 0.3.x
151
+
152
+ `blur` and `gaussianBlur(image, radius)` changed meaning in 1.0. They used to hand the number
153
+ straight to each platform's native blur, and the two platforms disagreed about what it meant; now
154
+ both read it as a Gaussian sigma in source-image pixels (see [Blur units](#blur-units)).
155
+
156
+ | | what `blur: n` did in 0.3.x | what it does in 1.0 |
157
+ |---|---|---|
158
+ | iOS | fed `n` to `CIGaussianBlur(inputRadius:)`, measured at `sigma ≈ 1.18 × n` | `sigma = n` |
159
+ | Android | RenderScript on a copy downscaled to 512px, so strength scaled with the source resolution: `sigma ≈ (0.4n + 0.6) × max(w, h) / 512` | `sigma = n`, resolution-independent |
160
+
161
+ To keep the look you had:
162
+
163
+ - **iOS:** multiply your old value by ~1.18 (`blur: 10` → `blur: 12`).
164
+ - **Android:** there is no single factor — the old result depended on the source image's
165
+ resolution. Re-tune against iOS, which the two platforms now agree with.
166
+
167
+ Also changed:
168
+
169
+ - `blur` above 25 used to reject the promise on Android. Sigma is now unbounded.
170
+ - Fractional values used to be truncated to whole numbers on iOS. They are honoured now.
171
+ - Blurred images used to fade out at the borders on iOS. Edges are clamped on both platforms now.
118
172
 
119
173
  ## Credits
120
174
 
@@ -22,6 +22,11 @@ import com.margelo.nitro.core.Promise
22
22
  import com.margelo.nitro.image.HybridImage
23
23
  import com.margelo.nitro.image.HybridImageSpec
24
24
  import com.margelo.nitro.nitroimagepipeline.transform.BlurTransformation
25
+ import kotlinx.coroutines.Dispatchers
26
+ import kotlinx.coroutines.async
27
+ import kotlinx.coroutines.awaitAll
28
+ import kotlinx.coroutines.coroutineScope
29
+ import kotlinx.coroutines.withContext
25
30
  import okhttp3.OkHttpClient
26
31
  import okio.Path.Companion.toOkioPath
27
32
  import org.chromium.net.CronetEngine
@@ -30,29 +35,8 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
30
35
  private val context
31
36
  get() = NitroModules.applicationContext as Context
32
37
 
33
- private val okHttpClient: OkHttpClient by lazy {
34
- val builder = OkHttpClient.Builder()
35
- try {
36
- val cronetEngine = CronetEngine.Builder(context).build()
37
- builder.addInterceptor(CronetInterceptor.newBuilder(cronetEngine).build())
38
- } catch (_: Exception) {
39
- // Cronet unavailable (no GMS or unsupported device) — use plain OkHttp
40
- }
41
- builder.build()
42
- }
43
-
44
- private val imageLoader: ImageLoader by lazy {
45
- ImageLoader.Builder(context)
46
- .components { add(OkHttpNetworkFetcherFactory(callFactory = okHttpClient)) }
47
- .memoryCache { MemoryCache.Builder().maxSizePercent(context, 0.25).build() }
48
- .diskCache {
49
- DiskCache.Builder()
50
- .directory(context.cacheDir.resolve("nitro_image_cache").toOkioPath())
51
- .maxSizeBytes(256L * 1024 * 1024)
52
- .build()
53
- }
54
- .build()
55
- }
38
+ private val imageLoader: ImageLoader
39
+ get() = getOrCreateImageLoader(context)
56
40
 
57
41
  override fun loadImage(url: String, options: Options?): Promise<HybridImageSpec> = Promise.async {
58
42
  val blur = options?.blur?.toFloat() ?: 0f
@@ -61,11 +45,6 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
61
45
  if (blur > 0f) add(BlurTransformation(context, blur))
62
46
  if (cornerRadius > 0f) add(RoundedCornersTransformation(cornerRadius))
63
47
  }
64
- val memoryCacheKey = buildString {
65
- append(url)
66
- if (blur > 0f) append("_blur$blur")
67
- if (cornerRadius > 0f) append("_corner$cornerRadius")
68
- }
69
48
  val request =
70
49
  ImageRequest.Builder(context)
71
50
  .data(url)
@@ -87,7 +66,6 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
87
66
  }
88
67
  }
89
68
  .allowHardware(true)
90
- .memoryCacheKey(memoryCacheKey)
91
69
  .transformations(transformations)
92
70
  .build()
93
71
 
@@ -111,23 +89,75 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
111
89
  }
112
90
 
113
91
  override fun preLoadImages(urls: Array<String>): Promise<Unit> = Promise.async {
114
- for (url in urls) {
115
- val request = ImageRequest.Builder(context).data(url).build()
116
- imageLoader.execute(request)
92
+ coroutineScope {
93
+ urls
94
+ .map { url ->
95
+ async {
96
+ val request = ImageRequest.Builder(context).data(url).build()
97
+ imageLoader.execute(request)
98
+ }
99
+ }
100
+ .awaitAll()
117
101
  }
102
+ Unit
118
103
  }
119
104
 
120
105
  override fun gaussianBlur(image: HybridImageSpec, radius: Double): Promise<HybridImageSpec> =
121
106
  Promise.async {
122
107
  val hybridImage = image as? HybridImage ?: throw Error("Image is not a HybridImage")
123
- val clampedRadius = radius.toFloat().coerceIn(0.01f, 25f)
124
- val blurred =
125
- BlurTransformation(context, clampedRadius).transform(hybridImage.bitmap, Size.ORIGINAL)
126
- HybridImage(blurred)
108
+ // `radius` is a Gaussian sigma in source-image pixels — see BlurTransformation.
109
+ val sigma = radius.toFloat()
110
+ if (sigma <= 0f) {
111
+ hybridImage
112
+ } else {
113
+ val blurred =
114
+ BlurTransformation(context, sigma).transform(hybridImage.bitmap, Size.ORIGINAL)
115
+ HybridImage(blurred)
116
+ }
127
117
  }
128
118
 
129
- override fun clearCache() {
119
+ override fun clearCache(): Promise<Unit> = Promise.async {
130
120
  imageLoader.memoryCache?.clear()
131
- imageLoader.diskCache?.clear()
121
+ // DiskCache.clear() does file I/O; keep it off the JS thread but await
122
+ // completion so callers can rely on the cache being empty, and so an
123
+ // IOException rejects the promise instead of crashing the process.
124
+ withContext(Dispatchers.IO) { imageLoader.diskCache?.clear() }
125
+ }
126
+
127
+ companion object {
128
+ // Coil requires a single DiskCache instance per directory, so the loader
129
+ // must be shared across all HybridNitroImagePipeline instances.
130
+ @Volatile private var sharedImageLoader: ImageLoader? = null
131
+
132
+ private fun getOrCreateImageLoader(context: Context): ImageLoader =
133
+ sharedImageLoader
134
+ ?: synchronized(this) {
135
+ sharedImageLoader ?: createImageLoader(context).also { sharedImageLoader = it }
136
+ }
137
+
138
+ private fun createImageLoader(context: Context): ImageLoader {
139
+ val okHttpClient =
140
+ OkHttpClient.Builder()
141
+ .apply {
142
+ try {
143
+ val cronetEngine = CronetEngine.Builder(context).build()
144
+ addInterceptor(CronetInterceptor.newBuilder(cronetEngine).build())
145
+ } catch (_: Exception) {
146
+ // Cronet unavailable (no GMS or unsupported device) — use plain OkHttp
147
+ }
148
+ }
149
+ .build()
150
+
151
+ return ImageLoader.Builder(context)
152
+ .components { add(OkHttpNetworkFetcherFactory(callFactory = okHttpClient)) }
153
+ .memoryCache { MemoryCache.Builder().maxSizePercent(context, 0.25).build() }
154
+ .diskCache {
155
+ DiskCache.Builder()
156
+ .directory(context.cacheDir.resolve("nitro_image_cache").toOkioPath())
157
+ .maxSizeBytes(256L * 1024 * 1024)
158
+ .build()
159
+ }
160
+ .build()
161
+ }
132
162
  }
133
163
  }
@@ -13,33 +13,57 @@ import androidx.core.graphics.applyCanvas
13
13
  import androidx.core.graphics.scale
14
14
  import coil3.size.Size
15
15
  import coil3.transform.Transformation
16
+ import kotlin.math.ceil
17
+ import kotlin.math.max
16
18
 
19
+ /**
20
+ * Gaussian blur of standard deviation [sigma], measured in *source image pixels*.
21
+ *
22
+ * The unit is the point of this class: the same sigma on the same source file produces the same
23
+ * result here and on iOS. It is deliberately not a "radius" — RenderScript, CoreImage and React
24
+ * Native's `blurRadius` each define radius differently, which is why blurs never matched across
25
+ * platforms.
26
+ *
27
+ * For reference, React Native's `<Image blurRadius={n} />` halves its input before convolving, so
28
+ * `blurRadius={n}` is roughly `sigma = n / 2`.
29
+ */
17
30
  class BlurTransformation(
18
31
  private val context: Context,
19
- private val radius: Float = 10f,
20
- private val sampling: Float = 1f,
32
+ private val sigma: Float,
21
33
  ) : Transformation() {
22
34
 
23
35
  init {
24
- require(radius in 0.01..25.0) { "radius must be in (0, 25]." }
25
- require(sampling >= 1f) { "sampling must be >= 1." }
36
+ require(sigma > 0f && sigma.isFinite()) { "sigma must be > 0." }
26
37
  }
27
38
 
28
- override val cacheKey = "${BlurTransformation::class.java.name}-$radius-$sampling"
39
+ override val cacheKey = "${BlurTransformation::class.java.name}-$sigma"
29
40
 
30
41
  override suspend fun transform(input: Bitmap, size: Size): Bitmap {
42
+ // ScriptIntrinsicBlur's `radius` maps to a Gaussian sigma of 0.4 * radius + 0.6, and radius is
43
+ // capped at 25 — so a single pass tops out at sigma ~10.6px. Larger blurs are reached by
44
+ // blurring a downscaled copy: scaling down by `s`, blurring with sigma/s and scaling back up
45
+ // multiplies the effective sigma by `s`. A blur is a low-pass filter, so the detail the
46
+ // downscale drops is detail the blur was going to remove anyway. Downscale only as far as the
47
+ // sigma ceiling forces, which keeps small blurs pixel-exact.
48
+ val sampling = max(1f, sigma / MAX_SIGMA)
49
+ val scaledWidth = ceil(input.width / sampling).toInt().coerceAtLeast(1)
50
+ val scaledHeight = ceil(input.height / sampling).toInt().coerceAtLeast(1)
51
+ // Rounding to whole pixels shifts the scale slightly; derive the sigma from the scale actually
52
+ // applied rather than the requested one.
53
+ val scaleX = scaledWidth.toFloat() / input.width
54
+ val scaleY = scaledHeight.toFloat() / input.height
55
+ val scaledSigma = sigma * max(scaleX, scaleY)
56
+ val radius = ((scaledSigma - SIGMA_INTERCEPT) / SIGMA_SLOPE).coerceIn(MIN_RADIUS, MAX_RADIUS)
57
+
31
58
  val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
32
- // Auto-scale so the blurred image is at most 512px on the longest side.
33
- // Blur is a low-pass filter so blurring at lower resolution is identical in quality.
34
- val autoSampling = (maxOf(input.width, input.height) / 512f).coerceAtLeast(sampling)
35
- val scaledWidth = (input.width / autoSampling).toInt().coerceAtLeast(1)
36
- val scaledHeight = (input.height / autoSampling).toInt().coerceAtLeast(1)
37
- val softwareInput = if (input.config == Bitmap.Config.HARDWARE) input.copy(Bitmap.Config.ARGB_8888, false) else input
59
+ val softwareInput =
60
+ if (input.config == Bitmap.Config.HARDWARE) input.copy(Bitmap.Config.ARGB_8888, false)
61
+ else input
38
62
  val softwareConfig = softwareInput.config ?: Bitmap.Config.ARGB_8888
39
63
 
40
64
  val output = Bitmap.createBitmap(scaledWidth, scaledHeight, softwareConfig)
41
65
  output.applyCanvas {
42
- scale(1 / autoSampling, 1 / autoSampling)
66
+ scale(scaleX, scaleY)
43
67
  drawBitmap(softwareInput, 0f, 0f, paint)
44
68
  }
45
69
 
@@ -69,23 +93,25 @@ class BlurTransformation(
69
93
  blur?.destroy()
70
94
  }
71
95
 
72
- return output.scale(input.width, input.height)
96
+ return if (scaledWidth == input.width && scaledHeight == input.height) output
97
+ else output.scale(input.width, input.height)
73
98
  }
74
99
 
75
100
  override fun equals(other: Any?): Boolean {
76
101
  if (this === other) return true
77
- return other is BlurTransformation &&
78
- context == other.context &&
79
- radius == other.radius &&
80
- sampling == other.sampling
102
+ return other is BlurTransformation && context == other.context && sigma == other.sigma
81
103
  }
82
104
 
83
- override fun hashCode(): Int {
84
- var result = context.hashCode()
85
- result = 31 * result + radius.hashCode()
86
- result = 31 * result + sampling.hashCode()
87
- return result
88
- }
105
+ override fun hashCode(): Int = 31 * context.hashCode() + sigma.hashCode()
89
106
 
90
- override fun toString() = "BlurTransformation(radius=$radius, sampling=$sampling)"
107
+ override fun toString() = "BlurTransformation(sigma=$sigma)"
108
+
109
+ private companion object {
110
+ // sigma = 0.4 * radius + 0.6, per RenderScript's ScriptIntrinsicBlur.
111
+ const val SIGMA_SLOPE = 0.4f
112
+ const val SIGMA_INTERCEPT = 0.6f
113
+ const val MIN_RADIUS = 0.01f
114
+ const val MAX_RADIUS = 25f
115
+ const val MAX_SIGMA = SIGMA_SLOPE * MAX_RADIUS + SIGMA_INTERCEPT
116
+ }
91
117
  }
@@ -0,0 +1,135 @@
1
+ //
2
+ // GaussianBlur.swift
3
+ // NitroImagePipeline
4
+ //
5
+ // Cross-platform Gaussian blur — the kernel itself.
6
+ //
7
+ // This file deliberately depends on nothing but Accelerate and CoreGraphics
8
+ // so `scripts/verify-blur.swift` can compile and measure it on the host
9
+ // machine. The UIImage/Nuke plumbing lives in GaussianBlurProcessor.swift.
10
+ //
11
+ // `sigma` is the standard deviation of the Gaussian, measured in *source
12
+ // image pixels* — the same sigma applied to the same source file produces
13
+ // the same result on iOS and Android. It is deliberately not a "radius":
14
+ // CIGaussianBlur, RenderScript and React Native's `blurRadius` each define
15
+ // radius differently, which is why blurs never matched across platforms.
16
+ //
17
+ // For reference, React Native's `<Image blurRadius={n} />` halves its input
18
+ // before convolving, so `blurRadius={n}` is roughly `sigma = n / 2`.
19
+ //
20
+
21
+ import Accelerate
22
+ import CoreGraphics
23
+ import Foundation
24
+
25
+ enum GaussianBlur {
26
+ /// Runs the box-blur passes over `cgImage`. Returns `nil` if the pixel
27
+ /// buffers could not be allocated.
28
+ static func convolve(_ cgImage: CGImage, boxes: [UInt32]) -> CGImage? {
29
+ // Normalise to premultiplied ARGB8888 — vImageBoxConvolve_ARGB8888
30
+ // needs four 8-bit channels, and premultiplied alpha is what keeps
31
+ // transparent edges from bleeding dark halos into the blur.
32
+ guard var format = vImage_CGImageFormat(
33
+ bitsPerComponent: 8,
34
+ bitsPerPixel: 32,
35
+ colorSpace: CGColorSpaceCreateDeviceRGB(),
36
+ bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue),
37
+ renderingIntent: .defaultIntent
38
+ ) else { return nil }
39
+
40
+ var source = vImage_Buffer()
41
+ guard vImageBuffer_InitWithCGImage(
42
+ &source, &format, nil, cgImage, vImage_Flags(kvImageNoFlags)
43
+ ) == kvImageNoError else { return nil }
44
+ defer { free(source.data) }
45
+
46
+ var scratch = vImage_Buffer()
47
+ guard vImageBuffer_Init(
48
+ &scratch, source.height, source.width, 32, vImage_Flags(kvImageNoFlags)
49
+ ) == kvImageNoError else { return nil }
50
+ defer { free(scratch.data) }
51
+
52
+ // kvImageEdgeExtend clamps at the borders instead of sampling
53
+ // transparent black, so the image keeps its edges instead of fading
54
+ // out into the frame (CIGaussianBlur's default, and the reason blurred
55
+ // images used to come back with washed-out borders).
56
+ let flags = vImage_Flags(kvImageEdgeExtend)
57
+ // The passes use two different kernel widths; size the scratch buffer
58
+ // for the widest one so every pass fits in it.
59
+ let widest = boxes.max() ?? 1
60
+ let tempSize = vImageBoxConvolve_ARGB8888(
61
+ &source, &scratch, nil, 0, 0, widest, widest, nil,
62
+ vImage_Flags(kvImageEdgeExtend | kvImageGetTempBufferSize)
63
+ )
64
+ guard tempSize > 0, let temp = malloc(tempSize) else { return nil }
65
+ defer { free(temp) }
66
+
67
+ // Ping-pong between the two buffers; after an odd number of passes the
68
+ // result sits in `input`.
69
+ var input = source
70
+ var output = scratch
71
+ for box in boxes {
72
+ guard vImageBoxConvolve_ARGB8888(
73
+ &input, &output, temp, 0, 0, box, box, nil, flags
74
+ ) == kvImageNoError else { return nil }
75
+ swap(&input, &output)
76
+ }
77
+
78
+ var error = vImage_Error(kvImageNoError)
79
+ guard let blurred = vImageCreateCGImageFromBuffer(
80
+ &input, &format, nil, nil, vImage_Flags(kvImageNoFlags), &error
81
+ )?.takeRetainedValue(), error == kvImageNoError else { return nil }
82
+
83
+ return blurred
84
+ }
85
+
86
+ /// Widths for the three box-blur passes that approximate a Gaussian of
87
+ /// `sigma` (the classic approximation: three boxes land within a few
88
+ /// percent of a true Gaussian, and Accelerate runs them in O(1) per pixel
89
+ /// regardless of how wide they are).
90
+ ///
91
+ /// Three boxes of widths w1…w3 produce a standard deviation of
92
+ /// `sqrt((w1² + w2² + w3² - 3) / 12)`. Box widths have to be odd, so the
93
+ /// passes are split between the two odd integers straddling the ideal
94
+ /// width and the split landing closest to `sigma` wins — which keeps the
95
+ /// error under half a pixel at any sigma instead of always undershooting.
96
+ static func boxSizes(forSigma sigma: Double) -> [UInt32] {
97
+ let passes = 3
98
+ var lower = Int(((12 * sigma * sigma / Double(passes)) + 1).squareRoot())
99
+ if lower % 2 == 0 { lower -= 1 }
100
+ // Upper bound keeps `upper` inside UInt32 for absurd sigmas; a kernel
101
+ // that wide is already far larger than any real image.
102
+ lower = min(max(lower, 1), Int(UInt32.max) - 2)
103
+ let upper = lower + 2
104
+
105
+ // Written as a plain loop rather than map/min(by:): the inferred
106
+ // version tripped "type of expression is ambiguous" on Swift 6.2.
107
+ var best: [UInt32] = []
108
+ var bestError: Double = .infinity
109
+ for lowerCount in 0...passes {
110
+ var candidate: [UInt32] = []
111
+ for pass in 0..<passes {
112
+ candidate.append(UInt32(pass < lowerCount ? lower : upper))
113
+ }
114
+ // `.magnitude` rather than `abs()`: Nitro's C++ interop puts
115
+ // std::abs overloads in scope, and Swift 6.2 calls the resulting
116
+ // `abs` ambiguous when this file is compiled as part of the pod.
117
+ let error: Double = (standardDeviation(of: candidate) - sigma).magnitude
118
+ if error < bestError {
119
+ bestError = error
120
+ best = candidate
121
+ }
122
+ }
123
+ return best
124
+ }
125
+
126
+ /// The standard deviation three box blurs of the given widths add up to.
127
+ private static func standardDeviation(of boxes: [UInt32]) -> Double {
128
+ var sum: Double = 0
129
+ for box in boxes {
130
+ let width = Double(box)
131
+ sum += width * width - 1
132
+ }
133
+ return (sum / 12).squareRoot()
134
+ }
135
+ }
@@ -0,0 +1,41 @@
1
+ //
2
+ // GaussianBlurProcessor.swift
3
+ // NitroImagePipeline
4
+ //
5
+ // UIImage and Nuke plumbing around the kernel in GaussianBlur.swift.
6
+ //
7
+
8
+ import CoreGraphics
9
+ import Foundation
10
+ import Nuke
11
+ import UIKit
12
+
13
+ extension GaussianBlur {
14
+ /// Applies a Gaussian blur of standard deviation `sigma` (in image pixels).
15
+ ///
16
+ /// Returns `image` unchanged when `sigma` is too small to affect a pixel,
17
+ /// and `nil` only when the pixel buffers could not be allocated.
18
+ static func apply(to image: UIImage, sigma: Double) -> UIImage? {
19
+ guard sigma > 0, sigma.isFinite, let cgImage = image.cgImage else { return image }
20
+
21
+ let boxes = boxSizes(forSigma: sigma)
22
+ guard boxes.contains(where: { $0 > 1 }) else { return image }
23
+ guard let blurred = convolve(cgImage, boxes: boxes) else { return nil }
24
+
25
+ return UIImage(cgImage: blurred, scale: image.scale, orientation: image.imageOrientation)
26
+ }
27
+ }
28
+
29
+ /// Nuke processor so `loadImage(url:, { blur })` and `gaussianBlur(image:, sigma)`
30
+ /// go through the exact same kernel.
31
+ struct GaussianBlurProcessor: ImageProcessing {
32
+ let sigma: Double
33
+
34
+ var identifier: String { "com.nitroimagepipeline.gaussianBlur?sigma=\(sigma)" }
35
+
36
+ var hashableIdentifier: AnyHashable { identifier }
37
+
38
+ func process(_ image: PlatformImage) -> PlatformImage? {
39
+ GaussianBlur.apply(to: image, sigma: sigma)
40
+ }
41
+ }
@@ -11,14 +11,48 @@ import NitroImage
11
11
  import Nuke
12
12
 
13
13
  import UIKit
14
- import CoreImage
15
14
 
16
15
  private class HybridImage: HybridImageSpec, NativeImage {
17
16
  let uiImage: UIImage
18
17
 
18
+ // PNG encoding is expensive; encode once and reuse for both
19
+ // toArrayBuffer() and toBase64(). Guarded by a lock (lazy var is not
20
+ // thread-safe) and dropped on memory pressure so a JS-retained image
21
+ // doesn't pin its encoding forever.
22
+ private let pngLock = NSLock()
23
+ private var cachedPngData: Data?
24
+ private var memoryWarningObserver: (any NSObjectProtocol)?
25
+
19
26
  init(uiImage: UIImage) {
20
27
  self.uiImage = uiImage
21
28
  super.init()
29
+ memoryWarningObserver = NotificationCenter.default.addObserver(
30
+ forName: UIApplication.didReceiveMemoryWarningNotification,
31
+ object: nil,
32
+ queue: nil
33
+ ) { [weak self] _ in
34
+ guard let self else { return }
35
+ self.pngLock.lock()
36
+ self.cachedPngData = nil
37
+ self.pngLock.unlock()
38
+ }
39
+ }
40
+
41
+ deinit {
42
+ if let memoryWarningObserver {
43
+ NotificationCenter.default.removeObserver(memoryWarningObserver)
44
+ }
45
+ }
46
+
47
+ private func pngData() -> Data? {
48
+ pngLock.lock()
49
+ defer { pngLock.unlock() }
50
+ if let cachedPngData {
51
+ return cachedPngData
52
+ }
53
+ let data = uiImage.pngData()
54
+ cachedPngData = data
55
+ return data
22
56
  }
23
57
 
24
58
  var width: Double {
@@ -30,14 +64,14 @@ private class HybridImage: HybridImageSpec, NativeImage {
30
64
  }
31
65
 
32
66
  func toArrayBuffer() throws -> ArrayBuffer {
33
- guard let data = uiImage.pngData() else {
67
+ guard let data = pngData() else {
34
68
  throw RuntimeError.error(withMessage: "Failed to encode image to PNG")
35
69
  }
36
70
  return try ArrayBuffer.copy(data: data)
37
71
  }
38
72
 
39
73
  func toBase64() throws -> String {
40
- guard let data = uiImage.pngData() else {
74
+ guard let data = pngData() else {
41
75
  throw RuntimeError.error(withMessage: "Failed to encode image")
42
76
  }
43
77
 
@@ -50,14 +84,31 @@ private class HybridImage: HybridImageSpec, NativeImage {
50
84
  }
51
85
 
52
86
  class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
53
- private let prefetcher = ImagePrefetcher()
54
-
55
- override init() {
56
- ImagePipeline.shared = ImagePipeline(configuration: .withDataCache)
57
- }
87
+ // One shared pipeline (instead of mutating ImagePipeline.shared) so the
88
+ // prefetcher and loadImage read/write the exact same caches, other Nuke
89
+ // users in the host app are left untouched, and repeated instantiation
90
+ // (e.g. Metro reloads) never puts two DataCache instances on the same
91
+ // directory.
92
+ private static let sharedPipeline: ImagePipeline = {
93
+ var configuration = ImagePipeline.Configuration.withDataCache
94
+ // Store processed (blurred/rounded) variants on disk in addition to
95
+ // the original download, so they survive memory eviction and
96
+ // restarts without dropping the original for other variants.
97
+ configuration.dataCachePolicy = .storeAll
98
+ return ImagePipeline(configuration: configuration)
99
+ }()
100
+
101
+ private static let sharedPrefetcher = ImagePrefetcher(pipeline: sharedPipeline)
102
+
103
+ private var pipeline: ImagePipeline { Self.sharedPipeline }
104
+ private var prefetcher: ImagePrefetcher { Self.sharedPrefetcher }
58
105
 
59
106
  func loadImage(url: String, options: Options?) throws -> Promise<any HybridImageSpec> {
60
107
  return Promise.async {
108
+ guard let imageUrl = URL(string: url) else {
109
+ throw RuntimeError.error(withMessage: "Invalid URL: \(url)")
110
+ }
111
+
61
112
  let cacheOptions: ImageRequest.Options = switch options?.cache {
62
113
  case .memory: [.disableDiskCache]
63
114
  case .disk: [.disableMemoryCache]
@@ -67,19 +118,19 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
67
118
 
68
119
  var processors: [any ImageProcessing] = []
69
120
  if let blur = options?.blur, blur > 0 {
70
- processors.append(.gaussianBlur(radius: Int(blur)))
121
+ processors.append(GaussianBlurProcessor(sigma: blur))
71
122
  }
72
123
  if let cornerRadius = options?.cornerRadius, cornerRadius > 0 {
73
124
  processors.append(.roundedCorners(radius: cornerRadius))
74
125
  }
75
126
 
76
127
  let imgRequest = ImageRequest(
77
- url: URL(string: url),
128
+ url: imageUrl,
78
129
  processors: processors,
79
130
  options: cacheOptions
80
131
  )
81
132
 
82
- let image = try await ImagePipeline.shared.image(for: imgRequest)
133
+ let image = try await self.pipeline.image(for: imgRequest)
83
134
  return HybridImage(uiImage: image)
84
135
  }
85
136
  }
@@ -101,8 +152,10 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
101
152
  }
102
153
  }
103
154
 
104
- func clearCache() throws {
105
- ImagePipeline.shared.cache.removeAll()
155
+ func clearCache() throws -> Promise<Void> {
156
+ return Promise.async {
157
+ self.pipeline.cache.removeAll()
158
+ }
106
159
  }
107
160
 
108
161
  func gaussianBlur(image: any HybridImageSpec, radius: Double) throws -> Promise<any HybridImageSpec> {
@@ -111,19 +164,11 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
111
164
  throw RuntimeError.error(withMessage: "Unsupported image type")
112
165
  }
113
166
 
114
- let uiImage = nativeImage.uiImage
115
-
116
- let ciImage = CIImage(image: uiImage)
117
-
118
- let filter = CIFilter(name: "CIGaussianBlur")!
119
- filter.setValue(ciImage, forKey: kCIInputImageKey)
120
- filter.setValue(radius, forKey: kCIInputRadiusKey)
121
-
122
- let context = CIContext()
123
- let output = filter.outputImage
124
- let cgImage = context.createCGImage(output!, from: ciImage!.extent)
167
+ guard let blurred = GaussianBlur.apply(to: nativeImage.uiImage, sigma: radius) else {
168
+ throw RuntimeError.error(withMessage: "Failed to apply gaussian blur")
169
+ }
125
170
 
126
- return HybridImage(uiImage: UIImage(cgImage: cgImage!))
171
+ return HybridImage(uiImage: blurred)
127
172
  }
128
173
  }
129
174
 
@@ -19,32 +19,53 @@ const NitroImagePipeline = exports.NitroImagePipeline = _reactNativeNitroModules
19
19
  function useImage({
20
20
  url,
21
21
  blur = 0,
22
- cornerRadius = 0
22
+ cornerRadius = 0,
23
+ cache
23
24
  }) {
24
25
  const [image, setImage] = (0, _react.useState)({
25
26
  image: undefined,
26
27
  error: undefined
27
28
  });
29
+ const loadedUrlRef = (0, _react.useRef)(url);
28
30
  (0, _react.useEffect)(() => {
31
+ let cancelled = false;
32
+ // Only reset to the loading state when the URL changes; for same-URL
33
+ // param tweaks (blur/cornerRadius/cache) keep showing the current image
34
+ // until the new variant resolves, to avoid flashing empty.
35
+ if (loadedUrlRef.current !== url) {
36
+ loadedUrlRef.current = url;
37
+ setImage({
38
+ image: undefined,
39
+ error: undefined
40
+ });
41
+ }
29
42
  (async () => {
30
43
  try {
31
44
  const result = await NitroImagePipeline.loadImage(url, {
32
45
  blur,
33
- cornerRadius
34
- });
35
- setImage({
36
- image: result,
37
- error: undefined
46
+ cornerRadius,
47
+ cache
38
48
  });
49
+ if (!cancelled) {
50
+ setImage({
51
+ image: result,
52
+ error: undefined
53
+ });
54
+ }
39
55
  } catch (e) {
40
56
  const error = e instanceof Error ? e : new Error(`${e}`);
41
- setImage({
42
- image: undefined,
43
- error: error
44
- });
57
+ if (!cancelled) {
58
+ setImage({
59
+ image: undefined,
60
+ error: error
61
+ });
62
+ }
45
63
  }
46
64
  })();
47
- }, [url, blur]);
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ }, [url, blur, cornerRadius, cache]);
48
69
  return image;
49
70
  }
50
71
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_react","require","_reactNativeNitroModules","NitroImagePipeline","exports","NitroModules","createHybridObject","useImage","url","blur","cornerRadius","image","setImage","useState","undefined","error","useEffect","result","loadImage","e","Error"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,wBAAA,GAAAD,OAAA;AAIO,MAAME,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAC7BE,qCAAY,CAACC,kBAAkB,CAAyB,oBAAoB,CAAC;AAmB/E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,QAAQA,CAAC;EACvBC,GAAG;EACHC,IAAI,GAAG,CAAC;EACRC,YAAY,GAAG;AAKjB,CAAC,EAAU;EACT,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAG,IAAAC,eAAQ,EAAS;IACzCF,KAAK,EAAEG,SAAS;IAChBC,KAAK,EAAED;EACT,CAAC,CAAC;EAEF,IAAAE,gBAAS,EAAC,MAAM;IACd,CAAC,YAAY;MACX,IAAI;QACF,MAAMC,MAAM,GAAG,MAAMd,kBAAkB,CAACe,SAAS,CAACV,GAAG,EAAE;UACrDC,IAAI;UACJC;QACF,CAAC,CAAC;QAEFE,QAAQ,CAAC;UAAED,KAAK,EAAEM,MAAM;UAAEF,KAAK,EAAED;QAAU,CAAC,CAAC;MAC/C,CAAC,CAAC,OAAOK,CAAC,EAAE;QACV,MAAMJ,KAAK,GAAGI,CAAC,YAAYC,KAAK,GAAGD,CAAC,GAAG,IAAIC,KAAK,CAAC,GAAGD,CAAC,EAAE,CAAC;QACxDP,QAAQ,CAAC;UAAED,KAAK,EAAEG,SAAS;UAAEC,KAAK,EAAEA;QAAM,CAAC,CAAC;MAC9C;IACF,CAAC,EAAE,CAAC;EACN,CAAC,EAAE,CAACP,GAAG,EAAEC,IAAI,CAAC,CAAC;EAEf,OAAOE,KAAK;AACd","ignoreList":[]}
1
+ {"version":3,"names":["_react","require","_reactNativeNitroModules","NitroImagePipeline","exports","NitroModules","createHybridObject","useImage","url","blur","cornerRadius","cache","image","setImage","useState","undefined","error","loadedUrlRef","useRef","useEffect","cancelled","current","result","loadImage","e","Error"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,wBAAA,GAAAD,OAAA;AAUO,MAAME,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAC7BE,qCAAY,CAACC,kBAAkB,CAAyB,oBAAoB,CAAC;AAmB/E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,QAAQA,CAAC;EACvBC,GAAG;EACHC,IAAI,GAAG,CAAC;EACRC,YAAY,GAAG,CAAC;EAChBC;AAWF,CAAC,EAAU;EACT,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAG,IAAAC,eAAQ,EAAS;IACzCF,KAAK,EAAEG,SAAS;IAChBC,KAAK,EAAED;EACT,CAAC,CAAC;EACF,MAAME,YAAY,GAAG,IAAAC,aAAM,EAACV,GAAG,CAAC;EAEhC,IAAAW,gBAAS,EAAC,MAAM;IACd,IAAIC,SAAS,GAAG,KAAK;IACrB;IACA;IACA;IACA,IAAIH,YAAY,CAACI,OAAO,KAAKb,GAAG,EAAE;MAChCS,YAAY,CAACI,OAAO,GAAGb,GAAG;MAC1BK,QAAQ,CAAC;QAAED,KAAK,EAAEG,SAAS;QAAEC,KAAK,EAAED;MAAU,CAAC,CAAC;IAClD;IAEA,CAAC,YAAY;MACX,IAAI;QACF,MAAMO,MAAM,GAAG,MAAMnB,kBAAkB,CAACoB,SAAS,CAACf,GAAG,EAAE;UACrDC,IAAI;UACJC,YAAY;UACZC;QACF,CAAC,CAAC;QAEF,IAAI,CAACS,SAAS,EAAE;UACdP,QAAQ,CAAC;YAAED,KAAK,EAAEU,MAAM;YAAEN,KAAK,EAAED;UAAU,CAAC,CAAC;QAC/C;MACF,CAAC,CAAC,OAAOS,CAAC,EAAE;QACV,MAAMR,KAAK,GAAGQ,CAAC,YAAYC,KAAK,GAAGD,CAAC,GAAG,IAAIC,KAAK,CAAC,GAAGD,CAAC,EAAE,CAAC;QACxD,IAAI,CAACJ,SAAS,EAAE;UACdP,QAAQ,CAAC;YAAED,KAAK,EAAEG,SAAS;YAAEC,KAAK,EAAEA;UAAM,CAAC,CAAC;QAC9C;MACF;IACF,CAAC,EAAE,CAAC;IAEJ,OAAO,MAAM;MACXI,SAAS,GAAG,IAAI;IAClB,CAAC;EACH,CAAC,EAAE,CAACZ,GAAG,EAAEC,IAAI,EAAEC,YAAY,EAAEC,KAAK,CAAC,CAAC;EAEpC,OAAOC,KAAK;AACd","ignoreList":[]}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useRef, useState } from 'react';
4
4
  import { NitroModules } from 'react-native-nitro-modules';
5
5
  export const NitroImagePipeline = NitroModules.createHybridObject('NitroImagePipeline');
6
6
  /**
@@ -14,32 +14,53 @@ export const NitroImagePipeline = NitroModules.createHybridObject('NitroImagePip
14
14
  export function useImage({
15
15
  url,
16
16
  blur = 0,
17
- cornerRadius = 0
17
+ cornerRadius = 0,
18
+ cache
18
19
  }) {
19
20
  const [image, setImage] = useState({
20
21
  image: undefined,
21
22
  error: undefined
22
23
  });
24
+ const loadedUrlRef = useRef(url);
23
25
  useEffect(() => {
26
+ let cancelled = false;
27
+ // Only reset to the loading state when the URL changes; for same-URL
28
+ // param tweaks (blur/cornerRadius/cache) keep showing the current image
29
+ // until the new variant resolves, to avoid flashing empty.
30
+ if (loadedUrlRef.current !== url) {
31
+ loadedUrlRef.current = url;
32
+ setImage({
33
+ image: undefined,
34
+ error: undefined
35
+ });
36
+ }
24
37
  (async () => {
25
38
  try {
26
39
  const result = await NitroImagePipeline.loadImage(url, {
27
40
  blur,
28
- cornerRadius
29
- });
30
- setImage({
31
- image: result,
32
- error: undefined
41
+ cornerRadius,
42
+ cache
33
43
  });
44
+ if (!cancelled) {
45
+ setImage({
46
+ image: result,
47
+ error: undefined
48
+ });
49
+ }
34
50
  } catch (e) {
35
51
  const error = e instanceof Error ? e : new Error(`${e}`);
36
- setImage({
37
- image: undefined,
38
- error: error
39
- });
52
+ if (!cancelled) {
53
+ setImage({
54
+ image: undefined,
55
+ error: error
56
+ });
57
+ }
40
58
  }
41
59
  })();
42
- }, [url, blur]);
60
+ return () => {
61
+ cancelled = true;
62
+ };
63
+ }, [url, blur, cornerRadius, cache]);
43
64
  return image;
44
65
  }
45
66
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["useEffect","useState","NitroModules","NitroImagePipeline","createHybridObject","useImage","url","blur","cornerRadius","image","setImage","undefined","error","result","loadImage","e","Error"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAE3C,SAASC,YAAY,QAAQ,4BAA4B;AAIzD,OAAO,MAAMC,kBAAkB,GAC7BD,YAAY,CAACE,kBAAkB,CAAyB,oBAAoB,CAAC;AAmB/E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAAC;EACvBC,GAAG;EACHC,IAAI,GAAG,CAAC;EACRC,YAAY,GAAG;AAKjB,CAAC,EAAU;EACT,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAGT,QAAQ,CAAS;IACzCQ,KAAK,EAAEE,SAAS;IAChBC,KAAK,EAAED;EACT,CAAC,CAAC;EAEFX,SAAS,CAAC,MAAM;IACd,CAAC,YAAY;MACX,IAAI;QACF,MAAMa,MAAM,GAAG,MAAMV,kBAAkB,CAACW,SAAS,CAACR,GAAG,EAAE;UACrDC,IAAI;UACJC;QACF,CAAC,CAAC;QAEFE,QAAQ,CAAC;UAAED,KAAK,EAAEI,MAAM;UAAED,KAAK,EAAED;QAAU,CAAC,CAAC;MAC/C,CAAC,CAAC,OAAOI,CAAC,EAAE;QACV,MAAMH,KAAK,GAAGG,CAAC,YAAYC,KAAK,GAAGD,CAAC,GAAG,IAAIC,KAAK,CAAC,GAAGD,CAAC,EAAE,CAAC;QACxDL,QAAQ,CAAC;UAAED,KAAK,EAAEE,SAAS;UAAEC,KAAK,EAAEA;QAAM,CAAC,CAAC;MAC9C;IACF,CAAC,EAAE,CAAC;EACN,CAAC,EAAE,CAACN,GAAG,EAAEC,IAAI,CAAC,CAAC;EAEf,OAAOE,KAAK;AACd","ignoreList":[]}
1
+ {"version":3,"names":["useEffect","useRef","useState","NitroModules","NitroImagePipeline","createHybridObject","useImage","url","blur","cornerRadius","cache","image","setImage","undefined","error","loadedUrlRef","cancelled","current","result","loadImage","e","Error"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAEnD,SAASC,YAAY,QAAQ,4BAA4B;AAUzD,OAAO,MAAMC,kBAAkB,GAC7BD,YAAY,CAACE,kBAAkB,CAAyB,oBAAoB,CAAC;AAmB/E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAAC;EACvBC,GAAG;EACHC,IAAI,GAAG,CAAC;EACRC,YAAY,GAAG,CAAC;EAChBC;AAWF,CAAC,EAAU;EACT,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAGV,QAAQ,CAAS;IACzCS,KAAK,EAAEE,SAAS;IAChBC,KAAK,EAAED;EACT,CAAC,CAAC;EACF,MAAME,YAAY,GAAGd,MAAM,CAACM,GAAG,CAAC;EAEhCP,SAAS,CAAC,MAAM;IACd,IAAIgB,SAAS,GAAG,KAAK;IACrB;IACA;IACA;IACA,IAAID,YAAY,CAACE,OAAO,KAAKV,GAAG,EAAE;MAChCQ,YAAY,CAACE,OAAO,GAAGV,GAAG;MAC1BK,QAAQ,CAAC;QAAED,KAAK,EAAEE,SAAS;QAAEC,KAAK,EAAED;MAAU,CAAC,CAAC;IAClD;IAEA,CAAC,YAAY;MACX,IAAI;QACF,MAAMK,MAAM,GAAG,MAAMd,kBAAkB,CAACe,SAAS,CAACZ,GAAG,EAAE;UACrDC,IAAI;UACJC,YAAY;UACZC;QACF,CAAC,CAAC;QAEF,IAAI,CAACM,SAAS,EAAE;UACdJ,QAAQ,CAAC;YAAED,KAAK,EAAEO,MAAM;YAAEJ,KAAK,EAAED;UAAU,CAAC,CAAC;QAC/C;MACF,CAAC,CAAC,OAAOO,CAAC,EAAE;QACV,MAAMN,KAAK,GAAGM,CAAC,YAAYC,KAAK,GAAGD,CAAC,GAAG,IAAIC,KAAK,CAAC,GAAGD,CAAC,EAAE,CAAC;QACxD,IAAI,CAACJ,SAAS,EAAE;UACdJ,QAAQ,CAAC;YAAED,KAAK,EAAEE,SAAS;YAAEC,KAAK,EAAEA;UAAM,CAAC,CAAC;QAC9C;MACF;IACF,CAAC,EAAE,CAAC;IAEJ,OAAO,MAAM;MACXE,SAAS,GAAG,IAAI;IAClB,CAAC;EACH,CAAC,EAAE,CAACT,GAAG,EAAEC,IAAI,EAAEC,YAAY,EAAEC,KAAK,CAAC,CAAC;EAEpC,OAAOC,KAAK;AACd","ignoreList":[]}
@@ -1,5 +1,6 @@
1
1
  import type { Image } from 'react-native-nitro-image';
2
- import type { NitroImagePipeline as NitroImagePipelineSpec } from './specs/nitro-image-toolkit.nitro';
2
+ import type { CacheOption, NitroImagePipeline as NitroImagePipelineSpec, Options } from './specs/nitro-image-toolkit.nitro';
3
+ export type { CacheOption, Options };
3
4
  export declare const NitroImagePipeline: NitroImagePipelineSpec;
4
5
  type Result = {
5
6
  image: undefined;
@@ -19,10 +20,15 @@ type Result = {
19
20
  * const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
20
21
  * ```
21
22
  */
22
- export declare function useImage({ url, blur, cornerRadius, }: {
23
+ export declare function useImage({ url, blur, cornerRadius, cache, }: {
23
24
  url: string;
25
+ /**
26
+ * Gaussian blur strength, as the standard deviation (sigma) of the blur in
27
+ * source-image pixels. Matches across iOS and Android; roughly half of
28
+ * React Native's `blurRadius`.
29
+ */
24
30
  blur?: number;
25
31
  cornerRadius?: number;
32
+ cache?: CacheOption;
26
33
  }): Result;
27
- export {};
28
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,KAAK,EAAE,kBAAkB,IAAI,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAEtG,eAAO,MAAM,kBAAkB,wBACgD,CAAC;AAEhF,KAAK,MAAM,GAEP;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,IAAQ,EACR,YAAgB,GACjB,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,CAuBT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,IAAI,sBAAsB,EAC5C,OAAO,EACR,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AAErC,eAAO,MAAM,kBAAkB,wBACgD,CAAC;AAEhF,KAAK,MAAM,GAEP;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,IAAQ,EACR,YAAgB,EAChB,KAAK,GACN,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,MAAM,CA0CT"}
@@ -1,7 +1,16 @@
1
1
  import type { Image } from 'react-native-nitro-image';
2
2
  import type { HybridObject } from 'react-native-nitro-modules';
3
- type CacheOption = 'memory' | 'disk' | 'none';
4
- type Options = {
3
+ export type CacheOption = 'memory' | 'disk' | 'none';
4
+ export type Options = {
5
+ /**
6
+ * Gaussian blur strength, given as the standard deviation (sigma) of the
7
+ * blur in **source-image pixels**. The same value on the same source image
8
+ * produces the same result on iOS and Android.
9
+ *
10
+ * React Native's `<Image blurRadius={n} />` is roughly `blur: n / 2`.
11
+ *
12
+ * @default 0 (no blur)
13
+ */
5
14
  blur?: number;
6
15
  cache?: CacheOption;
7
16
  cornerRadius?: number;
@@ -13,8 +22,12 @@ export interface NitroImagePipeline extends HybridObject<{
13
22
  loadImage(url: string, options?: Options): Promise<Image>;
14
23
  preLoadImage(url: string): Promise<void>;
15
24
  preLoadImages(urls: string[]): Promise<void>;
25
+ /**
26
+ * Blurs an already-loaded image. `radius` is the standard deviation (sigma)
27
+ * of the Gaussian in **source-image pixels**, the same unit as
28
+ * {@linkcode Options.blur}.
29
+ */
16
30
  gaussianBlur(image: Image, radius: number): Promise<Image>;
17
- clearCache(): void;
31
+ clearCache(): Promise<void>;
18
32
  }
19
- export {};
20
33
  //# sourceMappingURL=nitro-image-toolkit.nitro.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nitro-image-toolkit.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/nitro-image-toolkit.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,KAAK,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAC9C,KAAK,OAAO,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,kBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1D,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAG3D,UAAU,IAAI,IAAI,CAAC;CACpB"}
1
+ {"version":3,"file":"nitro-image-toolkit.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/nitro-image-toolkit.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,OAAO,GAAG;IACpB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,kBAAmB,SAAQ,YAAY,CAAC;IACvD,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,QAAQ,CAAC;CACnB,CAAC;IACA,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1D,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAG3D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B"}
@@ -132,9 +132,20 @@ namespace margelo::nitro::nitroimagepipeline {
132
132
  return __promise;
133
133
  }();
134
134
  }
135
- void JHybridNitroImagePipelineSpec::clearCache() {
136
- static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("clearCache");
137
- method(_javaPart);
135
+ std::shared_ptr<Promise<void>> JHybridNitroImagePipelineSpec::clearCache() {
136
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("clearCache");
137
+ auto __result = method(_javaPart);
138
+ return [&]() {
139
+ auto __promise = Promise<void>::create();
140
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& /* unit */) {
141
+ __promise->resolve();
142
+ });
143
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
144
+ jni::JniException __jniError(__throwable);
145
+ __promise->reject(std::make_exception_ptr(__jniError));
146
+ });
147
+ return __promise;
148
+ }();
138
149
  }
139
150
 
140
151
  } // namespace margelo::nitro::nitroimagepipeline
@@ -58,7 +58,7 @@ namespace margelo::nitro::nitroimagepipeline {
58
58
  std::shared_ptr<Promise<void>> preLoadImage(const std::string& url) override;
59
59
  std::shared_ptr<Promise<void>> preLoadImages(const std::vector<std::string>& urls) override;
60
60
  std::shared_ptr<Promise<std::shared_ptr<margelo::nitro::image::HybridImageSpec>>> gaussianBlur(const std::shared_ptr<margelo::nitro::image::HybridImageSpec>& image, double radius) override;
61
- void clearCache() override;
61
+ std::shared_ptr<Promise<void>> clearCache() override;
62
62
 
63
63
  private:
64
64
  jni::global_ref<JHybridNitroImagePipelineSpec::JavaPart> _javaPart;
@@ -49,7 +49,7 @@ abstract class HybridNitroImagePipelineSpec: HybridObject() {
49
49
 
50
50
  @DoNotStrip
51
51
  @Keep
52
- abstract fun clearCache(): Unit
52
+ abstract fun clearCache(): Promise<Unit>
53
53
 
54
54
  // Default implementation of `HybridObject.toString()`
55
55
  override fun toString(): String {
@@ -231,14 +231,5 @@ namespace margelo::nitro::nitroimagepipeline::bridge::swift {
231
231
  inline Result_std__shared_ptr_Promise_void___ create_Result_std__shared_ptr_Promise_void___(const std::exception_ptr& error) noexcept {
232
232
  return Result<std::shared_ptr<Promise<void>>>::withError(error);
233
233
  }
234
-
235
- // pragma MARK: Result<void>
236
- using Result_void_ = Result<void>;
237
- inline Result_void_ create_Result_void_() noexcept {
238
- return Result<void>::withValue();
239
- }
240
- inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept {
241
- return Result<void>::withError(error);
242
- }
243
234
 
244
235
  } // namespace margelo::nitro::nitroimagepipeline::bridge::swift
@@ -110,11 +110,13 @@ namespace margelo::nitro::nitroimagepipeline {
110
110
  auto __value = std::move(__result.value());
111
111
  return __value;
112
112
  }
113
- inline void clearCache() override {
113
+ inline std::shared_ptr<Promise<void>> clearCache() override {
114
114
  auto __result = _swiftPart.clearCache();
115
115
  if (__result.hasError()) [[unlikely]] {
116
116
  std::rethrow_exception(__result.error());
117
117
  }
118
+ auto __value = std::move(__result.value());
119
+ return __value;
118
120
  }
119
121
 
120
122
  private:
@@ -18,7 +18,7 @@ public protocol HybridNitroImagePipelineSpec_protocol: HybridObject {
18
18
  func preLoadImage(url: String) throws -> Promise<Void>
19
19
  func preLoadImages(urls: [String]) throws -> Promise<Void>
20
20
  func gaussianBlur(image: (any HybridImageSpec), radius: Double) throws -> Promise<(any HybridImageSpec)>
21
- func clearCache() throws -> Void
21
+ func clearCache() throws -> Promise<Void>
22
22
  }
23
23
 
24
24
  public extension HybridNitroImagePipelineSpec_protocol {
@@ -212,13 +212,21 @@ open class HybridNitroImagePipelineSpec_cxx {
212
212
  }
213
213
 
214
214
  @inline(__always)
215
- public final func clearCache() -> bridge.Result_void_ {
215
+ public final func clearCache() -> bridge.Result_std__shared_ptr_Promise_void___ {
216
216
  do {
217
- try self.__implementation.clearCache()
218
- return bridge.create_Result_void_()
217
+ let __result = try self.__implementation.clearCache()
218
+ let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in
219
+ let __promise = bridge.create_std__shared_ptr_Promise_void__()
220
+ let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise)
221
+ __result
222
+ .then({ __result in __promiseHolder.resolve() })
223
+ .catch({ __error in __promiseHolder.reject(__error.toCpp()) })
224
+ return __promise
225
+ }()
226
+ return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp)
219
227
  } catch (let __error) {
220
228
  let __exceptionPtr = __error.toCpp()
221
- return bridge.create_Result_void_(__exceptionPtr)
229
+ return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr)
222
230
  }
223
231
  }
224
232
  }
@@ -61,7 +61,7 @@ namespace margelo::nitro::nitroimagepipeline {
61
61
  virtual std::shared_ptr<Promise<void>> preLoadImage(const std::string& url) = 0;
62
62
  virtual std::shared_ptr<Promise<void>> preLoadImages(const std::vector<std::string>& urls) = 0;
63
63
  virtual std::shared_ptr<Promise<std::shared_ptr<margelo::nitro::image::HybridImageSpec>>> gaussianBlur(const std::shared_ptr<margelo::nitro::image::HybridImageSpec>& image, double radius) = 0;
64
- virtual void clearCache() = 0;
64
+ virtual std::shared_ptr<Promise<void>> clearCache() = 0;
65
65
 
66
66
  protected:
67
67
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-image-pipeline",
3
- "version": "0.3.4",
3
+ "version": "1.0.0",
4
4
  "description": "High-performance image loading, caching, and processing for React Native, built with Nitro Modules",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -9,12 +9,15 @@
9
9
  "source": "src/index",
10
10
  "scripts": {
11
11
  "typecheck": "tsc --noEmit",
12
- "lint": "biome check .",
13
- "format": "biome format --write .",
12
+ "lint": "oxlint && oxfmt --check",
13
+ "lint:fix": "oxlint --fix && oxfmt",
14
+ "format": "oxfmt",
14
15
  "clean": "git clean -dfX",
16
+ "prepare": "lefthook install || true",
15
17
  "release": "semantic-release",
16
18
  "build": "bun run typecheck && bob build",
17
- "codegen": "nitrogen --logLevel=\"debug\" && bun run build"
19
+ "codegen": "nitrogen --logLevel=\"debug\" && bun run build",
20
+ "verify:blur": "swiftc -O -o \"${TMPDIR:-/tmp}/nitro-verify-blur\" scripts/verify-blur.swift ios/GaussianBlur.swift && \"${TMPDIR:-/tmp}/nitro-verify-blur\""
18
21
  },
19
22
  "keywords": [
20
23
  "react-native",
@@ -59,26 +62,28 @@
59
62
  "registry": "https://registry.npmjs.org/"
60
63
  },
61
64
  "devDependencies": {
62
- "@biomejs/biome": "^2.5.6",
63
65
  "@semantic-release/changelog": "^7.0.0",
64
66
  "@semantic-release/git": "^11.0.1",
65
- "@types/bun": "^1.3.14",
67
+ "@types/bun": "^1.4.0",
66
68
  "@types/jest": "^30.0.0",
67
69
  "@types/react": "19.2.18",
68
- "conventional-changelog-conventionalcommits": "^10.2.1",
69
- "nitrogen": "0.36.5",
70
+ "conventional-changelog-conventionalcommits": "^9.1.0",
71
+ "lefthook": "^2.1.10",
72
+ "nitrogen": "0.37.0",
73
+ "oxfmt": "^0.65.0",
74
+ "oxlint": "^1.80.0",
70
75
  "react": "19.2.3",
71
76
  "react-native": "0.84.1",
72
77
  "react-native-builder-bob": "^0.43.0",
73
- "react-native-nitro-modules": "0.36.5",
74
- "semantic-release": "^25.0.8",
75
- "typescript": "^5.8.3"
78
+ "react-native-nitro-modules": "0.37.0",
79
+ "semantic-release": "^25.0.9",
80
+ "typescript": "^7.0.2"
76
81
  },
77
82
  "peerDependencies": {
78
83
  "react": "*",
79
84
  "react-native": "*",
80
85
  "react-native-nitro-modules": "*",
81
- "react-native-nitro-image": "*"
86
+ "react-native-nitro-image": "0.15.1"
82
87
  },
83
88
  "overrides": {
84
89
  "lodash-es": "4.17.21"
@@ -96,5 +101,8 @@
96
101
  }
97
102
  ]
98
103
  ]
99
- }
104
+ },
105
+ "trustedDependencies": [
106
+ "lefthook"
107
+ ]
100
108
  }
package/src/index.ts CHANGED
@@ -1,8 +1,14 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import type { Image } from 'react-native-nitro-image';
3
3
  import { NitroModules } from 'react-native-nitro-modules';
4
4
 
5
- import type { NitroImagePipeline as NitroImagePipelineSpec } from './specs/nitro-image-toolkit.nitro';
5
+ import type {
6
+ CacheOption,
7
+ NitroImagePipeline as NitroImagePipelineSpec,
8
+ Options,
9
+ } from './specs/nitro-image-toolkit.nitro';
10
+
11
+ export type { CacheOption, Options };
6
12
 
7
13
  export const NitroImagePipeline =
8
14
  NitroModules.createHybridObject<NitroImagePipelineSpec>('NitroImagePipeline');
@@ -36,31 +42,57 @@ export function useImage({
36
42
  url,
37
43
  blur = 0,
38
44
  cornerRadius = 0,
45
+ cache,
39
46
  }: {
40
47
  url: string;
48
+ /**
49
+ * Gaussian blur strength, as the standard deviation (sigma) of the blur in
50
+ * source-image pixels. Matches across iOS and Android; roughly half of
51
+ * React Native's `blurRadius`.
52
+ */
41
53
  blur?: number;
42
54
  cornerRadius?: number;
55
+ cache?: CacheOption;
43
56
  }): Result {
44
57
  const [image, setImage] = useState<Result>({
45
58
  image: undefined,
46
59
  error: undefined,
47
60
  });
61
+ const loadedUrlRef = useRef(url);
48
62
 
49
63
  useEffect(() => {
64
+ let cancelled = false;
65
+ // Only reset to the loading state when the URL changes; for same-URL
66
+ // param tweaks (blur/cornerRadius/cache) keep showing the current image
67
+ // until the new variant resolves, to avoid flashing empty.
68
+ if (loadedUrlRef.current !== url) {
69
+ loadedUrlRef.current = url;
70
+ setImage({ image: undefined, error: undefined });
71
+ }
72
+
50
73
  (async () => {
51
74
  try {
52
75
  const result = await NitroImagePipeline.loadImage(url, {
53
76
  blur,
54
77
  cornerRadius,
78
+ cache,
55
79
  });
56
80
 
57
- setImage({ image: result, error: undefined });
81
+ if (!cancelled) {
82
+ setImage({ image: result, error: undefined });
83
+ }
58
84
  } catch (e) {
59
85
  const error = e instanceof Error ? e : new Error(`${e}`);
60
- setImage({ image: undefined, error: error });
86
+ if (!cancelled) {
87
+ setImage({ image: undefined, error: error });
88
+ }
61
89
  }
62
90
  })();
63
- }, [url, blur]);
91
+
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, [url, blur, cornerRadius, cache]);
64
96
 
65
97
  return image;
66
98
  }
@@ -1,16 +1,36 @@
1
1
  import type { Image } from 'react-native-nitro-image';
2
2
  import type { HybridObject } from 'react-native-nitro-modules';
3
3
 
4
- type CacheOption = 'memory' | 'disk' | 'none';
5
- type Options = { blur?: number; cache?: CacheOption; cornerRadius?: number };
4
+ export type CacheOption = 'memory' | 'disk' | 'none';
5
+ export type Options = {
6
+ /**
7
+ * Gaussian blur strength, given as the standard deviation (sigma) of the
8
+ * blur in **source-image pixels**. The same value on the same source image
9
+ * produces the same result on iOS and Android.
10
+ *
11
+ * React Native's `<Image blurRadius={n} />` is roughly `blur: n / 2`.
12
+ *
13
+ * @default 0 (no blur)
14
+ */
15
+ blur?: number;
16
+ cache?: CacheOption;
17
+ cornerRadius?: number;
18
+ };
6
19
 
7
- export interface NitroImagePipeline
8
- extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
20
+ export interface NitroImagePipeline extends HybridObject<{
21
+ ios: 'swift';
22
+ android: 'kotlin';
23
+ }> {
9
24
  loadImage(url: string, options?: Options): Promise<Image>;
10
25
  preLoadImage(url: string): Promise<void>;
11
26
  preLoadImages(urls: string[]): Promise<void>;
27
+ /**
28
+ * Blurs an already-loaded image. `radius` is the standard deviation (sigma)
29
+ * of the Gaussian in **source-image pixels**, the same unit as
30
+ * {@linkcode Options.blur}.
31
+ */
12
32
  gaussianBlur(image: Image, radius: number): Promise<Image>;
13
33
  // Future: brightness, saturation, tint, etc.
14
34
 
15
- clearCache(): void;
35
+ clearCache(): Promise<void>;
16
36
  }