spb/focale Public
Swift 100%
1//2// PhotoCaptureProcessor.swift3// Focale4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//8// Per-capture AVCapturePhotoCaptureDelegate. Collects the processed and9// RAW representations, embeds the capture context into the file's own10// metadata (so the information survives an uninstall), and hands the11// result off. No work happens in the shutter path itself.12//1314@preconcurrency import AVFoundation15import Foundation16import ImageIO1718/// Everything produced by one shutter press, ready for PhotoKit.19struct CapturedPhoto: Sendable {20 var processedData: Data? // HEIC, context embedded in EXIF UserComment21 var rawData: Data? // ProRAW DNG, untouched22 var context: CaptureContext23}2425final class PhotoCaptureProcessor: NSObject, AVCapturePhotoCaptureDelegate {2627 private let context: CaptureContext28 private let completion: @Sendable (Result<CapturedPhoto, Error>) -> Void29 private var processedData: Data?30 private var rawData: Data?31 private var captureError: Error?3233 init(34 context: CaptureContext,35 completion: @escaping @Sendable (Result<CapturedPhoto, Error>) -> Void36 ) {37 self.context = context38 self.completion = completion39 }4041 func photoOutput(42 _ output: AVCapturePhotoOutput,43 didFinishProcessingPhoto photo: AVCapturePhoto,44 error: Error?45 ) {46 if let error {47 captureError = error48 return49 }50 if photo.isRawPhoto {51 // RAW stays byte-for-byte untouched; context lives in the index52 // and in the processed companion.53 rawData = photo.fileDataRepresentation()54 } else {55 processedData = photo.fileDataRepresentation(with: self)56 ?? photo.fileDataRepresentation()57 }58 }5960 func photoOutput(61 _ output: AVCapturePhotoOutput,62 didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings,63 error: Error?64 ) {65 if let error = captureError ?? error {66 completion(.failure(error))67 return68 }69 guard processedData != nil || rawData != nil else {70 completion(.failure(CaptureError.captureFailed))71 return72 }73 completion(.success(CapturedPhoto(74 processedData: processedData,75 rawData: rawData,76 context: context77 )))78 }79}8081// MARK: - Metadata embedding (no re-encode — the customizer swaps only metadata)8283extension PhotoCaptureProcessor: AVCapturePhotoFileDataRepresentationCustomizer {84 func replacementMetadata(for photo: AVCapturePhoto) -> [String: Any]? {85 guard let json = context.metadataJSON() else { return photo.metadata }86 var metadata = photo.metadata87 var exif = metadata[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:]88 exif[kCGImagePropertyExifUserComment as String] = "focale:" + json89 metadata[kCGImagePropertyExifDictionary as String] = exif90 return metadata91 }92}93