@smileid/usesmileid_vision_face 12.1.1 → 12.2.1-SNAPSHOT.44
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 +4 -2
- package/ios/UseSmileIDVisionFaceModule/ResizingFrameStorage.swift +105 -31
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDVisionFaceKameraAnalysisTick.swift +5 -38
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDVisionFaceModule.swift +17 -8
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDVisionFaceSessionHolder.swift +23 -2
- package/package.json +6 -6
- package/plugin/build/index.js +1 -1
- package/plugin/build/lib/podfile.js +1 -1
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDAnalyzerExecutor.swift +0 -119
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
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
|
+
|
|
3
7
|
## 12.1.1
|
|
4
8
|
|
|
5
9
|
See the repository CHANGELOG for the 12.1.1 release notes.
|
|
6
10
|
|
|
7
|
-
|
|
8
11
|
## 12.1.0
|
|
9
12
|
|
|
10
13
|
See the repository CHANGELOG for the 12.1.0 release notes.
|
|
11
14
|
|
|
12
|
-
|
|
13
15
|
## 12.0.2
|
|
14
16
|
|
|
15
17
|
See the repository CHANGELOG for the 12.0.2 release notes.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// MIRRORED FILE — duplicated in both RN Expo iOS vision modules
|
|
2
|
-
// (usesmileid_vision_face / usesmileid_vision_document)
|
|
3
|
-
// targets can't share sources
|
|
2
|
+
// (usesmileid_vision_face / usesmileid_vision_document). The per-module pod
|
|
3
|
+
// targets can't share sources, and the bridge pod that both depend on doesn't
|
|
4
|
+
// link the native UseSmileIDBridge package this conforms to. Apply any edit to
|
|
5
|
+
// BOTH copies in the same commit.
|
|
4
6
|
|
|
5
7
|
import Foundation
|
|
6
8
|
import UIKit
|
|
@@ -17,11 +19,34 @@ import UseSmileIDBridge
|
|
|
17
19
|
final class ResizingFrameStorage: FrameStorage {
|
|
18
20
|
/// Longest-edge pixel bound applied to document frames before encoding.
|
|
19
21
|
private static let documentMaxDimension = 1600
|
|
20
|
-
///
|
|
21
|
-
///
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
|
|
22
|
+
/// The hidden border kept around the guide rect: this fraction of the saved
|
|
23
|
+
/// image's short side on every edge, the rule of the native bridges'
|
|
24
|
+
/// `DocumentCropGeometry.documentBorderFraction` and the selfie's interior
|
|
25
|
+
/// crop. The border is where presentation-attack cues live; the backend reads
|
|
26
|
+
/// it, and the preview shows only the interior.
|
|
27
|
+
static let documentBorderFraction: CGFloat = 0.15
|
|
28
|
+
|
|
29
|
+
/// The uniform border, in pixels, to grow `guide` by: the b that is
|
|
30
|
+
/// `documentBorderFraction` of the grown rect's own short side,
|
|
31
|
+
/// b = f × s / (1 − 2f) for the guide's short side s. Shrinks to the most
|
|
32
|
+
/// every edge of an `imageSize` image can hold; 0 for an empty guide or one
|
|
33
|
+
/// touching an edge. Mirrors the native bridges' `DocumentCropGeometry.borderAround`.
|
|
34
|
+
static func borderAround(_ guide: CGRect, imageSize: CGSize) -> CGFloat {
|
|
35
|
+
let shortSide = min(guide.width, guide.height)
|
|
36
|
+
guard shortSide > 0 else { return 0 }
|
|
37
|
+
let wanted = documentBorderFraction * shortSide / (1 - 2 * documentBorderFraction)
|
|
38
|
+
let room = min(guide.minX, guide.minY, imageSize.width - guide.maxX, imageSize.height - guide.maxY)
|
|
39
|
+
return max(min(wanted, room), 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A `border` around a crop of `size` as a fraction of the crop's short side,
|
|
43
|
+
/// held to `documentBorderFraction` so the crop's pixel rounding never reports
|
|
44
|
+
/// more border than was kept; 0 for an empty crop.
|
|
45
|
+
static func borderFraction(_ border: CGFloat, of size: CGSize) -> CGFloat {
|
|
46
|
+
let shortSide = min(size.width, size.height)
|
|
47
|
+
guard shortSide > 0 else { return 0 }
|
|
48
|
+
return min(max(border / shortSide, 0), documentBorderFraction)
|
|
49
|
+
}
|
|
25
50
|
|
|
26
51
|
private let sessionDirectory: URL
|
|
27
52
|
private let captureType: CaptureType
|
|
@@ -34,12 +59,18 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
34
59
|
private let livenessQuality: CGFloat
|
|
35
60
|
private let documentQuality: CGFloat
|
|
36
61
|
private let writer: Writer
|
|
62
|
+
/// The hidden border each document this storage saved kept, recorded by the
|
|
63
|
+
/// write tasks once their file is on disk and read after `flush()`.
|
|
64
|
+
private let savedBorders = SavedDocumentBorders()
|
|
37
65
|
// The analysis tick calls `setCropRegion` on the module's async-function
|
|
38
66
|
// thread; `persist` reads the value on that same serialised scan path before
|
|
39
67
|
// handing it (by value) to the background encode task. The Swift value is a
|
|
40
68
|
// `Sendable` struct captured by value into `Writer.enqueue`, so no background
|
|
41
69
|
// task ever observes a torn read of this property.
|
|
42
|
-
|
|
70
|
+
//
|
|
71
|
+
// Read back as `FrameStorage.cropRegion`, which the document transitioner judges the detected
|
|
72
|
+
// document against, so auto-capture waits for the document inside the outline (ios-v12 #370).
|
|
73
|
+
nonisolated(unsafe) private(set) var cropRegion: ViewportCropRegion?
|
|
43
74
|
|
|
44
75
|
init(
|
|
45
76
|
sessionDirectory: URL,
|
|
@@ -124,7 +155,8 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
124
155
|
resize: resize,
|
|
125
156
|
maxDimension: maxDimension,
|
|
126
157
|
quality: quality,
|
|
127
|
-
documentCropRegion: documentCropRegion
|
|
158
|
+
documentCropRegion: documentCropRegion,
|
|
159
|
+
savedBorders: savedBorders
|
|
128
160
|
)
|
|
129
161
|
}
|
|
130
162
|
}
|
|
@@ -133,6 +165,16 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
133
165
|
try await writer.flush()
|
|
134
166
|
}
|
|
135
167
|
|
|
168
|
+
/// The hidden border the document at `url` was saved with, as a fraction of its
|
|
169
|
+
/// short side on every edge, once `flush()` has returned:
|
|
170
|
+
/// `documentBorderFraction` when the frame had room for all of it, less where it
|
|
171
|
+
/// did not, and 0 for a document saved without a guide crop or a file this
|
|
172
|
+
/// storage did not write. The screens that show the document hide exactly this
|
|
173
|
+
/// much.
|
|
174
|
+
func documentBorderFraction(at url: URL) -> CGFloat {
|
|
175
|
+
savedBorders.fraction(for: url)
|
|
176
|
+
}
|
|
177
|
+
|
|
136
178
|
func clear() {
|
|
137
179
|
// Selfie and document captures share one session directory; delete
|
|
138
180
|
// only this role's own frames.
|
|
@@ -143,6 +185,7 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
143
185
|
)) ?? []
|
|
144
186
|
for fileURL in contents where ownsForClear(fileURL) {
|
|
145
187
|
try? fileManager.removeItem(at: fileURL)
|
|
188
|
+
savedBorders.record(0, for: fileURL)
|
|
146
189
|
}
|
|
147
190
|
}
|
|
148
191
|
|
|
@@ -170,17 +213,24 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
170
213
|
resize: (Int, Int)?,
|
|
171
214
|
maxDimension: Int?,
|
|
172
215
|
quality: CGFloat,
|
|
173
|
-
documentCropRegion: ViewportCropRegion
|
|
216
|
+
documentCropRegion: ViewportCropRegion?,
|
|
217
|
+
savedBorders: SavedDocumentBorders
|
|
174
218
|
) {
|
|
175
219
|
let task = Task<Void, Error>.detached(priority: .utility) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
do {
|
|
221
|
+
let borderFraction = try Self.encodeAndWrite(
|
|
222
|
+
image: image,
|
|
223
|
+
to: url,
|
|
224
|
+
resize: resize,
|
|
225
|
+
maxDimension: maxDimension,
|
|
226
|
+
quality: quality,
|
|
227
|
+
documentCropRegion: documentCropRegion
|
|
228
|
+
)
|
|
229
|
+
savedBorders.record(borderFraction, for: url)
|
|
230
|
+
} catch {
|
|
231
|
+
savedBorders.record(0, for: url)
|
|
232
|
+
throw error
|
|
233
|
+
}
|
|
184
234
|
}
|
|
185
235
|
inFlight.append(task)
|
|
186
236
|
}
|
|
@@ -208,7 +258,7 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
208
258
|
maxDimension: Int?,
|
|
209
259
|
quality: CGFloat,
|
|
210
260
|
documentCropRegion: ViewportCropRegion?
|
|
211
|
-
) throws {
|
|
261
|
+
) throws -> CGFloat {
|
|
212
262
|
try FileManager.default.createDirectory(
|
|
213
263
|
at: url.deletingLastPathComponent(),
|
|
214
264
|
withIntermediateDirectories: true
|
|
@@ -219,6 +269,8 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
219
269
|
// render the file sideways.
|
|
220
270
|
let upright = image.normalizingOrientation()
|
|
221
271
|
let data: Data?
|
|
272
|
+
// The border the saved document kept, as a fraction of its short side.
|
|
273
|
+
var borderFraction: CGFloat = 0
|
|
222
274
|
if let (width, height) = resize, let cgImage = upright.cgImage {
|
|
223
275
|
// Center-crop and scale to the token-resolved target size
|
|
224
276
|
// (selfie / liveness path).
|
|
@@ -236,20 +288,25 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
236
288
|
let pixelWidth = cgImage.width
|
|
237
289
|
let pixelHeight = cgImage.height
|
|
238
290
|
let box = region.toFrameBoundingBox(frameWidth: pixelWidth, frameHeight: pixelHeight)
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
let
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
291
|
+
// Grow the guide rect by the hidden border (documentBorderFraction of
|
|
292
|
+
// the saved image's short side, uniform on every edge), which the
|
|
293
|
+
// preview never shows; clamp to the image bounds.
|
|
294
|
+
let imageSize = CGSize(width: pixelWidth, height: pixelHeight)
|
|
295
|
+
let guide = CGRect(
|
|
296
|
+
x: CGFloat(box.left) * imageSize.width,
|
|
297
|
+
y: CGFloat(box.top) * imageSize.height,
|
|
298
|
+
width: CGFloat(box.width) * imageSize.width,
|
|
299
|
+
height: CGFloat(box.height) * imageSize.height
|
|
300
|
+
)
|
|
301
|
+
let border = ResizingFrameStorage.borderAround(guide, imageSize: imageSize)
|
|
302
|
+
let cropRect = guide.insetBy(dx: -border, dy: -border)
|
|
303
|
+
.intersection(CGRect(origin: .zero, size: imageSize))
|
|
251
304
|
if let cropped = cgImage.cropping(to: cropRect) {
|
|
252
305
|
cgImage = cropped
|
|
306
|
+
borderFraction = ResizingFrameStorage.borderFraction(
|
|
307
|
+
border,
|
|
308
|
+
of: CGSize(width: cropped.width, height: cropped.height)
|
|
309
|
+
)
|
|
253
310
|
}
|
|
254
311
|
}
|
|
255
312
|
let pixelWidth = cgImage.width
|
|
@@ -277,10 +334,27 @@ final class ResizingFrameStorage: FrameStorage {
|
|
|
277
334
|
throw ResizingFrameStorageError.encodeFailed(url: url)
|
|
278
335
|
}
|
|
279
336
|
try data.write(to: url, options: .atomic)
|
|
337
|
+
return borderFraction
|
|
280
338
|
}
|
|
281
339
|
}
|
|
282
340
|
}
|
|
283
341
|
|
|
342
|
+
/// The hidden border each document a `ResizingFrameStorage` saved kept, as a
|
|
343
|
+
/// fraction of its short side, by file. Shared with the detached write tasks, so it
|
|
344
|
+
/// is lock-guarded.
|
|
345
|
+
final class SavedDocumentBorders: @unchecked Sendable {
|
|
346
|
+
private let lock = NSLock()
|
|
347
|
+
private var fractions: [URL: CGFloat] = [:]
|
|
348
|
+
|
|
349
|
+
func record(_ fraction: CGFloat, for url: URL) {
|
|
350
|
+
lock.withLock { fractions[url.standardizedFileURL] = fraction > 0 ? fraction : nil }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
func fraction(for url: URL) -> CGFloat {
|
|
354
|
+
lock.withLock { fractions[url.standardizedFileURL] ?? 0 }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
284
358
|
enum ResizingFrameStorageError: Error, Sendable {
|
|
285
359
|
case encodeFailed(url: URL)
|
|
286
360
|
}
|
|
@@ -1,49 +1,16 @@
|
|
|
1
|
-
import CoreMedia
|
|
2
1
|
import Kamera
|
|
3
|
-
import CoreImage
|
|
4
2
|
import UIKit
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
private extension CGImagePropertyOrientation {
|
|
8
|
-
/// Kamera rotation + mirror metadata → the preview's selfie-mirrored view. The mirror
|
|
9
|
-
/// applies after the rotation, so the quarter-turn cases swap (`.right` ↔ `.leftMirrored`).
|
|
10
|
-
init(rotationDegrees: Int, isMirrored: Bool) {
|
|
11
|
-
let normalised = ((rotationDegrees % 360) + 360) % 360
|
|
12
|
-
switch (normalised, isMirrored) {
|
|
13
|
-
case (90, false): self = .right
|
|
14
|
-
case (90, true): self = .leftMirrored
|
|
15
|
-
case (180, false): self = .down
|
|
16
|
-
case (180, true): self = .downMirrored
|
|
17
|
-
case (270, false): self = .left
|
|
18
|
-
case (270, true): self = .rightMirrored
|
|
19
|
-
case (_, true): self = .upMirrored
|
|
20
|
-
default: self = .up
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
4
|
/// Scans the latest `KameraFrameHub` frame; no pending frame returns the `no_frame` wire error.
|
|
26
5
|
enum UseSmileIDVisionFaceKameraTick {
|
|
27
|
-
/// Reused across ticks — per-tick creation recompiles the GPU pipeline.
|
|
28
|
-
private static let ciContext = CIContext(options: [.useSoftwareRenderer: false])
|
|
29
|
-
|
|
30
6
|
static func analyze(arguments: [String: Any?]?) -> Any {
|
|
31
7
|
guard let frame = KameraFrameHub.take() else {
|
|
32
8
|
return ["error": "no_frame"]
|
|
33
9
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let orientedImage = CIImage(cvPixelBuffer: concrete.pixelBuffer)
|
|
40
|
-
.oriented(CGImagePropertyOrientation(
|
|
41
|
-
rotationDegrees: frame.rotationDegrees,
|
|
42
|
-
isMirrored: frame.isMirrored
|
|
43
|
-
))
|
|
44
|
-
let cgImage = ciContext.createCGImage(orientedImage, from: orientedImage.extent)
|
|
45
|
-
frame.close()
|
|
46
|
-
guard let cgImage else {
|
|
10
|
+
// The session reads the live frame's readings as the scan runs; closing it afterwards releases
|
|
11
|
+
// it to the pipeline for the next one.
|
|
12
|
+
defer { frame.close() }
|
|
13
|
+
guard frame is CVPixelBufferKameraFrame else {
|
|
47
14
|
return ["error": "frame_conversion_failed"]
|
|
48
15
|
}
|
|
49
16
|
|
|
@@ -51,7 +18,7 @@ enum UseSmileIDVisionFaceKameraTick {
|
|
|
51
18
|
let rotation = (arguments?["rotation"] as? NSNumber)?.intValue ?? 0
|
|
52
19
|
|
|
53
20
|
guard let result = UseSmileIDVisionFaceSessionHolder.shared.scanAndDrain(
|
|
54
|
-
|
|
21
|
+
frame: frame,
|
|
55
22
|
rotation: rotation
|
|
56
23
|
) else {
|
|
57
24
|
return ["error": "session_not_configured"]
|
|
@@ -129,12 +129,11 @@ private extension Dictionary where Key == String, Value == Any? {
|
|
|
129
129
|
func toFaceScanConfig() -> FaceScanConfig {
|
|
130
130
|
let base = FaceDetectorMode.from(token: self["faceDetectorMode"] as? String).config
|
|
131
131
|
return FaceScanConfig(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
stayInFoundDuration: (self["stayInFoundDuration"] as? NSNumber)?.intValue ?? 3_000,
|
|
132
|
+
numSamples: (self["numSamples"] as? NSNumber)?.intValue ?? ThresholdConfig.FaceCapture.numSamples,
|
|
133
|
+
sampleInterval: (self["sampleInterval"] as? NSNumber)?.intValue
|
|
134
|
+
?? ThresholdConfig.FaceCapture.sampleIntervalMs,
|
|
135
|
+
stayInFoundDuration: (self["stayInFoundDuration"] as? NSNumber)?.intValue
|
|
136
|
+
?? ThresholdConfig.FaceCapture.defaultStayInFoundDuration,
|
|
138
137
|
faceDetectorConfig: FaceDetectorConfig(
|
|
139
138
|
minFillThreshold: (self["minFillThreshold"] as? NSNumber)?.floatValue ?? base.minFillThreshold,
|
|
140
139
|
maxFillThreshold: (self["maxFillThreshold"] as? NSNumber)?.floatValue ?? base.maxFillThreshold,
|
|
@@ -262,12 +261,22 @@ extension UseSmileIDBridge.UseSmileIDScanState {
|
|
|
262
261
|
if let captureProgress {
|
|
263
262
|
map["captureProgress"] = Double(captureProgress)
|
|
264
263
|
}
|
|
265
|
-
// Latched pose-match flag for the ESS head turn —
|
|
266
|
-
//
|
|
264
|
+
// Latched pose-match flag for the ESS head turn — keeps the direction arrow
|
|
265
|
+
// green through a hold at the threshold. Emit only while matched (omit ⇒ false on the JS side),
|
|
267
266
|
// matching the optional-field omission convention above.
|
|
268
267
|
if poseMatched {
|
|
269
268
|
map["poseMatched"] = true
|
|
270
269
|
}
|
|
270
|
+
switch captureStep {
|
|
271
|
+
case .holdStill: map["captureStep"] = "holdStill"
|
|
272
|
+
case .smile: map["captureStep"] = "smile"
|
|
273
|
+
// None yet, and a step a newer bridge adds crosses as none.
|
|
274
|
+
case nil: break
|
|
275
|
+
@unknown default: break
|
|
276
|
+
}
|
|
277
|
+
if let turnProgress {
|
|
278
|
+
map["turnProgress"] = Double(turnProgress)
|
|
279
|
+
}
|
|
271
280
|
return map
|
|
272
281
|
}
|
|
273
282
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import Kamera
|
|
2
3
|
import UIKit
|
|
3
4
|
import UseSmileIDBridge
|
|
4
5
|
import UseSmileIDVisionFace
|
|
@@ -66,7 +67,11 @@ final class UseSmileIDVisionFaceSessionHolder {
|
|
|
66
67
|
|
|
67
68
|
/// Run `session.scan` + `drainCaptureReadyEvents` as one atomic unit, blocking the
|
|
68
69
|
/// caller. Returns `nil` when no session is configured; never throws.
|
|
69
|
-
|
|
70
|
+
///
|
|
71
|
+
/// The session analyses the kamera `frame`'s pixel buffer as the native capture screens do, with
|
|
72
|
+
/// the readings kamera took on it (device motion, tracked face, live-frame quality), and draws an
|
|
73
|
+
/// image only for a frame a capture keeps. The caller keeps `frame` open until this returns.
|
|
74
|
+
func scanAndDrain(frame: any KameraFrame, rotation: Int) -> [String: Any]? {
|
|
70
75
|
var result: [String: Any]?
|
|
71
76
|
operationQueue.sync {
|
|
72
77
|
guard let currentSession = session else {
|
|
@@ -74,8 +79,14 @@ final class UseSmileIDVisionFaceSessionHolder {
|
|
|
74
79
|
}
|
|
75
80
|
let resultBox = ScanResultBox()
|
|
76
81
|
let semaphore = DispatchSemaphore(value: 0)
|
|
82
|
+
let frameBox = KameraFrameBox(frame)
|
|
77
83
|
Task.detached(priority: .userInitiated) {
|
|
78
|
-
let scanResult = await currentSession.scan(
|
|
84
|
+
guard let scanResult = await currentSession.scan(kameraFrame: frameBox.frame, rotation: rotation) else {
|
|
85
|
+
// A frame with no pixel buffer; the tick only hands over pixel-buffer frames.
|
|
86
|
+
resultBox.value = ["error": "frame_conversion_failed"]
|
|
87
|
+
semaphore.signal()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
79
90
|
let nativeOutput = scanResult.output as? UseSmileIDBridge.FaceDetectorOutput
|
|
80
91
|
let state = scanResult.state
|
|
81
92
|
let events = currentSession.drainCaptureReadyEvents()
|
|
@@ -93,6 +104,16 @@ final class UseSmileIDVisionFaceSessionHolder {
|
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
|
|
107
|
+
/// Carries the kamera frame into the scan Task. Kamera frames guard their planes with their own
|
|
108
|
+
/// lock, and the caller blocks on the Task while it reads the frame.
|
|
109
|
+
private final class KameraFrameBox: @unchecked Sendable {
|
|
110
|
+
let frame: any KameraFrame
|
|
111
|
+
|
|
112
|
+
init(_ frame: any KameraFrame) {
|
|
113
|
+
self.frame = frame
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
/// Shuttles a scan result from the cooperative-pool Task to the semaphore-blocked caller.
|
|
97
118
|
private final class ScanResultBox: @unchecked Sendable {
|
|
98
119
|
var value: [String: Any] = [:]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/usesmileid_vision_face",
|
|
3
|
-
"version": "12.1.
|
|
3
|
+
"version": "12.2.1-SNAPSHOT.44",
|
|
4
4
|
"description": "Apple Vision face detection implementation for the UseSmileID React Native (Expo) SDK.",
|
|
5
5
|
"homepage": "https://usesmileid.com",
|
|
6
6
|
"repository": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"CHANGELOG.md"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@smileid/usesmileid_bridge": "12.1.
|
|
50
|
-
"@smileid/usesmileid_platform_interface": "12.1.
|
|
49
|
+
"@smileid/usesmileid_bridge": "12.2.1-SNAPSHOT.44",
|
|
50
|
+
"@smileid/usesmileid_platform_interface": "12.2.1-SNAPSHOT.44"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"expo": ">=56.0.0",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"react-native": ">=0.83.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@types/node": "^26.4
|
|
59
|
-
"expo": "~57.0.
|
|
60
|
-
"expo-modules-core": "^57.0.
|
|
58
|
+
"@types/node": "^26.6.4",
|
|
59
|
+
"expo": "~57.0.26",
|
|
60
|
+
"expo-modules-core": "^57.0.20",
|
|
61
61
|
"react-native": "^0.86.3",
|
|
62
62
|
"typescript": "^6.0.3",
|
|
63
63
|
"xcode": "^3.0.1"
|
package/plugin/build/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var __createBinding=this&&this.__createBinding||(Object.create?(function(i,r,t,e){e===void 0&&(e=t);var o=Object.getOwnPropertyDescriptor(r,t);(!o||("get"in o?!r.__esModule:o.writable||o.configurable))&&(o={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(i,e,o)}):(function(i,r,t,e){e===void 0&&(e=t),i[e]=r[t]})),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?(function(i,r){Object.defineProperty(i,"default",{enumerable:!0,value:r})}):function(i,r){i.default=r}),__importStar=this&&this.__importStar||(function(){var i=function(r){return i=Object.getOwnPropertyNames||function(t){var e=[];for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[e.length]=o);return e},i(r)};return function(r){if(r&&r.__esModule)return r;var t={};if(r!=null)for(var e=i(r),o=0;o<e.length;o++)e[o]!=="default"&&__createBinding(t,r,e[o]);return __setModuleDefault(t,r),t}})();Object.defineProperty(exports,"__esModule",{value:!0}),exports.SPM_PRODUCTS=void 0;const config_plugins_1=require("expo/config-plugins"),fs=__importStar(require("fs")),path=__importStar(require("path")),deployment_target_1=require("./lib/deployment_target"),podfile_1=require("./lib/podfile"),signature_dedup_1=require("./lib/signature_dedup"),spm_1=require("./lib/spm"),IOS_DEPLOYMENT_TARGET="16.4",SMILE_ID_PACKAGE={url:"https://github.com/smileidentity/ios-spm",requirement:{kind:"exactVersion",version:"12.
|
|
1
|
+
"use strict";var __createBinding=this&&this.__createBinding||(Object.create?(function(i,r,t,e){e===void 0&&(e=t);var o=Object.getOwnPropertyDescriptor(r,t);(!o||("get"in o?!r.__esModule:o.writable||o.configurable))&&(o={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(i,e,o)}):(function(i,r,t,e){e===void 0&&(e=t),i[e]=r[t]})),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?(function(i,r){Object.defineProperty(i,"default",{enumerable:!0,value:r})}):function(i,r){i.default=r}),__importStar=this&&this.__importStar||(function(){var i=function(r){return i=Object.getOwnPropertyNames||function(t){var e=[];for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[e.length]=o);return e},i(r)};return function(r){if(r&&r.__esModule)return r;var t={};if(r!=null)for(var e=i(r),o=0;o<e.length;o++)e[o]!=="default"&&__createBinding(t,r,e[o]);return __setModuleDefault(t,r),t}})();Object.defineProperty(exports,"__esModule",{value:!0}),exports.SPM_PRODUCTS=void 0;const config_plugins_1=require("expo/config-plugins"),fs=__importStar(require("fs")),path=__importStar(require("path")),deployment_target_1=require("./lib/deployment_target"),podfile_1=require("./lib/podfile"),signature_dedup_1=require("./lib/signature_dedup"),spm_1=require("./lib/spm"),IOS_DEPLOYMENT_TARGET="16.4",SMILE_ID_PACKAGE={url:"https://github.com/smileidentity/ios-spm",requirement:{kind:"exactVersion",version:"12.2.0-SNAPSHOT.20261011230808.38182841286"}},POD_TARGET="UseSmileIDVisionFaceExpo";exports.SPM_PRODUCTS=["UseSmileIDVisionFace"];const PODFILE_MARKER="usesmileid_vision_face",BRIDGE_POD_TARGET="UseSmileIDBridgeExpo",BRIDGE_SPM_PRODUCTS=["UseSmileIDBridge"],BRIDGE_PODFILE_MARKER="usesmileid_bridge",withVisionFaceSpm=(i,r)=>(i=(0,deployment_target_1.withIosDeploymentTargetFloor)(i,IOS_DEPLOYMENT_TARGET),i=(0,config_plugins_1.withXcodeProject)(i,t=>{const e=t.modRequest.projectName;if(!e)throw new Error("[usesmileid_vision_face] Unable to resolve host app native target name from Expo config.");return(0,spm_1.linkSpmPackage)(t.modResults,SMILE_ID_PACKAGE,{targetName:e,products:[...exports.SPM_PRODUCTS]}),r?.iosSignatureAggregation==="disabled"?(0,signature_dedup_1.disableSignatureAggregation)(t.modResults,e):(0,signature_dedup_1.ensureSignatureDedupPhase)(t.modResults,e),t}),i=(0,config_plugins_1.withDangerousMod)(i,["ios",t=>{const e=path.join(t.modRequest.platformProjectRoot,"Podfile"),o=fs.readFileSync(e,"utf-8");let n=(0,podfile_1.injectPodfileSpmBlock)(o,{marker:PODFILE_MARKER,package:SMILE_ID_PACKAGE,podTarget:POD_TARGET,products:[...exports.SPM_PRODUCTS]});return n=(0,podfile_1.injectPodfileSpmBlock)(n,{marker:BRIDGE_PODFILE_MARKER,package:SMILE_ID_PACKAGE,podTarget:BRIDGE_POD_TARGET,products:[...BRIDGE_SPM_PRODUCTS],required:!0}),n=(0,podfile_1.injectPodfileKameraLinkBlock)(n),n=(0,podfile_1.injectPodfileSignatureDedupBlock)(n),n!==o&&fs.writeFileSync(e,n),t}]),i);exports.default=withVisionFaceSpm;
|
|
@@ -145,7 +145,7 @@ begin
|
|
|
145
145
|
# @rpath/Kamera.framework/Kamera"). Xcode embeds a binary product only for an app
|
|
146
146
|
# target that depends on it, so the app target takes both products directly.
|
|
147
147
|
kamera_spm_url = 'https://github.com/smileidentity/kamera-spm'
|
|
148
|
-
kamera_spm_version = JSON.parse(File.read(kamera_package_json))['version'].sub(/-SNAPSHOT
|
|
148
|
+
kamera_spm_version = JSON.parse(File.read(kamera_package_json))['version'].sub(/-SNAPSHOT$/, '')
|
|
149
149
|
kamera_spm_app_products = %w[Kamera KameraVision]
|
|
150
150
|
# A reference an app added by hand may spell the same repository differently (.git suffix,
|
|
151
151
|
# trailing slash, case); a second reference to one repository fails Xcode's resolution.
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// MIRRORED FILE — this file is duplicated byte-identical across the two
|
|
2
|
-
// RN Expo iOS bridge modules. Canonical location:
|
|
3
|
-
// usesmileid_vision_face/ios/UseSmileIDVisionFaceModule/
|
|
4
|
-
// UseSmileIDAnalyzerExecutor.swift
|
|
5
|
-
// Mirror location:
|
|
6
|
-
// usesmileid_vision_document/ios/UseSmileIDVisionDocumentModule/
|
|
7
|
-
// UseSmileIDAnalyzerExecutor.swift
|
|
8
|
-
//
|
|
9
|
-
// Each RN module is its own CocoaPods bridge target with its own
|
|
10
|
-
// `source_files` glob; source cannot be shared across them without
|
|
11
|
-
// inventing a new pod. The helper is duplicated verbatim into both
|
|
12
|
-
// vision modules. Any edit here MUST be copied to the other location in
|
|
13
|
-
// the same commit.
|
|
14
|
-
//
|
|
15
|
-
// Purpose: collapse the analyze / flush plumbing of the two RN Expo
|
|
16
|
-
// vision modules into a single generic executor whose Task closures
|
|
17
|
-
// capture only @Sendable values (closures + locals) — no module-class
|
|
18
|
-
// state, no implicit `self`. This forces both modules to share one set
|
|
19
|
-
// of Sendable correctness properties by construction: if one compiles
|
|
20
|
-
// cleanly under strict concurrency, the other must too.
|
|
21
|
-
//
|
|
22
|
-
// RN-specific differences from the Flutter twin:
|
|
23
|
-
// • RN modules receive pre-decoded JPEG/PNG bytes (`UIImage(data:)`),
|
|
24
|
-
// so the executor accepts `UIImage` directly rather than Pigeon's
|
|
25
|
-
// `YuvImage` and an associated decoder. JPEG decoding happens in
|
|
26
|
-
// the per-module wrapper before the executor is invoked.
|
|
27
|
-
// • The completion signature is `(Result<PigeonResult, Error>) -> Void`
|
|
28
|
-
// identical to Flutter — the per-module wrapper translates that
|
|
29
|
-
// into Expo's `Promise.resolve`/`reject`. Keeping the executor's
|
|
30
|
-
// public surface a `Result` consumer means both bridges share the
|
|
31
|
-
// same Task discipline.
|
|
32
|
-
|
|
33
|
-
import ExpoModulesCore
|
|
34
|
-
import UIKit
|
|
35
|
-
import UseSmileIDBridge
|
|
36
|
-
|
|
37
|
-
/// Bridges an Expo Modules @escaping completion handler into Swift 6
|
|
38
|
-
/// Task captures. Expo's `Promise` is itself reference-typed but not
|
|
39
|
-
/// `Sendable`; the executor never captures a `Promise` — it captures a
|
|
40
|
-
/// `Result` consumer closure that the per-module wrapper translates
|
|
41
|
-
/// into `resolve`/`reject`. This box mirrors the Flutter PigeonCompletion
|
|
42
|
-
/// pattern.
|
|
43
|
-
final class ExecutorCompletion<T>: @unchecked Sendable {
|
|
44
|
-
let call: (T) -> Void
|
|
45
|
-
init(_ fn: @escaping (T) -> Void) { self.call = fn }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/// Generic helper that owns the analyze / flush bridging between the
|
|
49
|
-
/// RN Expo Module's completion-based API and the native session's async
|
|
50
|
-
/// API.
|
|
51
|
-
///
|
|
52
|
-
/// The class itself is `@unchecked Sendable` because it stores only
|
|
53
|
-
/// `@Sendable` closures (which are themselves Sendable). The downstream
|
|
54
|
-
/// `Task.detached` bodies capture those closures — never an instance of a
|
|
55
|
-
/// concrete module class — so the compiler can prove the Task body is
|
|
56
|
-
/// Sendable-by-construction.
|
|
57
|
-
///
|
|
58
|
-
/// `NativeOutput` is the concrete native analyzer output type (e.g.
|
|
59
|
-
/// `FaceDetectorOutput`, `DocumentDetectorOutput`); `Wire` is the
|
|
60
|
-
/// JS-side wire dictionary the module returns to JS (typed as
|
|
61
|
-
/// `[String: Any?]` after Expo bridge marshalling).
|
|
62
|
-
final class UseSmileIDAnalyzerExecutor<NativeOutput: AnalyzerOutput, Wire>: @unchecked Sendable {
|
|
63
|
-
|
|
64
|
-
private let scan: @Sendable (UIImage, Int) async -> (NativeOutput?, UseSmileIDScanState)
|
|
65
|
-
private let drainEvents: @Sendable () -> [UseSmileIDBridge.CaptureReadyEvent]
|
|
66
|
-
private let flushFn: @Sendable () async throws -> Void
|
|
67
|
-
private let buildResult: @Sendable (NativeOutput?, UseSmileIDScanState, [UseSmileIDBridge.CaptureReadyEvent]) -> Wire
|
|
68
|
-
|
|
69
|
-
init(
|
|
70
|
-
scan: @escaping @Sendable (UIImage, Int) async -> (NativeOutput?, UseSmileIDScanState),
|
|
71
|
-
drainEvents: @escaping @Sendable () -> [UseSmileIDBridge.CaptureReadyEvent],
|
|
72
|
-
flush: @escaping @Sendable () async throws -> Void,
|
|
73
|
-
buildResult: @escaping @Sendable (NativeOutput?, UseSmileIDScanState, [UseSmileIDBridge.CaptureReadyEvent]) -> Wire
|
|
74
|
-
) {
|
|
75
|
-
self.scan = scan
|
|
76
|
-
self.drainEvents = drainEvents
|
|
77
|
-
self.flushFn = flush
|
|
78
|
-
self.buildResult = buildResult
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// Hands the already-decoded `UIImage` to the captured `scan` closure
|
|
82
|
-
/// on a detached task. The Task's capture list explicitly lists every
|
|
83
|
-
/// value it consumes so the compiler can verify the closure is
|
|
84
|
-
/// Sendable without falling back on implicit `self` capture.
|
|
85
|
-
///
|
|
86
|
-
/// The `completion` consumer is a `Result<Wire, Error>` — the
|
|
87
|
-
/// per-module wrapper translates that into `Promise.resolve` /
|
|
88
|
-
/// `Promise.reject`.
|
|
89
|
-
func analyze(
|
|
90
|
-
image: UIImage,
|
|
91
|
-
rotation: Int,
|
|
92
|
-
completion: @escaping (Result<Wire, Error>) -> Void
|
|
93
|
-
) {
|
|
94
|
-
let scan = self.scan
|
|
95
|
-
let drainEvents = self.drainEvents
|
|
96
|
-
let buildResult = self.buildResult
|
|
97
|
-
let completionBox = ExecutorCompletion(completion)
|
|
98
|
-
Task.detached(priority: .userInitiated) {
|
|
99
|
-
let (output, state) = await scan(image, rotation)
|
|
100
|
-
let events = drainEvents()
|
|
101
|
-
completionBox.call(.success(buildResult(output, state, events)))
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/// Awaits the captured `flushFn` on a detached task. Mirrors `analyze`'s
|
|
106
|
-
/// capture discipline — no `self`, no class state.
|
|
107
|
-
func flush(completion: @escaping (Result<Void, Error>) -> Void) {
|
|
108
|
-
let flushFn = self.flushFn
|
|
109
|
-
let completionBox = ExecutorCompletion(completion)
|
|
110
|
-
Task.detached(priority: .userInitiated) {
|
|
111
|
-
do {
|
|
112
|
-
try await flushFn()
|
|
113
|
-
completionBox.call(.success(()))
|
|
114
|
-
} catch {
|
|
115
|
-
completionBox.call(.failure(error))
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|