@smileid/usesmileid_vision_document 12.1.1 → 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,15 +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
+
3
8
  ## 12.1.1
4
9
 
5
10
  See the repository CHANGELOG for the 12.1.1 release notes.
6
11
 
7
-
8
12
  ## 12.1.0
9
13
 
10
14
  See the repository CHANGELOG for the 12.1.0 release notes.
11
15
 
12
-
13
16
  ## 12.0.2
14
17
 
15
18
  See the repository CHANGELOG for the 12.0.2 release notes.
@@ -17,11 +17,34 @@ import UseSmileIDBridge
17
17
  final class ResizingFrameStorage: FrameStorage {
18
18
  /// Longest-edge pixel bound applied to document frames before encoding.
19
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
20
+ /// The hidden border kept around the guide rect: this fraction of the saved
21
+ /// image's short side on every edge, the rule of the native bridges'
22
+ /// `DocumentCropGeometry.documentBorderFraction` and the selfie's interior
23
+ /// crop. The border is where presentation-attack cues live; the backend reads
24
+ /// it, and the preview shows only the interior.
25
+ static let documentBorderFraction: CGFloat = 0.15
26
+
27
+ /// The uniform border, in pixels, to grow `guide` by: the b that is
28
+ /// `documentBorderFraction` of the grown rect's own short side,
29
+ /// b = f × s / (1 − 2f) for the guide's short side s. Shrinks to the most
30
+ /// every edge of an `imageSize` image can hold; 0 for an empty guide or one
31
+ /// touching an edge. Mirrors the native bridges' `DocumentCropGeometry.borderAround`.
32
+ static func borderAround(_ guide: CGRect, imageSize: CGSize) -> CGFloat {
33
+ let shortSide = min(guide.width, guide.height)
34
+ guard shortSide > 0 else { return 0 }
35
+ let wanted = documentBorderFraction * shortSide / (1 - 2 * documentBorderFraction)
36
+ let room = min(guide.minX, guide.minY, imageSize.width - guide.maxX, imageSize.height - guide.maxY)
37
+ return max(min(wanted, room), 0)
38
+ }
39
+
40
+ /// A `border` around a crop of `size` as a fraction of the crop's short side,
41
+ /// held to `documentBorderFraction` so the crop's pixel rounding never reports
42
+ /// more border than was kept; 0 for an empty crop.
43
+ static func borderFraction(_ border: CGFloat, of size: CGSize) -> CGFloat {
44
+ let shortSide = min(size.width, size.height)
45
+ guard shortSide > 0 else { return 0 }
46
+ return min(max(border / shortSide, 0), documentBorderFraction)
47
+ }
25
48
 
26
49
  private let sessionDirectory: URL
27
50
  private let captureType: CaptureType
@@ -34,12 +57,18 @@ final class ResizingFrameStorage: FrameStorage {
34
57
  private let livenessQuality: CGFloat
35
58
  private let documentQuality: CGFloat
36
59
  private let writer: Writer
60
+ /// The hidden border each document this storage saved kept, recorded by the
61
+ /// write tasks once their file is on disk and read after `flush()`.
62
+ private let savedBorders = SavedDocumentBorders()
37
63
  // The analysis tick calls `setCropRegion` on the module's async-function
38
64
  // thread; `persist` reads the value on that same serialised scan path before
39
65
  // handing it (by value) to the background encode task. The Swift value is a
40
66
  // `Sendable` struct captured by value into `Writer.enqueue`, so no background
41
67
  // task ever observes a torn read of this property.
42
- nonisolated(unsafe) private var cropRegion: ViewportCropRegion?
68
+ //
69
+ // Read back as `FrameStorage.cropRegion`, which the document transitioner judges the detected
70
+ // document against, so auto-capture waits for the document inside the outline (ios-v12 #370).
71
+ nonisolated(unsafe) private(set) var cropRegion: ViewportCropRegion?
43
72
 
44
73
  init(
45
74
  sessionDirectory: URL,
@@ -124,7 +153,8 @@ final class ResizingFrameStorage: FrameStorage {
124
153
  resize: resize,
125
154
  maxDimension: maxDimension,
126
155
  quality: quality,
127
- documentCropRegion: documentCropRegion
156
+ documentCropRegion: documentCropRegion,
157
+ savedBorders: savedBorders
128
158
  )
129
159
  }
130
160
  }
@@ -133,6 +163,16 @@ final class ResizingFrameStorage: FrameStorage {
133
163
  try await writer.flush()
134
164
  }
135
165
 
166
+ /// The hidden border the document at `url` was saved with, as a fraction of its
167
+ /// short side on every edge, once `flush()` has returned:
168
+ /// `documentBorderFraction` when the frame had room for all of it, less where it
169
+ /// did not, and 0 for a document saved without a guide crop or a file this
170
+ /// storage did not write. The screens that show the document hide exactly this
171
+ /// much.
172
+ func documentBorderFraction(at url: URL) -> CGFloat {
173
+ savedBorders.fraction(for: url)
174
+ }
175
+
136
176
  func clear() {
137
177
  // Selfie and document captures share one session directory; delete
138
178
  // only this role's own frames.
@@ -143,6 +183,7 @@ final class ResizingFrameStorage: FrameStorage {
143
183
  )) ?? []
144
184
  for fileURL in contents where ownsForClear(fileURL) {
145
185
  try? fileManager.removeItem(at: fileURL)
186
+ savedBorders.record(0, for: fileURL)
146
187
  }
147
188
  }
148
189
 
@@ -170,17 +211,24 @@ final class ResizingFrameStorage: FrameStorage {
170
211
  resize: (Int, Int)?,
171
212
  maxDimension: Int?,
172
213
  quality: CGFloat,
173
- documentCropRegion: ViewportCropRegion?
214
+ documentCropRegion: ViewportCropRegion?,
215
+ savedBorders: SavedDocumentBorders
174
216
  ) {
175
217
  let task = Task<Void, Error>.detached(priority: .utility) {
176
- try Self.encodeAndWrite(
177
- image: image,
178
- to: url,
179
- resize: resize,
180
- maxDimension: maxDimension,
181
- quality: quality,
182
- documentCropRegion: documentCropRegion
183
- )
218
+ do {
219
+ let borderFraction = try Self.encodeAndWrite(
220
+ image: image,
221
+ to: url,
222
+ resize: resize,
223
+ maxDimension: maxDimension,
224
+ quality: quality,
225
+ documentCropRegion: documentCropRegion
226
+ )
227
+ savedBorders.record(borderFraction, for: url)
228
+ } catch {
229
+ savedBorders.record(0, for: url)
230
+ throw error
231
+ }
184
232
  }
185
233
  inFlight.append(task)
186
234
  }
@@ -208,7 +256,7 @@ final class ResizingFrameStorage: FrameStorage {
208
256
  maxDimension: Int?,
209
257
  quality: CGFloat,
210
258
  documentCropRegion: ViewportCropRegion?
211
- ) throws {
259
+ ) throws -> CGFloat {
212
260
  try FileManager.default.createDirectory(
213
261
  at: url.deletingLastPathComponent(),
214
262
  withIntermediateDirectories: true
@@ -219,6 +267,8 @@ final class ResizingFrameStorage: FrameStorage {
219
267
  // render the file sideways.
220
268
  let upright = image.normalizingOrientation()
221
269
  let data: Data?
270
+ // The border the saved document kept, as a fraction of its short side.
271
+ var borderFraction: CGFloat = 0
222
272
  if let (width, height) = resize, let cgImage = upright.cgImage {
223
273
  // Center-crop and scale to the token-resolved target size
224
274
  // (selfie / liveness path).
@@ -236,20 +286,25 @@ final class ResizingFrameStorage: FrameStorage {
236
286
  let pixelWidth = cgImage.width
237
287
  let pixelHeight = cgImage.height
238
288
  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))
289
+ // Grow the guide rect by the hidden border (documentBorderFraction of
290
+ // the saved image's short side, uniform on every edge), which the
291
+ // preview never shows; clamp to the image bounds.
292
+ let imageSize = CGSize(width: pixelWidth, height: pixelHeight)
293
+ let guide = CGRect(
294
+ x: CGFloat(box.left) * imageSize.width,
295
+ y: CGFloat(box.top) * imageSize.height,
296
+ width: CGFloat(box.width) * imageSize.width,
297
+ height: CGFloat(box.height) * imageSize.height
298
+ )
299
+ let border = ResizingFrameStorage.borderAround(guide, imageSize: imageSize)
300
+ let cropRect = guide.insetBy(dx: -border, dy: -border)
301
+ .intersection(CGRect(origin: .zero, size: imageSize))
251
302
  if let cropped = cgImage.cropping(to: cropRect) {
252
303
  cgImage = cropped
304
+ borderFraction = ResizingFrameStorage.borderFraction(
305
+ border,
306
+ of: CGSize(width: cropped.width, height: cropped.height)
307
+ )
253
308
  }
254
309
  }
255
310
  let pixelWidth = cgImage.width
@@ -277,10 +332,27 @@ final class ResizingFrameStorage: FrameStorage {
277
332
  throw ResizingFrameStorageError.encodeFailed(url: url)
278
333
  }
279
334
  try data.write(to: url, options: .atomic)
335
+ return borderFraction
280
336
  }
281
337
  }
282
338
  }
283
339
 
340
+ /// The hidden border each document a `ResizingFrameStorage` saved kept, as a
341
+ /// fraction of its short side, by file. Shared with the detached write tasks, so it
342
+ /// is lock-guarded.
343
+ final class SavedDocumentBorders: @unchecked Sendable {
344
+ private let lock = NSLock()
345
+ private var fractions: [URL: CGFloat] = [:]
346
+
347
+ func record(_ fraction: CGFloat, for url: URL) {
348
+ lock.withLock { fractions[url.standardizedFileURL] = fraction > 0 ? fraction : nil }
349
+ }
350
+
351
+ func fraction(for url: URL) -> CGFloat {
352
+ lock.withLock { fractions[url.standardizedFileURL] ?? 0 }
353
+ }
354
+ }
355
+
284
356
  enum ResizingFrameStorageError: Error, Sendable {
285
357
  case encodeFailed(url: URL)
286
358
  }
@@ -12,6 +12,9 @@ public class UseSmileIDVisionDocumentModule: Module {
12
12
 
13
13
 
14
14
  private var session: UseSmileIDDocumentSession?
15
+ /// The latest session's storage, kept past `close` so `documentBorderFraction` can read the
16
+ /// border of a document saved before the flow moved on.
17
+ private var storage: ResizingFrameStorage?
15
18
 
16
19
  public func definition() -> ModuleDefinition {
17
20
  Name("UseSmileIDVisionDocument")
@@ -43,6 +46,7 @@ public class UseSmileIDVisionDocumentModule: Module {
43
46
  frameStorage: storage
44
47
  )
45
48
  self.session = newSession
49
+ self.storage = storage
46
50
  // Publish the session through the holder's operation queue so the
47
51
  // write is serialised with any concurrent scan on the FP thread.
48
52
  UseSmileIDVisionDocumentSessionHolder.shared.setSession(newSession)
@@ -73,6 +77,14 @@ public class UseSmileIDVisionDocumentModule: Module {
73
77
  }
74
78
  }
75
79
 
80
+ // The hidden border the document at `url` was saved with, as a fraction of its short side,
81
+ // once `flush` has returned; 0 for a document saved without one.
82
+ AsyncFunction("documentBorderFraction") { (url: String) -> Double in
83
+ let fileURL = url.hasPrefix("file:") ? URL(string: url) : URL(fileURLWithPath: url)
84
+ guard let fileURL, let storage = self.storage else { return 0 }
85
+ return Double(storage.documentBorderFraction(at: fileURL))
86
+ }
87
+
76
88
  AsyncFunction("close") {
77
89
  UseSmileIDVisionDocumentSessionHolder.shared.setSession(nil)
78
90
  UseSmileIDVisionDocumentSessionHolder.shared.setStorage(nil)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smileid/usesmileid_vision_document",
3
- "version": "12.1.1",
3
+ "version": "12.2.0",
4
4
  "description": "Apple Vision document 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.1",
50
- "@smileid/usesmileid_platform_interface": "12.1.1"
49
+ "@smileid/usesmileid_bridge": "12.2.0",
50
+ "@smileid/usesmileid_platform_interface": "12.2.0"
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.1",
59
- "expo": "~57.0.20",
60
- "expo-modules-core": "^57.0.16",
58
+ "@types/node": "^26.6.2",
59
+ "expo": "~57.0.25",
60
+ "expo-modules-core": "^57.0.19",
61
61
  "react-native": "^0.86.3",
62
62
  "typescript": "^6.0.3",
63
63
  "xcode": "^3.0.1"
@@ -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.1"}},POD_TARGET="UseSmileIDVisionDocumentExpo";exports.SPM_PRODUCTS=["UseSmileIDVisionDocument"];const PODFILE_MARKER="usesmileid_vision_document",BRIDGE_POD_TARGET="UseSmileIDBridgeExpo",BRIDGE_SPM_PRODUCTS=["UseSmileIDBridge"],BRIDGE_PODFILE_MARKER="usesmileid_bridge",withVisionDocumentSpm=(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_document] 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=withVisionDocumentSpm;
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"}},POD_TARGET="UseSmileIDVisionDocumentExpo";exports.SPM_PRODUCTS=["UseSmileIDVisionDocument"];const PODFILE_MARKER="usesmileid_vision_document",BRIDGE_POD_TARGET="UseSmileIDBridgeExpo",BRIDGE_SPM_PRODUCTS=["UseSmileIDBridge"],BRIDGE_PODFILE_MARKER="usesmileid_bridge",withVisionDocumentSpm=(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_document] 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=withVisionDocumentSpm;
@@ -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.