mana-swift-llm/Sources/ManaLLM/Byok/ByokImagePrep.swift
till 3ceca017f1 Nativer EXIF-Strip für Vision: ByokImagePrep + describeImage(rawImageData:)
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>
2026-06-05 16:18:08 +02:00

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")
}
}