@scr2em/capacitor-scanner 6.0.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.
@@ -0,0 +1,346 @@
1
+ import AVFoundation
2
+ import Capacitor
3
+ import Foundation
4
+
5
+ /**
6
+ * Please read the Capacitor iOS Plugin Development Guide
7
+ * here: https://capacitorjs.com/docs/plugins/ios
8
+ */
9
+ struct VoteStatus {
10
+ var votes: Int
11
+ var done: Bool
12
+ }
13
+
14
+ @objc(CapacitorScannerPlugin)
15
+ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetadataOutputObjectsDelegate, AVCapturePhotoCaptureDelegate {
16
+ public let identifier = "CapacitorScannerPlugin"
17
+ public let jsName = "CapacitorScanner"
18
+ public let pluginMethods: [CAPPluginMethod] = [
19
+ CAPPluginMethod(name: "startScanning", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "stopScanning", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "capturePhoto", returnType: CAPPluginReturnPromise)
25
+ ]
26
+
27
+ private var captureSession: AVCaptureSession?
28
+ private var cameraView: UIView?
29
+ private var previewLayer: AVCaptureVideoPreviewLayer?
30
+
31
+ private let voteThreshold = 5
32
+ private var scannedCodesVotes: [String: VoteStatus] = [:]
33
+
34
+ // for capturing images
35
+ private var photoOutput: AVCapturePhotoOutput?
36
+ private var capturePhotoCall: CAPPluginCall?
37
+
38
+ private var orientationObserver: NSObjectProtocol?
39
+
40
+ @objc func capturePhoto(_ call: CAPPluginCall) {
41
+ guard let photoOutput = self.photoOutput, captureSession?.isRunning == true else {
42
+ call.reject("Camera is not set up or running")
43
+ return
44
+ }
45
+
46
+ let photoSettings = AVCapturePhotoSettings()
47
+ self.capturePhotoCall = call
48
+ photoOutput.capturePhoto(with: photoSettings, delegate: self)
49
+ }
50
+
51
+ public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
52
+ if let captureError = error {
53
+ self.capturePhotoCall?.reject("Photo capture failed: \(captureError.localizedDescription)")
54
+ return
55
+ }
56
+
57
+ guard let imageData = photo.fileDataRepresentation() else {
58
+ self.capturePhotoCall?.reject("Unable to get image data")
59
+ return
60
+ }
61
+
62
+ let base64String = imageData.base64EncodedString()
63
+ self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
64
+ }
65
+
66
+ @objc func startScanning(_ call: CAPPluginCall) {
67
+ self.stopScanning(call)
68
+
69
+ DispatchQueue.main.async {
70
+ let captureSession = AVCaptureSession()
71
+ captureSession.sessionPreset = AVCaptureSession.Preset.hd1280x720
72
+
73
+ let cameraDirection: AVCaptureDevice.Position = call.getString("cameraDirection", "BACK") == "BACK" ? .back : .front
74
+
75
+ guard let videoCaptureDevice = self.getCaptureDevice(position: cameraDirection) else {
76
+ call.reject("Unable to access the camera")
77
+ return
78
+ }
79
+
80
+ let videoInput: AVCaptureDeviceInput
81
+
82
+ do {
83
+ videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
84
+ } catch {
85
+ call.reject("Unable to initialize video input")
86
+ return
87
+ }
88
+
89
+ if captureSession.canAddInput(videoInput) {
90
+ captureSession.addInput(videoInput)
91
+ } else {
92
+ call.reject("Unable to add video input to capture session")
93
+ return
94
+ }
95
+
96
+ let metadataOutput = AVCaptureMetadataOutput()
97
+
98
+ if captureSession.canAddOutput(metadataOutput) {
99
+ captureSession.addOutput(metadataOutput)
100
+
101
+ metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
102
+
103
+ let formats = call.getArray("formats", String.self) ?? []
104
+ let metadataObjectTypes = self.getMetadataObjectTypes(from: formats)
105
+ metadataOutput.metadataObjectTypes = metadataObjectTypes
106
+ } else {
107
+ call.reject("Unable to add metadata output to capture session")
108
+ return
109
+ }
110
+
111
+ // Add photo output
112
+ let photoOutput = AVCapturePhotoOutput()
113
+ self.photoOutput = photoOutput
114
+ if captureSession.canAddOutput(photoOutput) {
115
+ captureSession.addOutput(photoOutput)
116
+ } else {
117
+ call.reject("Unable to add photo output to capture session")
118
+ return
119
+ }
120
+
121
+ self.hideWebViewBackground()
122
+
123
+ if let webView = self.webView, let superView = webView.superview {
124
+ let cameraView = UIView(frame: superView.bounds)
125
+ let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
126
+ previewLayer.videoGravity = .resizeAspectFill
127
+ previewLayer.frame = superView.bounds
128
+ cameraView.layer.addSublayer(previewLayer)
129
+
130
+ webView.superview?.insertSubview(cameraView, belowSubview: webView)
131
+
132
+ self.captureSession = captureSession
133
+ self.cameraView = cameraView
134
+ self.previewLayer = previewLayer
135
+
136
+ // Add orientation change observer
137
+ self.addOrientationChangeObserver()
138
+
139
+ // Initial orientation setup
140
+ self.updatePreviewOrientation()
141
+
142
+ DispatchQueue.global(qos: .background).async {
143
+ captureSession.startRunning()
144
+ call.resolve()
145
+ }
146
+
147
+ } else {
148
+ call.reject("unknown")
149
+ }
150
+ }
151
+ }
152
+
153
+ @objc func stopScanning(_ call: CAPPluginCall) {
154
+ DispatchQueue.main.async {
155
+ if let captureSession = self.captureSession {
156
+ captureSession.stopRunning()
157
+ }
158
+
159
+ self.previewLayer?.removeFromSuperlayer()
160
+ self.cameraView?.removeFromSuperview()
161
+
162
+ self.captureSession = nil
163
+ self.cameraView = nil
164
+ self.previewLayer = nil
165
+ self.scannedCodesVotes = [:]
166
+ self.showWebViewBackground()
167
+
168
+ self.removeOrientationChangeObserver()
169
+
170
+ call.resolve()
171
+ }
172
+ }
173
+
174
+ private func addOrientationChangeObserver() {
175
+ self.orientationObserver = NotificationCenter.default.addObserver(
176
+ forName: UIDevice.orientationDidChangeNotification,
177
+ object: nil,
178
+ queue: .main
179
+ ) { [weak self] _ in
180
+ self?.updatePreviewOrientation()
181
+ }
182
+ }
183
+
184
+ private func removeOrientationChangeObserver() {
185
+ if let observer = self.orientationObserver {
186
+ NotificationCenter.default.removeObserver(observer)
187
+ self.orientationObserver = nil
188
+ }
189
+ }
190
+
191
+ private func updatePreviewOrientation() {
192
+ guard let previewLayer = self.previewLayer,
193
+ let connection = previewLayer.connection,
194
+ let cameraView = self.cameraView
195
+ else {
196
+ return
197
+ }
198
+
199
+ let deviceOrientation = UIDevice.current.orientation
200
+
201
+ if deviceOrientation.isFlat == true || deviceOrientation == .portraitUpsideDown {
202
+ return
203
+ }
204
+
205
+ let newOrientation: AVCaptureVideoOrientation
206
+
207
+ switch deviceOrientation {
208
+ case .landscapeLeft:
209
+ newOrientation = .landscapeRight
210
+ case .landscapeRight:
211
+ newOrientation = .landscapeLeft
212
+ default:
213
+ newOrientation = .portrait
214
+ }
215
+
216
+ connection.videoOrientation = newOrientation
217
+
218
+ // Update camera view and preview layer frames
219
+ let screenBounds = UIScreen.main.bounds
220
+ let screenWidth = screenBounds.width
221
+ let screenHeight = screenBounds.height
222
+
223
+ // Determine the correct dimensions based on orientation
224
+ let width: CGFloat
225
+ let height: CGFloat
226
+ if newOrientation == .portrait {
227
+ width = min(screenWidth, screenHeight)
228
+ height = max(screenWidth, screenHeight)
229
+ } else {
230
+ width = max(screenWidth, screenHeight)
231
+ height = min(screenWidth, screenHeight)
232
+ }
233
+
234
+ // Update frames
235
+ cameraView.frame = CGRect(x: 0, y: 0, width: width, height: height)
236
+ previewLayer.frame = cameraView.bounds
237
+ }
238
+
239
+ public func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
240
+ guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
241
+ let stringValue = metadataObject.stringValue
242
+ else {
243
+ return
244
+ }
245
+
246
+ /*
247
+ this is a voting system to
248
+ 1. avoid scanning the same code
249
+ 2. avoid scanning any code that doesn't appear for at least in 10 frames of the video stream to reduce the number of false positives
250
+ */
251
+
252
+ var voteStatus = self.scannedCodesVotes[stringValue] ?? VoteStatus(votes: 0, done: false)
253
+
254
+ if !voteStatus.done {
255
+ voteStatus.votes += 1
256
+
257
+ if voteStatus.votes >= self.voteThreshold {
258
+ voteStatus.done = true
259
+
260
+ self.notifyListeners("barcodeScanned", data: [
261
+ "scannedCode": stringValue,
262
+ "format": CapacitorScannerHelpers.convertBarcodeScannerFormatToString(metadataObject.type)
263
+ ])
264
+ }
265
+ }
266
+
267
+ self.scannedCodesVotes[stringValue] = voteStatus
268
+ }
269
+
270
+ private func getCaptureDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
271
+ let discoverySession = AVCaptureDevice.DiscoverySession(
272
+ deviceTypes: [.builtInWideAngleCamera],
273
+ mediaType: .video,
274
+ position: position
275
+ )
276
+ return discoverySession.devices.first
277
+ }
278
+
279
+ private func getMetadataObjectTypes(from formats: [String]) -> [BarcodeFormat] {
280
+ if formats.isEmpty {
281
+ return CapacitorScannerHelpers.getAllSupportedFormats()
282
+ }
283
+
284
+ return formats.compactMap { format in
285
+ CapacitorScannerHelpers.convertStringToBarcodeScannerFormat(format)
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Must run on UI thread.
291
+ */
292
+ private func hideWebViewBackground() {
293
+ guard let webView = self.webView else {
294
+ return
295
+ }
296
+ webView.isOpaque = false
297
+ webView.backgroundColor = UIColor.clear
298
+ webView.scrollView.backgroundColor = UIColor.clear
299
+ }
300
+
301
+ /**
302
+ * Must run on UI thread.
303
+ */
304
+ private func showWebViewBackground() {
305
+ guard let webView = self.webView else {
306
+ return
307
+ }
308
+ webView.isOpaque = true
309
+ webView.backgroundColor = UIColor.white
310
+ webView.scrollView.backgroundColor = UIColor.white
311
+ }
312
+
313
+ @objc override public func checkPermissions(_ call: CAPPluginCall) {
314
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
315
+
316
+ var stringStatus = "prompt"
317
+
318
+ if status == .denied || status == .restricted {
319
+ stringStatus = "denied"
320
+ }
321
+
322
+ if status == .authorized {
323
+ stringStatus = "granted"
324
+ }
325
+
326
+ call.resolve(["camera": stringStatus])
327
+ }
328
+
329
+ @objc override public func requestPermissions(_ call: CAPPluginCall) {
330
+ AVCaptureDevice.requestAccess(for: .video) { _ in
331
+ self.checkPermissions(call)
332
+ }
333
+ }
334
+
335
+ @objc func openSettings(_ call: CAPPluginCall) {
336
+ let url = URL(string: UIApplication.openSettingsURLString)
337
+ DispatchQueue.main.async {
338
+ if let url = url, UIApplication.shared.canOpenURL(url) {
339
+ UIApplication.shared.open(url)
340
+ call.resolve()
341
+ } else {
342
+ call.reject("unknown")
343
+ }
344
+ }
345
+ }
346
+ }
@@ -0,0 +1,8 @@
1
+
2
+ import Foundation
3
+ import AVFoundation
4
+
5
+ @objc public class ScanSettings: NSObject {
6
+ public var formats: [AVMetadataObject.ObjectType] = []
7
+ public var cameraPosition: AVCaptureDevice.Position = .back
8
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@scr2em/capacitor-scanner",
3
+ "version": "6.0.0",
4
+ "description": "scan codes",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapacitorScanner.podspec"
17
+ ],
18
+ "author": "Mohamed M. Abdelgwad",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/scr2em/capacitor-scanner/src.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/scr2em/capacitor-scanner/src/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "native"
31
+ ],
32
+ "scripts": {
33
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
34
+ "verify:ios": "xcodebuild -scheme CapacitorScanner -destination generic/platform=iOS",
35
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
36
+ "verify:web": "npm run build",
37
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
38
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
39
+ "eslint": "eslint . --ext ts",
40
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
41
+ "swiftlint": "node-swiftlint",
42
+ "docgen": "docgen --api CapacitorScannerPlugin --output-readme README.md --output-json dist/docs.json",
43
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
44
+ "clean": "rimraf ./dist",
45
+ "watch": "tsc --watch",
46
+ "prepublishOnly": "npm run build",
47
+ "prepare": "npm run build"
48
+ },
49
+ "devDependencies": {
50
+ "@capacitor/android": "6.0.0",
51
+ "@capacitor/cli": "6.0.0",
52
+ "@capacitor/core": "6.0.0",
53
+ "@capacitor/docgen": "0.2.2",
54
+ "@capacitor/ios": "6.0.0",
55
+ "@ionic/eslint-config": "0.4.0",
56
+ "@ionic/prettier-config": "4.0.0",
57
+ "@ionic/swiftlint-config": "2.0.0",
58
+ "eslint": "8.57.0",
59
+ "prettier": "3.3.3",
60
+ "prettier-plugin-java": "2.6.4",
61
+ "rimraf": "6.0.1",
62
+ "rollup": "4.24.0",
63
+ "swiftlint": "2.0.0",
64
+ "typescript": "4.1.5"
65
+ },
66
+ "peerDependencies": {
67
+ "@capacitor/core": "6.0.0"
68
+ },
69
+ "prettier": "@ionic/prettier-config",
70
+ "swiftlint": "@ionic/swiftlint-config",
71
+ "eslintConfig": {
72
+ "extends": "@ionic/eslint-config/recommended"
73
+ },
74
+ "capacitor": {
75
+ "ios": {
76
+ "src": "ios"
77
+ },
78
+ "android": {
79
+ "src": "android"
80
+ }
81
+ }
82
+ }