diff --git a/NotebookSaver/AppleIntelligenceEnhancer.swift b/NotebookSaver/AppleIntelligenceEnhancer.swift new file mode 100644 index 0000000..f653337 --- /dev/null +++ b/NotebookSaver/AppleIntelligenceEnhancer.swift @@ -0,0 +1,55 @@ +import Foundation +import UIKit +import CoreImage + +enum AppleIntelligenceSupport { + static var isAvailable: Bool { + if #available(iOS 26.0, *) { + #if canImport(FoundationModels) + return true + #else + return false + #endif + } + return false + } +} + +struct AppleIntelligenceEnhancer { + static func enhance(image: UIImage) async -> UIImage { + if #available(iOS 26.0, *) { + #if canImport(FoundationModels) + // NOTE: Replace this block with real Foundation Models integration when SDK is available. + // For now, we keep behavior identical to fallback to preserve OCR fidelity. + return fallbackEnhance(image) + #else + return fallbackEnhance(image) + #endif + } else { + return fallbackEnhance(image) + } + } + + private static func fallbackEnhance(_ image: UIImage) -> UIImage { + guard let ciImage = CIImage(image: image) else { return image } + + // Mild exposure increase + let exposure = CIFilter(name: "CIExposureAdjust") + exposure?.setValue(ciImage, forKey: kCIInputImageKey) + exposure?.setValue(0.25, forKey: kCIInputEVKey) + let exposureOutput = exposure?.outputImage ?? ciImage + + // Slight contrast bump, reduced saturation for text clarity + let controls = CIFilter(name: "CIColorControls") + controls?.setValue(exposureOutput, forKey: kCIInputImageKey) + controls?.setValue(1.05, forKey: kCIInputContrastKey) + controls?.setValue(0.0, forKey: kCIInputSaturationKey) + let finalOutput = controls?.outputImage ?? exposureOutput + + let context = CIContext() + if let cg = context.createCGImage(finalOutput, from: finalOutput.extent) { + return UIImage(cgImage: cg) + } + return image + } +} \ No newline at end of file diff --git a/NotebookSaver/CameraView.swift b/NotebookSaver/CameraView.swift index fc5e986..7cfadf1 100644 --- a/NotebookSaver/CameraView.swift +++ b/NotebookSaver/CameraView.swift @@ -319,9 +319,27 @@ extension CameraView { throw CameraManager.CameraError.processingFailed("Could not create UIImage from captured data.") } - // For now, return the original image without any processing - // This could be enhanced with resizing/optimization if needed for the UI - return imageToProcess + let modeRaw = UserDefaults.standard.string(forKey: "imageProcessingMode") ?? ImageProcessingMode.none.rawValue + let mode = ImageProcessingMode(rawValue: modeRaw) ?? .none + + switch mode { + case .none: + return imageToProcess + case .optimized: + // Use existing Core Image based resizing for UI responsiveness + let processor = ImageProcessor() + let maxDimension: CGFloat = 1500 + do { + return try processor.resizeImage(imageToProcess, maxDimension: maxDimension) + } catch { + print("Optimized resize failed, returning original image: \(error.localizedDescription)") + return imageToProcess + } + case .appleIntelligence: + // Use Apple Intelligence if available, otherwise fallback enhancement + let enhanced = await AppleIntelligenceEnhancer.enhance(image: imageToProcess) + return enhanced + } } private func extractTextFromProcessedImage(_ processedImage: UIImage) async throws -> String { diff --git a/NotebookSaver/ImageProcessingMode.swift b/NotebookSaver/ImageProcessingMode.swift new file mode 100644 index 0000000..1481980 --- /dev/null +++ b/NotebookSaver/ImageProcessingMode.swift @@ -0,0 +1,9 @@ +import Foundation + +enum ImageProcessingMode: String, CaseIterable, Identifiable { + case none = "None" + case optimized = "Optimized" + case appleIntelligence = "Apple Intelligence" + + var id: String { rawValue } +} \ No newline at end of file diff --git a/NotebookSaver/SettingsView.swift b/NotebookSaver/SettingsView.swift index 0ae6a99..b45b7da 100644 --- a/NotebookSaver/SettingsView.swift +++ b/NotebookSaver/SettingsView.swift @@ -71,6 +71,8 @@ struct SettingsView: View { static let visionUsesLanguageCorrection = "visionUsesLanguageCorrection" // AI thinking toggle static let thinkingEnabled = "thinkingEnabled" + // Image processing mode + static let imageProcessingMode = "imageProcessingMode" } // === Persisted Settings === @@ -90,6 +92,8 @@ struct SettingsView: View { @AppStorage(StorageKeys.thinkingEnabled) private var thinkingEnabled: Bool = false // Default to false (thinking off) // Text extraction service selection @AppStorage("textExtractorService") private var textExtractorService: String = AppDefaults.textExtractorService + // Image processing mode + @AppStorage(StorageKeys.imageProcessingMode) private var imageProcessingMode: String = ImageProcessingMode.none.rawValue // === State for API Key (using Keychain) === @State private var apiKey: String = "" @@ -368,11 +372,36 @@ struct SettingsView: View { .transition(.opacity.combined(with: .move(edge: .top))) } + // Image Processing Mode - horizontal layout + HStack(spacing: 16) { + Text("Image Processing") + .font(.headline) + .foregroundColor(Color.orangeTabbyText.opacity(0.7)) + .frame(width: 120, alignment: .leading) + + Picker("Image Processing", selection: $imageProcessingMode) { + ForEach(ImageProcessingMode.allCases, id: \.rawValue) { mode in + Text(mode.rawValue).tag(mode.rawValue) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + // Save Photo Toggle - horizontal layout HStack(spacing: 16) { Text("Save Photo") .font(.headline) .foregroundColor(Color.orangeTabbyText.opacity(0.7)) + .overlay( + Group { + if ImageProcessingMode(rawValue: imageProcessingMode) == .appleIntelligence && !AppleIntelligenceSupport.isAvailable { + Text("(AI not available on this device)") + .font(.caption) + .foregroundColor(.red) + .padding(.leading, 8) + } + }, alignment: .trailing + ) Spacer() @@ -1032,6 +1061,8 @@ struct SettingsView: View { thinkingEnabled = false // Reset prompt to default based on thinking state updatePromptForThinking(enabled: thinkingEnabled) + // Reset image processing mode + imageProcessingMode = ImageProcessingMode.none.rawValue // Note: API key is not reset as it's sensitive information }