218 lines
7.1 KiB
Swift
218 lines
7.1 KiB
Swift
//
|
|
// CameraView.swift
|
|
// CreditCardScannerPackageDescription
|
|
//
|
|
// Created by josh on 2020/07/23.
|
|
//
|
|
#if canImport(UIKit)
|
|
#if canImport(AVFoundation)
|
|
|
|
import AVFoundation
|
|
import UIKit
|
|
import VideoToolbox
|
|
|
|
protocol CameraViewDelegate: AnyObject {
|
|
func didCapture(image: CGImage)
|
|
func didError(with: CreditCardScannerError)
|
|
}
|
|
|
|
@available(iOS 13, *)
|
|
final class CameraView: UIView {
|
|
|
|
weak var delegate: CameraViewDelegate?
|
|
private let creditCardFrameStrokeColor: UIColor = .white
|
|
private let maskLayerColor: UIColor = .black
|
|
private let maskLayerAlpha: CGFloat = 0.7
|
|
|
|
// MARK: - Capture related
|
|
|
|
private let captureSessionQueue = DispatchQueue(
|
|
label: "com.yhkaplan.credit-card-scanner.captureSessionQueue"
|
|
)
|
|
|
|
// MARK: - Capture related
|
|
|
|
private let sampleBufferQueue = DispatchQueue(
|
|
label: "com.yhkaplan.credit-card-scanner.sampleBufferQueue"
|
|
)
|
|
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
}
|
|
|
|
private let imageRatio: ImageRatio = .vga640x480
|
|
|
|
// MARK: - Region of interest and text orientation
|
|
|
|
/// Region of video data output buffer that recognition should be run on.
|
|
/// Gets recalculated once the bounds of the preview layer are known.
|
|
private var regionOfInterest: CGRect?
|
|
|
|
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
|
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
|
|
fatalError("Expected `AVCaptureVideoPreviewLayer` type for layer. Check PreviewView.layerClass implementation.")
|
|
}
|
|
|
|
return layer
|
|
}
|
|
|
|
private var videoSession: AVCaptureSession? {
|
|
get {
|
|
videoPreviewLayer.session
|
|
}
|
|
set {
|
|
videoPreviewLayer.session = newValue
|
|
}
|
|
}
|
|
|
|
let semaphore = DispatchSemaphore(value: 1)
|
|
|
|
override class var layerClass: AnyClass {
|
|
AVCaptureVideoPreviewLayer.self
|
|
}
|
|
|
|
func stopSession() {
|
|
videoSession?.stopRunning()
|
|
}
|
|
|
|
func startSession() {
|
|
videoSession?.startRunning()
|
|
}
|
|
|
|
func setupCamera() {
|
|
captureSessionQueue.async { [weak self] in
|
|
self?._setupCamera()
|
|
}
|
|
}
|
|
|
|
private func _setupCamera() {
|
|
let session = AVCaptureSession()
|
|
session.beginConfiguration()
|
|
session.sessionPreset = imageRatio.preset
|
|
|
|
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
|
|
for: .video,
|
|
position: .back) else {
|
|
delegate?.didError(with: CreditCardScannerError(kind: .cameraSetup))
|
|
return
|
|
}
|
|
|
|
do {
|
|
let deviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
|
session.canAddInput(deviceInput)
|
|
session.addInput(deviceInput)
|
|
} catch {
|
|
delegate?.didError(with: CreditCardScannerError(kind: .cameraSetup, underlyingError: error))
|
|
}
|
|
|
|
let videoOutput = AVCaptureVideoDataOutput()
|
|
videoOutput.alwaysDiscardsLateVideoFrames = true
|
|
videoOutput.setSampleBufferDelegate(self, queue: sampleBufferQueue)
|
|
|
|
guard session.canAddOutput(videoOutput) else {
|
|
delegate?.didError(with: CreditCardScannerError(kind: .cameraSetup))
|
|
return
|
|
}
|
|
|
|
session.addOutput(videoOutput)
|
|
session.connections.forEach {
|
|
$0.videoOrientation = .portrait
|
|
}
|
|
session.commitConfiguration()
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.videoPreviewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
|
self?.videoSession = session
|
|
self?.startSession()
|
|
}
|
|
}
|
|
|
|
func setupRegionOfInterest() {
|
|
guard regionOfInterest == nil else { return }
|
|
/// Mask layer that covering area around camera view
|
|
|
|
let boundsWithSafeArea = CGRect(x: bounds.minX, y: bounds.minY, width: bounds.width, height: bounds.height + safeAreaInsets.bottom + 10)
|
|
let backLayer = CALayer()
|
|
backLayer.frame = boundsWithSafeArea
|
|
backLayer.backgroundColor = maskLayerColor.withAlphaComponent(maskLayerAlpha).cgColor
|
|
|
|
// culcurate cutoutted frame
|
|
let cuttedWidth: CGFloat = bounds.width - 40.0
|
|
let cuttedHeight: CGFloat = cuttedWidth * CreditCard.heightRatioAgainstWidth
|
|
|
|
let centerVertical = (bounds.height / 2.5)
|
|
let cuttedY: CGFloat = centerVertical - (cuttedHeight / 2.0)
|
|
let cuttedX: CGFloat = 20.0
|
|
|
|
let cuttedRect = CGRect(x: cuttedX,
|
|
y: cuttedY,
|
|
width: cuttedWidth,
|
|
height: cuttedHeight)
|
|
|
|
let maskLayer = CAShapeLayer()
|
|
maskLayer.path = UIBezierPath(rect: boundsWithSafeArea).cgPath
|
|
maskLayer.fillRule = .evenOdd
|
|
backLayer.mask = maskLayer
|
|
layer.addSublayer(backLayer)
|
|
|
|
let strokeLayer = CAShapeLayer()
|
|
strokeLayer.lineWidth = 3.0
|
|
strokeLayer.strokeColor = creditCardFrameStrokeColor.cgColor
|
|
strokeLayer.path = UIBezierPath(roundedRect: cuttedRect, cornerRadius: 10.0).cgPath
|
|
strokeLayer.fillColor = nil
|
|
layer.addSublayer(strokeLayer)
|
|
|
|
let imageHeight: CGFloat = imageRatio.imageHeight
|
|
let imageWidth: CGFloat = imageRatio.imageWidth
|
|
|
|
let acutualImageRatioAgainstVisibleSize = imageWidth / bounds.width
|
|
let interestX = cuttedRect.origin.x * acutualImageRatioAgainstVisibleSize
|
|
let interestWidth = cuttedRect.width * acutualImageRatioAgainstVisibleSize
|
|
let interestHeight = interestWidth * CreditCard.heightRatioAgainstWidth
|
|
let interestY = (imageHeight / 2.0) - (interestHeight / 2.0)
|
|
regionOfInterest = CGRect(x: interestX,
|
|
y: interestY,
|
|
width: interestWidth,
|
|
height: interestHeight)
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, *)
|
|
extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
|
|
semaphore.wait()
|
|
defer { semaphore.signal() }
|
|
|
|
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
|
delegate?.didError(with: CreditCardScannerError(kind: .capture))
|
|
delegate = nil
|
|
return
|
|
}
|
|
|
|
var cgImage: CGImage?
|
|
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
|
|
|
|
guard let regionOfInterest = regionOfInterest else {
|
|
return
|
|
}
|
|
|
|
guard let fullCameraImage = cgImage,
|
|
let croppedImage = fullCameraImage.cropping(to: regionOfInterest) else {
|
|
delegate?.didError(with: CreditCardScannerError(kind: .capture))
|
|
delegate = nil
|
|
return
|
|
}
|
|
|
|
delegate?.didCapture(image: croppedImage)
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
extension CreditCard {
|
|
// The aspect ratio of credit-card is Golden-ratio
|
|
static let heightRatioAgainstWidth: CGFloat = 0.6180469716
|
|
}
|