import Foundation import Vision import CoreGraphics import ImageIO struct CardFingerprint: Codable, Identifiable, Sendable { let id: UUID let name: String let setCode: String let collectorNumber: String let hasFoilPrinting: Bool let hasSerializedPrinting: Bool? let featureData: Data var priceScanned: Double? = nil } struct CardMetadata: Identifiable, Sendable { let id: UUID let name: String let setCode: String let collectorNumber: String let hasFoilPrinting: Bool let hasSerializedPrinting: Bool var priceScanned: Double? = nil var rarity: String? = nil var colorIdentity: [String]? = nil var isSerialized: Bool = false } struct SavedCard: Codable, Identifiable, Hashable, Sendable { let id: UUID let scryfallID: String var name: String var setCode: String var collectorNumber: String let imageFileName: String var condition: String var foilType: String var currentValuation: Double? var previousValuation: Double? var dateAdded: Date var classification: String var collectionName: String var storageLocation: String var rarity: String? var colorIdentity: [String]? // Grading Fields var gradingService: String? // PSA, BGS var grade: String? // 10, 9.5 var certNumber: String? // 123456 var isCustomValuation: Bool = false var isSerialized: Bool? = false var currencyCode: String? init(from scan: CardMetadata, imageName: String, collection: String, location: String) { self.id = UUID() self.scryfallID = "\(scan.setCode)-\(scan.collectorNumber)" self.name = scan.name self.setCode = scan.setCode self.collectorNumber = scan.collectorNumber self.imageFileName = imageName self.condition = AppConfig.Defaults.defaultCondition self.foilType = AppConfig.Defaults.defaultFoil self.currentValuation = scan.priceScanned self.previousValuation = scan.priceScanned self.dateAdded = Date() self.classification = "Unknown" self.collectionName = collection self.storageLocation = location self.rarity = scan.rarity self.colorIdentity = scan.colorIdentity self.isSerialized = scan.isSerialized } } class FeatureMatcher { static let revision = VNGenerateImageFeaturePrintRequest.Revision.revision1 static func generateFingerprint(from image: CGImage, orientation: CGImagePropertyOrientation = .up) async throws -> VNFeaturePrintObservation { let req = VNGenerateImageFeaturePrintRequest() req.revision = revision req.imageCropAndScaleOption = .scaleFill let handler = VNImageRequestHandler(cgImage: image, orientation: orientation, options: [:]) try handler.perform([req]) guard let result = req.results?.first as? VNFeaturePrintObservation else { throw NSError(domain: "FeatureMatcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "No features detected"]) } return result } static func identify(scan: VNFeaturePrintObservation, database: [CardMetadata], cache: [UUID: VNFeaturePrintObservation]) -> MatchResult { var candidates: [(CardMetadata, Float)] = [] for card in database { guard let obs = cache[card.id] else { continue } var dist: Float = 0 if (try? scan.computeDistance(&dist, to: obs)) != nil && dist < 18.0 { candidates.append((card, dist)) } } let sorted = candidates.sorted { $0.1 < $1.1 } guard let best = sorted.first else { return .unknown } if best.1 < 6.0 { return .exact(best.0) } let close = sorted.filter { $0.1 < (best.1 + 3.0) } if close.count > 1 { return .ambiguous(name: best.0.name, candidates: close.map { $0.0 }) } return .exact(best.0) } } enum MatchResult { case exact(CardMetadata) case ambiguous(name: String, candidates: [CardMetadata]) case unknown }