@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.
- package/README.md +10 -6
- package/android/src/main/java/com/leadliaion/capacitorscanner/CapacitorScannerPlugin.java +42 -32
- package/dist/docs.json +13 -8
- package/dist/esm/definitions.d.ts +23 -15
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorScannerPlugin/CapacitorScannerPlugin.swift +98 -311
- package/ios/Sources/CapacitorScannerPlugin/RectangleDetector.swift +338 -0
- package/ios/Sources/CapacitorScannerPlugin/RectangleSmoother.swift +229 -0
- package/package.json +2 -2
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
2
|
import Capacitor
|
|
3
|
-
import CoreMotion
|
|
4
3
|
import Foundation
|
|
5
|
-
import Vision
|
|
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?
|
|
50
|
-
private var currentPixelBuffer: CVPixelBuffer? // Store the current pixel buffer for rectangle detection
|
|
48
|
+
private var videoDataOutput: AVCaptureVideoDataOutput?
|
|
51
49
|
|
|
52
|
-
//
|
|
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
|
|
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
|
-
//
|
|
61
|
+
// Barcode overlay smoothing
|
|
71
62
|
private var barcodeLayer: CAShapeLayer?
|
|
72
|
-
private var cardLayer: CAShapeLayer?
|
|
73
63
|
private var prevBarcodeRect: CGRect?
|
|
74
|
-
private
|
|
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
|
-
//
|
|
79
|
-
private
|
|
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
|
-
//
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
150
|
-
self.
|
|
151
|
-
self.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
//
|
|
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
|
|
982
|
+
cgOrientation = .up
|
|
1195
983
|
}
|
|
1196
984
|
|
|
1197
|
-
let imageRequestHandler = VNImageRequestHandler(
|
|
1198
|
-
|
|
1199
|
-
|
|
985
|
+
let imageRequestHandler = VNImageRequestHandler(
|
|
986
|
+
cvPixelBuffer: pixelBuffer,
|
|
987
|
+
orientation: cgOrientation,
|
|
988
|
+
options: [:]
|
|
989
|
+
)
|
|
1200
990
|
|
|
1201
|
-
//
|
|
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
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1012
|
+
// Check overlay timeouts
|
|
1222
1013
|
DispatchQueue.main.async {
|
|
1223
1014
|
let now = Date()
|
|
1224
|
-
if let t = self.lastBarcodeDetectionTime, now.timeIntervalSince(t) > self.
|
|
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
|
-
|
|
1230
|
-
self.cardLayer?.removeFromSuperlayer()
|
|
1231
|
-
self.cardLayer = nil
|
|
1232
|
-
self.prevCardRect = nil
|
|
1233
|
-
}
|
|
1020
|
+
self.rectangleDetector?.checkTimeout()
|
|
1234
1021
|
}
|
|
1235
1022
|
}
|
|
1236
1023
|
}
|