Files
Ascently/ios/OpenClimb/Utils/OrientationAwareImage.swift
Atridad Lahiji 30d2b3938e
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m29s
[Android] 1.9.2
2025-10-12 20:41:39 -06:00

148 lines
4.7 KiB
Swift

import SwiftUI
import UIKit
struct OrientationAwareImage: View {
let imagePath: String
let contentMode: ContentMode
@State private var uiImage: UIImage?
@State private var isLoading = true
@State private var hasFailed = false
init(imagePath: String, contentMode: ContentMode = .fit) {
self.imagePath = imagePath
self.contentMode = contentMode
}
var body: some View {
Group {
if let uiImage = uiImage {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: contentMode)
} else if hasFailed {
Image(systemName: "photo")
.foregroundColor(.gray)
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
.onAppear {
loadImageWithCorrectOrientation()
}
.onChange(of: imagePath) { _ in
loadImageWithCorrectOrientation()
}
}
private func loadImageWithCorrectOrientation() {
Task.detached(priority: .userInitiated) {
let correctedImage = await loadAndCorrectImage()
await MainActor.run {
self.uiImage = correctedImage
self.isLoading = false
self.hasFailed = correctedImage == nil
}
}
}
private func loadAndCorrectImage() async -> UIImage? {
guard let data = ImageManager.shared.loadImageData(fromPath: imagePath) else { return nil }
guard let originalImage = UIImage(data: data) else { return nil }
return correctImageOrientation(originalImage)
}
/// Corrects the orientation of a UIImage based on its EXIF data
private func correctImageOrientation(_ image: UIImage) -> UIImage {
// If the image is already in the correct orientation, return as-is
if image.imageOrientation == .up {
return image
}
// Calculate the proper transformation matrix
var transform = CGAffineTransform.identity
switch image.imageOrientation {
case .down, .downMirrored:
transform = transform.translatedBy(x: image.size.width, y: image.size.height)
transform = transform.rotated(by: .pi)
case .left, .leftMirrored:
transform = transform.translatedBy(x: image.size.width, y: 0)
transform = transform.rotated(by: .pi / 2)
case .right, .rightMirrored:
transform = transform.translatedBy(x: 0, y: image.size.height)
transform = transform.rotated(by: -.pi / 2)
case .up, .upMirrored:
break
@unknown default:
break
}
switch image.imageOrientation {
case .upMirrored, .downMirrored:
transform = transform.translatedBy(x: image.size.width, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .leftMirrored, .rightMirrored:
transform = transform.translatedBy(x: image.size.height, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .up, .down, .left, .right:
break
@unknown default:
break
}
// Create a new image context and apply the transformation
guard let cgImage = image.cgImage else { return image }
let context = CGContext(
data: nil,
width: Int(image.size.width),
height: Int(image.size.height),
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: 0,
space: cgImage.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
bitmapInfo: cgImage.bitmapInfo.rawValue
)
guard let ctx = context else { return image }
ctx.concatenate(transform)
switch image.imageOrientation {
case .left, .leftMirrored, .right, .rightMirrored:
ctx.draw(
cgImage, in: CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width))
default:
ctx.draw(
cgImage, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
}
guard let newCGImage = ctx.makeImage() else { return image }
return UIImage(cgImage: newCGImage)
}
}
// MARK: - Convenience Extensions
extension OrientationAwareImage {
/// Creates an orientation-aware image with fill content mode
static func fill(imagePath: String) -> OrientationAwareImage {
OrientationAwareImage(imagePath: imagePath, contentMode: .fill)
}
/// Creates an orientation-aware image with fit content mode
static func fit(imagePath: String) -> OrientationAwareImage {
OrientationAwareImage(imagePath: imagePath, contentMode: .fit)
}
}