Skip to content
This repository was archived by the owner on Jan 7, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions WeScan.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
74F7D03A211ACC4B0046AF7E /* UIImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F7D039211ACC4B0046AF7E /* UIImageTests.swift */; };
74F7D03C211ACC6B0046AF7E /* CGAffineTransformTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F7D03B211ACC6B0046AF7E /* CGAffineTransformTests.swift */; };
74F7D03E211ACC890046AF7E /* AVCaptureVideoOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F7D03D211ACC890046AF7E /* AVCaptureVideoOrientationTests.swift */; };
9308ED6323434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9308ED6223434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift */; };
9308ED6423434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9308ED6223434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift */; };
9308ED6523434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9308ED6223434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift */; };
A11C5B9C2046A20C005075FE /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11C5B9B2046A20C005075FE /* Error.swift */; };
A11C5CD920495EA1005075FE /* RectangleFeaturesFunnelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11C5CD820495EA1005075FE /* RectangleFeaturesFunnelTests.swift */; };
A11C5CDB20495EC9005075FE /* AVCaptureVideoOrientation+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11C5CDA20495EC9005075FE /* AVCaptureVideoOrientation+Utils.swift */; };
Expand Down Expand Up @@ -195,6 +198,7 @@
74F7D039211ACC4B0046AF7E /* UIImageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageTests.swift; sourceTree = "<group>"; };
74F7D03B211ACC6B0046AF7E /* CGAffineTransformTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGAffineTransformTests.swift; sourceTree = "<group>"; };
74F7D03D211ACC890046AF7E /* AVCaptureVideoOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVCaptureVideoOrientationTests.swift; sourceTree = "<group>"; };
9308ED6223434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIInterfaceOrientation+Utils.swift"; sourceTree = "<group>"; };
9541C84122155DD3005FBCD3 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
A11C5B9B2046A20C005075FE /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
A11C5CD820495EA1005075FE /* RectangleFeaturesFunnelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RectangleFeaturesFunnelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -440,6 +444,7 @@
C3E2EB8D20B8970800A42E58 /* UIImage+Utils.swift */,
A1DF90F52037187500841A11 /* UIImage+Orientation.swift */,
362967772294C23700B9FC4A /* CGImagePropertyOrientation.swift */,
9308ED6223434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -792,6 +797,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9308ED6323434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift in Sources */,
A1D4BCBD202C4F3800FCDDEC /* HomeViewController.swift in Sources */,
A1D4BCBB202C4F3800FCDDEC /* AppDelegate.swift in Sources */,
);
Expand All @@ -805,6 +811,7 @@
A1DF90F22035992A00841A11 /* CGAffineTransform+Utils.swift in Sources */,
A1F22EA3202DAA74001723AD /* RectangleFeaturesFunnel.swift in Sources */,
A1D4BD0E202C57A400FCDDEC /* CaptureSessionManager.swift in Sources */,
9308ED6423434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift in Sources */,
B9AAE88B219E6C0400205620 /* FocusRectangleView.swift in Sources */,
A1F22ECE2031937E001723AD /* EditScanViewController.swift in Sources */,
A1F22E9F202C8D70001723AD /* Quadrilateral.swift in Sources */,
Expand Down Expand Up @@ -841,6 +848,7 @@
files = (
C3789C9B20CC69AD001B423F /* QuadrilateralViewTests.swift in Sources */,
74F7D038211ACBF90046AF7E /* VisionRectangleDetectorTests.swift in Sources */,
9308ED6523434CB800BE8F37 /* UIInterfaceOrientation+Utils.swift in Sources */,
A1DF90FD203B412600841A11 /* CGPointTests.swift in Sources */,
74F7D03E211ACC890046AF7E /* AVCaptureVideoOrientationTests.swift in Sources */,
B94E76AC221AB7D100C1945D /* CGSizeTests.swift in Sources */,
Expand Down Expand Up @@ -1037,14 +1045,14 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7P4LQNMZS5;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = WeScanSampleProject/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.WeTransfer.WeScanSampleProject;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
Expand All @@ -1054,14 +1062,14 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 7P4LQNMZS5;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = WeScanSampleProject/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.WeTransfer.WeScanSampleProject;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
Expand Down
2 changes: 1 addition & 1 deletion WeScan/Common/Quadrilateral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public struct Quadrilateral: Transformable {
var invertedfromSize = fromSize
let rotated = rotationAngle != 0.0

if rotated && rotationAngle != CGFloat.pi {
if rotated && (rotationAngle.truncatingRemainder(dividingBy: CGFloat.pi) != 0) {
invertedfromSize = CGSize(width: fromSize.height, height: fromSize.width)
}

Expand Down
33 changes: 18 additions & 15 deletions WeScan/Extensions/AVCaptureVideoOrientation+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ extension AVCaptureVideoOrientation {
/// Maps UIDeviceOrientation to AVCaptureVideoOrientation
init?(deviceOrientation: UIDeviceOrientation) {
switch deviceOrientation {
case .portrait:
self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue)
case .portraitUpsideDown:
self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue)
case .landscapeLeft:
self.init(rawValue: AVCaptureVideoOrientation.landscapeLeft.rawValue)
case .landscapeRight:
self.init(rawValue: AVCaptureVideoOrientation.landscapeRight.rawValue)
case .faceUp:
self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue)
case .faceDown:
self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue)
default:
self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue)
case .portrait: self = .portrait
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .portraitUpsideDown: self = .portraitUpsideDown
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeLeft: self = .landscapeLeft
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeRight: self = .landscapeRight
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .faceUp: self = .portrait
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .faceDown: self = .portraitUpsideDown
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

default: self = .portrait
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

}
}

/// Maps UIInterfaceOrientation to AVCaptureVideoOrientation
init?(interfaceOrientation: UIInterfaceOrientation) {
switch interfaceOrientation {
case .portrait: self = .portrait
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .portraitUpsideDown: self = .portraitUpsideDown
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeLeft: self = .landscapeLeft
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeRight: self = .landscapeRight
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

default: return nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

}
}

}
28 changes: 28 additions & 0 deletions WeScan/Extensions/CGImagePropertyOrientation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,32 @@ extension CGImagePropertyOrientation {
self = .right
}
}

init(deviceOrientation: UIDeviceOrientation) {
switch deviceOrientation {
case .portrait: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .portraitUpsideDown: self = .down
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeLeft: self = .left
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeRight: self = .right
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .unknown: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .faceUp: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .faceDown: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

@unknown default:
assertionFailure("Unknow orientation, falling to default")
self = .right
}
}

init(interfaceOrientation: UIInterfaceOrientation) {
switch interfaceOrientation {
case .portrait: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .portraitUpsideDown: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeLeft: self = .left
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .landscapeRight: self = .right
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

case .unknown: self = .up
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Cases inside a switch should always be on a newline

@unknown default:
assertionFailure("Unknow orientation, falling to default")
self = .right
}
}
}
27 changes: 27 additions & 0 deletions WeScan/Extensions/UIInterfaceOrientation+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// UIInterfaceOrientation+Utils.swift
// WeScan
//
// Created by Antoine Harlin on 01/10/2019.
// Copyright © 2019 WeTransfer. All rights reserved.
//

import Foundation
import UIKit

extension UIInterfaceOrientation {
var rotationAngle: CGFloat {
switch self {
case UIInterfaceOrientation.portrait:
return CGFloat.pi / 2
case UIInterfaceOrientation.portraitUpsideDown:
return -CGFloat.pi / 2
case UIInterfaceOrientation.landscapeLeft:
return CGFloat.pi
case UIInterfaceOrientation.landscapeRight:
return CGFloat.pi * 2
default:
return CGFloat.pi / 2
}
}
}
27 changes: 12 additions & 15 deletions WeScan/Scan/CaptureSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ extension CaptureSessionManager: AVCapturePhotoCaptureDelegate {
/// Completes the image capture by processing the image, and passing it to the delegate object.
/// This function is necessary because the capture functions for iOS 10 and 11 are decoupled.
private func completeImageCapture(with imageData: Data) {
let statusBarOrientation = UIApplication.shared.statusBarOrientation
DispatchQueue.global(qos: .background).async { [weak self] in
CaptureSession.current.isEditing = true
guard let image = UIImage(data: imageData) else {
Expand All @@ -292,29 +293,25 @@ extension CaptureSessionManager: AVCapturePhotoCaptureDelegate {
}
return
}

var angle: CGFloat = 0.0

switch image.imageOrientation {
case .right:
angle = CGFloat.pi / 2
case .up:
angle = CGFloat.pi
default:
break
}


let floatAngle = statusBarOrientation.rotationAngle + CGFloat.pi
let angle = Measurement(value: Double(floatAngle), unit: UnitAngle.radians)
let orientedImage = image.rotated(by: angle) ?? image
var quad: Quadrilateral?
if let displayedRectangleResult = self?.displayedRectangleResult {
quad = self?.displayRectangleResult(rectangleResult: displayedRectangleResult)
quad = quad?.scale(displayedRectangleResult.imageSize, image.size, withRotationAngle: angle)
quad = quad?.scale(
displayedRectangleResult.imageSize,
orientedImage.size,
withRotationAngle: statusBarOrientation.rotationAngle
)
}

DispatchQueue.main.async {
guard let strongSelf = self else {
return
}
strongSelf.delegate?.captureSessionManager(strongSelf, didCapturePicture: image, withQuad: quad)
strongSelf.delegate?.captureSessionManager(strongSelf, didCapturePicture: orientedImage, withQuad: quad)
}
}
}
Expand Down
66 changes: 50 additions & 16 deletions WeScan/Scan/ScannerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,23 @@ final class ScannerViewController: UIViewController {
navigationController?.navigationBar.sendSubviewToBack(visualEffectView)

navigationController?.navigationBar.barStyle = .blackTranslucent

setupVideoOrientation()
setupViewsForCurrentOrientation()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

videoPreviewLayer.frame = view.layer.bounds

let statusBarHeight = UIApplication.shared.statusBarFrame.size.height
let visualEffectRect = self.navigationController?.navigationBar.bounds.insetBy(dx: 0, dy: -(statusBarHeight)).offsetBy(dx: 0, dy: -statusBarHeight)

visualEffectView.frame = visualEffectRect ?? CGRect.zero

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupViewsForCurrentOrientation()
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] (context) in
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Unused parameter “context” in a closure should be replaced with _.

self?.setupViewsForCurrentOrientation()
}, completion: { [weak self] (_) in
self?.setupVideoOrientation()
})
}

override func viewWillDisappear(_ animated: Bool) {
Expand Down Expand Up @@ -140,7 +146,20 @@ final class ScannerViewController: UIViewController {
view.addSubview(shutterButton)
view.addSubview(activityIndicator)
}


private func setupViewsForCurrentOrientation() {
let statusBarHeight = UIApplication.shared.statusBarFrame.size.height
let visualEffectRect = self.navigationController?.navigationBar.bounds.insetBy(
dx: 0,
dy: -(statusBarHeight)
).offsetBy(
dx: 0,
dy: -statusBarHeight
)
self.visualEffectView.frame = visualEffectRect ?? CGRect.zero
videoPreviewLayer.frame = view.layer.bounds
}

private func setupNavigationBar() {
navigationItem.setLeftBarButton(flashButton, animated: false)
navigationItem.setRightBarButton(autoScanButton, animated: false)
Expand Down Expand Up @@ -196,6 +215,17 @@ final class ScannerViewController: UIViewController {

NSLayoutConstraint.activate(quadViewConstraints + cancelButtonConstraints + shutterButtonConstraints + activityIndicatorConstraints)
}

func setupVideoOrientation() {
let statusBarOrientation = UIApplication.shared.statusBarOrientation
var initialVideoOrientation: AVCaptureVideoOrientation = .portrait
if statusBarOrientation != .unknown {
if let videoOrientation = AVCaptureVideoOrientation(rawValue: statusBarOrientation.rawValue) {
initialVideoOrientation = videoOrientation
}
}
videoPreviewLayer.connection?.videoOrientation = initialVideoOrientation
}

// MARK: - Tap to Focus

Expand Down Expand Up @@ -302,7 +332,6 @@ extension ScannerViewController: RectangleDetectionDelegateProtocol {

func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didCapturePicture picture: UIImage, withQuad quad: Quadrilateral?) {
activityIndicator.stopAnimating()

let editVC = EditScanViewController(image: picture, quad: quad)
navigationController?.pushViewController(editVC, animated: false)

Expand All @@ -315,13 +344,18 @@ extension ScannerViewController: RectangleDetectionDelegateProtocol {
quadView.removeQuadrilateral()
return
}

let statusBarOrientation = UIApplication.shared.statusBarOrientation

let orientedImageSize = CGSize(
width: statusBarOrientation.isPortrait ? imageSize.height : imageSize.width,
height: statusBarOrientation.isPortrait ? imageSize.width : imageSize.height
)

let portraitImageSize = CGSize(width: imageSize.height, height: imageSize.width)

let scaleTransform = CGAffineTransform.scaleTransform(forSize: portraitImageSize, aspectFillInSize: quadView.bounds.size)
let scaleTransform = CGAffineTransform.scaleTransform(forSize: orientedImageSize, aspectFillInSize: quadView.bounds.size)
let scaledImageSize = imageSize.applying(scaleTransform)

let rotationTransform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0)
let rotationTransform = CGAffineTransform(rotationAngle: statusBarOrientation.rotationAngle)

let imageBounds = CGRect(origin: .zero, size: scaledImageSize).applying(rotationTransform)

Expand Down
4 changes: 3 additions & 1 deletion WeScanSampleProject/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ final class HomeViewController: UIViewController {

@objc func scanOrSelectImage(_ sender: UIButton) {
let actionSheet = UIAlertController(title: "Would you like to scan an image or select one from your photo library?", message: nil, preferredStyle: .actionSheet)

actionSheet.popoverPresentationController?.sourceView = sender
let scanAction = UIAlertAction(title: "Scan", style: .default) { (_) in
self.scanImage()
}
Expand All @@ -116,6 +116,7 @@ final class HomeViewController: UIViewController {

func scanImage() {
let scannerViewController = ImageScannerController(delegate: self)
scannerViewController.modalPresentationStyle = .fullScreen
present(scannerViewController, animated: true)
}

Expand Down Expand Up @@ -153,6 +154,7 @@ extension HomeViewController: UIImagePickerControllerDelegate, UINavigationContr

guard let image = info[.originalImage] as? UIImage else { return }
let scannerViewController = ImageScannerController(image: image, delegate: self)
scannerViewController.modalPresentationStyle = .fullScreen
present(scannerViewController, animated: true)
}
}