@smileid/usesmileid_vision_document 12.0.0-beta01 → 12.0.1

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,3 +1,11 @@
1
+ ## 12.0.1
2
+
3
+ See the repository CHANGELOG for the 12.0.1 release notes.
4
+
5
+ ## 12.0.0
6
+
7
+ First stable release of the Smile ID v12 SDK. Provides the iOS Apple Vision document detection module for `usesmileid`.
8
+
1
9
  ## 12.0.0-beta01
2
10
 
3
11
  First public beta release. Apple Vision document detection for the Smile ID React Native (Expo) SDK v12.
package/README.md CHANGED
@@ -13,7 +13,6 @@ and a TS wrapper that bridges the four contract methods (`configure /
13
13
  analyze / reset / close`) with Zod-validated wire payloads.
14
14
 
15
15
  No ML logic is reimplemented in this package — it's a pure passthrough.
16
- See `react-native-expo/docs/Architecture.md`.
17
16
 
18
17
  ## Installation
19
18
 
@@ -1,34 +1,30 @@
1
- // MIRRORED FILE — duplicated into both RN Expo iOS vision modules
2
- // (usesmileid_vision_face / usesmileid_vision_document). The per-module
3
- // CocoaPods bridge target sources cannot share files without a new pod.
4
- // Canonical location:
5
- // usesmileid_vision_face/ios/UseSmileIDVisionFaceModule/ResizingFrameStorage.swift
6
- // Mirror location:
7
- // usesmileid_vision_document/ios/UseSmileIDVisionDocumentModule/ResizingFrameStorage.swift
8
- //
9
- // Any edit here MUST be applied to the other location in the same commit.
1
+ // MIRRORED FILE — duplicated in both RN Expo iOS vision modules
2
+ // (usesmileid_vision_face / usesmileid_vision_document); the per-module pod
3
+ // targets can't share sources. Apply any edit to BOTH copies in the same commit.
10
4
 
11
5
  import Foundation
12
6
  import UIKit
13
7
  import UseSmileIDBridge
14
8
 
15
- /// `FrameStorage` implementation that center-crops and scales every selfie and
16
- /// liveness JPEG to the configured target dimensions before writing.
17
- ///
18
- /// Target dimensions are resolved at configure-time from the token
19
- /// `sdk_config` claim; the executor falls back to 480 × 640 when the token
20
- /// carries no override, so the on-disk pixel size always matches the
21
- /// `image_width` / `image_height` values reported in job metadata.
22
- ///
23
- /// Document frames are not resized — they are written at the analyzer's
24
- /// native frame resolution using the specified `documentQuality`. Document
25
- /// dimensions are not part of the token `sdk_config` contract.
26
- ///
27
- /// Encode and write run on background tasks; `persist` is fire-and-forget so
28
- /// the analyzer keeps running at frame rate. `flush()` awaits all in-flight
29
- /// writes before upload proceeds.
9
+ /// `FrameStorage` that center-crops + scales selfie/liveness JPEGs to the
10
+ /// token-resolved target size (falling back to 480×640, matching the
11
+ /// `image_width`/`image_height` job metadata), and downscales document frames
12
+ /// so their longest edge is bounded at `documentMaxDimension` (no upscaling).
13
+ /// When a crop region is set, document frames are first cropped to the on-screen
14
+ /// guide so the saved capture matches what the user framed (`setCropRegion(nil)`
15
+ /// clears it; selfie/liveness are never guide-cropped). Encode + write run on
16
+ /// background tasks; `persist` is fire-and-forget, `flush()` awaits all writes.
30
17
  final class ResizingFrameStorage: FrameStorage {
18
+ /// Longest-edge pixel bound applied to document frames before encoding.
19
+ private static let documentMaxDimension = 1600
20
+ /// Outward padding fraction applied to the guide-rect crop on each side
21
+ /// before cropping. Fifteen percent of the box's own width/height keeps the
22
+ /// document's corners and edges from being clipped when the card is framed
23
+ /// tight against the on-screen guide.
24
+ private static let documentCropPaddingFraction: CGFloat = 0.15
25
+
31
26
  private let sessionDirectory: URL
27
+ private let captureType: CaptureType
32
28
  private let documentSide: DocumentSide
33
29
  private let selfieTargetWidth: Int
34
30
  private let selfieTargetHeight: Int
@@ -38,9 +34,16 @@ final class ResizingFrameStorage: FrameStorage {
38
34
  private let livenessQuality: CGFloat
39
35
  private let documentQuality: CGFloat
40
36
  private let writer: Writer
37
+ // The frame-processor plugin calls `setCropRegion` on the VisionCamera FP
38
+ // thread; `persist` reads the value on that same serialised scan path before
39
+ // handing it (by value) to the background encode task. The Swift value is a
40
+ // `Sendable` struct captured by value into `Writer.enqueue`, so no background
41
+ // task ever observes a torn read of this property.
42
+ nonisolated(unsafe) private var cropRegion: ViewportCropRegion?
41
43
 
42
44
  init(
43
45
  sessionDirectory: URL,
46
+ captureType: CaptureType,
44
47
  documentSide: DocumentSide = .front,
45
48
  selfieTargetWidth: Int = 480,
46
49
  selfieTargetHeight: Int = 640,
@@ -51,6 +54,7 @@ final class ResizingFrameStorage: FrameStorage {
51
54
  documentQuality: CGFloat = 0.85
52
55
  ) {
53
56
  self.sessionDirectory = sessionDirectory
57
+ self.captureType = captureType
54
58
  self.documentSide = documentSide
55
59
  self.selfieTargetWidth = selfieTargetWidth
56
60
  self.selfieTargetHeight = selfieTargetHeight
@@ -62,6 +66,13 @@ final class ResizingFrameStorage: FrameStorage {
62
66
  self.writer = Writer()
63
67
  }
64
68
 
69
+ /// Set the on-screen document guide rectangle so document frames are cropped
70
+ /// to exactly what the user saw. Pass `nil` to clear the crop and save the
71
+ /// full upright frame. Selfie and liveness frames are unaffected.
72
+ func setCropRegion(_ region: ViewportCropRegion?) {
73
+ cropRegion = region
74
+ }
75
+
65
76
  func url(for kind: FrameKind, captureIndex: Int) -> URL {
66
77
  let name: String
67
78
  switch kind {
@@ -84,20 +95,37 @@ final class ResizingFrameStorage: FrameStorage {
84
95
  func persist(image: UIImage, to url: URL) {
85
96
  let name = url.lastPathComponent
86
97
  let resize: (Int, Int)?
98
+ let maxDimension: Int?
87
99
  let quality: CGFloat
100
+ let documentCropRegion: ViewportCropRegion?
88
101
  if name.hasPrefix("selfie") {
89
102
  resize = (selfieTargetWidth, selfieTargetHeight)
103
+ maxDimension = nil
90
104
  quality = selfieQuality
105
+ documentCropRegion = nil
91
106
  } else if name.hasPrefix("liveness") {
92
107
  resize = (livenessTargetWidth, livenessTargetHeight)
108
+ maxDimension = nil
93
109
  quality = livenessQuality
110
+ documentCropRegion = nil
94
111
  } else {
95
- // Document frames: encode at the analyzer's native resolution.
112
+ // Document frames: optionally crop to the guide overlay, then downscale
113
+ // so the longest edge is bounded at documentMaxDimension; frames already
114
+ // within the bound are written as-is (no upscaling).
96
115
  resize = nil
116
+ maxDimension = Self.documentMaxDimension
97
117
  quality = documentQuality
118
+ documentCropRegion = cropRegion
98
119
  }
99
120
  Task {
100
- await writer.enqueue(image: image, to: url, resize: resize, quality: quality)
121
+ await writer.enqueue(
122
+ image: image,
123
+ to: url,
124
+ resize: resize,
125
+ maxDimension: maxDimension,
126
+ quality: quality,
127
+ documentCropRegion: documentCropRegion
128
+ )
101
129
  }
102
130
  }
103
131
 
@@ -106,25 +134,53 @@ final class ResizingFrameStorage: FrameStorage {
106
134
  }
107
135
 
108
136
  func clear() {
137
+ // Selfie and document captures share one session directory; delete
138
+ // only this role's own frames.
109
139
  let fileManager = FileManager.default
110
140
  let contents = (try? fileManager.contentsOfDirectory(
111
141
  at: sessionDirectory,
112
142
  includingPropertiesForKeys: nil
113
143
  )) ?? []
114
- for fileURL in contents {
144
+ for fileURL in contents where ownsForClear(fileURL) {
115
145
  try? fileManager.removeItem(at: fileURL)
116
146
  }
117
147
  }
118
148
 
149
+ private func ownsForClear(_ fileURL: URL) -> Bool {
150
+ let name = fileURL.lastPathComponent
151
+ switch captureType {
152
+ case .selfie:
153
+ return name.hasPrefix("selfie") || name.hasPrefix("liveness")
154
+ case .document:
155
+ return name == url(for: .document, captureIndex: 0).lastPathComponent
156
+ @unknown default:
157
+ return false
158
+ }
159
+ }
160
+
119
161
  /// Serialises writes through an actor so concurrent `persist` calls for
120
162
  /// the same URL don't race and `flush()` can join every outstanding task
121
163
  /// without external synchronisation.
122
164
  private actor Writer {
123
165
  private var inFlight: [Task<Void, Error>] = []
124
166
 
125
- func enqueue(image: UIImage, to url: URL, resize: (Int, Int)?, quality: CGFloat) {
167
+ func enqueue(
168
+ image: UIImage,
169
+ to url: URL,
170
+ resize: (Int, Int)?,
171
+ maxDimension: Int?,
172
+ quality: CGFloat,
173
+ documentCropRegion: ViewportCropRegion?
174
+ ) {
126
175
  let task = Task<Void, Error>.detached(priority: .utility) {
127
- try Self.encodeAndWrite(image: image, to: url, resize: resize, quality: quality)
176
+ try Self.encodeAndWrite(
177
+ image: image,
178
+ to: url,
179
+ resize: resize,
180
+ maxDimension: maxDimension,
181
+ quality: quality,
182
+ documentCropRegion: documentCropRegion
183
+ )
128
184
  }
129
185
  inFlight.append(task)
130
186
  }
@@ -149,7 +205,9 @@ final class ResizingFrameStorage: FrameStorage {
149
205
  image: UIImage,
150
206
  to url: URL,
151
207
  resize: (Int, Int)?,
152
- quality: CGFloat
208
+ maxDimension: Int?,
209
+ quality: CGFloat,
210
+ documentCropRegion: ViewportCropRegion?
153
211
  ) throws {
154
212
  try FileManager.default.createDirectory(
155
213
  at: url.deletingLastPathComponent(),
@@ -162,9 +220,56 @@ final class ResizingFrameStorage: FrameStorage {
162
220
  let upright = image.normalizingOrientation()
163
221
  let data: Data?
164
222
  if let (width, height) = resize, let cgImage = upright.cgImage {
165
- // Center-crop and scale to the token-resolved target size.
223
+ // Center-crop and scale to the token-resolved target size
224
+ // (selfie / liveness path).
166
225
  let cropped = cgImage.centerCroppedExact(width: width, height: height)
167
226
  data = cropped.flatMap { UIImage(cgImage: $0).jpegData(compressionQuality: quality) }
227
+ } else if let maxDimension, var cgImage = upright.cgImage {
228
+ // Document path: optionally crop to the on-screen guide overlay, then
229
+ // downscale so the longest pixel edge is bounded at maxDimension.
230
+ // Never upscale frames already within the bound.
231
+ //
232
+ // Geometric correctness of the crop depends on `upright` being
233
+ // pixel-upright (orientation baked above); `toFrameBoundingBox`
234
+ // maps viewport coordinates onto an upright frame.
235
+ if let region = documentCropRegion {
236
+ let pixelWidth = cgImage.width
237
+ let pixelHeight = cgImage.height
238
+ let box = region.toFrameBoundingBox(frameWidth: pixelWidth, frameHeight: pixelHeight)
239
+ // Pad the guide rect outward by documentCropPaddingFraction of its
240
+ // own size on each side so the document's corners and edges keep
241
+ // breathing room and are never clipped when framed tight. Intersect
242
+ // with the image bounds so a padded edge never runs off-frame.
243
+ let padX = CGFloat(box.width) * ResizingFrameStorage.documentCropPaddingFraction
244
+ let padY = CGFloat(box.height) * ResizingFrameStorage.documentCropPaddingFraction
245
+ let cropRect = CGRect(
246
+ x: (CGFloat(box.left) - padX) * CGFloat(pixelWidth),
247
+ y: (CGFloat(box.top) - padY) * CGFloat(pixelHeight),
248
+ width: (CGFloat(box.width) + padX * 2) * CGFloat(pixelWidth),
249
+ height: (CGFloat(box.height) + padY * 2) * CGFloat(pixelHeight)
250
+ ).intersection(CGRect(x: 0, y: 0, width: pixelWidth, height: pixelHeight))
251
+ if let cropped = cgImage.cropping(to: cropRect) {
252
+ cgImage = cropped
253
+ }
254
+ }
255
+ let pixelWidth = cgImage.width
256
+ let pixelHeight = cgImage.height
257
+ let longest = max(pixelWidth, pixelHeight)
258
+ if longest > maxDimension {
259
+ let scale = CGFloat(maxDimension) / CGFloat(longest)
260
+ let newSize = CGSize(
261
+ width: (CGFloat(pixelWidth) * scale).rounded(),
262
+ height: (CGFloat(pixelHeight) * scale).rounded()
263
+ )
264
+ let format = UIGraphicsImageRendererFormat()
265
+ format.scale = 1
266
+ let scaled = UIGraphicsImageRenderer(size: newSize, format: format).image { _ in
267
+ UIImage(cgImage: cgImage).draw(in: CGRect(origin: .zero, size: newSize))
268
+ }
269
+ data = scaled.jpegData(compressionQuality: quality)
270
+ } else {
271
+ data = UIImage(cgImage: cgImage).jpegData(compressionQuality: quality)
272
+ }
168
273
  } else {
169
274
  data = upright.jpegData(compressionQuality: quality)
170
275
  }
@@ -1,15 +1,14 @@
1
1
  import CoreMedia
2
2
  import CoreImage
3
3
  import UIKit
4
+ import UseSmileIDBridge
4
5
  import VisionCamera
5
6
 
6
- /// VisionCamera frame processor plugin that feeds camera frames directly
7
- /// into the active `UseSmileIDDocumentSession` without crossing the JS thread.
8
- ///
9
- /// Registered programmatically via `FrameProcessorPluginRegistry` (avoids ObjC bridging-header
10
- /// constraint); the `OnCreate` guard prevents double-registration on fast-refresh reloads.
11
- /// All session ops are serialised through `UseSmileIDVisionDocumentSessionHolder.operationQueue`.
12
- /// `CIContext` is created once per instance — per-frame creation causes GPU pipeline re-compilation.
7
+ /// VisionCamera frame processor that feeds camera frames into the active
8
+ /// `UseSmileIDDocumentSession` without crossing the JS thread. Session ops
9
+ /// serialise through `UseSmileIDVisionDocumentSessionHolder.operationQueue`;
10
+ /// the `CIContext` is reused across frames (per-frame creation re-compiles the
11
+ /// GPU pipeline).
13
12
  @objc(UseSmileIDVisionDocumentFrameProcessorPlugin)
14
13
  public class UseSmileIDVisionDocumentFrameProcessorPlugin: FrameProcessorPlugin {
15
14
 
@@ -29,14 +28,21 @@ public class UseSmileIDVisionDocumentFrameProcessorPlugin: FrameProcessorPlugin
29
28
  return ["error": "frame_conversion_failed"]
30
29
  }
31
30
 
32
- // Bake rotation + mirror physically into pixels (CIImage.oriented) rather than
33
- // tagging UIImage with an orientation flag: some consumers honour the flag
34
- // (UIKit draw) but others ignore it (raw CGImage ops), so physical pixels are safer.
31
+ // Bake rotation + mirror into the pixels (CIImage.oriented) rather than an
32
+ // EXIF flag — some consumers ignore the flag. `frame.orientation` only
33
+ // uprights to the sensor-natural (portrait) axis; the document screen
34
+ // force-locks the interface, so we additionally rotate onto that LOCKED
35
+ // INTERFACE axis so the saved JPEG matches the preview and the guide-rect
36
+ // crop. `viewportOrientation` is the interface orientation in degrees
37
+ // (0/90/270); each maps to a device-verified rotation in
38
+ // `rotatedOntoViewportAxis` — including the portrait (0°) half-turn.
35
39
  let orientationFlag = frame.isMirrored
36
40
  ? frame.orientation.composedWithHorizontalMirror()
37
41
  : frame.orientation
42
+ let viewportOrientation = (arguments?["viewportOrientation"] as? NSNumber)?.intValue ?? 0
38
43
  let orientedImage = CIImage(cvPixelBuffer: pixelBuffer)
39
44
  .oriented(CGImagePropertyOrientation(orientationFlag))
45
+ .rotatedOntoViewportAxis(degrees: viewportOrientation)
40
46
 
41
47
  guard let cgImage = ciContext.createCGImage(orientedImage, from: orientedImage.extent) else {
42
48
  return ["error": "frame_conversion_failed"]
@@ -45,6 +51,13 @@ public class UseSmileIDVisionDocumentFrameProcessorPlugin: FrameProcessorPlugin
45
51
  let rotation = (arguments?["rotation"] as? NSNumber)?.intValue ?? 0
46
52
  let uiImage = UIImage(cgImage: cgImage)
47
53
 
54
+ // Forward the on-screen document guide rectangle (if JS supplied it) so the
55
+ // saved capture is cropped to exactly what the user framed. Set before the
56
+ // scan so the storage holds the region when this frame's `persist` runs.
57
+ // Both calls serialise through the holder's operation queue, so the region
58
+ // is stable for the duration of this frame's scan.
59
+ UseSmileIDVisionDocumentSessionHolder.shared.setCropRegion(cropRegionFrom(arguments))
60
+
48
61
  guard let result = UseSmileIDVisionDocumentSessionHolder.shared.scanAndDrain(
49
62
  image: uiImage,
50
63
  rotation: rotation
@@ -54,13 +67,31 @@ public class UseSmileIDVisionDocumentFrameProcessorPlugin: FrameProcessorPlugin
54
67
 
55
68
  return result
56
69
  }
70
+
71
+ /// Build the `ViewportCropRegion` from the flat numeric crop args the JS
72
+ /// worklet supplies. Returns `nil` when `cropWidth <= 0` (the "no region"
73
+ /// signal for selfie / before the viewport is measured). Flat primitives
74
+ /// because frame-processor arguments don't reliably accept nested objects.
75
+ private func cropRegionFrom(_ arguments: [AnyHashable: Any]?) -> ViewportCropRegion? {
76
+ guard let width = (arguments?["cropWidth"] as? NSNumber)?.floatValue, width > 0 else {
77
+ return nil
78
+ }
79
+ return ViewportCropRegion(
80
+ left: (arguments?["cropLeft"] as? NSNumber)?.floatValue ?? 0,
81
+ top: (arguments?["cropTop"] as? NSNumber)?.floatValue ?? 0,
82
+ width: width,
83
+ height: (arguments?["cropHeight"] as? NSNumber)?.floatValue ?? 0,
84
+ viewportWidthPx: (arguments?["cropViewportWidth"] as? NSNumber)?.intValue ?? 0,
85
+ viewportHeightPx: (arguments?["cropViewportHeight"] as? NSNumber)?.intValue ?? 0,
86
+ contentScale: .crop
87
+ )
88
+ }
57
89
  }
58
90
 
59
91
 
60
92
  private extension UIImage.Orientation {
61
- /// Compose the buffer's horizontal mirror into this orientation so rendering
62
- /// undoes the flip together with the rotation. EXIF mirrored cases pair
63
- /// directly with their rotation counterparts (.left ↔ .leftMirrored).
93
+ /// Fold the buffer's horizontal mirror into this orientation so rendering
94
+ /// undoes the flip with the rotation (.left ↔ .leftMirrored, etc.).
64
95
  func composedWithHorizontalMirror() -> UIImage.Orientation {
65
96
  switch self {
66
97
  case .up: return .upMirrored
@@ -77,6 +108,27 @@ private extension UIImage.Orientation {
77
108
  }
78
109
 
79
110
 
111
+ private extension CIImage {
112
+ /// Rotate the sensor-natural (portrait) image onto the locked interface
113
+ /// (viewport) axis so the guide-rect crop lands on the right pixels and the
114
+ /// preview matches the capture. `degrees` is the interface orientation
115
+ /// (0/90/270). Directions are device-verified on iPhone — the opposite
116
+ /// assignment lands the document upside down.
117
+ func rotatedOntoViewportAxis(degrees: Int) -> CIImage {
118
+ let normalized = ((degrees % 360) + 360) % 360
119
+ switch normalized {
120
+ // Portrait document (0° interface): the rear camera's sensor-natural upright
121
+ // sits 180° from the portrait interface — the phone is held flat over the
122
+ // document — so the saved portrait document needs a half-turn. The guide
123
+ // rect is centred, so the half-turn leaves the crop aligned.
124
+ case 0: return oriented(.down)
125
+ case 90: return oriented(.left)
126
+ case 270: return oriented(.right)
127
+ default: return self
128
+ }
129
+ }
130
+ }
131
+
80
132
  private extension CGImagePropertyOrientation {
81
133
  /// Case-for-case translation from `UIImage.Orientation` — the two
82
134
  /// enums share semantics but not raw values, so a direct cast would
@@ -4,22 +4,11 @@ import UseSmileIDBridge
4
4
  import UseSmileIDVisionDocument
5
5
  import VisionCamera
6
6
 
7
- /// Expo Module bridging the existing native session
8
- /// `UseSmileIDDocumentSession(analyzeFrame:)` (shipped in the
9
- /// `UseSmileIDVisionDocument` Swift Package) to the JS side.
10
- ///
11
- /// No ML logic lives here — the native session owns the entire
12
- /// pipeline. This module adapts the four contract methods (`configure /
13
- /// reset / flush / close`) and publishes the active session to
14
- /// `UseSmileIDVisionDocumentSessionHolder` so the frame processor plugin
15
- /// can feed camera frames directly without crossing the JS thread.
16
- ///
17
- /// All session operations (configure, reset, flush, close, per-frame scan)
18
- /// are serialised through `UseSmileIDVisionDocumentSessionHolder.shared`,
19
- /// which owns the single `operationQueue` serialisation point.
20
- ///
21
- /// Lifecycle: Expo instantiates exactly one `Module` per app process;
22
- /// `session` is owned for the module's lifetime.
7
+ /// Expo Module bridging the native `UseSmileIDDocumentSession` (from the
8
+ /// `UseSmileIDVisionDocument` Swift Package) to JS. No ML logic here — it
9
+ /// adapts `configure / reset / flush / close` and publishes the session to
10
+ /// `UseSmileIDVisionDocumentSessionHolder` (which serialises every op on one
11
+ /// `operationQueue`) so the frame processor feeds frames without the JS thread.
23
12
  public class UseSmileIDVisionDocumentModule: Module {
24
13
  private lazy var analyzer: DocumentDetectorAnalyzer = DocumentDetectorAnalyzer.Factory().newInstance()!
25
14
 
@@ -54,6 +43,7 @@ public class UseSmileIDVisionDocumentModule: Module {
54
43
  .map { CGFloat($0) / 100.0 } ?? 0.85
55
44
  let storage = ResizingFrameStorage(
56
45
  sessionDirectory: Self.freshSessionDirectory(),
46
+ captureType: .document,
57
47
  documentSide: documentSide,
58
48
  documentQuality: documentQuality
59
49
  )
@@ -69,6 +59,9 @@ public class UseSmileIDVisionDocumentModule: Module {
69
59
  // Publish the session through the holder's operation queue so the
70
60
  // write is serialised with any concurrent scan on the FP thread.
71
61
  UseSmileIDVisionDocumentSessionHolder.shared.setSession(newSession)
62
+ // Publish the storage too so the frame-processor plugin can forward the
63
+ // document guide-rectangle crop region to it per frame.
64
+ UseSmileIDVisionDocumentSessionHolder.shared.setStorage(storage)
72
65
  }
73
66
 
74
67
  AsyncFunction("reset") {
@@ -90,11 +83,13 @@ public class UseSmileIDVisionDocumentModule: Module {
90
83
 
91
84
  AsyncFunction("close") {
92
85
  UseSmileIDVisionDocumentSessionHolder.shared.setSession(nil)
86
+ UseSmileIDVisionDocumentSessionHolder.shared.setStorage(nil)
93
87
  self.session = nil
94
88
  }
95
89
 
96
90
  OnDestroy {
97
91
  UseSmileIDVisionDocumentSessionHolder.shared.setSession(nil)
92
+ UseSmileIDVisionDocumentSessionHolder.shared.setStorage(nil)
98
93
  self.session = nil
99
94
  }
100
95
  }
@@ -125,7 +120,7 @@ private extension Dictionary where Key == String, Value == Any? {
125
120
  documentDetectorConfig: DocumentDetectorConfig(
126
121
  timeout: (self["timeout"] as? NSNumber)?.intValue ?? 15_000,
127
122
  iouThreshold: (self["iouThreshold"] as? NSNumber)?.floatValue ?? 0.85,
128
- timeRequired: (self["timeRequired"] as? NSNumber)?.intValue ?? 500,
123
+ timeRequired: (self["timeRequired"] as? NSNumber)?.intValue ?? 1_000,
129
124
  minCoverageThreshold: (self["minCoverageThreshold"] as? NSNumber)?.floatValue ?? 0.18,
130
125
  maxCoverageThreshold: (self["maxCoverageThreshold"] as? NSNumber)?.floatValue ?? 0.78,
131
126
  centerToleranceX: (self["centerToleranceX"] as? NSNumber)?.floatValue ?? 0.05,
@@ -19,6 +19,12 @@ final class UseSmileIDVisionDocumentSessionHolder {
19
19
 
20
20
  private var session: UseSmileIDDocumentSession?
21
21
 
22
+ /// The active session's frame storage. Published alongside `session` so the
23
+ /// frame-processor plugin can forward the document guide-rectangle crop
24
+ /// region (via `ResizingFrameStorage.setCropRegion`) without reaching through
25
+ /// the session. Read on the FP scan path; nulled when the session closes.
26
+ private var storage: ResizingFrameStorage?
27
+
22
28
  private init() {}
23
29
 
24
30
  /// Replace or clear the active session. Called from the Expo module's
@@ -29,6 +35,24 @@ final class UseSmileIDVisionDocumentSessionHolder {
29
35
  }
30
36
  }
31
37
 
38
+ /// Publish (or clear) the active session's frame storage. Serialised on the
39
+ /// same `operationQueue` as `setSession` so the write is ordered with any
40
+ /// concurrent scan reading it on the FP thread.
41
+ func setStorage(_ newStorage: ResizingFrameStorage?) {
42
+ operationQueue.sync {
43
+ storage = newStorage
44
+ }
45
+ }
46
+
47
+ /// Forward the on-screen document guide rectangle to the active storage so
48
+ /// the saved capture is cropped to exactly what the user framed. Pass `nil`
49
+ /// to clear the crop. No-op when no session/storage is configured.
50
+ func setCropRegion(_ region: ViewportCropRegion?) {
51
+ operationQueue.sync {
52
+ storage?.setCropRegion(region)
53
+ }
54
+ }
55
+
32
56
  /// Run `session.reset()` serialised with any in-flight scan. Called from
33
57
  /// the Expo module's `reset` AsyncFunction.
34
58
  func reset() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smileid/usesmileid_vision_document",
3
- "version": "12.0.0-beta01",
4
- "description": "UseSmileID v12 — Apple Vision document provider (iOS-only). Thin Expo Module shim around the existing Swift Package `UseSmileIDVisionDocument`.",
3
+ "version": "12.0.1",
4
+ "description": "Apple Vision document detection implementation for the UseSmileID React Native (Expo) SDK.",
5
5
  "homepage": "https://usesmileid.com",
6
6
  "repository": {
7
7
  "type": "git",
@@ -48,8 +48,8 @@
48
48
  "CHANGELOG.md"
49
49
  ],
50
50
  "dependencies": {
51
- "@smileid/usesmileid_bridge": "12.0.0-beta01",
52
- "@smileid/usesmileid_platform_interface": "12.0.0-beta01"
51
+ "@smileid/usesmileid_bridge": "12.0.1",
52
+ "@smileid/usesmileid_platform_interface": "12.0.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "expo": ">=56.0.0",
@@ -64,8 +64,7 @@
64
64
  "typescript": "^6.0.3"
65
65
  },
66
66
  "publishConfig": {
67
- "access": "public",
68
- "provenance": false
67
+ "access": "public"
69
68
  },
70
69
  "scripts": {
71
70
  "build": "pnpm run build:lib && pnpm run build:plugin",
@@ -51,15 +51,15 @@ const fs = __importStar(require("fs"));
51
51
  const path = __importStar(require("path"));
52
52
  const podfile_1 = require("./lib/podfile");
53
53
  const spm_1 = require("./lib/spm");
54
- // Pinned to the exact `12.0.0-beta01` release tag — `from: 12.0.0`
54
+ // Pinned to the exact `12.0.0` release tag — `from: 12.0.0`
55
55
  // would not resolve because `ios-spm` publishes pre-release tags (e.g.
56
- // `12.0.0-beta01`), and SPM excludes pre-release tags from open-ended
56
+ // `12.0.0`), and SPM excludes pre-release tags from open-ended
57
57
  // major-version requirements.
58
58
  const SMILE_ID_PACKAGE = {
59
59
  url: "https://github.com/smileidentity/ios-spm",
60
60
  requirement: {
61
61
  kind: "exactVersion",
62
- version: "12.0.0-beta01",
62
+ version: "12.0.1-SNAPSHOT.20260729180601.30478033861",
63
63
  },
64
64
  };
65
65
  const POD_TARGET = "UseSmileIDVisionDocumentExpo";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,iEAAiE;AACjE,0DAA0D;AAC1D,qEAAqE;AACrE,2CAA2C;AAC3C,mEAAmE;AACnE,wEAAwE;AACxE,2EAA2E;AAC3E,EAAE;AACF,oDAAoD;AACpD,oEAAoE;AACpE,sEAAsE;AACtE,oEAAoE;AACpE,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9B,yDAI8B;AAC9B,uCAAyB;AACzB,2CAA6B;AAE7B,2CAAsD;AACtD,mCAAuD;AAEvD,mEAAmE;AACnE,uEAAuE;AACvE,sEAAsE;AACtE,8BAA8B;AAC9B,MAAM,gBAAgB,GAAe;IACnC,GAAG,EAAE,0CAA0C;IAC/C,WAAW,EAAE;QACX,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,eAAe;KACzB;CACF,CAAC;AAEF,MAAM,UAAU,GAAG,8BAA8B,CAAC;AAClD,MAAM,YAAY,GAAG,CAAC,0BAA0B,EAAE,kBAAkB,CAAU,CAAC;AAC/E,MAAM,cAAc,GAAG,4BAA4B,CAAC;AAEpD,MAAM,qBAAqB,GAAiB,CAAC,MAAM,EAAE,EAAE;IACrD,mEAAmE;IACnE,mEAAmE;IACnE,wBAAwB;IACxB,MAAM,GAAG,IAAA,iCAAgB,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,8FAA8F,CAC/F,CAAC;QACJ,CAAC;QACD,IAAA,oBAAc,EAAC,MAAM,CAAC,UAAU,EAAE,gBAAgB,EAAE;YAClD,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC;SAC5B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,mEAAmE;IACnE,iEAAiE;IACjE,+DAA+D;IAC/D,oEAAoE;IACpE,MAAM,GAAG,IAAA,iCAAgB,EAAC,MAAM,EAAE;QAChC,KAAK;QACL,CAAC,MAAM,EAAE,EAAE;YACT,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,MAAM,CAAC,UAAU,CAAC,mBAAmB,EACrC,SAAS,CACV,CAAC;YACF,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YACvD,MAAM,IAAI,GAAG,IAAA,+BAAqB,EAAC,QAAQ,EAAE;gBAC3C,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,gBAAgB;gBACzB,SAAS,EAAE,UAAU;gBACrB,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC;aAC5B,CAAC,CAAC;YACH,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtB,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YACtC,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,kBAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,iEAAiE;AACjE,0DAA0D;AAC1D,qEAAqE;AACrE,2CAA2C;AAC3C,mEAAmE;AACnE,wEAAwE;AACxE,2EAA2E;AAC3E,EAAE;AACF,oDAAoD;AACpD,oEAAoE;AACpE,sEAAsE;AACtE,oEAAoE;AACpE,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9B,yDAI8B;AAC9B,uCAAyB;AACzB,2CAA6B;AAE7B,2CAAsD;AACtD,mCAAuD;AAEvD,4DAA4D;AAC5D,uEAAuE;AACvE,+DAA+D;AAC/D,8BAA8B;AAC9B,MAAM,gBAAgB,GAAe;IACnC,GAAG,EAAE,0CAA0C;IAC/C,WAAW,EAAE;QACX,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,4CAA4C;KACtD;CACF,CAAC;AAEF,MAAM,UAAU,GAAG,8BAA8B,CAAC;AAClD,MAAM,YAAY,GAAG,CAAC,0BAA0B,EAAE,kBAAkB,CAAU,CAAC;AAC/E,MAAM,cAAc,GAAG,4BAA4B,CAAC;AAEpD,MAAM,qBAAqB,GAAiB,CAAC,MAAM,EAAE,EAAE;IACrD,mEAAmE;IACnE,mEAAmE;IACnE,wBAAwB;IACxB,MAAM,GAAG,IAAA,iCAAgB,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,8FAA8F,CAC/F,CAAC;QACJ,CAAC;QACD,IAAA,oBAAc,EAAC,MAAM,CAAC,UAAU,EAAE,gBAAgB,EAAE;YAClD,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC;SAC5B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,mEAAmE;IACnE,iEAAiE;IACjE,+DAA+D;IAC/D,oEAAoE;IACpE,MAAM,GAAG,IAAA,iCAAgB,EAAC,MAAM,EAAE;QAChC,KAAK;QACL,CAAC,MAAM,EAAE,EAAE;YACT,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,MAAM,CAAC,UAAU,CAAC,mBAAmB,EACrC,SAAS,CACV,CAAC;YACF,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YACvD,MAAM,IAAI,GAAG,IAAA,+BAAqB,EAAC,QAAQ,EAAE;gBAC3C,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,gBAAgB;gBACzB,SAAS,EAAE,UAAU;gBACrB,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC;aAC5B,CAAC,CAAC;YACH,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtB,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YACtC,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,kBAAe,qBAAqB,CAAC"}
@@ -23,15 +23,15 @@ import * as path from "path";
23
23
  import { injectPodfileSpmBlock } from "./lib/podfile";
24
24
  import { linkSpmPackage, SpmPackage } from "./lib/spm";
25
25
 
26
- // Pinned to the exact `12.0.0-beta01` release tag — `from: 12.0.0`
26
+ // Pinned to the exact `12.0.0` release tag — `from: 12.0.0`
27
27
  // would not resolve because `ios-spm` publishes pre-release tags (e.g.
28
- // `12.0.0-beta01`), and SPM excludes pre-release tags from open-ended
28
+ // `12.0.0`), and SPM excludes pre-release tags from open-ended
29
29
  // major-version requirements.
30
30
  const SMILE_ID_PACKAGE: SpmPackage = {
31
31
  url: "https://github.com/smileidentity/ios-spm",
32
32
  requirement: {
33
33
  kind: "exactVersion",
34
- version: "12.0.0-beta01",
34
+ version: "12.0.1-SNAPSHOT.20260729180601.30478033861",
35
35
  },
36
36
  };
37
37