@onekeyfe/react-native-image 6.7.0-alpha.415 → 6.7.0-alpha.421

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 (83) hide show
  1. package/README.md +12 -0
  2. package/android/build.gradle +3 -1
  3. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyIcon.kt +93 -0
  4. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageGlideRegistry.kt +1 -1
  5. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImagePackage.kt +2 -1
  6. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyNamedSVG.kt +217 -0
  7. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeySVG.kt +31 -0
  8. package/ios/OneKeyIcon.swift +90 -0
  9. package/ios/OneKeyImageCoderBridge.h +4 -0
  10. package/ios/OneKeyImageCoderBridge.m +9 -0
  11. package/ios/OneKeyNamedSVG.swift +282 -0
  12. package/ios/tests/OneKeyIconTests.swift +484 -0
  13. package/lib/module/OneKeyIcon.nitro.js +4 -0
  14. package/lib/module/icon/assetUrls.js +13 -0
  15. package/lib/module/icon/dom.js +21 -0
  16. package/lib/module/icon/index.js +29 -0
  17. package/lib/module/icon/index.web.js +32 -0
  18. package/lib/module/icon/props.js +22 -0
  19. package/lib/module/package.json +6 -1
  20. package/lib/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.cpp +67 -0
  21. package/lib/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.hpp +66 -0
  22. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.cpp +79 -0
  23. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.hpp +54 -0
  24. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyIconSpec.kt +65 -0
  25. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconManager.kt +96 -0
  26. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconStateUpdater.kt +23 -0
  27. package/lib/nitrogen/generated/android/onekeyimage+autolinking.cmake +4 -0
  28. package/lib/nitrogen/generated/android/onekeyimageOnLoad.cpp +18 -0
  29. package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +17 -0
  30. package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +32 -15
  31. package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +5 -0
  32. package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.mm +8 -0
  33. package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.swift +12 -0
  34. package/lib/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.cpp +11 -0
  35. package/lib/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.hpp +89 -0
  36. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyIconComponent.mm +150 -0
  37. package/lib/nitrogen/generated/ios/swift/HybridOneKeyIconSpec.swift +56 -0
  38. package/lib/nitrogen/generated/ios/swift/HybridOneKeyIconSpec_cxx.swift +193 -0
  39. package/lib/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.cpp +24 -0
  40. package/lib/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.hpp +66 -0
  41. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.cpp +36 -0
  42. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.hpp +88 -0
  43. package/lib/nitrogen/generated/shared/json/OneKeyIconConfig.json +11 -0
  44. package/lib/typescript/src/OneKeyIcon.nitro.d.ts +7 -0
  45. package/lib/typescript/src/icon/assetUrls.d.ts +2 -0
  46. package/lib/typescript/src/icon/dom.d.ts +3 -0
  47. package/lib/typescript/src/icon/index.d.ts +140 -0
  48. package/lib/typescript/src/icon/index.web.d.ts +134 -0
  49. package/lib/typescript/src/icon/props.d.ts +8 -0
  50. package/nitro.json +10 -0
  51. package/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.cpp +67 -0
  52. package/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.hpp +66 -0
  53. package/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.cpp +79 -0
  54. package/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.hpp +54 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyIconSpec.kt +65 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconManager.kt +96 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconStateUpdater.kt +23 -0
  58. package/nitrogen/generated/android/onekeyimage+autolinking.cmake +4 -0
  59. package/nitrogen/generated/android/onekeyimageOnLoad.cpp +18 -0
  60. package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +17 -0
  61. package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +32 -15
  62. package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +5 -0
  63. package/nitrogen/generated/ios/OneKeyImageAutolinking.mm +8 -0
  64. package/nitrogen/generated/ios/OneKeyImageAutolinking.swift +12 -0
  65. package/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.cpp +11 -0
  66. package/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.hpp +89 -0
  67. package/nitrogen/generated/ios/c++/views/HybridOneKeyIconComponent.mm +150 -0
  68. package/nitrogen/generated/ios/swift/HybridOneKeyIconSpec.swift +56 -0
  69. package/nitrogen/generated/ios/swift/HybridOneKeyIconSpec_cxx.swift +193 -0
  70. package/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.cpp +24 -0
  71. package/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.hpp +66 -0
  72. package/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.cpp +36 -0
  73. package/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.hpp +88 -0
  74. package/nitrogen/generated/shared/json/OneKeyIconConfig.json +11 -0
  75. package/package.json +40 -7
  76. package/scripts/configure-icon-imports.mjs +7 -0
  77. package/scripts/icon-fixtures.test.mjs +398 -0
  78. package/src/OneKeyIcon.nitro.ts +8 -0
  79. package/src/icon/assetUrls.ts +13 -0
  80. package/src/icon/dom.ts +21 -0
  81. package/src/icon/index.tsx +38 -0
  82. package/src/icon/index.web.tsx +36 -0
  83. package/src/icon/props.ts +38 -0
package/README.md CHANGED
@@ -95,3 +95,15 @@ pod 'SDWebImage', '5.21.7', :modular_headers => true
95
95
  pod 'SDWebImageWebPCoder', '0.14.6', :modular_headers => true
96
96
  pod 'SDWebImageSVGCoder', '1.7.0', :modular_headers => true
97
97
  ```
98
+
99
+ ## Named icon resource ownership
100
+
101
+ The host owns canonical SVG artwork and build output. Native builds supply a
102
+ `OneKeyIcons.bundle` on iOS and `assets/icons` on Android, including the manifest.
103
+ Web builds alias `#onekey-icon-assets` to a generated module exporting
104
+ `iconResourceHref(name: string): string | undefined`; the default SDK provider
105
+ is empty. `OneKeyIcon` props are unchanged. See [the resource contract](docs/SPEC.md#16-host-owned-icon-resources--implemented-sdk-fixture-runtime-verified).
106
+
107
+ The SDK example carries 63 independent test samples. `icons:check` and
108
+ `icons:test` are repository maintenance commands that require the example
109
+ fixtures; npm consumers do not receive those samples or the complete catalog.
@@ -63,7 +63,9 @@ android {
63
63
  targetCompatibility JavaVersion.VERSION_1_8
64
64
  }
65
65
  sourceSets {
66
- main { java.srcDirs += ["generated/java", "generated/jni"] }
66
+ main {
67
+ java.srcDirs += ["generated/java", "generated/jni"]
68
+ }
67
69
  }
68
70
  }
69
71
 
@@ -0,0 +1,93 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import android.graphics.PorterDuff
6
+ import android.graphics.PorterDuffColorFilter
7
+ import android.widget.ImageView
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.margelo.nitro.views.RecyclableView
10
+ import java.lang.ref.WeakReference
11
+
12
+ private class OneKeyIconHost(context: Context) : ImageView(context) {
13
+ var changed: (() -> Unit)? = null
14
+ var attached = false
15
+ private set
16
+ init { scaleType = ScaleType.FIT_CENTER }
17
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
18
+ super.onSizeChanged(w, h, oldw, oldh)
19
+ changed?.invoke()
20
+ }
21
+ override fun onAttachedToWindow() { super.onAttachedToWindow(); attached = true; changed?.invoke() }
22
+ override fun onDetachedFromWindow() { attached = false; super.onDetachedFromWindow(); changed?.invoke() }
23
+ override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig); changed?.invoke() }
24
+ }
25
+
26
+ class HybridOneKeyIcon(context: ThemedReactContext) : HybridOneKeyIconSpec(), RecyclableView {
27
+ private val host = OneKeyIconHost(context)
28
+ private val owner = Any()
29
+ private var generation = 0L
30
+ private var identity: String? = null
31
+ private var displayedGeometry: String? = null
32
+ private var displayed: OneKeyNamedSVG.RenderResult? = null
33
+ override val view: ImageView get() = host
34
+ override var iconName: String? = null
35
+ override var resolvedColor: String? = null
36
+ init { host.changed = { refresh() } }
37
+
38
+ override fun afterUpdate() { super.afterUpdate(); refresh() }
39
+
40
+ private fun clearImage() {
41
+ displayed = null; displayedGeometry = null
42
+ host.colorFilter = null; host.setImageDrawable(null)
43
+ }
44
+
45
+ private fun applyTint(result: OneKeyNamedSVG.RenderResult, color: String) {
46
+ host.colorFilter = result.tintColor(color)?.let { PorterDuffColorFilter(it, PorterDuff.Mode.SRC_IN) }
47
+ }
48
+
49
+ private fun refresh() {
50
+ val scale = host.resources.displayMetrics.density
51
+ val next = "$iconName|$resolvedColor|${host.width}|${host.height}|$scale|${host.attached}"
52
+ if (identity == next) return
53
+ identity = next; generation++
54
+ val revision = generation
55
+ OneKeyNamedSVG.cancel(owner)
56
+ val name = iconName
57
+ val color = resolvedColor
58
+ if (!host.attached || host.width !in 1..4096 || host.height !in 1..4096 ||
59
+ host.width.toLong() * host.height > 4194304 || name.isNullOrEmpty() || color == null ||
60
+ !OneKeyNamedSVG.isValidColor(color)) { clearImage(); return }
61
+ val geometry = "$name|${host.width}|${host.height}|$scale"
62
+ val ready = displayed
63
+ if (displayedGeometry == geometry && ready != null && host.drawable != null) {
64
+ if (ready.canTint(color)) { applyTint(ready, color); return }
65
+ } else clearImage()
66
+ OneKeyNamedSVG.cachedRender(name, color, host.width, host.height, scale)?.let { hit ->
67
+ displayed = hit; displayedGeometry = geometry
68
+ applyTint(hit, color)
69
+ host.setImageBitmap(hit.bitmap)
70
+ return
71
+ }
72
+ val weakHost = WeakReference(host)
73
+ val weakSelf = WeakReference(this)
74
+ OneKeyNamedSVG.requestRender(owner, host.context.applicationContext.assets, name, color,
75
+ host.width, host.height, scale) { result ->
76
+ val current = weakSelf.get()
77
+ val target = weakHost.get()
78
+ if (current?.generation != revision || target?.attached != true) return@requestRender
79
+ if (result == null) { current.clearImage(); return@requestRender }
80
+ current.displayed = result; current.displayedGeometry = geometry
81
+ current.applyTint(result, color)
82
+ target.setImageBitmap(result.bitmap)
83
+ }
84
+ }
85
+
86
+ override fun prepareForRecycle() {
87
+ iconName = null
88
+ resolvedColor = null
89
+ refresh()
90
+ }
91
+ override fun onDropView() { prepareForRecycle(); super.onDropView() }
92
+ override fun dispose() { prepareForRecycle(); host.changed = null; super.dispose() }
93
+ }
@@ -266,7 +266,7 @@ private class OneKeySvgDecoder : ResourceDecoder<InputStream, SVG> {
266
266
  options: Options,
267
267
  ): Resource<SVG> {
268
268
  val svg = try {
269
- SVG.getFromInputStream(source)
269
+ OneKeySVG.parse(source)
270
270
  } catch (error: SVGParseException) {
271
271
  throw IOException("Cannot decode SVG", error)
272
272
  }
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.module.model.ReactModuleInfoProvider
7
7
  import com.facebook.react.uimanager.ViewManager
8
+ import com.margelo.nitro.onekeyimage.views.HybridOneKeyIconManager
8
9
  import com.margelo.nitro.onekeyimage.views.HybridOneKeyImageManager
9
10
 
10
11
  class OneKeyImagePackage : BaseReactPackage() {
@@ -17,7 +18,7 @@ class OneKeyImagePackage : BaseReactPackage() {
17
18
  reactContext: ReactApplicationContext,
18
19
  ): List<ViewManager<*, *>> {
19
20
  OneKeyImageGlideRegistry.ensureRegistered(reactContext)
20
- return listOf(HybridOneKeyImageManager())
21
+ return listOf(HybridOneKeyImageManager(), HybridOneKeyIconManager())
21
22
  }
22
23
 
23
24
  companion object {
@@ -0,0 +1,217 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import android.content.res.AssetManager
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.PorterDuff
9
+ import android.graphics.PorterDuffColorFilter
10
+ import android.os.Handler
11
+ import android.os.Looper
12
+ import android.util.Log
13
+ import org.json.JSONObject
14
+ import java.util.concurrent.Executors
15
+ import kotlin.math.roundToInt
16
+
17
+ /** Process-owned named resources; no React runtime or Activity is retained. */
18
+ object OneKeyNamedSVG {
19
+ enum class RecolorMode { FIXED, SINGLE, MULTIPLE, RASTER }
20
+ data class IconMetadata(
21
+ val width: Float,
22
+ val height: Float,
23
+ val mode: RecolorMode,
24
+ )
25
+ data class RenderResult(val bitmap: Bitmap, val mode: RecolorMode,
26
+ val sourceIdentity: String, val maskAlpha: Int) {
27
+ fun canTint(color: String): Boolean {
28
+ val value = parseColor(color) ?: return false
29
+ return mode == RecolorMode.FIXED || mode == RecolorMode.SINGLE ||
30
+ (mode == RecolorMode.MULTIPLE && Color.alpha(value) == maskAlpha)
31
+ }
32
+ fun tintColor(color: String): Int? {
33
+ val value = parseColor(color) ?: return null
34
+ return when(mode) {
35
+ RecolorMode.SINGLE -> value
36
+ RecolorMode.MULTIPLE -> value or (0xff shl 24)
37
+ else -> null
38
+ }
39
+ }
40
+ internal fun finalBitmap(color: String): Bitmap {
41
+ val tint = tintColor(color) ?: return bitmap
42
+ if (tint == Color.BLACK) return bitmap
43
+ val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
44
+ val paint = Paint().apply { colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN) }
45
+ Canvas(output).drawBitmap(bitmap, 0f, 0f, paint)
46
+ return output
47
+ }
48
+ }
49
+
50
+ private data class Request(val assets: AssetManager, val name: String, val color: String,
51
+ val width: Int, val height: Int, val scale: Float, val finalColor: Boolean, val complete: (RenderResult?) -> Unit)
52
+ private val lock = Any()
53
+ private val pending = LinkedHashMap<Any, Request>()
54
+ private val cache = LinkedHashMap<String, RenderResult>(128, 0.75f, true)
55
+ private var cacheBytes = 0L
56
+ private var activeWorkers = 0
57
+ private var manifest: JSONObject? = null
58
+ private val manifestLock = Any()
59
+ private val workers = Executors.newFixedThreadPool(2)
60
+ private val main = Handler(Looper.getMainLooper())
61
+ private val colorPattern = Regex("rgba\\(\\d{1,3},\\d{1,3},\\d{1,3},(?:0(?:\\.\\d+)?|1(?:\\.0+)?)\\)")
62
+
63
+ private var manifestMissingLogged = false
64
+
65
+ fun cancel(owner: Any) { synchronized(lock) { pending.remove(owner) } }
66
+
67
+ /** Caller holds manifestLock. Hosts own the resources, so a miss is logged once. */
68
+ private fun loadManifestLocked(assets: AssetManager): JSONObject? {
69
+ manifest?.let { return it }
70
+ return try {
71
+ assets.open("icons/manifest.json").bufferedReader().use { JSONObject(it.readText()) }
72
+ .also { manifest = it }
73
+ } catch (error: Exception) {
74
+ if (!manifestMissingLogged) {
75
+ manifestMissingLogged = true
76
+ Log.w("OneKeyIcon", "assets/icons/manifest.json is missing or invalid; named icons will not render", error)
77
+ }
78
+ null
79
+ }
80
+ }
81
+
82
+ private fun modeOf(metadata: JSONObject): RecolorMode = try {
83
+ RecolorMode.valueOf(metadata.optString("recolorMode", "raster").uppercase())
84
+ } catch (_: IllegalArgumentException) {
85
+ RecolorMode.RASTER
86
+ }
87
+
88
+ private data class RenderKey(val identity: String, val key: String, val maskAlpha: Int)
89
+
90
+ private fun renderKey(name: String, digest: String, width: Int, height: Int, scale: Float,
91
+ mode: RecolorMode, requestColor: String, color: Int): RenderKey {
92
+ val identity = "$name|$digest|$width|$height|$scale"
93
+ val maskAlpha = if (mode == RecolorMode.MULTIPLE) Color.alpha(color) else 255
94
+ val colorKey = when(mode) {
95
+ RecolorMode.RASTER -> requestColor
96
+ RecolorMode.MULTIPLE -> maskAlpha.toString()
97
+ else -> ""
98
+ }
99
+ return RenderKey(identity, "$identity|$mode|$colorKey", maskAlpha)
100
+ }
101
+
102
+ /**
103
+ * Main-thread fast path: returns an already-decoded result without asset I/O or
104
+ * manifest loading, so remounted and recycled views paint in the same frame.
105
+ */
106
+ fun cachedRender(name: String, color: String, width: Int, height: Int, scale: Float): RenderResult? {
107
+ val value = parseColor(color) ?: return null
108
+ val metadata = synchronized(manifestLock) { manifest?.optJSONObject(name) } ?: return null
109
+ val digest = metadata.optString("digest").takeIf { it.isNotEmpty() } ?: return null
110
+ val key = renderKey(name, digest, width, height, scale, modeOf(metadata), color, value).key
111
+ return synchronized(lock) { cache[key] }
112
+ }
113
+
114
+ fun iconMetadata(assets: AssetManager, name: String): IconMetadata? {
115
+ val trimmed = name.trim()
116
+ if (trimmed.isEmpty()) return null
117
+ val metadata = try {
118
+ synchronized(manifestLock) {
119
+ loadManifestLocked(assets)?.optJSONObject(trimmed)
120
+ }
121
+ } catch (_: Exception) {
122
+ null
123
+ } ?: return null
124
+ val mode = modeOf(metadata)
125
+ val viewBox = metadata.optJSONArray("viewBox")
126
+ val vbWidth = if (viewBox != null && viewBox.length() >= 4) viewBox.optDouble(2, 24.0).toFloat() else 24f
127
+ val vbHeight = if (viewBox != null && viewBox.length() >= 4) viewBox.optDouble(3, 24.0).toFloat() else 24f
128
+ val width = if (vbWidth > 0f) vbWidth else 24f
129
+ val height = if (vbHeight > 0f) vbHeight else 24f
130
+ return IconMetadata(width = width, height = height, mode = mode)
131
+ }
132
+
133
+ fun request(owner: Any, assets: AssetManager, name: String, color: String,
134
+ width: Int, height: Int, scale: Float, complete: (Bitmap?) -> Unit) {
135
+ enqueue(owner, assets, name, color, width, height, scale, true) { complete(it?.bitmap) }
136
+ }
137
+
138
+ fun requestRender(owner: Any, assets: AssetManager, name: String, color: String,
139
+ width: Int, height: Int, scale: Float, complete: (RenderResult?) -> Unit) {
140
+ enqueue(owner, assets, name, color, width, height, scale, false, complete)
141
+ }
142
+
143
+ private fun enqueue(owner: Any, assets: AssetManager, name: String, color: String,
144
+ width: Int, height: Int, scale: Float, finalColor: Boolean, complete: (RenderResult?) -> Unit) {
145
+ synchronized(lock) {
146
+ pending[owner] = Request(assets, name, color, width, height, scale, finalColor, complete)
147
+ while (activeWorkers < 2 && activeWorkers < pending.size) {
148
+ activeWorkers++
149
+ workers.execute { drain() }
150
+ }
151
+ }
152
+ }
153
+
154
+ private fun parseColor(color: String): Int? {
155
+ if (!colorPattern.matches(color)) return null
156
+ val channels = color.removePrefix("rgba(").removeSuffix(")").split(',')
157
+ val rgb = channels.take(3).map { it.toInt() }
158
+ if (rgb.any { it !in 0..255 }) return null
159
+ val alpha = (channels[3].toDouble() * 255).roundToInt()
160
+ return Color.argb(alpha, rgb[0], rgb[1], rgb[2])
161
+ }
162
+ fun isValidColor(color: String): Boolean = parseColor(color) != null
163
+
164
+ private fun drain() {
165
+ while (true) {
166
+ val request = synchronized(lock) {
167
+ val first = pending.entries.firstOrNull()
168
+ if (first == null) { activeWorkers--; null }
169
+ else { pending.remove(first.key); first.value }
170
+ } ?: return
171
+ val bitmap = try {
172
+ decode(request)?.let { result ->
173
+ if (request.finalColor) result.copy(bitmap = result.finalBitmap(request.color), mode = RecolorMode.RASTER)
174
+ else result
175
+ }
176
+ } catch (_: Throwable) { null } // OOM must not strand activeWorkers.
177
+ main.post { request.complete(bitmap) }
178
+ }
179
+ }
180
+
181
+ private fun decode(request: Request): RenderResult? {
182
+ if (request.width !in 1..4096 || request.height !in 1..4096 ||
183
+ request.width.toLong() * request.height > 4194304 || !request.scale.isFinite() ||
184
+ request.scale <= 0) return null
185
+ val color = parseColor(request.color) ?: return null
186
+ val metadata = synchronized(manifestLock) {
187
+ loadManifestLocked(request.assets)?.optJSONObject(request.name)
188
+ } ?: return null
189
+ val mode = modeOf(metadata)
190
+ val (sourceIdentity, key, maskAlpha) = renderKey(request.name, metadata.getString("digest"),
191
+ request.width, request.height, request.scale, mode, request.color, color)
192
+ val paintColor = when(mode) {
193
+ RecolorMode.FIXED, RecolorMode.SINGLE -> Color.BLACK
194
+ RecolorMode.MULTIPLE -> Color.argb(maskAlpha, 0, 0, 0)
195
+ RecolorMode.RASTER -> color
196
+ }
197
+ synchronized(lock) { cache[key]?.let { return it } }
198
+ // AndroidSVG rgba quantizes with 256; SVG hex alpha preserves the exact CSS 8-bit channel.
199
+ val svgColor = listOf(Color.red(paintColor), Color.green(paintColor), Color.blue(paintColor), Color.alpha(paintColor))
200
+ .joinToString(separator = "", prefix = "#") { it.toString(16).padStart(2, '0') }
201
+ val xml = request.assets.open("icons/${request.name}.svg").bufferedReader().use { it.readText() }
202
+ .replace(Regex("((?:fill|stroke)\\s*=\\s*[\"'])currentColor([\"'])"), "$1${svgColor}$2")
203
+ val bitmap = OneKeySVG.decode(xml.byteInputStream()).render(request.width, request.height)
204
+ val result = RenderResult(bitmap, mode, sourceIdentity, maskAlpha)
205
+ val bytes = bitmap.allocationByteCount.toLong()
206
+ if (bytes <= 8388608) synchronized(lock) {
207
+ cache.put(key, result)?.let { cacheBytes -= it.bitmap.allocationByteCount }
208
+ cacheBytes += bytes
209
+ while (cache.size > 128 || cacheBytes > 8388608) {
210
+ val first = cache.entries.first()
211
+ cacheBytes -= first.value.bitmap.allocationByteCount
212
+ cache.remove(first.key)
213
+ }
214
+ }
215
+ return result
216
+ }
217
+ }
@@ -0,0 +1,31 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import com.caverock.androidsvg.SVG
6
+ import java.io.InputStream
7
+
8
+ /** Synchronous per-request document; loaders retain scheduling and cancellation. */
9
+ object OneKeySVG {
10
+ fun decode(source: InputStream): OneKeySVGDocument = OneKeySVGDocument(parse(source))
11
+ internal fun parse(source: InputStream): SVG = SVG.getFromInputStream(source)
12
+ }
13
+
14
+ class OneKeySVGDocument internal constructor(private val svg: SVG) {
15
+ val viewBoxWidth: Float? = svg.documentViewBox?.width()
16
+ val viewBoxHeight: Float? = svg.documentViewBox?.height()
17
+ val intrinsicWidth: Float = svg.documentWidth
18
+ val intrinsicHeight: Float = svg.documentHeight
19
+
20
+ fun render(width: Int, height: Int): Bitmap {
21
+ require(width > 0 && height > 0)
22
+ if (svg.documentViewBox == null && intrinsicWidth > 0 && intrinsicHeight > 0) {
23
+ svg.setDocumentViewBox(0f, 0f, intrinsicWidth, intrinsicHeight)
24
+ }
25
+ svg.setDocumentWidth(width.toFloat())
26
+ svg.setDocumentHeight(height.toFloat())
27
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
28
+ svg.renderToCanvas(Canvas(bitmap))
29
+ return bitmap
30
+ }
31
+ }
@@ -0,0 +1,90 @@
1
+ import NitroModules
2
+ import UIKit
3
+
4
+ private final class OneKeyIconHost: UIImageView {
5
+ var changed: (() -> Void)?
6
+ override func layoutSubviews() { super.layoutSubviews(); changed?() }
7
+ override func didMoveToWindow() { super.didMoveToWindow(); changed?() }
8
+ override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
9
+ super.traitCollectionDidChange(previousTraitCollection)
10
+ if previousTraitCollection?.displayScale != traitCollection.displayScale { changed?() }
11
+ }
12
+ }
13
+
14
+ final class HybridOneKeyIcon: HybridOneKeyIconSpec, RecyclableView {
15
+ private let host = OneKeyIconHost()
16
+ private let owner = NSObject()
17
+ private var generation: UInt64 = 0
18
+ private var identity: String?
19
+ private var displayedGeometry: String?
20
+ private var displayed: OneKeyNamedSVG.RenderResult?
21
+ var iconName: String?
22
+ var resolvedColor: String?
23
+ var view: UIView { host }
24
+
25
+ override init() {
26
+ super.init()
27
+ host.contentMode = .scaleAspectFit
28
+ host.changed = { [weak self] in self?.refresh() }
29
+ }
30
+
31
+ func afterUpdate() { refresh() }
32
+
33
+ private func clearImage() {
34
+ displayed = nil; displayedGeometry = nil; host.image = nil
35
+ }
36
+
37
+ private func apply(_ result: OneKeyNamedSVG.RenderResult, color: String) {
38
+ displayed = result
39
+ if let tint = result.tintColor(color) {
40
+ host.image = result.image.withRenderingMode(.alwaysTemplate)
41
+ host.tintColor = tint
42
+ } else {
43
+ host.image = result.image.withRenderingMode(.alwaysOriginal)
44
+ }
45
+ }
46
+
47
+ private func refresh() {
48
+ let scale = host.window?.screen.scale ?? 1
49
+ let width = ceil(host.bounds.width * scale), height = ceil(host.bounds.height * scale)
50
+ let attached = host.window != nil
51
+ let next = "\(iconName ?? "")|\(resolvedColor ?? "")|\(width)|\(height)|\(scale)|\(attached)"
52
+ guard next != identity else { return }
53
+ identity = next; generation &+= 1
54
+ let revision = generation
55
+ OneKeyNamedSVG.shared.cancel(owner: owner)
56
+ guard attached, width.isFinite, height.isFinite,
57
+ width > 0, height > 0, width <= 4096, height <= 4096, width * height <= 4_194_304,
58
+ let name = iconName, !name.isEmpty,
59
+ let color = resolvedColor, OneKeyNamedSVG.isValidColor(color) else { clearImage(); return }
60
+ let geometry = "\(name)|\(width)|\(height)|\(scale)"
61
+ if displayedGeometry == geometry, let result = displayed, host.image != nil {
62
+ if result.canTint(color) {
63
+ if let tint = result.tintColor(color) { host.tintColor = tint }
64
+ return
65
+ }
66
+ // Keep only a successful same-source/geometry raster while new color decodes.
67
+ } else { clearImage() }
68
+ if let hit = OneKeyNamedSVG.shared.cachedRender(name: name, color: color,
69
+ width: Int(width), height: Int(height), scale: scale) {
70
+ displayedGeometry = geometry
71
+ apply(hit, color: color)
72
+ return
73
+ }
74
+ OneKeyNamedSVG.shared.requestRender(owner: owner, name: name, color: color,
75
+ width: Int(width), height: Int(height), scale: scale) { [weak self] result in
76
+ guard let self, self.generation == revision, self.host.window != nil else { return }
77
+ guard let result else { self.clearImage(); return }
78
+ self.displayedGeometry = geometry
79
+ self.apply(result, color: color)
80
+ }
81
+ }
82
+
83
+ func prepareForRecycle() {
84
+ iconName = nil; resolvedColor = nil; refresh()
85
+ }
86
+
87
+ func onDropView() { prepareForRecycle() }
88
+
89
+ deinit { OneKeyNamedSVG.shared.cancel(owner: owner) }
90
+ }
@@ -1,4 +1,5 @@
1
1
  #import <Foundation/Foundation.h>
2
+ #import <UIKit/UIKit.h>
2
3
 
3
4
  NS_ASSUME_NONNULL_BEGIN
4
5
 
@@ -8,6 +9,9 @@ NS_ASSUME_NONNULL_BEGIN
8
9
 
9
10
  + (void)addCodersToManager:(id)manager NS_SWIFT_NAME(addCoders(to:));
10
11
  + (void)ensureWebPCoderRegistered;
12
+ // A zero target preserves the decoder's intrinsic/vector result.
13
+ + (nullable UIImage *)decodeSVGData:(NSData *)data pixelSize:(CGSize)pixelSize
14
+ NS_SWIFT_NAME(decodeSVG(_:pixelSize:));
11
15
 
12
16
  @end
13
17
 
@@ -6,6 +6,15 @@
6
6
 
7
7
  @implementation OneKeyImageCoderBridge
8
8
 
9
+ + (UIImage *)decodeSVGData:(NSData *)data pixelSize:(CGSize)pixelSize {
10
+ NSDictionary *options = nil;
11
+ if (isfinite(pixelSize.width) && isfinite(pixelSize.height) && pixelSize.width > 0 && pixelSize.height > 0) {
12
+ options = @{SDImageCoderDecodeThumbnailPixelSize: [NSValue valueWithCGSize:pixelSize],
13
+ SDImageCoderDecodePreserveAspectRatio: @YES};
14
+ }
15
+ return [SDImageSVGCoder.sharedCoder decodedImageWithData:data options:options];
16
+ }
17
+
9
18
  + (void)addCodersToManager:(id)manager {
10
19
  SDImageCodersManager *coderManager = (SDImageCodersManager *)manager;
11
20
  [coderManager addCoder:SDImageSVGCoder.sharedCoder];