···2626 /// Normalized face bounding boxes (Vision coordinates: origin bottom-left, 0-1 range)
2727 var faceRegions: [CGRect] = []
28282929+ // Image metadata (populated during import)
3030+ var pixelWidth: Int = 0
3131+ var pixelHeight: Int = 0
3232+ var fileSize: Int64 = 0
3333+ // Paired file metadata (only set when pairedURL exists)
3434+ var pairedPixelWidth: Int = 0
3535+ var pairedPixelHeight: Int = 0
3636+ var pairedFileSize: Int64 = 0
3737+2938 // Populated by ShotGrouper
3039 var captureDate: Date?
3140
+35-1
cull/Services/PhotoImporter.swift
···6060 }
6161 }
62626363- // Read EXIF dates sequentially (header-only reads are fast, ~1ms each)
6363+ // Read EXIF dates + image metadata sequentially (header-only reads are fast, ~1ms each)
6464 for photo in photos {
6565 let dateURL = photo.pairedURL ?? photo.url
6666 photo.captureDate = readCaptureDate(from: dateURL)
6767+ readImageMetadata(from: photo.url, into: photo)
6868+ if let pairedURL = photo.pairedURL {
6969+ readPairedMetadata(from: pairedURL, into: photo)
7070+ }
6771 }
68726973 photos.sort { ($0.captureDate ?? .distantPast) < ($1.captureDate ?? .distantPast) }
···8286 formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
8387 formatter.locale = Locale(identifier: "en_US_POSIX")
8488 return formatter.date(from: dateString)
8989+ }
9090+9191+ nonisolated static func readPairedMetadata(from url: URL, into photo: Photo) {
9292+ if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
9393+ let size = attrs[.size] as? Int64 {
9494+ photo.pairedFileSize = size
9595+ }
9696+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
9797+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any],
9898+ let width = properties[kCGImagePropertyPixelWidth as String] as? Int,
9999+ let height = properties[kCGImagePropertyPixelHeight as String] as? Int
100100+ else { return }
101101+ photo.pairedPixelWidth = width
102102+ photo.pairedPixelHeight = height
103103+ }
104104+105105+ nonisolated static func readImageMetadata(from url: URL, into photo: Photo) {
106106+ // File size
107107+ if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
108108+ let size = attrs[.size] as? Int64 {
109109+ photo.fileSize = size
110110+ }
111111+ // Pixel dimensions from image header (no full decode)
112112+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
113113+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any],
114114+ let width = properties[kCGImagePropertyPixelWidth as String] as? Int,
115115+ let height = properties[kCGImagePropertyPixelHeight as String] as? Int
116116+ else { return }
117117+ photo.pixelWidth = width
118118+ photo.pixelHeight = height
85119 }
8612087121 static func isRAWExtension(_ ext: String) -> Bool {