@smileid/usesmileid_huawei_face 12.1.0 → 12.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/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
Full release notes: [Smile ID docs](https://docs.usesmileid.com/developer-resources/sdks/mobile/release-notes/react-native-expo).
|
|
2
2
|
|
|
3
|
+
## 12.2.0
|
|
4
|
+
|
|
5
|
+
See the repository CHANGELOG for the 12.2.0 release notes.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## 12.1.1
|
|
9
|
+
|
|
10
|
+
See the repository CHANGELOG for the 12.1.1 release notes.
|
|
11
|
+
|
|
3
12
|
## 12.1.0
|
|
4
13
|
|
|
5
14
|
See the repository CHANGELOG for the 12.1.0 release notes.
|
|
6
15
|
|
|
7
|
-
|
|
8
16
|
## 12.0.2
|
|
9
17
|
|
|
10
18
|
See the repository CHANGELOG for the 12.0.2 release notes.
|
package/android/build.gradle
CHANGED
|
@@ -17,7 +17,7 @@ plugins {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
group = 'com.usesmileid.huawei.face'
|
|
20
|
-
version = '12.
|
|
20
|
+
version = '12.2.0'
|
|
21
21
|
|
|
22
22
|
// HMS ML is published only to Huawei's own Maven repository — none of the
|
|
23
23
|
// com.huawei.hms:ml-computer-vision-* artifacts exist on Maven Central. Expo
|
|
@@ -56,7 +56,7 @@ android {
|
|
|
56
56
|
// android/build.gradle and the rest of the @expo/* modules.
|
|
57
57
|
defaultConfig {
|
|
58
58
|
versionCode 1
|
|
59
|
-
versionName '12.
|
|
59
|
+
versionName '12.2.0'
|
|
60
60
|
// R8 keep-rules to bundle into partner apps that depend on this
|
|
61
61
|
// module. Without these, R8 may rename the Expo module entry
|
|
62
62
|
// point and the JS-bridged member functions, breaking autolinking.
|
|
@@ -69,13 +69,13 @@ dependencies {
|
|
|
69
69
|
// kamera native core: the analyzer executor takes analysis frames from
|
|
70
70
|
// kamera's KameraFrameHub so pixels never cross the JS bridge. Resolves from
|
|
71
71
|
// Maven Central; a local publishToMavenLocal build wins (see kamera.md).
|
|
72
|
-
implementation 'com.usesmileid:kamera:1.0
|
|
72
|
+
implementation 'com.usesmileid:kamera:1.1.0'
|
|
73
73
|
|
|
74
74
|
// Existing Smile ID native AAR — the entire ML pipeline lives here.
|
|
75
75
|
// Targets Huawei AppGallery / no-GMS devices via HMS ML kit. The
|
|
76
76
|
// Expo Module is a thin Kotlin shim that constructs the session
|
|
77
77
|
// and bridges its four methods to JS via Expo Modules.
|
|
78
|
-
implementation 'com.usesmileid:huawei-face:12.
|
|
78
|
+
implementation 'com.usesmileid:huawei-face:12.2.0'
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
}
|
|
@@ -17,9 +17,12 @@ import com.usesmileid.bridge.storage.writeJpegToFileExact
|
|
|
17
17
|
import com.usesmileid.bridge.util.toArgbBitmap
|
|
18
18
|
import java.io.File
|
|
19
19
|
import java.io.IOException
|
|
20
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
20
21
|
import java.util.concurrent.ConcurrentLinkedDeque
|
|
21
22
|
import java.util.concurrent.atomic.AtomicReference
|
|
23
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
22
24
|
import kotlin.math.roundToInt
|
|
25
|
+
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
23
26
|
import kotlinx.coroutines.CoroutineScope
|
|
24
27
|
import kotlinx.coroutines.Dispatchers
|
|
25
28
|
import kotlinx.coroutines.Job
|
|
@@ -73,20 +76,77 @@ internal class ResizingFrameStorage(
|
|
|
73
76
|
private const val DOCUMENT_MAX_DIMENSION = 1600
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
79
|
+
* The hidden border kept around the guide rect: this fraction of the
|
|
80
|
+
* saved image's short side on every edge, the rule of the native
|
|
81
|
+
* bridges' `DocumentCropGeometry.DOCUMENT_BORDER_FRACTION` and the
|
|
82
|
+
* selfie's interior crop. The border is where presentation-attack cues
|
|
83
|
+
* live; the backend reads it, and the preview shows only the interior.
|
|
80
84
|
*/
|
|
81
|
-
|
|
85
|
+
internal const val DOCUMENT_BORDER_FRACTION = 0.15f
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The uniform border, in pixels, to grow a [left], [top], [right],
|
|
89
|
+
* [bottom] guide by: the b that is [DOCUMENT_BORDER_FRACTION] of the
|
|
90
|
+
* grown rect's own short side, b = f × s / (1 − 2f) for the guide's
|
|
91
|
+
* short side s. Shrinks to the most every edge of a [width] × [height]
|
|
92
|
+
* image can hold; 0 for an empty guide or one touching an edge.
|
|
93
|
+
* Mirrors the native bridges' `DocumentCropGeometry.borderAround`.
|
|
94
|
+
*/
|
|
95
|
+
internal fun borderAround(left: Float, top: Float, right: Float, bottom: Float, width: Int, height: Int): Float {
|
|
96
|
+
val shortSide = minOf(right - left, bottom - top)
|
|
97
|
+
if (shortSide <= 0f) return 0f
|
|
98
|
+
val wanted = DOCUMENT_BORDER_FRACTION * shortSide / (1f - 2f * DOCUMENT_BORDER_FRACTION)
|
|
99
|
+
val room = minOf(left, top, width - right, height - bottom)
|
|
100
|
+
return minOf(wanted, room).coerceAtLeast(0f)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A [border] of pixels around a [width] × [height] crop as a fraction of the
|
|
105
|
+
* crop's short side, held to [DOCUMENT_BORDER_FRACTION] so the crop's pixel
|
|
106
|
+
* rounding never reports more border than was kept; 0 for an empty crop.
|
|
107
|
+
*/
|
|
108
|
+
internal fun borderFraction(border: Float, width: Int, height: Int): Float {
|
|
109
|
+
val shortSide = minOf(width, height)
|
|
110
|
+
if (shortSide <= 0) return 0f
|
|
111
|
+
return (border / shortSide).coerceIn(0f, DOCUMENT_BORDER_FRACTION)
|
|
112
|
+
}
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
|
|
115
|
+
/** A backstop for anything that escapes the write's own catch; [flush] still surfaces it. */
|
|
116
|
+
private val scope = CoroutineScope(
|
|
117
|
+
SupervisorJob() + Dispatchers.IO + CoroutineExceptionHandler { _, e -> firstError.compareAndSet(null, e) },
|
|
118
|
+
)
|
|
85
119
|
private val pending = ConcurrentLinkedDeque<Job>()
|
|
86
120
|
private val firstError = AtomicReference<Throwable?>(null)
|
|
87
121
|
|
|
122
|
+
/** Read back as [FrameStorage.cropRegion], which the document transitioner judges the detected document against. */
|
|
88
123
|
@Volatile
|
|
89
|
-
|
|
124
|
+
override var cropRegion: NativeViewportCropRegion? = null
|
|
125
|
+
private set
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The hidden border of each document this storage saved with one, by absolute
|
|
129
|
+
* path, as a fraction of the document's short side. Written on the encode scope
|
|
130
|
+
* once the file is written, read after [flush].
|
|
131
|
+
*/
|
|
132
|
+
private val savedBorderFractions = ConcurrentHashMap<String, Float>()
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The hidden border the document at [file] was saved with, as a fraction of its
|
|
136
|
+
* short side on every edge: [DOCUMENT_BORDER_FRACTION] when the frame had room for
|
|
137
|
+
* all of it, less where it did not, and 0 for a document saved without a guide
|
|
138
|
+
* crop or a file this storage did not write. The screens that show the document
|
|
139
|
+
* hide exactly this much.
|
|
140
|
+
*/
|
|
141
|
+
fun borderFractionOf(file: File): Float = savedBorderFractions[file.absolutePath] ?: 0f
|
|
142
|
+
|
|
143
|
+
private fun recordBorder(file: File, borderFraction: Float) {
|
|
144
|
+
if (borderFraction > 0f) {
|
|
145
|
+
savedBorderFractions[file.absolutePath] = borderFraction
|
|
146
|
+
} else {
|
|
147
|
+
savedBorderFractions.remove(file.absolutePath)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
90
150
|
|
|
91
151
|
override fun setCropRegion(region: NativeViewportCropRegion?) {
|
|
92
152
|
cropRegion = region
|
|
@@ -100,6 +160,7 @@ internal class ResizingFrameStorage(
|
|
|
100
160
|
FrameKind.Document -> when (documentSide) {
|
|
101
161
|
DocumentSide.Front -> "document_front.jpg"
|
|
102
162
|
DocumentSide.Back -> "document_back.jpg"
|
|
163
|
+
DocumentSide.Visa -> "document_visa.jpg"
|
|
103
164
|
}
|
|
104
165
|
},
|
|
105
166
|
)
|
|
@@ -120,9 +181,15 @@ internal class ResizingFrameStorage(
|
|
|
120
181
|
val job = scope.launch {
|
|
121
182
|
try {
|
|
122
183
|
encodeAndWrite(frame, file, documentCropRegion)
|
|
123
|
-
} catch (
|
|
124
|
-
|
|
125
|
-
|
|
184
|
+
} catch (e: CancellationException) {
|
|
185
|
+
runCatching { file.delete() }
|
|
186
|
+
recordBorder(file, 0f)
|
|
187
|
+
throw e
|
|
188
|
+
} catch (e: Throwable) {
|
|
189
|
+
runCatching { file.delete() }
|
|
190
|
+
recordBorder(file, 0f)
|
|
191
|
+
// Surfaced by flush; a rethrow here would reach the host's uncaught handler.
|
|
192
|
+
firstError.compareAndSet(null, e)
|
|
126
193
|
}
|
|
127
194
|
}
|
|
128
195
|
// Add synchronously so flush() cannot race past an in-flight job.
|
|
@@ -140,7 +207,10 @@ internal class ResizingFrameStorage(
|
|
|
140
207
|
// Selfie and document captures share one session directory; delete
|
|
141
208
|
// only this role's own frames.
|
|
142
209
|
sessionDirectory.listFiles()?.forEach { file ->
|
|
143
|
-
if (ownsForClear(file))
|
|
210
|
+
if (ownsForClear(file)) {
|
|
211
|
+
file.delete()
|
|
212
|
+
recordBorder(file, 0f)
|
|
213
|
+
}
|
|
144
214
|
}
|
|
145
215
|
}
|
|
146
216
|
|
|
@@ -195,16 +265,23 @@ internal class ResizingFrameStorage(
|
|
|
195
265
|
// the whole frame, so a tightly framed card fills the saved pixels.
|
|
196
266
|
// toFrameBoundingBox returns a normalized 0–1 box in the upright
|
|
197
267
|
// frame's coordinate space, mapped to pixels by the bitmap dimensions.
|
|
198
|
-
// The box
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// against the guide.
|
|
268
|
+
// The box grows by the hidden border (DOCUMENT_BORDER_FRACTION of the
|
|
269
|
+
// saved image's short side, uniform on every edge), which the preview
|
|
270
|
+
// never shows, so it shows what the user framed.
|
|
202
271
|
val source = frame.toArgbBitmap(jpegQuality = documentQuality)
|
|
203
272
|
?: throw IOException("toArgbBitmap returned null for ${file.name}")
|
|
204
|
-
val cropped = if (documentCropRegion != null) {
|
|
273
|
+
val (cropped, borderFraction) = if (documentCropRegion != null) {
|
|
205
274
|
val box = documentCropRegion.toFrameBoundingBox(source.width, source.height)
|
|
206
|
-
val
|
|
207
|
-
|
|
275
|
+
val border = borderAround(
|
|
276
|
+
box.left * source.width,
|
|
277
|
+
box.top * source.height,
|
|
278
|
+
(box.left + box.width) * source.width,
|
|
279
|
+
(box.top + box.height) * source.height,
|
|
280
|
+
source.width,
|
|
281
|
+
source.height,
|
|
282
|
+
)
|
|
283
|
+
val padX = border / source.width
|
|
284
|
+
val padY = border / source.height
|
|
208
285
|
// Clamp left/top to leave room for at least one pixel and derive
|
|
209
286
|
// right/bottom from the clamped left/top so the crop never runs past
|
|
210
287
|
// the bitmap edge. Bitmap.createBitmap throws IllegalArgumentException
|
|
@@ -217,9 +294,10 @@ internal class ResizingFrameStorage(
|
|
|
217
294
|
val pixelBottom = ((box.top + box.height + padY) * source.height).roundToInt().coerceIn(pixelTop + 1, source.height)
|
|
218
295
|
val cropWidth = (pixelRight - pixelLeft).coerceAtLeast(1)
|
|
219
296
|
val cropHeight = (pixelBottom - pixelTop).coerceAtLeast(1)
|
|
220
|
-
android.graphics.Bitmap.createBitmap(source, pixelLeft, pixelTop, cropWidth, cropHeight)
|
|
297
|
+
android.graphics.Bitmap.createBitmap(source, pixelLeft, pixelTop, cropWidth, cropHeight) to
|
|
298
|
+
borderFraction(border, cropWidth, cropHeight)
|
|
221
299
|
} else {
|
|
222
|
-
source
|
|
300
|
+
source to 0f
|
|
223
301
|
}
|
|
224
302
|
// Downscale the crop so its longest edge is at most
|
|
225
303
|
// DOCUMENT_MAX_DIMENSION, preserving aspect ratio. Skip when the crop
|
|
@@ -238,6 +316,7 @@ internal class ResizingFrameStorage(
|
|
|
238
316
|
scaled.compress(android.graphics.Bitmap.CompressFormat.JPEG, documentQuality, it)
|
|
239
317
|
}
|
|
240
318
|
if (!success) throw IOException("Bitmap.compress(JPEG) failed for ${file.absolutePath}")
|
|
319
|
+
recordBorder(file, borderFraction)
|
|
241
320
|
} finally {
|
|
242
321
|
if (scaled !== cropped) scaled.recycle()
|
|
243
322
|
if (cropped !== source) cropped.recycle()
|
package/android/src/main/java/com/usesmileid/rn/analyzer/huaweiface/UseSmileIDAnalyzerExecutor.kt
CHANGED
|
@@ -56,6 +56,13 @@ abstract class UseSmileIDAnalyzerExecutor<
|
|
|
56
56
|
protected var session: Session? = null
|
|
57
57
|
private var storageRoot: File? = null
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The latest session's storage, read by [handleDocumentBorderFraction] after `flush`. Written by
|
|
61
|
+
* `configure` on the executor's IO scope and read on the module's async-function thread.
|
|
62
|
+
*/
|
|
63
|
+
@Volatile
|
|
64
|
+
private var storage: ResizingFrameStorage? = null
|
|
65
|
+
|
|
59
66
|
/**
|
|
60
67
|
* Scope used for all `analyze` / `flush` work. Bound to `Dispatchers.IO`
|
|
61
68
|
* + a `SupervisorJob` so a single failed frame analysis doesn't poison
|
|
@@ -192,11 +199,29 @@ abstract class UseSmileIDAnalyzerExecutor<
|
|
|
192
199
|
// clean-up policies can prune them.
|
|
193
200
|
val sessionDir = freshSessionDirectory()
|
|
194
201
|
val storage = buildResizingFrameStorage(sessionDir, storageDocumentSide(config), config as? Map<*, *>)
|
|
202
|
+
this.storage = storage
|
|
195
203
|
val newSession = createSession(config, storage)
|
|
196
204
|
session = newSession
|
|
197
205
|
onSessionCreated(newSession)
|
|
198
206
|
}
|
|
199
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Implementation behind `documentBorderFraction`: the hidden border the document at [url], a
|
|
210
|
+
* `file://` URL or a path, was saved with, as a fraction of its short side; 0 for a file the
|
|
211
|
+
* latest session's storage did not write with one.
|
|
212
|
+
*/
|
|
213
|
+
fun handleDocumentBorderFraction(url: String): Double {
|
|
214
|
+
val file = try {
|
|
215
|
+
if (url.startsWith("file:")) File(java.net.URI(url)) else File(url)
|
|
216
|
+
} catch (e: java.net.URISyntaxException) {
|
|
217
|
+
return 0.0
|
|
218
|
+
} catch (e: IllegalArgumentException) {
|
|
219
|
+
// Not a hierarchical file: URL.
|
|
220
|
+
return 0.0
|
|
221
|
+
}
|
|
222
|
+
return storage?.borderFractionOf(file)?.toDouble() ?: 0.0
|
|
223
|
+
}
|
|
224
|
+
|
|
200
225
|
/**
|
|
201
226
|
* Implementation behind `flush`. Same dispatcher hopping discipline;
|
|
202
227
|
* a missing session is treated as a successful
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/usesmileid_huawei_face",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "Huawei HMS face detection implementation for the UseSmileID React Native (Expo) SDK.",
|
|
5
5
|
"homepage": "https://usesmileid.com",
|
|
6
6
|
"repository": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"CHANGELOG.md"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@smileid/usesmileid_bridge": "12.
|
|
48
|
-
"@smileid/usesmileid_platform_interface": "12.
|
|
47
|
+
"@smileid/usesmileid_bridge": "12.2.0",
|
|
48
|
+
"@smileid/usesmileid_platform_interface": "12.2.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"expo": ">=56.0.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"react-native": ">=0.83.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"expo-modules-core": "^57.0.
|
|
56
|
+
"expo-modules-core": "^57.0.19",
|
|
57
57
|
"react-native": "^0.86.3",
|
|
58
58
|
"typescript": "^6.0.3"
|
|
59
59
|
},
|