@biso_gmbh/capacitor-plugin-ml-kit-barcode-scanner 1.0.8
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/CapacitorPluginMlKitBarcodeScanner.podspec +20 -0
- package/README.md +231 -0
- package/android/build.gradle +65 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/assets/beep.mp3 +0 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/BarcodeAnalyzer.java +119 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/BarcodeFormat.java +40 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/BarcodeFormats.java +38 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/BarcodeType.java +37 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/BarcodesListener.java +8 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/CameraOverlay.java +161 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/CaptureActivity.java +164 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/DetectedBarcode.java +174 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/MlKitBarcodeScannerPlugin.java +142 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/ScannerSettings.java +348 -0
- package/android/src/main/java/com/biso/capacitor/plugins/mlkit/barcode/scanner/Utils.java +118 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/drawable/torch_active.xml +12 -0
- package/android/src/main/res/drawable/torch_inactive.xml +12 -0
- package/android/src/main/res/drawable-mdpi/flashlight.png +0 -0
- package/android/src/main/res/layout/capture_activity.xml +28 -0
- package/dist/docs.json +279 -0
- package/dist/esm/definitions.d.ts +72 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +5 -0
- package/dist/esm/web.js +8 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +22 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +25 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Assets.xcassets/Contents.json +6 -0
- package/ios/Plugin/Assets.xcassets/beep.dataset/Contents.json +12 -0
- package/ios/Plugin/Assets.xcassets/beep.dataset/beep.mp3 +0 -0
- package/ios/Plugin/Assets.xcassets/flashlight.imageset/Contents.json +12 -0
- package/ios/Plugin/Assets.xcassets/flashlight.imageset/flashlight.png +0 -0
- package/ios/Plugin/BarcodeFormat.swift +45 -0
- package/ios/Plugin/BarcodeFormats.swift +26 -0
- package/ios/Plugin/BarcodeType.swift +45 -0
- package/ios/Plugin/BarcodesAnalyzer.swift +79 -0
- package/ios/Plugin/CameraOverlay.swift +119 -0
- package/ios/Plugin/CameraViewController.swift +301 -0
- package/ios/Plugin/DetectedBarcode.swift +70 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/MlKitBarcodeScannerPlugin.h +10 -0
- package/ios/Plugin/MlKitBarcodeScannerPlugin.m +8 -0
- package/ios/Plugin/MlKitBarcodeScannerPlugin.swift +69 -0
- package/ios/Plugin/PrivacyInfo.xcprivacy +27 -0
- package/ios/Plugin/ScannerSettings.swift +171 -0
- package/ios/Plugin/Utils.swift +145 -0
- package/package.json +84 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import MLKitBarcodeScanning
|
|
2
|
+
import MLKitVision
|
|
3
|
+
|
|
4
|
+
protocol BarcodesListener: NSObjectProtocol {
|
|
5
|
+
func onBarcodesFound(_ barcodes: [DetectedBarcode])
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class BarcodeAnalyzer {
|
|
9
|
+
private var scanner: BarcodeScanner
|
|
10
|
+
private var cameraOverlay: CameraOverlay
|
|
11
|
+
private var barcodesListener: BarcodesListener
|
|
12
|
+
private var settings: ScannerSettings
|
|
13
|
+
|
|
14
|
+
private var lastBarcodes: [DetectedBarcode] = []
|
|
15
|
+
private var stableCounter: Int = 0
|
|
16
|
+
|
|
17
|
+
init(settings: ScannerSettings, barcodesListener: BarcodesListener, cameraOverlay:CameraOverlay) {
|
|
18
|
+
let barcodeFormats = MLKitBarcodeScanning.BarcodeFormat(rawValue: settings.barcodeFormats)
|
|
19
|
+
scanner = BarcodeScanner.barcodeScanner(options: BarcodeScannerOptions(formats: barcodeFormats))
|
|
20
|
+
self.cameraOverlay = cameraOverlay
|
|
21
|
+
self.barcodesListener = barcodesListener
|
|
22
|
+
self.settings = settings
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func analyze(in image: VisionImage, width: CGFloat, height: CGFloat) {
|
|
26
|
+
var barcodes: [Barcode] = []
|
|
27
|
+
do {
|
|
28
|
+
barcodes = try scanner.results(in: image)
|
|
29
|
+
} catch let error {
|
|
30
|
+
print(error.localizedDescription)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var detectedBarcodes: [DetectedBarcode] = []
|
|
34
|
+
for barcode in barcodes {
|
|
35
|
+
let normalizedRect = CGRect(
|
|
36
|
+
x: barcode.frame.origin.x / width,
|
|
37
|
+
y: barcode.frame.origin.y / height,
|
|
38
|
+
width: barcode.frame.size.width / width,
|
|
39
|
+
height: barcode.frame.size.height / height
|
|
40
|
+
)
|
|
41
|
+
let convertedRect = cameraOverlay.previewLayer.layerRectConverted(fromMetadataOutputRect: normalizedRect)
|
|
42
|
+
|
|
43
|
+
detectedBarcodes.append(DetectedBarcode(barcode: barcode, bounds: convertedRect, centerX: cameraOverlay.previewLayer.bounds.midX, centerY: cameraOverlay.previewLayer.bounds.midY))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (settings.debugOverlay) {
|
|
47
|
+
cameraOverlay.drawDebugOverlay(barcodes: detectedBarcodes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (areBarcodesStable(barcodes: detectedBarcodes) && stableCounter >= settings.stableThreshold) {
|
|
51
|
+
var barcodesInScanArea: [DetectedBarcode] = []
|
|
52
|
+
for barcode in detectedBarcodes {
|
|
53
|
+
if (barcode.isInScanArea(scanArea: cameraOverlay.scanArea, ignoreRotated: settings.ignoreRotatedBarcodes)) {
|
|
54
|
+
barcodesInScanArea.append(barcode)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
barcodesInScanArea.sort {
|
|
58
|
+
$0.distanceToCenter < $1.distanceToCenter
|
|
59
|
+
}
|
|
60
|
+
if (!barcodesInScanArea.isEmpty) {
|
|
61
|
+
barcodesListener.onBarcodesFound(barcodesInScanArea)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private func areBarcodesStable(barcodes: [DetectedBarcode]) -> Bool {
|
|
67
|
+
let barcodesSet = Set(barcodes)
|
|
68
|
+
let lastBarcodesSet = Set(lastBarcodes)
|
|
69
|
+
let differences = barcodesSet.subtracting(lastBarcodesSet)
|
|
70
|
+
if (!barcodes.isEmpty && differences.isEmpty) {
|
|
71
|
+
stableCounter += 1
|
|
72
|
+
print("barcodes stable for \(stableCounter)/\(settings.stableThreshold)")
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
stableCounter = 0
|
|
76
|
+
lastBarcodes = barcodes
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CoreVideo
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
class CameraOverlay: UIView {
|
|
6
|
+
private var lastFrame: CMSampleBuffer?
|
|
7
|
+
public private(set) var scanArea: CGRect
|
|
8
|
+
private var settings: ScannerSettings
|
|
9
|
+
public private(set) var previewLayer: AVCaptureVideoPreviewLayer!
|
|
10
|
+
|
|
11
|
+
init(settings: ScannerSettings, parentView: UIView) {
|
|
12
|
+
|
|
13
|
+
self.scanArea = Utils.calculateCGRect(height: parentView.bounds.height, width: parentView.bounds.width, scaleFactor: settings.detectorSize, aspectRatio: settings.aspectRatioF)
|
|
14
|
+
self.settings = settings
|
|
15
|
+
|
|
16
|
+
super.init(frame: .zero)
|
|
17
|
+
|
|
18
|
+
parentView.addSubview(self)
|
|
19
|
+
NSLayoutConstraint.activate([
|
|
20
|
+
self.topAnchor.constraint(equalTo: parentView.topAnchor),
|
|
21
|
+
self.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
|
|
22
|
+
self.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
|
|
23
|
+
self.bottomAnchor.constraint(equalTo: parentView.bottomAnchor),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
self.translatesAutoresizingMaskIntoConstraints = false
|
|
27
|
+
self.contentMode = UIView.ContentMode.scaleAspectFill
|
|
28
|
+
self.isOpaque = false
|
|
29
|
+
self.backgroundColor = UIColor.clear
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
required init?(coder: NSCoder) {
|
|
33
|
+
fatalError("init(coder:) has not been implemented")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override func layoutSubviews() {
|
|
37
|
+
super.layoutSubviews()
|
|
38
|
+
self.setNeedsDisplay()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override func draw(_ rect: CGRect) {
|
|
42
|
+
super.draw(rect)
|
|
43
|
+
self.scanArea = Utils.calculateCGRect(height: bounds.height, width: bounds.width, scaleFactor: settings.detectorSize, aspectRatio: settings.aspectRatioF)
|
|
44
|
+
if let context = UIGraphicsGetCurrentContext() {
|
|
45
|
+
drawScanArea(context: context)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public func setPreviewLayer(_ previewLayer: AVCaptureVideoPreviewLayer) {
|
|
50
|
+
self.previewLayer = previewLayer
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public func drawDebugOverlay(barcodes: [DetectedBarcode]) {
|
|
54
|
+
weak var weakSelf = self
|
|
55
|
+
DispatchQueue.main.sync {
|
|
56
|
+
guard weakSelf != nil else {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
for annotationView in self.subviews {
|
|
60
|
+
annotationView.removeFromSuperview()
|
|
61
|
+
}
|
|
62
|
+
for barcode in barcodes {
|
|
63
|
+
let rectangleView = UIView(frame: barcode.bounds)
|
|
64
|
+
rectangleView.layer.borderColor = UIColor.blue.cgColor
|
|
65
|
+
rectangleView.layer.borderWidth = 2
|
|
66
|
+
addSubview(rectangleView)
|
|
67
|
+
|
|
68
|
+
let lineView = UIView(frame: barcode.getCenterLine())
|
|
69
|
+
|
|
70
|
+
lineView.layer.borderColor = UIColor.red.cgColor
|
|
71
|
+
lineView.layer.borderWidth = 2
|
|
72
|
+
addSubview(lineView)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func drawScanArea(context: CGContext) {
|
|
78
|
+
if (settings.drawFocusBackground){
|
|
79
|
+
drawFocusBackground(context: context, color: settings.focusBackgroundUIColor, radius: settings.focusRectBorderRadius)
|
|
80
|
+
}
|
|
81
|
+
if (settings.drawFocusLine) {
|
|
82
|
+
drawFocusLine(context: context, color: settings.focusLineUIColor, thickness: settings.focusLineThickness)
|
|
83
|
+
}
|
|
84
|
+
if (settings.drawFocusRect) {
|
|
85
|
+
drawScanAreaOutline(context: context, color: settings.focusRectUIColor, thickness: settings.focusRectBorderThickness, radius: settings.focusRectBorderRadius)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private func drawFocusLine(context: CGContext, color: UIColor, thickness: Int) {
|
|
90
|
+
context.setLineWidth(CGFloat(thickness))
|
|
91
|
+
context.setStrokeColor(color.cgColor)
|
|
92
|
+
context.beginPath()
|
|
93
|
+
context.move(to: CGPointMake(scanArea.minX, CGFloat(bounds.height/2)))
|
|
94
|
+
context.addLine(to: CGPointMake(scanArea.maxX, CGFloat(bounds.height/2)))
|
|
95
|
+
context.strokePath()
|
|
96
|
+
context.saveGState()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private func drawScanAreaOutline(context: CGContext, color: UIColor, thickness: Int, radius: Int) {
|
|
100
|
+
let rounded = UIBezierPath(roundedRect: scanArea, cornerRadius: CGFloat(radius))
|
|
101
|
+
context.addPath(rounded.cgPath)
|
|
102
|
+
context.setLineWidth(CGFloat(thickness))
|
|
103
|
+
context.setStrokeColor(color.cgColor)
|
|
104
|
+
context.strokePath()
|
|
105
|
+
context.saveGState()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func drawFocusBackground(context: CGContext, color: UIColor, radius: Int) {
|
|
109
|
+
let rounded = UIBezierPath(roundedRect: scanArea, cornerRadius: CGFloat(radius))
|
|
110
|
+
color.setFill()
|
|
111
|
+
context.fill(bounds)
|
|
112
|
+
context.saveGState()
|
|
113
|
+
context.setBlendMode(.destinationOut)
|
|
114
|
+
context.addPath(rounded.cgPath)
|
|
115
|
+
context.fillPath()
|
|
116
|
+
context.saveGState()
|
|
117
|
+
context.setBlendMode(.normal)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import AVFoundation
|
|
4
|
+
import MLImage
|
|
5
|
+
import MLKitVision
|
|
6
|
+
|
|
7
|
+
protocol CameraViewControllerDelegate: NSObjectProtocol {
|
|
8
|
+
func onComplete(_ result: [DetectedBarcode])
|
|
9
|
+
func onError(_ error: String)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class CameraViewController: UIViewController, BarcodesListener {
|
|
13
|
+
weak var delegate: CameraViewControllerDelegate?
|
|
14
|
+
private let captureSession = AVCaptureSession()
|
|
15
|
+
private let sessionQueue = DispatchQueue(label: "sessionQueue")
|
|
16
|
+
private var previewLayer = AVCaptureVideoPreviewLayer()
|
|
17
|
+
private var settings: ScannerSettings
|
|
18
|
+
private var cameraOverlay: CameraOverlay!
|
|
19
|
+
private var barcodeAnalyzer : BarcodeAnalyzer!
|
|
20
|
+
private var torchButton: UIButton?
|
|
21
|
+
private var finishedAlready: Bool = false // ensure we only actually finish once
|
|
22
|
+
|
|
23
|
+
func onBarcodesFound(_ barcodes: [DetectedBarcode]) {
|
|
24
|
+
finishWithResult(barcodes)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
init(settings: ScannerSettings) {
|
|
28
|
+
self.settings = settings
|
|
29
|
+
super.init(nibName: nil, bundle: nil)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
required init?(coder: NSCoder) {
|
|
33
|
+
self.settings = ScannerSettings()
|
|
34
|
+
super.init(coder: coder)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override func viewDidLoad() {
|
|
38
|
+
cameraOverlay = CameraOverlay(settings: settings, parentView: view)
|
|
39
|
+
barcodeAnalyzer = BarcodeAnalyzer(settings: settings, barcodesListener: self, cameraOverlay: cameraOverlay)
|
|
40
|
+
|
|
41
|
+
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
|
|
42
|
+
setupCaptureSession()
|
|
43
|
+
setupUI()
|
|
44
|
+
setOrientation()
|
|
45
|
+
} else {
|
|
46
|
+
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (authorized) in
|
|
47
|
+
DispatchQueue.main.async {
|
|
48
|
+
if authorized {
|
|
49
|
+
self.setupCaptureSession()
|
|
50
|
+
self.setupUI()
|
|
51
|
+
self.setOrientation()
|
|
52
|
+
} else {
|
|
53
|
+
self.finishWithError("NO_CAMERA_PERMISSION")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override func viewWillAppear(_ animated: Bool) {
|
|
61
|
+
startSession()
|
|
62
|
+
// slight delay so the camera has time to load before we show the modal
|
|
63
|
+
while (!captureSession.isRunning) {
|
|
64
|
+
usleep(100)
|
|
65
|
+
}
|
|
66
|
+
usleep(150000)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
70
|
+
stopSession()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
74
|
+
super.viewWillTransition(to: size, with: coordinator)
|
|
75
|
+
setOrientation()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func setOrientation() {
|
|
79
|
+
previewLayer.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
|
|
80
|
+
|
|
81
|
+
if #available(iOS 17.0, *) {
|
|
82
|
+
switch UIDevice.current.orientation {
|
|
83
|
+
case UIDeviceOrientation.portraitUpsideDown:
|
|
84
|
+
previewLayer.connection?.videoRotationAngle = 270
|
|
85
|
+
case UIDeviceOrientation.landscapeLeft:
|
|
86
|
+
previewLayer.connection?.videoRotationAngle = 0
|
|
87
|
+
case UIDeviceOrientation.landscapeRight:
|
|
88
|
+
previewLayer.connection?.videoRotationAngle = 180
|
|
89
|
+
case UIDeviceOrientation.portrait:
|
|
90
|
+
previewLayer.connection?.videoRotationAngle = 90
|
|
91
|
+
default:
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
switch UIDevice.current.orientation {
|
|
96
|
+
case UIDeviceOrientation.portraitUpsideDown:
|
|
97
|
+
previewLayer.connection?.videoOrientation = .portraitUpsideDown
|
|
98
|
+
case UIDeviceOrientation.landscapeLeft:
|
|
99
|
+
previewLayer.connection?.videoOrientation = .landscapeRight
|
|
100
|
+
case UIDeviceOrientation.landscapeRight:
|
|
101
|
+
previewLayer.connection?.videoOrientation = .landscapeLeft
|
|
102
|
+
case UIDeviceOrientation.portrait:
|
|
103
|
+
previewLayer.connection?.videoOrientation = .portrait
|
|
104
|
+
default:
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func setupCaptureSession() {
|
|
111
|
+
guard let videoDevice = captureDevice() else {
|
|
112
|
+
finishWithError("NO_CAMERA")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
guard let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
|
|
117
|
+
finishWithError("NO_CAMERA")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
guard captureSession.canAddInput(videoDeviceInput) else { return }
|
|
122
|
+
|
|
123
|
+
captureSession.addInput(videoDeviceInput)
|
|
124
|
+
|
|
125
|
+
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
|
126
|
+
previewLayer.backgroundColor = UIColor.black.cgColor
|
|
127
|
+
previewLayer.frame = view.bounds
|
|
128
|
+
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
|
129
|
+
|
|
130
|
+
if #available(iOS 17.0, *) {
|
|
131
|
+
previewLayer.connection?.videoRotationAngle = 90
|
|
132
|
+
} else {
|
|
133
|
+
previewLayer.connection?.videoOrientation = .portrait
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let output = AVCaptureVideoDataOutput()
|
|
137
|
+
output.alwaysDiscardsLateVideoFrames = true
|
|
138
|
+
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "sampleQueue"))
|
|
139
|
+
|
|
140
|
+
captureSession.addOutput(output)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private func setupUI() {
|
|
144
|
+
view.layer.addSublayer(previewLayer)
|
|
145
|
+
cameraOverlay.setPreviewLayer(previewLayer)
|
|
146
|
+
view.bringSubviewToFront(cameraOverlay)
|
|
147
|
+
|
|
148
|
+
torchButton = createTorchButton()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func createTorchButton() -> UIButton? {
|
|
152
|
+
if let device = AVCaptureDevice.default(for: AVMediaType.video) {
|
|
153
|
+
if device.hasTorch {
|
|
154
|
+
var conf = UIButton.Configuration.filled()
|
|
155
|
+
conf.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
|
|
156
|
+
conf.background.cornerRadius = 25
|
|
157
|
+
conf.baseForegroundColor = UIColor.black
|
|
158
|
+
conf.baseBackgroundColor = UIColor.white
|
|
159
|
+
|
|
160
|
+
if let image = UIImage(named: "flashlight")
|
|
161
|
+
{
|
|
162
|
+
conf.image = image.scalePreservingAspectRatio(targetSize: CGSize(width: 30, height: 30))
|
|
163
|
+
}
|
|
164
|
+
let torchButton = UIButton(configuration: conf)
|
|
165
|
+
torchButton.alpha = 0.5
|
|
166
|
+
torchButton.addTarget(self, action: #selector(toggleFlash), for: UIControl.Event.touchUpInside)
|
|
167
|
+
|
|
168
|
+
view.addSubview(torchButton)
|
|
169
|
+
torchButton.translatesAutoresizingMaskIntoConstraints = false
|
|
170
|
+
NSLayoutConstraint.activate([
|
|
171
|
+
torchButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
|
|
172
|
+
torchButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
|
|
173
|
+
torchButton.widthAnchor.constraint(equalToConstant: 50),
|
|
174
|
+
torchButton.heightAnchor.constraint(equalToConstant: 50)
|
|
175
|
+
])
|
|
176
|
+
return torchButton
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return nil
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@objc
|
|
183
|
+
private func toggleFlash() {
|
|
184
|
+
guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return }
|
|
185
|
+
guard device.hasTorch else { print("Torch isn't available"); return }
|
|
186
|
+
|
|
187
|
+
do {
|
|
188
|
+
try device.lockForConfiguration()
|
|
189
|
+
|
|
190
|
+
if (device.torchMode == AVCaptureDevice.TorchMode.on) {
|
|
191
|
+
device.torchMode = AVCaptureDevice.TorchMode.off
|
|
192
|
+
torchButton?.alpha = 0.5
|
|
193
|
+
} else {
|
|
194
|
+
do {
|
|
195
|
+
try device.setTorchModeOn(level: 1.0)
|
|
196
|
+
torchButton?.alpha = 1
|
|
197
|
+
} catch {
|
|
198
|
+
print(error)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
device.unlockForConfiguration()
|
|
203
|
+
} catch {
|
|
204
|
+
print(error)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func captureDevice() -> AVCaptureDevice? {
|
|
209
|
+
let discoverySession = AVCaptureDevice.DiscoverySession(
|
|
210
|
+
deviceTypes: [.builtInWideAngleCamera],
|
|
211
|
+
mediaType: .video,
|
|
212
|
+
position: .unspecified
|
|
213
|
+
)
|
|
214
|
+
return discoverySession.devices.first { $0.position == .back }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private func startSession() {
|
|
218
|
+
weak var weakSelf = self
|
|
219
|
+
sessionQueue.async {
|
|
220
|
+
guard let strongSelf = weakSelf else {return}
|
|
221
|
+
strongSelf.captureSession.startRunning()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func stopSession() {
|
|
226
|
+
weak var weakSelf = self
|
|
227
|
+
sessionQueue.async {
|
|
228
|
+
guard let strongSelf = weakSelf else {return}
|
|
229
|
+
strongSelf.captureSession.stopRunning()
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func finishWithError(_ error: String) {
|
|
234
|
+
if (!finishedAlready) {
|
|
235
|
+
finishedAlready = true
|
|
236
|
+
delegate?.onError(error)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private func finishWithResult(_ result: [DetectedBarcode]){
|
|
241
|
+
if (!finishedAlready) {
|
|
242
|
+
finishedAlready = true
|
|
243
|
+
delegate?.onComplete(result)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
extension UIImage {
|
|
249
|
+
func scalePreservingAspectRatio(targetSize: CGSize) -> UIImage {
|
|
250
|
+
// Determine the scale factor that preserves aspect ratio
|
|
251
|
+
let widthRatio = targetSize.width / size.width
|
|
252
|
+
let heightRatio = targetSize.height / size.height
|
|
253
|
+
|
|
254
|
+
let scaleFactor = min(widthRatio, heightRatio)
|
|
255
|
+
|
|
256
|
+
// Compute the new image size that preserves aspect ratio
|
|
257
|
+
let scaledImageSize = CGSize(
|
|
258
|
+
width: size.width * scaleFactor,
|
|
259
|
+
height: size.height * scaleFactor
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
// Draw and return the resized UIImage
|
|
263
|
+
let renderer = UIGraphicsImageRenderer(
|
|
264
|
+
size: scaledImageSize
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
let scaledImage = renderer.image { _ in
|
|
268
|
+
self.draw(in: CGRect(
|
|
269
|
+
origin: .zero,
|
|
270
|
+
size: scaledImageSize
|
|
271
|
+
))
|
|
272
|
+
}
|
|
273
|
+
return scaledImage
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
278
|
+
|
|
279
|
+
func captureOutput(_ output: AVCaptureOutput,didOutput sampleBuffer: CMSampleBuffer,from connection: AVCaptureConnection) {
|
|
280
|
+
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
|
281
|
+
print("Failed to get image buffer from sample buffer.")
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let visionImage = VisionImage(buffer: sampleBuffer)
|
|
286
|
+
let orientation = Utils.imageOrientation(fromDevicePosition: .back)
|
|
287
|
+
|
|
288
|
+
visionImage.orientation = orientation
|
|
289
|
+
|
|
290
|
+
guard let inputImage = MLImage(sampleBuffer: sampleBuffer) else {
|
|
291
|
+
print("Failed to create MLImage from sample buffer.")
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
inputImage.orientation = orientation
|
|
295
|
+
|
|
296
|
+
let imageWidth = CGFloat(CVPixelBufferGetWidth(imageBuffer))
|
|
297
|
+
let imageHeight = CGFloat(CVPixelBufferGetHeight(imageBuffer))
|
|
298
|
+
|
|
299
|
+
barcodeAnalyzer.analyze(in: visionImage, width: imageWidth, height: imageHeight)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import MLKitBarcodeScanning
|
|
2
|
+
|
|
3
|
+
class DetectedBarcode: Hashable, Equatable, CustomDebugStringConvertible {
|
|
4
|
+
|
|
5
|
+
var debugDescription: String {
|
|
6
|
+
var des: String = "\(type(of: self)) {"
|
|
7
|
+
for child in Mirror(reflecting: self).children {
|
|
8
|
+
if let propName = child.label {
|
|
9
|
+
des += "\n\t\(propName): \(child.value)"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return des + "\n}"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func hash(into hasher: inout Hasher) {
|
|
17
|
+
hasher.combine(value)
|
|
18
|
+
hasher.combine(barcodeType)
|
|
19
|
+
hasher.combine(format)
|
|
20
|
+
let _ = hasher.finalize()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func == (lhs: DetectedBarcode, rhs: DetectedBarcode) -> Bool {
|
|
24
|
+
return lhs.value.elementsEqual(rhs.value) && lhs.barcodeType == rhs.barcodeType && lhs.format == rhs.format
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public private(set) var bounds: CGRect
|
|
28
|
+
public private(set) var value: String
|
|
29
|
+
public private(set) var format: Int
|
|
30
|
+
public private(set) var barcodeType: Int
|
|
31
|
+
public private(set) var distanceToCenter: CGFloat
|
|
32
|
+
public private(set) var isPortrait: Bool
|
|
33
|
+
|
|
34
|
+
init(barcode: Barcode, bounds: CGRect, centerX: CGFloat, centerY: CGFloat) {
|
|
35
|
+
format = barcode.format.rawValue
|
|
36
|
+
barcodeType = barcode.valueType.rawValue
|
|
37
|
+
self.bounds = bounds
|
|
38
|
+
if let rawValue = barcode.rawValue {
|
|
39
|
+
value = rawValue
|
|
40
|
+
} else {
|
|
41
|
+
value = String(data: barcode.rawData!, encoding: .ascii)!;
|
|
42
|
+
}
|
|
43
|
+
self.isPortrait = bounds.height > bounds.width
|
|
44
|
+
distanceToCenter = hypot((centerX - bounds.midX), (centerY - bounds.midY));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func isInScanArea(scanArea: CGRect, ignoreRotated: Bool) -> Bool {
|
|
48
|
+
if (ignoreRotated && isPortrait) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return scanArea.contains(getCenterLine(forceScreenOrientation: ignoreRotated))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public func getCenterLine(forceScreenOrientation: Bool = false) -> CGRect {
|
|
56
|
+
if (!forceScreenOrientation && isPortrait) {
|
|
57
|
+
return CGRect(x: bounds.midX, y: bounds.minY, width: 1, height: bounds.height)
|
|
58
|
+
}
|
|
59
|
+
return CGRect(x: bounds.minX, y: bounds.midY, width: bounds.width, height: 1)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public func outputAsDictionary() -> [String: Any] {
|
|
63
|
+
return [
|
|
64
|
+
"value": value,
|
|
65
|
+
"type": BarcodeType.getFromInt(intValue: barcodeType)!.rawValue,
|
|
66
|
+
"format": BarcodeFormat.getFromInt(intValue: format)!.rawValue,
|
|
67
|
+
"distanceToCenter": round(distanceToCenter*100)/100.0
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
+
<key>CFBundleExecutable</key>
|
|
8
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
9
|
+
<key>CFBundleIdentifier</key>
|
|
10
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
11
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
+
<string>6.0</string>
|
|
13
|
+
<key>CFBundleName</key>
|
|
14
|
+
<string>$(PRODUCT_NAME)</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>FMWK</string>
|
|
17
|
+
<key>CFBundleShortVersionString</key>
|
|
18
|
+
<string>1.0</string>
|
|
19
|
+
<key>CFBundleVersion</key>
|
|
20
|
+
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
21
|
+
<key>NSPrincipalClass</key>
|
|
22
|
+
<string></string>
|
|
23
|
+
</dict>
|
|
24
|
+
</plist>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
//! Project version number for Plugin.
|
|
4
|
+
FOUNDATION_EXPORT double PluginVersionNumber;
|
|
5
|
+
|
|
6
|
+
//! Project version string for Plugin.
|
|
7
|
+
FOUNDATION_EXPORT const unsigned char PluginVersionString[];
|
|
8
|
+
|
|
9
|
+
// In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>
|
|
10
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <Capacitor/Capacitor.h>
|
|
3
|
+
|
|
4
|
+
// Define the plugin using the CAP_PLUGIN Macro, and
|
|
5
|
+
// each method the plugin supports using the CAP_PLUGIN_METHOD macro.
|
|
6
|
+
CAP_PLUGIN(MlKitBarcodeScannerPlugin, "MlKitBarcodeScanner",
|
|
7
|
+
CAP_PLUGIN_METHOD(scan, CAPPluginReturnPromise);
|
|
8
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
@objc(MlKitBarcodeScannerPlugin)
|
|
6
|
+
public class MlKitBarcodeScannerPlugin: CAPPlugin, CameraViewControllerDelegate {
|
|
7
|
+
private var call: CAPPluginCall?
|
|
8
|
+
private var settings: ScannerSettings!
|
|
9
|
+
private var player: AVAudioPlayer?
|
|
10
|
+
|
|
11
|
+
@objc func scan(_ call: CAPPluginCall) {
|
|
12
|
+
let options = call.jsObjectRepresentation
|
|
13
|
+
self.call = call
|
|
14
|
+
settings = ScannerSettings(options: options)
|
|
15
|
+
|
|
16
|
+
DispatchQueue.main.async {
|
|
17
|
+
let cameraViewController = CameraViewController(settings: self.settings)
|
|
18
|
+
cameraViewController.delegate = self
|
|
19
|
+
self.bridge!.viewController!.present(cameraViewController, animated: true)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func onComplete(_ result: [DetectedBarcode]) {
|
|
24
|
+
weak var weakSelf = self
|
|
25
|
+
DispatchQueue.main.sync {
|
|
26
|
+
guard weakSelf != nil else {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
self.bridge!.viewController!.dismiss(animated: true)
|
|
30
|
+
}
|
|
31
|
+
if (result.isEmpty) {
|
|
32
|
+
self.call!.reject("NO_BARCODE")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if (settings.vibrateOnSuccess) {
|
|
36
|
+
AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) { }
|
|
37
|
+
}
|
|
38
|
+
if (settings.beepOnSuccess) {
|
|
39
|
+
if let audioData = NSDataAsset(name: "beep")?.data {
|
|
40
|
+
do {
|
|
41
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
|
|
42
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
43
|
+
|
|
44
|
+
player = try AVAudioPlayer(data: audioData)
|
|
45
|
+
|
|
46
|
+
if let unwrappedPlayer = player {
|
|
47
|
+
unwrappedPlayer.play()
|
|
48
|
+
}
|
|
49
|
+
} catch let error {
|
|
50
|
+
print(error.localizedDescription)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
var resultBarcodes: [[String: Any]] = []
|
|
55
|
+
for detectedBarcode in result {
|
|
56
|
+
resultBarcodes.append(detectedBarcode.outputAsDictionary())
|
|
57
|
+
}
|
|
58
|
+
var output = JSObject()
|
|
59
|
+
output.updateValue(resultBarcodes, forKey: "barcodes")
|
|
60
|
+
self.call!.resolve(output)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func onError(_ error: String) {
|
|
64
|
+
print("error occurred")
|
|
65
|
+
self.bridge!.viewController!.dismiss(animated: false)
|
|
66
|
+
print("controller dismissed")
|
|
67
|
+
self.call!.reject(error)
|
|
68
|
+
}
|
|
69
|
+
}
|