All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m29s
148 lines
4.7 KiB
Swift
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)
|
|
}
|
|
}
|