11// CameraView.swift
22// SwiftUI camera preview with frame capture callback
33
4+ import UIKit
45import SwiftUI
56import AVFoundation
67
78struct CameraView : UIViewControllerRepresentable {
8- var frameHandler : ( UIImage ) -> Void
9+ var frameHandler : ( UIImage , CGRect ) -> Void
910 var captureInterval : TimeInterval = 0.5 // seconds
11+ var showOverlay : Bool = true // For debug purposes. Set this value in LiveMRZScannerView.swift
1012
1113 func makeUIViewController( context: Context ) -> CameraViewController {
1214 let controller = CameraViewController ( )
1315 controller. frameHandler = frameHandler
1416 controller. captureInterval = captureInterval
17+ controller. showOverlay = showOverlay
1518 return controller
1619 }
1720
18- func updateUIViewController( _ uiViewController: CameraViewController , context: Context ) { }
21+ func updateUIViewController( _ uiViewController: CameraViewController , context: Context ) {
22+ uiViewController. showOverlay = showOverlay
23+ }
1924}
2025
2126class CameraViewController : UIViewController , AVCaptureVideoDataOutputSampleBufferDelegate {
22- var frameHandler : ( ( UIImage ) -> Void ) ?
27+ var frameHandler : ( ( UIImage , CGRect ) -> Void ) ?
2328 var captureInterval : TimeInterval = 0.5
29+ var showOverlay : Bool = false
2430 private let session = AVCaptureSession ( )
2531 private let videoOutput = AVCaptureVideoDataOutput ( )
2632 private var lastCaptureTime = Date ( timeIntervalSince1970: 0 )
2733 private var previewLayer : AVCaptureVideoPreviewLayer ?
34+ private var roiOverlay : UIView ? = nil
2835
2936 override func viewDidLoad( ) {
3037 super. viewDidLoad ( )
@@ -45,12 +52,81 @@ class CameraViewController: UIViewController, AVCaptureVideoDataOutputSampleBuff
4552 if let previewLayer = previewLayer {
4653 view. layer. addSublayer ( previewLayer)
4754 }
55+ // ROI overlay - for debugging
56+ if showOverlay && roiOverlay == nil {
57+ let overlay = UIView ( )
58+ overlay. layer. borderColor = UIColor . green. cgColor
59+ overlay. layer. borderWidth = 2.0
60+ overlay. backgroundColor = UIColor . clear
61+ overlay. isUserInteractionEnabled = false
62+ view. addSubview ( overlay)
63+ roiOverlay = overlay
64+ }
4865 session. startRunning ( )
4966 }
5067
68+ private func calculateGreenBoxFrame( ) -> CGRect {
69+ guard let previewLayer = previewLayer else { return . zero }
70+ let videoRect = previewLayer. layerRectConverted ( fromMetadataOutputRect: CGRect ( x: 0 , y: 0 , width: 1 , height: 1 ) )
71+ let visibleRect = videoRect. intersection ( view. bounds)
72+
73+ //Lottie animation frame
74+ let lottieWidth = visibleRect. width * 1.3 // 130% of width
75+ let lottieHeight = visibleRect. height * 1.3 // 130% of height
76+
77+ //bottom 25% of the Lottie animation
78+ let boxHeight = lottieHeight * 0.25
79+
80+ // Center the box horizontally and ensure it's within bounds
81+ let boxX = max ( 0 , ( visibleRect. width - lottieWidth) / 2 )
82+ let boxWidth = min ( lottieWidth, visibleRect. width)
83+
84+ //Vertical offset to move the ROI a bit up. 15% in this case
85+ let verticalOffset = visibleRect. height * 0.15
86+
87+ //GreenBox should stay within the visible area
88+ let maxY = visibleRect. maxY - verticalOffset
89+ let minY = visibleRect. minY
90+ let boxY = max ( minY, min ( maxY - boxHeight, maxY - boxHeight) )
91+ // let boxY = visibleRect.maxY - boxHeight
92+
93+ return CGRect ( x: boxX, y: boxY, width: boxWidth, height: boxHeight)
94+ }
95+
96+ var roiInImageCoordinates : CGRect {
97+ guard let previewLayer = previewLayer else { return . zero }
98+ let videoRect = previewLayer. layerRectConverted ( fromMetadataOutputRect: CGRect ( x: 0 , y: 0 , width: 1 , height: 1 ) )
99+ let greenBox = calculateGreenBoxFrame ( )
100+
101+ // map greenBox to normalized coordinates within videoRect
102+ let normX = ( greenBox. minX - videoRect. minX) / videoRect. width
103+ let normY = ( greenBox. minY - videoRect. minY) / videoRect. height
104+ let normWidth = greenBox. width / videoRect. width
105+ let normHeight = greenBox. height / videoRect. height
106+
107+ // Ensure normalized coordinates are within [0,1] bounds as vision's max ROI is (0,0) to (1,1)
108+ let clampedX = max ( 0 , min ( 1 , normX) )
109+ let clampedY = max ( 0 , min ( 1 , normY) )
110+ let clampedWidth = max ( 0 , min ( 1 - clampedX, normWidth) )
111+ let clampedHeight = max ( 0 , min ( 1 - clampedY, normHeight) )
112+
113+ // Vision expects (0,0) at bottom-left, so flip Y
114+ let roiYVision = 1.0 - clampedY - clampedHeight
115+ let roi = CGRect ( x: clampedX, y: roiYVision, width: clampedWidth, height: clampedHeight)
116+
117+ print ( " [CameraViewController] FINAL ROI for Vision (flipped Y, visible only): \( roi) " )
118+ return roi
119+ }
120+
51121 override func viewDidLayoutSubviews( ) {
52122 super. viewDidLayoutSubviews ( )
123+ // Ensure previewLayer matches the visible area
53124 previewLayer? . frame = view. bounds
125+ print ( " [CameraViewController] view.bounds: \( view. bounds) " )
126+ if let overlay = roiOverlay {
127+ overlay. isHidden = !showOverlay
128+ overlay. frame = calculateGreenBoxFrame ( )
129+ }
54130 }
55131
56132 func captureOutput( _ output: AVCaptureOutput , didOutput sampleBuffer: CMSampleBuffer , from connection: AVCaptureConnection ) {
@@ -62,10 +138,11 @@ class CameraViewController: UIViewController, AVCaptureVideoDataOutputSampleBuff
62138 let context = CIContext ( )
63139 if let cgImage = context. createCGImage ( ciImage, from: ciImage. extent) {
64140 let originalImage = UIImage ( cgImage: cgImage, scale: UIScreen . main. scale, orientation: . right)
65- // Rotate to .up orientation
66141 let uprightImage = originalImage. fixedOrientation ( )
142+ print ( " [CameraViewController] cgImage size: \( cgImage. width) x \( cgImage. height) , preview size: \( view. bounds. size) , orientation: \( uprightImage. imageOrientation. rawValue) " )
143+ let roi = roiInImageCoordinates
67144 DispatchQueue . main. async { [ weak self] in
68- self ? . frameHandler ? ( uprightImage)
145+ self ? . frameHandler ? ( uprightImage, roi )
69146 }
70147 }
71148 }
0 commit comments