Schließt die compliance-kritische native Lücke (web hatte prepareImageForVision, nativ nicht). - ByokImagePrep.prepareForVision: ImageIO-Re-Encode → EXIF/GPS gestrippt + Downscale ≤2048px (Orientierung in Pixel gebacken, kein Metadaten-Dict). - ManaLLM.describeImage(rawImageData:…): Convenience, die den Strip selbst macht → Native-Konsument kann ihn nicht vergessen. - 3 Tests (Downscale-Cap verifiziert, Garbage→nil), swift test 39/39 grün. Native Vision/TTS-KONSUMENT bleibt Xcode (Tills Hand) — das hier ist das verifizierbare Fundament dafür. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.4 KiB
Swift
55 lines
2.4 KiB
Swift
import CoreGraphics
|
|
import Foundation
|
|
import ImageIO
|
|
import UniformTypeIdentifiers
|
|
|
|
/// Bereitet ein Bild für einen BYOK-Vision-Call vor: **strippt alle
|
|
/// Metadaten (EXIF inkl. GPS)** und **cappt die Kantenlänge**. Swift-
|
|
/// Pendant zu `prepareImageForVision` aus `@mana/llm-client` (web).
|
|
///
|
|
/// **Pflicht** vor `ManaLLM.describeImage` — Bilder gehen Gerät-direkt zum
|
|
/// Dritt-Anbieter, und sensible Inhalte (Standort/Gesichter) dürfen da
|
|
/// nicht mitfahren (COMPLIANCE.md §5). Das Re-Enkodieren über
|
|
/// `CGImageDestination` **ohne Metadaten-Dictionary** entfernt EXIF/GPS
|
|
/// vollständig; `kCGImageSourceCreateThumbnailWithTransform` bäckt die
|
|
/// Orientierung in die Pixel, sodass kein Orientation-Tag nötig bleibt.
|
|
public enum ByokImagePrep {
|
|
/// - Parameters:
|
|
/// - data: Roh-Bilddaten (JPEG/PNG/HEIC/…).
|
|
/// - maxEdge: maximale Kantenlänge in px (längste Seite). Default 2048.
|
|
/// - quality: JPEG-Qualität 0..1. Default 0.85.
|
|
/// - Returns: EXIF-freies JPEG + MIME-Type, oder `nil` bei Dekodier-Fehler.
|
|
public static func prepareForVision(
|
|
_ data: Data,
|
|
maxEdge: Int = 2048,
|
|
quality: Double = 0.85
|
|
) -> (data: Data, mimeType: String)? {
|
|
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
return nil
|
|
}
|
|
let thumbOptions: [CFString: Any] = [
|
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
// Orientierung in die Pixel backen → kein Orientation-EXIF nötig.
|
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
kCGImageSourceThumbnailMaxPixelSize: maxEdge,
|
|
]
|
|
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(
|
|
source, 0, thumbOptions as CFDictionary
|
|
) else { return nil }
|
|
|
|
let output = NSMutableData()
|
|
guard let dest = CGImageDestinationCreateWithData(
|
|
output as CFMutableData,
|
|
UTType.jpeg.identifier as CFString,
|
|
1,
|
|
nil
|
|
) else { return nil }
|
|
// Nur Kompressions-Qualität, KEIN Metadaten-Dict → kein EXIF/GPS.
|
|
let destOptions: [CFString: Any] = [
|
|
kCGImageDestinationLossyCompressionQuality: quality,
|
|
]
|
|
CGImageDestinationAddImage(dest, cgImage, destOptions as CFDictionary)
|
|
guard CGImageDestinationFinalize(dest) else { return nil }
|
|
return (output as Data, "image/jpeg")
|
|
}
|
|
}
|