ImageOrientation.swift
122 lignes · 5003 octets
import UIKit import ImageIO /// Port of android `scan/ImageOrientation.kt`. /// Normalizes a photo's orientation before scan/OCR: /// - EXIF rotations (90/180/270) and mirrored variants /// - camera rotation fallback when EXIF is absent/normal /// - EXIF and camera rotation are never applied together (no double rotation) enum ImageOrientation { struct OrientedBitmap { let bitmap: UIImage /// Applied quarter-turn degrees (0/90/180/270), mirrors excluded. let appliedRotationDegrees: Int /// true when the correction came from EXIF, false for the camera fallback. let fromExif: Bool let exifOrientation: CGImagePropertyOrientation } /// `UIImage(data:)` maps the JPEG EXIF orientation onto `imageOrientation`. static func fromJpegBytes(_ data: Data, cameraRotationDegrees: Int = 0) -> OrientedBitmap? { guard let image = UIImage(data: data) else { return nil } return orient(image, cameraRotationDegrees: cameraRotationDegrees) } static func fromFile(_ url: URL, cameraRotationDegrees: Int = 0) -> OrientedBitmap? { guard let data = try? Data(contentsOf: url) else { return nil } return fromJpegBytes(data, cameraRotationDegrees: cameraRotationDegrees) } /// Applies EXIF when present and not normal, otherwise the camera rotation. static func orient(_ image: UIImage, cameraRotationDegrees: Int = 0) -> OrientedBitmap { let exifOrientation = cgOrientation(from: image.imageOrientation) if exifOrientation != .up { return OrientedBitmap( bitmap: bakeOrientation(image), appliedRotationDegrees: exifRotationDegrees(exifOrientation), fromExif: true, exifOrientation: exifOrientation ) } let deg = normalizeDegrees(cameraRotationDegrees) return OrientedBitmap( bitmap: deg == 0 ? image : rotate(image, degrees: deg), appliedRotationDegrees: deg, fromExif: false, exifOrientation: exifOrientation ) } /// Kotlin `exifRotationDegrees` — rotation component per EXIF value. static func exifRotationDegrees(_ orientation: CGImagePropertyOrientation) -> Int { switch orientation { case .right, .leftMirrored: // EXIF 6 (rotate 90) / 5 (transpose) return 90 case .down, .downMirrored: // EXIF 3 (rotate 180) / 4 (flip vertical) return 180 case .left, .rightMirrored: // EXIF 8 (rotate 270) / 7 (transverse) return 270 case .up, .upMirrored: return 0 } } /// Kotlin `normalizeDegrees` — quantizes any angle to the nearest quarter turn. static func normalizeDegrees(_ degrees: Int) -> Int { var d = degrees % 360 if d < 0 { d += 360 } switch d { case 45..<135: return 90 case 135..<225: return 180 case 225..<315: return 270 default: return 0 } } static func cgOrientation(from orientation: UIImage.Orientation) -> CGImagePropertyOrientation { switch orientation { case .up: return .up case .upMirrored: return .upMirrored case .down: return .down case .downMirrored: return .downMirrored case .left: return .left case .leftMirrored: return .leftMirrored case .right: return .right case .rightMirrored: return .rightMirrored @unknown default: return .up } } /// Kotlin `applyExifTransform` equivalent: bakes the orientation (rotation and /// mirror) into the pixels so downstream Vision/CI passes see upright data. static func bakeOrientation(_ image: UIImage) -> UIImage { guard image.imageOrientation != .up else { return image } let format = UIGraphicsImageRendererFormat.default() format.scale = image.scale return UIGraphicsImageRenderer(size: image.size, format: format).image { _ in image.draw(in: CGRect(origin: .zero, size: image.size)) } } /// Clockwise quarter-turn rotation (Kotlin `rotate`). static func rotate(_ image: UIImage, degrees: Int) -> UIImage { let swaps = degrees == 90 || degrees == 270 let size = swaps ? CGSize(width: image.size.height, height: image.size.width) : image.size let format = UIGraphicsImageRendererFormat.default() format.scale = image.scale return UIGraphicsImageRenderer(size: size, format: format).image { context in let cg = context.cgContext cg.translateBy(x: size.width / 2, y: size.height / 2) cg.rotate(by: CGFloat(degrees) * .pi / 180) image.draw(in: CGRect( x: -image.size.width / 2, y: -image.size.height / 2, width: image.size.width, height: image.size.height )) } } }
GitRust