SPB Git

spb/focale Public

Swift 100%
3.0 KB · 93 lines swift
Raw Blame History
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