// // PhotoCaptureProcessor.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // Per-capture AVCapturePhotoCaptureDelegate. Collects the processed and // RAW representations, embeds the capture context into the file's own // metadata (so the information survives an uninstall), and hands the // result off. No work happens in the shutter path itself. // @preconcurrency import AVFoundation import Foundation import ImageIO /// Everything produced by one shutter press, ready for PhotoKit. struct CapturedPhoto: Sendable { var processedData: Data? // HEIC, context embedded in EXIF UserComment var rawData: Data? // ProRAW DNG, untouched var context: CaptureContext } final class PhotoCaptureProcessor: NSObject, AVCapturePhotoCaptureDelegate { private let context: CaptureContext private let completion: @Sendable (Result) -> Void private var processedData: Data? private var rawData: Data? private var captureError: Error? init( context: CaptureContext, completion: @escaping @Sendable (Result) -> Void ) { self.context = context self.completion = completion } func photoOutput( _ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error? ) { if let error { captureError = error return } if photo.isRawPhoto { // RAW stays byte-for-byte untouched; context lives in the index // and in the processed companion. rawData = photo.fileDataRepresentation() } else { processedData = photo.fileDataRepresentation(with: self) ?? photo.fileDataRepresentation() } } func photoOutput( _ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error? ) { if let error = captureError ?? error { completion(.failure(error)) return } guard processedData != nil || rawData != nil else { completion(.failure(CaptureError.captureFailed)) return } completion(.success(CapturedPhoto( processedData: processedData, rawData: rawData, context: context ))) } } // MARK: - Metadata embedding (no re-encode — the customizer swaps only metadata) extension PhotoCaptureProcessor: AVCapturePhotoFileDataRepresentationCustomizer { func replacementMetadata(for photo: AVCapturePhoto) -> [String: Any]? { guard let json = context.metadataJSON() else { return photo.metadata } var metadata = photo.metadata var exif = metadata[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:] exif[kCGImagePropertyExifUserComment as String] = "focale:" + json metadata[kCGImagePropertyExifDictionary as String] = exif return metadata } }