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.
- package/README.md +60 -6
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +68 -38
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt +50 -24
- package/ios/GaussianBlur.swift +135 -0
- package/ios/GaussianBlurProcessor.swift +41 -0
- package/ios/HybridNitroImagePipeline.swift +70 -25
- package/lib/commonjs/index.js +32 -11
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +33 -12
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +17 -4
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +14 -3
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.hpp +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipelineSpec.kt +1 -1
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +0 -9
- package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +3 -1
- package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec_cxx.swift +12 -4
- package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.hpp +1 -1
- package/package.json +21 -13
- package/src/index.ts +37 -5
- 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
|
|
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
|
|
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt
CHANGED
|
@@ -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
|
|
34
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
124
|
-
val
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
}
|
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt
CHANGED
|
@@ -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
|
|
20
|
-
private val sampling: Float = 1f,
|
|
32
|
+
private val sigma: Float,
|
|
21
33
|
) : Transformation() {
|
|
22
34
|
|
|
23
35
|
init {
|
|
24
|
-
require(
|
|
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}-$
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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:
|
|
128
|
+
url: imageUrl,
|
|
78
129
|
processors: processors,
|
|
79
130
|
options: cacheOptions
|
|
80
131
|
)
|
|
81
132
|
|
|
82
|
-
let image = try await
|
|
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
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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:
|
|
171
|
+
return HybridImage(uiImage: blurred)
|
|
127
172
|
}
|
|
128
173
|
}
|
|
129
174
|
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
if (!cancelled) {
|
|
58
|
+
setImage({
|
|
59
|
+
image: undefined,
|
|
60
|
+
error: error
|
|
61
|
+
});
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
64
|
})();
|
|
47
|
-
|
|
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;
|
|
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":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
if (!cancelled) {
|
|
53
|
+
setImage({
|
|
54
|
+
image: undefined,
|
|
55
|
+
error: error
|
|
56
|
+
});
|
|
57
|
+
}
|
|
40
58
|
}
|
|
41
59
|
})();
|
|
42
|
-
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
};
|
|
63
|
+
}, [url, blur, cornerRadius, cache]);
|
|
43
64
|
return image;
|
|
44
65
|
}
|
|
45
66
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -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;
|
|
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,
|
|
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,
|
|
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<
|
|
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.
|
|
215
|
+
public final func clearCache() -> bridge.Result_std__shared_ptr_Promise_void___ {
|
|
216
216
|
do {
|
|
217
|
-
try self.__implementation.clearCache()
|
|
218
|
-
|
|
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.
|
|
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
|
+
"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": "
|
|
13
|
-
"
|
|
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.
|
|
67
|
+
"@types/bun": "^1.4.0",
|
|
66
68
|
"@types/jest": "^30.0.0",
|
|
67
69
|
"@types/react": "19.2.18",
|
|
68
|
-
"conventional-changelog-conventionalcommits": "^
|
|
69
|
-
"
|
|
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.
|
|
74
|
-
"semantic-release": "^25.0.
|
|
75
|
-
"typescript": "^
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setImage({ image: undefined, error: error });
|
|
88
|
+
}
|
|
61
89
|
}
|
|
62
90
|
})();
|
|
63
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
}
|