Julian Steenbakker
Committed by GitHub

Merge pull request #347 from juliansteenbakker/feature/return-image-ios

feat: add new error and event handling
@@ -37,5 +37,9 @@ end @@ -37,5 +37,9 @@ end
37 post_install do |installer| 37 post_install do |installer|
38 installer.pods_project.targets.each do |target| 38 installer.pods_project.targets.each do |target|
39 flutter_additional_ios_build_settings(target) 39 flutter_additional_ios_build_settings(target)
  40 + target.build_configurations.each do |config|
  41 + config.build_settings['ENABLE_BITCODE'] = 'NO'
  42 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
  43 + end
40 end 44 end
41 end 45 end
  1 +//
  2 +// BarcodeHandler.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 24/08/2022.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +public class BarcodeHandler: NSObject, FlutterStreamHandler {
  11 +
  12 + var event: [String: Any?] = [:]
  13 +
  14 + private var eventSink: FlutterEventSink?
  15 + private let eventChannel: FlutterEventChannel
  16 +
  17 + init(registrar: FlutterPluginRegistrar) {
  18 + eventChannel = FlutterEventChannel(name:
  19 + "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
  20 + super.init()
  21 + eventChannel.setStreamHandler(self)
  22 + }
  23 +
  24 + func publishEvent(_ event: [String: Any?]) {
  25 + self.event = event
  26 + eventSink?(event)
  27 + }
  28 +
  29 + public func onListen(withArguments arguments: Any?,
  30 + eventSink: @escaping FlutterEventSink) -> FlutterError? {
  31 + self.eventSink = eventSink
  32 + return nil
  33 + }
  34 +
  35 + public func onCancel(withArguments arguments: Any?) -> FlutterError? {
  36 + eventSink = nil
  37 + return nil
  38 + }
  39 +}
  1 +//
  2 +// SwiftMobileScanner.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 15/02/2022.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +import AVFoundation
  11 +import MLKitVision
  12 +import MLKitBarcodeScanning
  13 +
  14 +typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
  15 +
  16 +public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
  17 + /// Capture session of the camera
  18 + var captureSession: AVCaptureSession!
  19 +
  20 + /// The selected camera
  21 + var device: AVCaptureDevice!
  22 +
  23 + /// Barcode scanner for results
  24 + var scanner = BarcodeScanner.barcodeScanner()
  25 +
  26 + /// Return image buffer with the Barcode event
  27 + var returnImage: Bool = false
  28 +
  29 + /// Default position of camera
  30 + var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
  31 +
  32 + /// When results are found, this callback will be called
  33 + let mobileScannerCallback: MobileScannerCallback
  34 +
  35 + /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
  36 + private let registry: FlutterTextureRegistry?
  37 +
  38 + /// Image to be sent to the texture
  39 + var latestBuffer: CVImageBuffer!
  40 +
  41 + /// Texture id of the camera preview for Flutter
  42 + private var textureId: Int64!
  43 +
  44 + var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
  45 +
  46 + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) {
  47 + self.registry = registry
  48 + self.mobileScannerCallback = mobileScannerCallback
  49 + super.init()
  50 + }
  51 +
  52 + /// Check permissions for video
  53 + func checkPermission() -> Int {
  54 + let status = AVCaptureDevice.authorizationStatus(for: .video)
  55 + switch status {
  56 + case .notDetermined:
  57 + return 0
  58 + case .authorized:
  59 + return 1
  60 + default:
  61 + return 2
  62 + }
  63 + }
  64 +
  65 + /// Request permissions for video
  66 + func requestPermission(_ result: @escaping FlutterResult) {
  67 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
  68 + }
  69 +
  70 + /// Start scanning for barcodes
  71 + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
  72 + self.detectionSpeed = detectionSpeed
  73 + if (device != nil) {
  74 + throw MobileScannerError.alreadyStarted
  75 + }
  76 +
  77 + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
  78 + captureSession = AVCaptureSession()
  79 + textureId = registry?.register(self)
  80 +
  81 + // Open the camera device
  82 + if #available(iOS 10.0, *) {
  83 + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first
  84 + } else {
  85 + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first
  86 + }
  87 +
  88 + if (device == nil) {
  89 + throw MobileScannerError.noCamera
  90 + }
  91 +
  92 + // Enable the torch if parameter is set and torch is available
  93 + if (device.hasTorch && device.isTorchAvailable) {
  94 + do {
  95 + try device.lockForConfiguration()
  96 + device.torchMode = torch
  97 + device.unlockForConfiguration()
  98 + } catch {
  99 + throw MobileScannerError.torchError(error)
  100 + }
  101 + }
  102 +
  103 + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
  104 + captureSession.beginConfiguration()
  105 +
  106 + // Add device input
  107 + do {
  108 + let input = try AVCaptureDeviceInput(device: device)
  109 + captureSession.addInput(input)
  110 + } catch {
  111 + throw MobileScannerError.cameraError(error)
  112 + }
  113 +
  114 + captureSession.sessionPreset = AVCaptureSession.Preset.photo;
  115 + // Add video output.
  116 + let videoOutput = AVCaptureVideoDataOutput()
  117 +
  118 + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
  119 + videoOutput.alwaysDiscardsLateVideoFrames = true
  120 +
  121 + videoPosition = cameraPosition
  122 + // calls captureOutput()
  123 + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
  124 +
  125 + captureSession.addOutput(videoOutput)
  126 + for connection in videoOutput.connections {
  127 + connection.videoOrientation = .portrait
  128 + if cameraPosition == .front && connection.isVideoMirroringSupported {
  129 + connection.isVideoMirrored = true
  130 + }
  131 + }
  132 + captureSession.commitConfiguration()
  133 + captureSession.startRunning()
  134 + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
  135 +
  136 + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
  137 + }
  138 +
  139 + struct MobileScannerStartParameters {
  140 + var width: Double = 0.0
  141 + var height: Double = 0.0
  142 + var hasTorch = false
  143 + var textureId: Int64 = 0
  144 + }
  145 +
  146 + /// Stop scanning for barcodes
  147 + func stop() throws {
  148 + if (device == nil) {
  149 + throw MobileScannerError.alreadyStopped
  150 + }
  151 + captureSession.stopRunning()
  152 + for input in captureSession.inputs {
  153 + captureSession.removeInput(input)
  154 + }
  155 + for output in captureSession.outputs {
  156 + captureSession.removeOutput(output)
  157 + }
  158 + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
  159 + registry?.unregisterTexture(textureId)
  160 + textureId = nil
  161 + captureSession = nil
  162 + device = nil
  163 + }
  164 +
  165 + /// Toggle the flashlight between on and off
  166 + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
  167 + if (device == nil) {
  168 + throw MobileScannerError.torchWhenStopped
  169 + }
  170 + do {
  171 + try device.lockForConfiguration()
  172 + device.torchMode = torch
  173 + device.unlockForConfiguration()
  174 + } catch {
  175 + throw MobileScannerError.torchError(error)
  176 + }
  177 + }
  178 +
  179 + /// Analyze a single image
  180 + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
  181 + let image = VisionImage(image: image)
  182 + image.orientation = imageOrientation(
  183 + deviceOrientation: UIDevice.current.orientation,
  184 + defaultOrientation: .portrait,
  185 + position: position
  186 + )
  187 +
  188 + scanner.process(image, completion: callback)
  189 + }
  190 +
  191 + var i = 0
  192 +
  193 + var barcodesString: Array<String?>?
  194 +
  195 + /// Gets called when a new image is added to the buffer
  196 + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  197 + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
  198 + print("Failed to get image buffer from sample buffer.")
  199 + return
  200 + }
  201 + latestBuffer = imageBuffer
  202 + registry?.textureFrameAvailable(textureId)
  203 + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
  204 + i = 0
  205 + let ciImage = latestBuffer.image
  206 +
  207 + let image = VisionImage(image: ciImage)
  208 + image.orientation = imageOrientation(
  209 + deviceOrientation: UIDevice.current.orientation,
  210 + defaultOrientation: .portrait,
  211 + position: videoPosition
  212 + )
  213 +
  214 + scanner.process(image) { [self] barcodes, error in
  215 + if (detectionSpeed == DetectionSpeed.noDuplicates) {
  216 + let newScannedBarcodes = barcodes?.map { barcode in
  217 + return barcode.rawValue
  218 + }
  219 + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
  220 + return
  221 + } else {
  222 + barcodesString = newScannedBarcodes
  223 + }
  224 + }
  225 +
  226 + mobileScannerCallback(barcodes, error, ciImage)
  227 + }
  228 + } else {
  229 + i+=1
  230 + }
  231 + }
  232 +
  233 + /// Convert image buffer to jpeg
  234 + private func ciImageToJpeg(ciImage: CIImage) -> Data {
  235 +
  236 + // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
  237 + let context:CIContext = CIContext.init(options: nil)
  238 + let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
  239 + let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
  240 +
  241 + return uiImage.jpegData(compressionQuality: 0.8)!;
  242 + }
  243 +
  244 + /// Rotates images accordingly
  245 + func imageOrientation(
  246 + deviceOrientation: UIDeviceOrientation,
  247 + defaultOrientation: UIDeviceOrientation,
  248 + position: AVCaptureDevice.Position
  249 + ) -> UIImage.Orientation {
  250 + switch deviceOrientation {
  251 + case .portrait:
  252 + return position == .front ? .leftMirrored : .right
  253 + case .landscapeLeft:
  254 + return position == .front ? .downMirrored : .up
  255 + case .portraitUpsideDown:
  256 + return position == .front ? .rightMirrored : .left
  257 + case .landscapeRight:
  258 + return position == .front ? .upMirrored : .down
  259 + case .faceDown, .faceUp, .unknown:
  260 + return .up
  261 + @unknown default:
  262 + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back)
  263 + }
  264 + }
  265 +
  266 + /// Sends output of OutputBuffer to a Flutter texture
  267 + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
  268 + if latestBuffer == nil {
  269 + return nil
  270 + }
  271 + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
  272 + }
  273 +
  274 +}
  275 +
  1 +//
  2 +// MobileScannerError.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 24/08/2022.
  6 +//
  7 +import Foundation
  8 +
  9 +enum MobileScannerError: Error {
  10 + case noCamera
  11 + case alreadyStarted
  12 + case alreadyStopped
  13 + case torchError(_ error: Error)
  14 + case cameraError(_ error: Error)
  15 + case torchWhenStopped
  16 + case analyzerError(_ error: Error)
  17 +}
1 -//  
2 -// SwiftMobileScanner.swift  
3 -// mobile_scanner  
4 -//  
5 -// Created by Julian Steenbakker on 15/02/2022.  
6 -//  
7 -  
8 -import Foundation  
@@ -395,3 +395,9 @@ class MapArgumentReader { @@ -395,3 +395,9 @@ class MapArgumentReader {
395 } 395 }
396 396
397 } 397 }
  398 +
  399 +enum DetectionSpeed: Int {
  400 + case noDuplicates = 0
  401 + case normal = 1
  402 + case unrestricted = 2
  403 +}