@scr2em/capacitor-scanner 6.0.23 → 6.0.26

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.
@@ -1,8 +1,7 @@
1
1
  import AVFoundation
2
2
  import Capacitor
3
- import CoreMotion
4
3
  import Foundation
5
- import Vision // For barcode detection
4
+ import Vision
6
5
 
7
6
  /**
8
7
  * Please read the Capacitor iOS Plugin Development Guide
@@ -14,7 +13,7 @@ struct VoteStatus {
14
13
  }
15
14
 
16
15
  @objc(CapacitorScannerPlugin)
17
- public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetadataOutputObjectsDelegate, AVCapturePhotoCaptureDelegate {
16
+ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetadataOutputObjectsDelegate, AVCapturePhotoCaptureDelegate, RectangleDetectorDelegate {
18
17
  public let identifier = "CapacitorScannerPlugin"
19
18
  public let jsName = "CapacitorScanner"
20
19
  public let pluginMethods: [CAPPluginMethod] = [
@@ -46,18 +45,11 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
46
45
  private var capturePhotoCall: CAPPluginCall?
47
46
 
48
47
  private var orientationObserver: NSObjectProtocol?
49
- private var videoDataOutput: AVCaptureVideoDataOutput? // For getting video frames
50
- private var currentPixelBuffer: CVPixelBuffer? // Store the current pixel buffer for rectangle detection
48
+ private var videoDataOutput: AVCaptureVideoDataOutput?
51
49
 
52
- // Motion detection properties
53
- private var motionManager: CMMotionManager?
54
- private var isDeviceStable = true
55
- private let motionThreshold: Double = 0.1 // Threshold for determining if device is moving
56
-
57
- // Rectangle visibility tracking properties
58
- private var lastRectangleDetectionTime: Date?
50
+ // Barcode visibility tracking
59
51
  private var lastBarcodeDetectionTime: Date?
60
- private let rectangleVisibilityTimeout: TimeInterval = 1.0 // Time in seconds before considering rectangle no longer visible
52
+ private let barcodeVisibilityTimeout: TimeInterval = 1.0
61
53
 
62
54
  // Detection state (controls what detection runs)
63
55
  private var enabledDetectionTypes: Set<String> = []
@@ -65,103 +57,82 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
65
57
  private var enabledHighlightTypes: Set<String> = []
66
58
  private var overlayView: UIView?
67
59
  private var barcodeOverlays: [CAShapeLayer] = []
68
- private var businessCardOverlays: [CAShapeLayer] = []
69
60
 
70
- // Smoothing & persistent overlay layers (for smooth visuals)
61
+ // Barcode overlay smoothing
71
62
  private var barcodeLayer: CAShapeLayer?
72
- private var cardLayer: CAShapeLayer?
73
63
  private var prevBarcodeRect: CGRect?
74
- private var prevCardRect: CGRect?
75
- private let smoothingAlpha: CGFloat = 0.25 // 0..1; lower = more smoothing
64
+ private let smoothingAlpha: CGFloat = 0.25
76
65
  private let overlayAnimDuration: CFTimeInterval = 0.12
77
66
 
78
- // Struct to store both rectangle coordinates and image snapshot
79
- private struct DetectedRectangle {
80
- let rectangle: VNRectangleObservation
81
- let snapshot: UIImage?
82
- }
83
-
84
- private var lastDetectedRectangle: DetectedRectangle?
85
- // Minimum confidence threshold for using a detected rectangle
86
- private let rectangleConfidenceThreshold: Float = 0.8
67
+ // Rectangle detection (extracted to separate class)
68
+ private var rectangleDetector: RectangleDetector?
87
69
 
88
- // This is how we create a Vision request for detecting barcodes
70
+ // Vision request for detecting barcodes
89
71
  private lazy var barcodeDetectionRequest: VNDetectBarcodesRequest = {
90
72
  let request = VNDetectBarcodesRequest { [weak self] request, error in
91
- // This closure is called when Vision finds barcodes
92
73
  self?.processClassification(request, error: error)
93
74
  }
94
75
  return request
95
76
  }()
96
77
 
97
- // Vision request for rectangle detection (business cards)
98
- private lazy var rectangleDetectionRequest: VNDetectRectanglesRequest = {
99
- let request = VNDetectRectanglesRequest { [weak self] request, error in
100
- self?.processRectangleDetection(request, error: error)
101
- }
102
- request.maximumObservations = 1
103
- request.minimumAspectRatio = 0.5 // Business cards are typically rectangular
104
- request.maximumAspectRatio = 0.8 // Allow some variation in orientation
105
- request.minimumSize = 0.2 // Minimum size threshold
106
- request.minimumConfidence = 0.8 // Confidence threshold
107
- request.quadratureTolerance = 45 // Increased tolerance to allow more skewed rectangles
108
- return request
109
- }()
110
-
111
78
  @objc func enableObjectDetection(_ call: CAPPluginCall) {
112
- guard let types = call.getArray("types", String.self) else {
113
- call.reject("Missing or invalid 'type' parameter. Expected array of strings.")
114
- return
115
- }
116
-
117
- // Only accept "businessCard" - barcode detection is handled separately
118
- let filteredTypes = types.filter { $0 == "businessCard" }
119
-
120
- if filteredTypes.isEmpty {
121
- call.reject("No valid detection types provided. Only 'businessCard' is supported. Use enableBarcodeDetection() for barcode detection.")
79
+ // If object detection is already enabled, just return success
80
+ if self.enabledDetectionTypes.contains("businessCard") {
81
+ call.resolve([
82
+ "enabled": true,
83
+ ])
122
84
  return
123
85
  }
124
86
 
125
87
  let showHighlight = call.getBool("showHighlight", true)
88
+ let emitIntervalSeconds = call.getDouble("emitIntervalSeconds") ?? 0
126
89
 
127
90
  DispatchQueue.main.async {
128
- // Add businessCard to enabled detection types
129
91
  self.enabledDetectionTypes.insert("businessCard")
130
- // Add to highlight types if showHighlight is true (default)
92
+
93
+ // Initialize rectangle detector if needed
94
+ if self.rectangleDetector == nil {
95
+ self.rectangleDetector = RectangleDetector()
96
+ self.rectangleDetector?.delegate = self
97
+ }
98
+
99
+ // Set the emit interval
100
+ self.rectangleDetector?.setEmitInterval(emitIntervalSeconds)
101
+
131
102
  if showHighlight {
132
103
  self.enabledHighlightTypes.insert("businessCard")
104
+ self.rectangleDetector?.isHighlightEnabled = true
133
105
  self.setupOverlayView()
134
106
  }
135
107
 
136
108
  call.resolve([
137
109
  "enabled": true,
138
- "types": filteredTypes,
139
110
  ])
140
111
  }
141
112
  }
142
113
 
143
114
  @objc func disableObjectDetection(_ call: CAPPluginCall) {
144
115
  DispatchQueue.main.async {
145
- // Remove businessCard from both detection and highlight types
146
116
  self.enabledDetectionTypes.remove("businessCard")
147
117
  self.enabledHighlightTypes.remove("businessCard")
148
118
 
149
- // Clear only businessCard overlay (not barcode overlay)
150
- self.cardLayer?.removeFromSuperlayer()
151
- self.cardLayer = nil
152
- self.prevCardRect = nil
153
- self.lastDetectedRectangle = nil
154
- self.currentPixelBuffer = nil
119
+ // Reset rectangle detector
120
+ self.rectangleDetector?.reset()
121
+ self.rectangleDetector = nil
155
122
 
156
123
  call.resolve([
157
124
  "enabled": false,
158
- "types": [],
159
125
  ])
160
126
  }
161
127
  }
162
128
 
163
129
  private func setupOverlayView() {
164
- guard let cameraView = self.cameraView else { return }
130
+ guard let cameraView = self.cameraView,
131
+ let previewLayer = self.previewLayer else {
132
+ print("[CapacitorScanner] setupOverlayView: cameraView or previewLayer is nil")
133
+ return
134
+ }
135
+ print("[CapacitorScanner] setupOverlayView: setting up overlay")
165
136
 
166
137
  // Remove existing overlay if present
167
138
  self.overlayView?.removeFromSuperview()
@@ -172,24 +143,23 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
172
143
  overlay.isUserInteractionEnabled = false
173
144
  cameraView.addSubview(overlay)
174
145
  self.overlayView = overlay
146
+
147
+ // Configure rectangle detector with overlay
148
+ if self.enabledHighlightTypes.contains("businessCard") {
149
+ self.rectangleDetector?.configure(overlayLayer: overlay.layer, previewLayer: previewLayer)
150
+ }
175
151
  }
176
152
 
177
153
  private func clearAllOverlays() {
178
- // Remove all barcode overlays (legacy)
154
+ // Remove barcode overlays
179
155
  self.barcodeOverlays.forEach { $0.removeFromSuperlayer() }
180
156
  self.barcodeOverlays.removeAll()
181
-
182
- // Remove all business card overlays (legacy)
183
- self.businessCardOverlays.forEach { $0.removeFromSuperlayer() }
184
- self.businessCardOverlays.removeAll()
185
-
186
- // Remove persistent layers used for smoothing
187
157
  self.barcodeLayer?.removeFromSuperlayer()
188
- self.cardLayer?.removeFromSuperlayer()
189
158
  self.barcodeLayer = nil
190
- self.cardLayer = nil
191
159
  self.prevBarcodeRect = nil
192
- self.prevCardRect = nil
160
+
161
+ // Reset rectangle detector
162
+ self.rectangleDetector?.reset()
193
163
 
194
164
  // Remove overlay view
195
165
  self.overlayView?.removeFromSuperview()
@@ -242,45 +212,6 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
242
212
  }
243
213
  }
244
214
 
245
- private func drawBusinessCardOverlay(for observation: VNRectangleObservation) {
246
- guard self.enabledHighlightTypes.contains("businessCard"),
247
- let overlayView = self.overlayView,
248
- let previewLayer = self.previewLayer else { return }
249
-
250
- // Convert Vision coordinates to preview layer coordinates
251
- let targetRect = self.convertVisionToPreviewCoordinates(boundingBox: observation.boundingBox, previewLayer: previewLayer)
252
-
253
- DispatchQueue.main.async {
254
- let layer: CAShapeLayer
255
- if let existing = self.cardLayer {
256
- layer = existing
257
- } else {
258
- let newLayer = CAShapeLayer()
259
- newLayer.strokeColor = UIColor.black.cgColor
260
- newLayer.fillColor = UIColor.black.withAlphaComponent(0.2).cgColor
261
- newLayer.lineWidth = 2.0
262
- overlayView.layer.addSublayer(newLayer)
263
- self.cardLayer = newLayer
264
- layer = newLayer
265
- }
266
-
267
- // IoU-based damping for large jumps
268
- let basePrev = self.prevCardRect ?? targetRect
269
- let dampingAlpha: CGFloat = (self.iou(basePrev, targetRect) < 0.15) ? 0.15 : self.smoothingAlpha
270
- let displayedRect = self.lerpRect(basePrev, targetRect, dampingAlpha)
271
- self.prevCardRect = displayedRect
272
-
273
- let path = UIBezierPath(roundedRect: displayedRect, cornerRadius: 8.0)
274
- CATransaction.begin()
275
- CATransaction.setAnimationDuration(self.overlayAnimDuration)
276
- CATransaction.setDisableActions(false)
277
- layer.path = path.cgPath
278
- CATransaction.commit()
279
-
280
- self.lastRectangleDetectionTime = Date()
281
- }
282
- }
283
-
284
215
  private func convertVisionToPreviewCoordinates(boundingBox: CGRect, previewLayer: AVCaptureVideoPreviewLayer) -> CGRect {
285
216
  // Vision coordinates are normalized (0-1) with origin at bottom-left
286
217
  // UIKit coordinates have origin at top-left
@@ -350,51 +281,43 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
350
281
  return
351
282
  }
352
283
 
353
- // Check if object detection is enabled and we have a detected rectangle with sufficient confidence
284
+ // Check if object detection is enabled and we have a detected rectangle
354
285
  if self.enabledDetectionTypes.contains("businessCard"),
355
- let detectedRect = self.lastDetectedRectangle { // Only use rectangles with sufficient confidence
286
+ let detectedRect = self.rectangleDetector?.lastDetectedRectangle {
356
287
  // If we have a snapshot from the detection, use it directly
357
288
  if let snapshot = detectedRect.snapshot {
358
- // Crop the snapshot to the detected rectangle
359
- if let croppedImage = self.cropImage(snapshot, toRectangle: detectedRect.rectangle) {
360
- // Convert cropped image back to data
289
+ if let croppedImage = self.cropImage(snapshot, toRectangle: detectedRect.observation) {
361
290
  guard let croppedImageData = croppedImage.jpegData(compressionQuality: 0.9) else {
362
291
  self.capturePhotoCall?.reject("Unable to convert cropped image to data")
363
292
  return
364
293
  }
365
-
366
294
  let base64String = croppedImageData.base64EncodedString()
367
295
  self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
368
296
  } else {
369
- // If cropping fails, return the original image
370
297
  let base64String = imageData.base64EncodedString()
371
298
  self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
372
299
  }
373
300
  } else {
374
- // If no snapshot is available, use the captured photo
301
+ // Use the captured photo if no snapshot available
375
302
  guard let image = UIImage(data: imageData) else {
376
303
  self.capturePhotoCall?.reject("Unable to create image from data")
377
304
  return
378
305
  }
379
306
 
380
- // Crop the image to the detected rectangle
381
- if let croppedImage = self.cropImage(image, toRectangle: detectedRect.rectangle) {
382
- // Convert cropped image back to data
307
+ if let croppedImage = self.cropImage(image, toRectangle: detectedRect.observation) {
383
308
  guard let croppedImageData = croppedImage.jpegData(compressionQuality: 0.9) else {
384
309
  self.capturePhotoCall?.reject("Unable to convert cropped image to data")
385
310
  return
386
311
  }
387
-
388
312
  let base64String = croppedImageData.base64EncodedString()
389
313
  self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
390
314
  } else {
391
- // If cropping fails, return the original image
392
315
  let base64String = imageData.base64EncodedString()
393
316
  self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
394
317
  }
395
318
  }
396
319
  } else {
397
- // If object detection is not enabled or no rectangle is detected, return the original image
320
+ // No rectangle detected, return original image
398
321
  let base64String = imageData.base64EncodedString()
399
322
  self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
400
323
  }
@@ -462,42 +385,6 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
462
385
  return UIImage(cgImage: cgImage, scale: image.scale, orientation: .up)
463
386
  }
464
387
 
465
- private func startDeviceMotionMonitoring() {
466
- // Initialize motion manager if needed
467
- if self.motionManager == nil {
468
- self.motionManager = CMMotionManager()
469
- }
470
-
471
- guard let motionManager = self.motionManager, motionManager.isDeviceMotionAvailable else {
472
- print("Device motion is not available")
473
- return
474
- }
475
-
476
- // Set update interval
477
- motionManager.deviceMotionUpdateInterval = 0.1
478
-
479
- // Start device motion updates
480
- motionManager.startDeviceMotionUpdates(to: .main) { [weak self] (motion, error) in
481
- guard let self = self, let motion = motion else { return }
482
-
483
- // Calculate total acceleration magnitude
484
- let userAcceleration = motion.userAcceleration
485
- let accelerationMagnitude = sqrt(
486
- pow(userAcceleration.x, 2) +
487
- pow(userAcceleration.y, 2) +
488
- pow(userAcceleration.z, 2)
489
- )
490
-
491
- // Update device stability status
492
- self.isDeviceStable = accelerationMagnitude < self.motionThreshold
493
- }
494
- }
495
-
496
- private func stopDeviceMotionMonitoring() {
497
- self.motionManager?.stopDeviceMotionUpdates()
498
- }
499
-
500
-
501
388
  // Linear interpolation helpers for smoothing
502
389
  private func lerp(_ a: CGFloat, _ b: CGFloat, _ t: CGFloat) -> CGFloat {
503
390
  return a + (b - a) * t
@@ -512,14 +399,6 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
512
399
  )
513
400
  }
514
401
 
515
- private func iou(_ a: CGRect, _ b: CGRect) -> CGFloat {
516
- let inter = a.intersection(b)
517
- if inter.isNull { return 0 }
518
- let interArea = inter.width * inter.height
519
- let unionArea = a.width * a.height + b.width * b.height - interArea
520
- return unionArea > 0 ? interArea / unionArea : 0
521
- }
522
-
523
402
  @objc func start(_ call: CAPPluginCall) {
524
403
  // Read detection options (defaults to false)
525
404
  let enableBarcodeDetection = call.getBool("barcodeDetection", false)
@@ -532,8 +411,6 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
532
411
  // Stop any existing session first (must be on main thread for UI operations)
533
412
  self.stopInternal()
534
413
 
535
- // Initialize timestamp properties
536
- self.lastRectangleDetectionTime = nil
537
414
  self.lastBarcodeDetectionTime = nil
538
415
 
539
416
  // Enable barcode detection if requested
@@ -550,6 +427,15 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
550
427
  if enableObjectHighlight {
551
428
  self.enabledHighlightTypes.insert("businessCard")
552
429
  }
430
+ // Initialize rectangle detector
431
+ self.rectangleDetector = RectangleDetector()
432
+ self.rectangleDetector?.delegate = self
433
+ if enableObjectHighlight {
434
+ self.rectangleDetector?.isHighlightEnabled = true
435
+ }
436
+ // Set emit interval if provided
437
+ let emitInterval = call.getDouble("rectangleEmitIntervalSeconds") ?? 0
438
+ self.rectangleDetector?.setEmitInterval(emitInterval)
553
439
  }
554
440
 
555
441
  // Handle Regex Filter
@@ -569,8 +455,6 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
569
455
  self.regexFilter = nil
570
456
  }
571
457
 
572
- // Start monitoring device motion
573
- self.startDeviceMotionMonitoring()
574
458
  let captureSession = AVCaptureSession()
575
459
  let cameraDirection: AVCaptureDevice.Position = call.getString("cameraDirection", "BACK") == "BACK" ? .back : .front
576
460
  guard let videoCaptureDevice = self.getCaptureDevice(position: cameraDirection) else {
@@ -666,29 +550,27 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
666
550
  captureSession.stopRunning()
667
551
  }
668
552
 
669
- // Stop monitoring device motion
670
- self.stopDeviceMotionMonitoring()
671
-
672
553
  self.previewLayer?.removeFromSuperlayer()
673
554
  self.cameraView?.removeFromSuperview()
674
555
  self.videoDataOutput?.setSampleBufferDelegate(nil, queue: nil)
675
- self.lastDetectedRectangle = nil
676
556
 
677
557
  self.captureSession = nil
678
558
  self.cameraView = nil
679
559
  self.previewLayer = nil
680
560
  self.videoDataOutput = nil
681
- self.currentPixelBuffer = nil
682
561
  self.scannedCodesVotes.clear()
683
562
  self.regexFilter = nil
684
- self.clearAllOverlays()
685
- self.showWebViewBackground()
686
- self.enabledDetectionTypes.removeAll()
687
- self.enabledHighlightTypes.removeAll()
688
- self.lastRectangleDetectionTime = nil
689
- self.lastBarcodeDetectionTime = nil
690
- self.removeOrientationChangeObserver()
691
563
 
564
+ // Reset rectangle detector
565
+ self.rectangleDetector?.reset()
566
+ self.rectangleDetector = nil
567
+
568
+ self.clearAllOverlays()
569
+ self.showWebViewBackground()
570
+ self.enabledDetectionTypes.removeAll()
571
+ self.enabledHighlightTypes.removeAll()
572
+ self.lastBarcodeDetectionTime = nil
573
+ self.removeOrientationChangeObserver()
692
574
  }
693
575
 
694
576
  private func addOrientationChangeObserver() {
@@ -908,104 +790,17 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
908
790
  }
909
791
  }
910
792
 
911
- private func processRectangleDetection(_ request: VNRequest, error: Error?) {
912
- // Handle any errors
913
- if let error = error {
914
- print("Rectangle detection error: \(error)")
915
- return
916
- }
917
-
918
- guard let observations = request.results as? [VNRectangleObservation] else {
919
- return
920
- }
921
-
922
- // Find the rectangle with highest confidence
923
- if let bestCandidate = observations.max(by: { $0.confidence < $1.confidence }) {
924
- // Check if all four corners are inside the screen limits (normalized coordinates 0-1)
925
- let corners = [
926
- bestCandidate.topLeft,
927
- bestCandidate.topRight,
928
- bestCandidate.bottomLeft,
929
- bestCandidate.bottomRight
930
- ]
931
-
932
- let isInsideScreen = corners.allSatisfy { point in
933
- // Define the common minimum and maximum allowed normalized coordinates
934
- let bufferMin: CGFloat = 0.1 // Minimum allowed coordinate (e.g., 10% from each edge)
935
- let bufferMax: CGFloat = 1.0 - bufferMin // Maximum allowed coordinate
936
-
937
- // Check if both X and Y coordinates of the point are within the allowed range
938
- return point.x >= bufferMin && point.x <= bufferMax &&
939
- point.y >= bufferMin && point.y <= bufferMax
940
- }
941
- // Only process if all edges are inside the screen
942
- if isInsideScreen {
943
- // Store the detected rectangle for later use (e.g., image cropping) only if confidence exceeds threshold
944
- if bestCandidate.confidence >= self.rectangleConfidenceThreshold {
945
- // Always store the rectangle coordinates
946
- let rectangle = bestCandidate
947
-
948
- // Always attempt to capture a snapshot for the accepted rectangle
949
- if let pixelBuffer = self.currentPixelBuffer {
950
- let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
951
- let context = CIContext(options: nil)
952
- if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
953
- // Create UIImage with the correct orientation (match Vision handler)
954
- let deviceOrientation = UIDevice.current.orientation
955
- let uiOrientation: UIImage.Orientation
956
- switch deviceOrientation {
957
- case .landscapeLeft:
958
- uiOrientation = .right
959
- case .landscapeRight:
960
- uiOrientation = .left
961
- case .portraitUpsideDown:
962
- uiOrientation = .down
963
- default:
964
- uiOrientation = .up // Default to portrait
965
- }
966
- let snapshot = UIImage(cgImage: cgImage, scale: 1.0, orientation: uiOrientation)
967
- self.lastDetectedRectangle = DetectedRectangle(rectangle: rectangle, snapshot: snapshot)
968
- } else {
969
- // Fallback: store rectangle without snapshot
970
- self.lastDetectedRectangle = DetectedRectangle(rectangle: rectangle, snapshot: nil)
971
- }
972
- } else {
973
- // No pixel buffer available; store rectangle without snapshot
974
- self.lastDetectedRectangle = DetectedRectangle(rectangle: rectangle, snapshot: nil)
975
- }
976
- }
977
-
978
- // Update the last rectangle detection time
979
- self.lastRectangleDetectionTime = Date()
980
-
981
- // Draw overlay for business card shape
982
- self.drawBusinessCardOverlay(for: bestCandidate)
983
-
984
- self.notifyListeners("rectangleDetected", data: [
985
- "topLeft": [
986
- "x": bestCandidate.topLeft.x,
987
- "y": bestCandidate.topLeft.y,
988
-
989
- ],
990
- "topRight": [
991
- "x": bestCandidate.topRight.x,
992
- "y": bestCandidate.topRight.y,
993
-
994
- ],
995
- "bottomLeft": [
996
- "x": bestCandidate.bottomLeft.x,
997
- "y": bestCandidate.bottomLeft.y,
998
-
999
- ],
1000
- "bottomRight": [
1001
- "x": bestCandidate.bottomRight.x,
1002
- "y": bestCandidate.bottomRight.y,
1003
-
1004
- ],
1005
- ])
1006
- }
1007
- }
1008
- }
793
+ // MARK: - RectangleDetectorDelegate
794
+
795
+ func rectangleDetector(_ detector: RectangleDetector, didDetect corners: RectangleCorners) {
796
+ print("[CapacitorScanner] rectangleDetector delegate called - emitting rectangleDetected event")
797
+ self.notifyListeners("rectangleDetected", data: [
798
+ "topLeft": ["x": corners.topLeft.x, "y": corners.topLeft.y],
799
+ "topRight": ["x": corners.topRight.x, "y": corners.topRight.y],
800
+ "bottomLeft": ["x": corners.bottomLeft.x, "y": corners.bottomLeft.y],
801
+ "bottomRight": ["x": corners.bottomRight.x, "y": corners.bottomRight.y],
802
+ ])
803
+ }
1009
804
 
1010
805
  @objc func flipCamera(_ call: CAPPluginCall) {
1011
806
  guard let session = self.captureSession else {
@@ -1169,20 +964,13 @@ extension CapacitorScannerPlugin: AVCaptureVideoDataOutputSampleBufferDelegate {
1169
964
  didOutput sampleBuffer: CMSampleBuffer,
1170
965
  from connection: AVCaptureConnection)
1171
966
  {
1172
- // This is called for every frame from the camera
1173
-
1174
- // 1. Get the pixel buffer from the frame
1175
967
  guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
1176
968
  return
1177
969
  }
1178
970
 
1179
- // Store the pixel buffer for rectangle detection
1180
- self.currentPixelBuffer = pixelBuffer
1181
-
1182
- // 2. Create a Vision image handler with the correct orientation
971
+ // Determine orientation for Vision
1183
972
  let deviceOrientation = UIDevice.current.orientation
1184
973
  let cgOrientation: CGImagePropertyOrientation
1185
-
1186
974
  switch deviceOrientation {
1187
975
  case .landscapeLeft:
1188
976
  cgOrientation = .right
@@ -1191,46 +979,45 @@ extension CapacitorScannerPlugin: AVCaptureVideoDataOutputSampleBufferDelegate {
1191
979
  case .portraitUpsideDown:
1192
980
  cgOrientation = .down
1193
981
  default:
1194
- cgOrientation = .up // Default to portrait
982
+ cgOrientation = .up
1195
983
  }
1196
984
 
1197
- let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
1198
- orientation: cgOrientation,
1199
- options: [:])
985
+ let imageRequestHandler = VNImageRequestHandler(
986
+ cvPixelBuffer: pixelBuffer,
987
+ orientation: cgOrientation,
988
+ options: [:]
989
+ )
1200
990
 
1201
- // 3. Prepare requests based on enabled detection types
991
+ // Prepare detection requests
1202
992
  var requests: [VNRequest] = []
1203
993
 
1204
- // Only include barcode detection if enabled
1205
994
  if self.enabledDetectionTypes.contains("barcode") {
1206
995
  requests.append(self.barcodeDetectionRequest)
1207
996
  }
1208
997
 
1209
- // Add rectangle detection if business card detection is enabled
1210
- if self.enabledDetectionTypes.contains("businessCard") {
1211
- requests.append(self.rectangleDetectionRequest)
998
+ if self.enabledDetectionTypes.contains("businessCard"),
999
+ let detector = self.rectangleDetector {
1000
+ print("[CapacitorScanner] Processing frame for businessCard detection")
1001
+ detector.updatePixelBuffer(pixelBuffer)
1002
+ requests.append(detector.visionRequest)
1212
1003
  }
1213
1004
 
1214
- // 4. Perform the detection requests
1005
+ // Perform detection
1215
1006
  do {
1216
1007
  try imageRequestHandler.perform(requests)
1217
1008
  } catch {
1218
1009
  print("Failed to perform Vision request: \(error)")
1219
1010
  }
1220
1011
 
1221
- // Centralized overlay visibility based on last detection timestamps
1012
+ // Check overlay timeouts
1222
1013
  DispatchQueue.main.async {
1223
1014
  let now = Date()
1224
- if let t = self.lastBarcodeDetectionTime, now.timeIntervalSince(t) > self.rectangleVisibilityTimeout {
1015
+ if let t = self.lastBarcodeDetectionTime, now.timeIntervalSince(t) > self.barcodeVisibilityTimeout {
1225
1016
  self.barcodeLayer?.removeFromSuperlayer()
1226
1017
  self.barcodeLayer = nil
1227
1018
  self.prevBarcodeRect = nil
1228
1019
  }
1229
- if let t = self.lastRectangleDetectionTime, now.timeIntervalSince(t) > self.rectangleVisibilityTimeout {
1230
- self.cardLayer?.removeFromSuperlayer()
1231
- self.cardLayer = nil
1232
- self.prevCardRect = nil
1233
- }
1020
+ self.rectangleDetector?.checkTimeout()
1234
1021
  }
1235
1022
  }
1236
1023
  }