Files
stackdex_neu/StackDex/Services/ImagePreprocessor.swift
Matthias a60a76b797 Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
2026-04-19 21:11:32 +02:00

100 lines
3.2 KiB
Swift

import CoreImage
import UIKit
struct PreparedImage {
let normalizedImage: UIImage
let analysisCGImage: CGImage
let uploadJPEGData: Data
let thumbnailJPEGData: Data?
}
enum ImagePreprocessorError: LocalizedError {
case unableToCreateImage
var errorDescription: String? {
switch self {
case .unableToCreateImage:
return "The selected image could not be prepared for OCR."
}
}
}
struct ImagePreprocessor {
private let ciContext = CIContext()
func prepare(_ image: UIImage) throws -> PreparedImage {
let upright = normalized(image)
let resized = resizedImage(from: upright, maxDimension: 2_048)
let enhanced = enhancedImage(from: resized) ?? resized
guard let cgImage = makeCGImage(from: enhanced) else {
throw ImagePreprocessorError.unableToCreateImage
}
return PreparedImage(
normalizedImage: enhanced,
analysisCGImage: cgImage,
uploadJPEGData: enhanced.jpegData(compressionQuality: 0.82) ?? Data(),
thumbnailJPEGData: resizedImage(from: enhanced, maxDimension: 240).jpegData(compressionQuality: 0.65)
)
}
private func normalized(_ image: UIImage) -> UIImage {
guard image.imageOrientation != .up else { return image }
let renderer = UIGraphicsImageRenderer(size: image.size)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: image.size))
}
}
private func resizedImage(from image: UIImage, maxDimension: CGFloat) -> UIImage {
let largestDimension = max(image.size.width, image.size.height)
guard largestDimension > maxDimension else { return image }
let scale = maxDimension / largestDimension
let targetSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
}
private func enhancedImage(from image: UIImage) -> UIImage? {
guard let ciImage = CIImage(image: image) else { return nil }
let adjusted = ciImage
.applyingFilter("CIColorControls", parameters: [
kCIInputContrastKey: 1.08,
kCIInputSaturationKey: 0.96,
kCIInputBrightnessKey: 0.01,
])
.applyingFilter("CISharpenLuminance", parameters: [
kCIInputSharpnessKey: 0.35,
])
guard let cgImage = ciContext.createCGImage(adjusted, from: adjusted.extent) else {
return nil
}
return UIImage(cgImage: cgImage)
}
private func makeCGImage(from image: UIImage) -> CGImage? {
if let cgImage = image.cgImage {
return cgImage
}
if let ciImage = image.ciImage {
return ciContext.createCGImage(ciImage, from: ciImage.extent)
}
let renderer = UIGraphicsImageRenderer(size: image.size)
let rendered = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: image.size))
}
return rendered.cgImage
}
}