Julian Steenbakker
Committed by GitHub

Merge pull request #352 from juliansteenbakker/imp/return-image-ios

fix: fixed build issue for ios
1 -import AVFoundation  
2 import Flutter 1 import Flutter
3 import MLKitVision 2 import MLKitVision
4 import MLKitBarcodeScanning 3 import MLKitBarcodeScanning
  4 +import AVFoundation
5 5
6 -public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {  
7 -  
8 - let registry: FlutterTextureRegistry  
9 -  
10 - // Sink for publishing event changes  
11 - var sink: FlutterEventSink!  
12 -  
13 - // Texture id of the camera preview  
14 - var textureId: Int64!  
15 -  
16 - // Capture session of the camera  
17 - var captureSession: AVCaptureSession!  
18 -  
19 - // The selected camera  
20 - var device: AVCaptureDevice!  
21 -  
22 - // Image to be sent to the texture  
23 - var latestBuffer: CVImageBuffer!  
24 -  
25 - // Return image buffer with the Barcode event  
26 - var returnImage: Bool = false  
27 -  
28 -// var analyzeMode: Int = 0  
29 - var analyzing: Bool = false  
30 - var position = AVCaptureDevice.Position.back 6 +public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
31 7
32 - var scanner = BarcodeScanner.barcodeScanner() 8 + /// The mobile scanner object that handles all logic
  9 + private let mobileScanner: MobileScanner
33 10
34 - public static func register(with registrar: FlutterPluginRegistrar) {  
35 - let instance = SwiftMobileScannerPlugin(registrar.textures()) 11 + /// The handler sends all information via an event channel back to Flutter
  12 + private let barcodeHandler: BarcodeHandler
36 13
37 - let method = FlutterMethodChannel(name:  
38 - "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())  
39 - let event = FlutterEventChannel(name:  
40 - "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())  
41 - registrar.addMethodCallDelegate(instance, channel: method)  
42 - event.setStreamHandler(instance) 14 + init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
  15 + self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
  16 + if barcodes != nil {
  17 + let barcodesMap = barcodes!.map { barcode in
  18 + return barcode.data
43 } 19 }
44 -  
45 - init(_ registry: FlutterTextureRegistry) {  
46 - self.registry = registry 20 + if (!barcodesMap.isEmpty) {
  21 + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)])
  22 + }
  23 + } else if (error != nil){
  24 + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
  25 + }
  26 + })
  27 + self.barcodeHandler = barcodeHandler
47 super.init() 28 super.init()
48 } 29 }
49 30
  31 + public static func register(with registrar: FlutterPluginRegistrar) {
  32 + let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures())
  33 + let methodChannel = FlutterMethodChannel(name:
  34 + "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
  35 + registrar.addMethodCallDelegate(instance, channel: methodChannel)
  36 + }
50 37
51 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 38 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
52 switch call.method { 39 switch call.method {
53 case "state": 40 case "state":
54 - checkPermission(call, result) 41 + result(mobileScanner.checkPermission())
55 case "request": 42 case "request":
56 - requestPermission(call, result) 43 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
57 case "start": 44 case "start":
58 start(call, result) 45 start(call, result)
59 - case "torch":  
60 - toggleTorch(call, result)  
61 -// case "analyze":  
62 -// switchAnalyzeMode(call, result)  
63 case "stop": 46 case "stop":
64 stop(result) 47 stop(result)
  48 + case "torch":
  49 + toggleTorch(call, result)
65 case "analyzeImage": 50 case "analyzeImage":
66 analyzeImage(call, result) 51 analyzeImage(call, result)
67 -  
68 default: 52 default:
69 result(FlutterMethodNotImplemented) 53 result(FlutterMethodNotImplemented)
70 } 54 }
71 } 55 }
72 56
73 - // FlutterStreamHandler  
74 - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {  
75 - sink = events  
76 - return nil  
77 - }  
78 -  
79 - // FlutterStreamHandler  
80 - public func onCancel(withArguments arguments: Any?) -> FlutterError? {  
81 - sink = nil  
82 - return nil  
83 - }  
84 -  
85 - // FlutterTexture  
86 - public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {  
87 - if latestBuffer == nil {  
88 - return nil  
89 - }  
90 - return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)  
91 - }  
92 -  
93 - private func ciImageToJpeg(ciImage: CIImage) -> Data {  
94 -  
95 - // let ciImage = CIImage(cvPixelBuffer: latestBuffer)  
96 - let context:CIContext = CIContext.init(options: nil)  
97 - let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!  
98 - let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)  
99 -  
100 - return uiImage.jpegData(compressionQuality: 0.8)!;  
101 - }  
102 -  
103 - // Gets called when a new image is added to the buffer  
104 - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {  
105 -  
106 - latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
107 - registry.textureFrameAvailable(textureId)  
108 -  
109 -// switch analyzeMode {  
110 -// case 1: // barcode  
111 - if analyzing {  
112 - return  
113 - }  
114 - analyzing = true  
115 - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
116 - let image = VisionImage(image: buffer!.image)  
117 - image.orientation = imageOrientation(  
118 - deviceOrientation: UIDevice.current.orientation,  
119 - defaultOrientation: .portrait  
120 - )  
121 -  
122 - scanner.process(image) { [self] barcodes, error in  
123 - if error == nil && barcodes != nil {  
124 - for barcode in barcodes! {  
125 -  
126 - var event: [String: Any?] = ["name": "barcode", "data": barcode.data]  
127 - if (returnImage && latestBuffer != nil) {  
128 - let image: CIImage = CIImage(cvPixelBuffer: latestBuffer)  
129 -  
130 - event["image"] = FlutterStandardTypedData(bytes: ciImageToJpeg(ciImage: image))  
131 - }  
132 - sink?(event)  
133 - }  
134 - }  
135 - analyzing = false  
136 - }  
137 -// default: // none  
138 -// break  
139 -// }  
140 - }  
141 -  
142 - func imageOrientation(  
143 - deviceOrientation: UIDeviceOrientation,  
144 - defaultOrientation: UIDeviceOrientation  
145 - ) -> UIImage.Orientation {  
146 - switch deviceOrientation {  
147 - case .portrait:  
148 - return position == .front ? .leftMirrored : .right  
149 - case .landscapeLeft:  
150 - return position == .front ? .downMirrored : .up  
151 - case .portraitUpsideDown:  
152 - return position == .front ? .rightMirrored : .left  
153 - case .landscapeRight:  
154 - return position == .front ? .upMirrored : .down  
155 - case .faceDown, .faceUp, .unknown:  
156 - return .up  
157 - @unknown default:  
158 - return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait)  
159 - }  
160 - }  
161 -  
162 - func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
163 - let status = AVCaptureDevice.authorizationStatus(for: .video)  
164 - switch status {  
165 - case .notDetermined:  
166 - result(0)  
167 - case .authorized:  
168 - result(1)  
169 - default:  
170 - result(2)  
171 - }  
172 - }  
173 -  
174 - func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
175 - AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })  
176 - }  
177 -  
178 - func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
179 - if (device != nil) {  
180 - result(FlutterError(code: "MobileScanner",  
181 - message: "Called start() while already started!",  
182 - details: nil))  
183 - return  
184 - }  
185 -  
186 - textureId = registry.register(self)  
187 - captureSession = AVCaptureSession() 57 + /// Parses all parameters and starts the mobileScanner
  58 + private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  59 + // let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int
  60 + let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
  61 + let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
  62 + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
  63 + let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
188 64
189 - let argReader = MapArgumentReader(call.arguments as? [String: Any]) 65 + let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
  66 + var barcodeOptions: BarcodeScannerOptions? = nil
190 67
191 - returnImage = argReader.bool(key: "returnImage") ?? false  
192 -  
193 -// let ratio: Int = argReader.int(key: "ratio")  
194 - let torch: Bool = argReader.bool(key: "torch") ?? false  
195 - let facing: Int = argReader.int(key: "facing") ?? 1  
196 - let formats: Array = argReader.intArray(key: "formats") ?? []  
197 -  
198 - if (formats.count != 0) { 68 + if (formatList.count != 0) {
199 var barcodeFormats: BarcodeFormat = [] 69 var barcodeFormats: BarcodeFormat = []
200 for index in formats { 70 for index in formats {
201 barcodeFormats.insert(BarcodeFormat(rawValue: index)) 71 barcodeFormats.insert(BarcodeFormat(rawValue: index))
202 } 72 }
203 -  
204 - let barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)  
205 - scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions) 73 + barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
206 } 74 }
207 75
208 - // Set the camera to use  
209 - position = facing == 0 ? AVCaptureDevice.Position.front : .back  
210 76
211 - // Open the camera device  
212 - if #available(iOS 10.0, *) {  
213 - device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first  
214 - } else {  
215 - device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first  
216 - } 77 + let position = facing == 0 ? AVCaptureDevice.Position.front : .back
  78 + let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)!
217 79
218 - if (device == nil) { 80 + do {
  81 + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed)
  82 + result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
  83 + } catch MobileScannerError.alreadyStarted {
  84 + result(FlutterError(code: "MobileScanner",
  85 + message: "Called start() while already started!",
  86 + details: nil))
  87 + } catch MobileScannerError.noCamera {
219 result(FlutterError(code: "MobileScanner", 88 result(FlutterError(code: "MobileScanner",
220 message: "No camera found or failed to open camera!", 89 message: "No camera found or failed to open camera!",
221 details: nil)) 90 details: nil))
222 - return  
223 - }  
224 -  
225 - // Enable the torch if parameter is set and torch is available  
226 - if (device.hasTorch && device.isTorchAvailable) {  
227 - do {  
228 - try device.lockForConfiguration()  
229 - device.torchMode = torch ? .on : .off  
230 - device.unlockForConfiguration() 91 + } catch MobileScannerError.torchError(let error) {
  92 + result(FlutterError(code: "MobileScanner",
  93 + message: "Error occured when setting toch!",
  94 + details: error))
  95 + } catch MobileScannerError.cameraError(let error) {
  96 + result(FlutterError(code: "MobileScanner",
  97 + message: "Error occured when setting up camera!",
  98 + details: error))
231 } catch { 99 } catch {
232 - error.throwNative(result) 100 + result(FlutterError(code: "MobileScanner",
  101 + message: "Unknown error occured..",
  102 + details: nil))
233 } 103 }
234 } 104 }
235 105
236 - device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)  
237 - captureSession.beginConfiguration()  
238 -  
239 - // Add device input 106 + /// Stops the mobileScanner and closes the texture
  107 + private func stop(_ result: @escaping FlutterResult) {
240 do { 108 do {
241 - let input = try AVCaptureDeviceInput(device: device)  
242 - captureSession.addInput(input) 109 + try mobileScanner.stop()
243 } catch { 110 } catch {
244 - error.throwNative(result)  
245 - }  
246 - captureSession.sessionPreset = AVCaptureSession.Preset.photo;  
247 - // Add video output.  
248 - let videoOutput = AVCaptureVideoDataOutput()  
249 -  
250 - videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]  
251 - videoOutput.alwaysDiscardsLateVideoFrames = true  
252 -  
253 - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)  
254 - captureSession.addOutput(videoOutput)  
255 - for connection in videoOutput.connections {  
256 - connection.videoOrientation = .portrait  
257 - if position == .front && connection.isVideoMirroringSupported {  
258 - connection.isVideoMirrored = true  
259 - } 111 + result(FlutterError(code: "MobileScanner",
  112 + message: "Called stop() while already stopped!",
  113 + details: nil))
260 } 114 }
261 - captureSession.commitConfiguration()  
262 - captureSession.startRunning()  
263 - let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)  
264 - let width = Double(demensions.height)  
265 - let height = Double(demensions.width)  
266 - let size = ["width": width, "height": height]  
267 - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]  
268 - result(answer) 115 + result(nil)
269 } 116 }
270 117
271 - func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
272 - if (device == nil) { 118 + /// Toggles the torch
  119 + private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  120 + do {
  121 + try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
  122 + } catch {
273 result(FlutterError(code: "MobileScanner", 123 result(FlutterError(code: "MobileScanner",
274 message: "Called toggleTorch() while stopped!", 124 message: "Called toggleTorch() while stopped!",
275 details: nil)) 125 details: nil))
276 - return  
277 } 126 }
278 - do {  
279 - try device.lockForConfiguration()  
280 - device.torchMode = call.arguments as! Int == 1 ? .on : .off  
281 - device.unlockForConfiguration()  
282 result(nil) 127 result(nil)
283 - } catch {  
284 - error.throwNative(result)  
285 - }  
286 } 128 }
287 129
288 -// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
289 -// analyzeMode = call.arguments as! Int  
290 -// result(nil)  
291 -// }  
292 -  
293 - func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
294 - let uiImage = UIImage(contentsOfFile: call.arguments as! String) 130 + /// Analyzes a single image
  131 + private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  132 + let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
295 133
296 if (uiImage == nil) { 134 if (uiImage == nil) {
297 result(FlutterError(code: "MobileScanner", 135 result(FlutterError(code: "MobileScanner",
@@ -299,103 +137,32 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -299,103 +137,32 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
299 details: nil)) 137 details: nil))
300 return 138 return
301 } 139 }
302 -  
303 - let image = VisionImage(image: uiImage!)  
304 - image.orientation = imageOrientation(  
305 - deviceOrientation: UIDevice.current.orientation,  
306 - defaultOrientation: .portrait  
307 - )  
308 -  
309 - var barcodeFound = false  
310 -  
311 - scanner.process(image) { [self] barcodes, error in 140 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
312 if error == nil && barcodes != nil { 141 if error == nil && barcodes != nil {
313 for barcode in barcodes! { 142 for barcode in barcodes! {
314 - barcodeFound = true  
315 let event: [String: Any?] = ["name": "barcode", "data": barcode.data] 143 let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
316 - sink?(event) 144 + barcodeHandler.publishEvent(event)
317 } 145 }
318 } else if error != nil { 146 } else if error != nil {
319 - result(FlutterError(code: "MobileScanner",  
320 - message: error?.localizedDescription,  
321 - details: "analyzeImage()"))  
322 - }  
323 - analyzing = false  
324 - result(barcodeFound)  
325 - }  
326 -  
327 - }  
328 -  
329 - func stop(_ result: FlutterResult) {  
330 - if (device == nil) {  
331 - result(FlutterError(code: "MobileScanner",  
332 - message: "Called stop() while already stopped!",  
333 - details: nil))  
334 - return 147 + barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
335 } 148 }
336 - captureSession.stopRunning()  
337 - for input in captureSession.inputs {  
338 - captureSession.removeInput(input)  
339 - }  
340 - for output in captureSession.outputs {  
341 - captureSession.removeOutput(output)  
342 - }  
343 - device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))  
344 - registry.unregisterTexture(textureId)  
345 -  
346 -// analyzeMode = 0  
347 - latestBuffer = nil  
348 - captureSession = nil  
349 - device = nil  
350 - textureId = nil  
351 - 149 + })
352 result(nil) 150 result(nil)
353 } 151 }
354 152
355 - // Observer for torch state 153 + /// Observer for torch state
356 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 154 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
357 switch keyPath { 155 switch keyPath {
358 case "torchMode": 156 case "torchMode":
359 // off = 0; on = 1; auto = 2; 157 // off = 0; on = 1; auto = 2;
360 let state = change?[.newKey] as? Int 158 let state = change?[.newKey] as? Int
361 - let event: [String: Any?] = ["name": "torchState", "data": state]  
362 - sink?(event) 159 + barcodeHandler.publishEvent(["name": "torchState", "data": state])
363 default: 160 default:
364 break 161 break
365 } 162 }
366 } 163 }
367 } 164 }
368 165
369 -class MapArgumentReader {  
370 -  
371 - let args: [String: Any]?  
372 -  
373 - init(_ args: [String: Any]?) {  
374 - self.args = args  
375 - }  
376 -  
377 - func string(key: String) -> String? {  
378 - return args?[key] as? String  
379 - }  
380 -  
381 - func int(key: String) -> Int? {  
382 - return (args?[key] as? NSNumber)?.intValue  
383 - }  
384 -  
385 - func bool(key: String) -> Bool? {  
386 - return (args?[key] as? NSNumber)?.boolValue  
387 - }  
388 -  
389 - func stringArray(key: String) -> [String]? {  
390 - return args?[key] as? [String]  
391 - }  
392 -  
393 - func intArray(key: String) -> [Int]? {  
394 - return args?[key] as? [Int]  
395 - }  
396 -  
397 -}  
398 -  
399 enum DetectionSpeed: Int { 166 enum DetectionSpeed: Int {
400 case noDuplicates = 0 167 case noDuplicates = 0
401 case normal = 1 168 case normal = 1