react-native-nitro-image-pipeline 0.1.1 → 0.2.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/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +59 -45
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/NitroImagePipelinePackage.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt +59 -53
- package/ios/HybridNitroImagePipeline.swift +14 -4
- package/package.json +2 -2
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt
CHANGED
|
@@ -8,6 +8,7 @@ import coil3.ImageLoader
|
|
|
8
8
|
import coil3.disk.DiskCache
|
|
9
9
|
import coil3.memory.MemoryCache
|
|
10
10
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|
11
|
+
import coil3.request.CachePolicy
|
|
11
12
|
import coil3.request.ErrorResult
|
|
12
13
|
import coil3.request.ImageRequest
|
|
13
14
|
import coil3.request.SuccessResult
|
|
@@ -53,55 +54,68 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
53
54
|
.build()
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
override fun loadImage(url: String, options: Options?): Promise<HybridImageSpec> =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
val result = imageLoader.execute(request)
|
|
78
|
-
when (result) {
|
|
79
|
-
is ErrorResult -> throw result.throwable
|
|
80
|
-
is SuccessResult -> {
|
|
81
|
-
val bitmap =
|
|
82
|
-
when (val img = result.image) {
|
|
83
|
-
is BitmapImage -> img.bitmap
|
|
84
|
-
is DrawableImage -> img.drawable.toBitmap()
|
|
85
|
-
else -> throw Error("Unsupported image type: ${img.javaClass.simpleName}")
|
|
57
|
+
override fun loadImage(url: String, options: Options?): Promise<HybridImageSpec> = Promise.async {
|
|
58
|
+
val blur = options?.blur?.toFloat() ?: 0f
|
|
59
|
+
val cornerRadius = options?.cornerRadius?.toFloat() ?: 0f
|
|
60
|
+
val transformations = buildList {
|
|
61
|
+
if (blur > 0f) add(BlurTransformation(context, blur))
|
|
62
|
+
if (cornerRadius > 0f) add(RoundedCornersTransformation(cornerRadius))
|
|
63
|
+
}
|
|
64
|
+
val memoryCacheKey = buildString {
|
|
65
|
+
append(url)
|
|
66
|
+
if (blur > 0f) append("_blur$blur")
|
|
67
|
+
if (cornerRadius > 0f) append("_corner$cornerRadius")
|
|
68
|
+
}
|
|
69
|
+
val request =
|
|
70
|
+
ImageRequest.Builder(context)
|
|
71
|
+
.data(url)
|
|
72
|
+
.apply {
|
|
73
|
+
when (options?.cache) {
|
|
74
|
+
CacheOption.MEMORY -> {
|
|
75
|
+
memoryCachePolicy(CachePolicy.ENABLED)
|
|
76
|
+
diskCachePolicy(CachePolicy.DISABLED)
|
|
86
77
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
CacheOption.DISK -> {
|
|
79
|
+
memoryCachePolicy(CachePolicy.DISABLED)
|
|
80
|
+
diskCachePolicy(CachePolicy.ENABLED)
|
|
81
|
+
}
|
|
82
|
+
CacheOption.NONE -> {
|
|
83
|
+
memoryCachePolicy(CachePolicy.DISABLED)
|
|
84
|
+
diskCachePolicy(CachePolicy.DISABLED)
|
|
85
|
+
}
|
|
86
|
+
null -> Unit // Coil defaults: both enabled
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
.allowHardware(true)
|
|
90
|
+
.memoryCacheKey(memoryCacheKey)
|
|
91
|
+
.transformations(transformations)
|
|
92
|
+
.build()
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
when (val result = imageLoader.execute(request)) {
|
|
95
|
+
is ErrorResult -> throw result.throwable
|
|
96
|
+
is SuccessResult -> {
|
|
97
|
+
val bitmap =
|
|
98
|
+
when (val img = result.image) {
|
|
99
|
+
is BitmapImage -> img.bitmap
|
|
100
|
+
is DrawableImage -> img.drawable.toBitmap()
|
|
101
|
+
else -> throw Error("Unsupported image type: ${img.javaClass.simpleName}")
|
|
102
|
+
}
|
|
103
|
+
HybridImage(bitmap)
|
|
96
104
|
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
97
107
|
|
|
98
|
-
override fun
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
override fun preLoadImage(url: String): Promise<Unit> = Promise.async {
|
|
109
|
+
val request = ImageRequest.Builder(context).data(url).build()
|
|
110
|
+
imageLoader.execute(request)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
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)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
105
119
|
|
|
106
120
|
override fun gaussianBlur(image: HybridImageSpec, radius: Double): Promise<HybridImageSpec> =
|
|
107
121
|
Promise.async {
|
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/NitroImagePipelinePackage.kt
CHANGED
|
@@ -8,12 +8,13 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
|
8
8
|
public class NitroImagePipelinePackage : BaseReactPackage() {
|
|
9
9
|
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
|
|
10
10
|
|
|
11
|
-
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
|
|
12
|
-
|
|
11
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
12
|
+
emptyMap()
|
|
13
|
+
}
|
|
13
14
|
|
|
14
15
|
companion object {
|
|
15
16
|
init {
|
|
16
|
-
NitroImagePipelineOnLoad.initializeNative()
|
|
17
|
+
NitroImagePipelineOnLoad.initializeNative()
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
}
|
|
20
|
+
}
|
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt
CHANGED
|
@@ -20,65 +20,71 @@ class BlurTransformation(
|
|
|
20
20
|
private val sampling: Float = 1f,
|
|
21
21
|
) : Transformation() {
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
override val cacheKey = "${BlurTransformation::class.java.name}-$radius-$sampling"
|
|
29
|
-
|
|
30
|
-
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
|
31
|
-
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 config = input.config ?: Bitmap.Config.ARGB_8888
|
|
23
|
+
init {
|
|
24
|
+
require(radius in 0.01..25.0) { "radius must be in (0, 25]." }
|
|
25
|
+
require(sampling >= 1f) { "sampling must be >= 1." }
|
|
26
|
+
}
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
output.applyCanvas {
|
|
41
|
-
scale(1 / autoSampling, 1 / autoSampling)
|
|
42
|
-
drawBitmap(input, 0f, 0f, paint)
|
|
43
|
-
}
|
|
28
|
+
override val cacheKey = "${BlurTransformation::class.java.name}-$radius-$sampling"
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
blur = ScriptIntrinsicBlur.create(script, Element.U8_4(script))
|
|
54
|
-
blur.setRadius(radius)
|
|
55
|
-
blur.setInput(tmpIn)
|
|
56
|
-
blur.forEach(tmpOut)
|
|
57
|
-
tmpOut.copyTo(output)
|
|
58
|
-
} finally {
|
|
59
|
-
script?.destroy()
|
|
60
|
-
tmpIn?.destroy()
|
|
61
|
-
tmpOut?.destroy()
|
|
62
|
-
blur?.destroy()
|
|
63
|
-
}
|
|
30
|
+
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
|
31
|
+
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 config = input.config ?: Bitmap.Config.ARGB_8888
|
|
64
38
|
|
|
65
|
-
|
|
39
|
+
val output = Bitmap.createBitmap(scaledWidth, scaledHeight, config)
|
|
40
|
+
output.applyCanvas {
|
|
41
|
+
scale(1 / autoSampling, 1 / autoSampling)
|
|
42
|
+
drawBitmap(input, 0f, 0f, paint)
|
|
66
43
|
}
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
45
|
+
var script: RenderScript? = null
|
|
46
|
+
var tmpIn: Allocation? = null
|
|
47
|
+
var tmpOut: Allocation? = null
|
|
48
|
+
var blur: ScriptIntrinsicBlur? = null
|
|
49
|
+
try {
|
|
50
|
+
script = RenderScript.create(context)
|
|
51
|
+
tmpIn =
|
|
52
|
+
Allocation.createFromBitmap(
|
|
53
|
+
script,
|
|
54
|
+
output,
|
|
55
|
+
Allocation.MipmapControl.MIPMAP_NONE,
|
|
56
|
+
Allocation.USAGE_SCRIPT,
|
|
57
|
+
)
|
|
58
|
+
tmpOut = Allocation.createTyped(script, tmpIn.type)
|
|
59
|
+
blur = ScriptIntrinsicBlur.create(script, Element.U8_4(script))
|
|
60
|
+
blur.setRadius(radius)
|
|
61
|
+
blur.setInput(tmpIn)
|
|
62
|
+
blur.forEach(tmpOut)
|
|
63
|
+
tmpOut.copyTo(output)
|
|
64
|
+
} finally {
|
|
65
|
+
script?.destroy()
|
|
66
|
+
tmpIn?.destroy()
|
|
67
|
+
tmpOut?.destroy()
|
|
68
|
+
blur?.destroy()
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
return output.scale(input.width, input.height)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun equals(other: Any?): Boolean {
|
|
75
|
+
if (this === other) return true
|
|
76
|
+
return other is BlurTransformation &&
|
|
77
|
+
context == other.context &&
|
|
78
|
+
radius == other.radius &&
|
|
79
|
+
sampling == other.sampling
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun hashCode(): Int {
|
|
83
|
+
var result = context.hashCode()
|
|
84
|
+
result = 31 * result + radius.hashCode()
|
|
85
|
+
result = 31 * result + sampling.hashCode()
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
override fun toString() = "BlurTransformation(radius=$radius, sampling=$sampling)"
|
|
84
90
|
}
|
|
@@ -58,11 +58,21 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
|
|
|
58
58
|
|
|
59
59
|
func loadImage(url: String, options: Options?) throws -> Promise<any HybridImageSpec> {
|
|
60
60
|
return Promise.async {
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
let cacheOptions: ImageRequest.Options = switch options?.cache {
|
|
62
|
+
case .memory: [.disableDiskCache]
|
|
63
|
+
case .disk: [.disableMemoryCache]
|
|
64
|
+
case .none?: [.disableDiskCache, .disableMemoryCache]
|
|
65
|
+
default: []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let imgRequest = ImageRequest(
|
|
69
|
+
url: URL(string: url),
|
|
70
|
+
processors: [
|
|
71
|
+
.gaussianBlur(radius: Int(options?.blur ?? 0)),
|
|
72
|
+
.roundedCorners(radius: options?.cornerRadius ?? 0)
|
|
73
|
+
], options: cacheOptions
|
|
74
|
+
)
|
|
64
75
|
|
|
65
|
-
])
|
|
66
76
|
let image = try await ImagePipeline.shared.image(for: imgRequest)
|
|
67
77
|
return HybridImage(uiImage: image)
|
|
68
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-image-pipeline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@biomejs/biome": "^2.4.10",
|
|
63
63
|
"@semantic-release/changelog": "^6.0.3",
|
|
64
64
|
"@semantic-release/git": "^10.0.1",
|
|
65
|
-
"@types/jest": "^
|
|
65
|
+
"@types/jest": "^30.0.0",
|
|
66
66
|
"@types/react": "19.2.0",
|
|
67
67
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
68
68
|
"nitrogen": "0.35.2",
|