From 51435ab960dcb6cddb61d261ed4cdca6e1ca1cca Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Tue, 24 Mar 2026 15:34:35 -0400 Subject: [PATCH] feat: add sidecar support --- cull/CullApp.swift | 15 +- cull/Models/CullSession.swift | 8 ++ cull/Services/PhotoExporter.swift | 21 ++- cull/Services/PhotoImporter.swift | 7 + cull/Services/XMPSidecar.swift | 136 +++++++++++++++++++ cull/Views/ExportSheet.swift | 5 +- cull/Views/SettingsView.swift | 24 ++++ cull/cull.xcodeproj/project.pbxproj | 12 ++ cull/icon.icon/Assets/C.png | Bin 0 -> 10153 bytes cull/icon.icon/Assets/Subtract.svg | 16 --- cull/icon.icon/Assets/emoji_u1f39e 1 (1).svg | 27 ---- cull/icon.icon/Assets/emoji_u1f52a 1.svg | 16 --- cull/icon.icon/icon.json | 56 +++----- 13 files changed, 244 insertions(+), 99 deletions(-) create mode 100644 cull/Services/XMPSidecar.swift create mode 100644 cull/Views/SettingsView.swift create mode 100644 cull/icon.icon/Assets/C.png delete mode 100644 cull/icon.icon/Assets/Subtract.svg delete mode 100644 cull/icon.icon/Assets/emoji_u1f39e 1 (1).svg delete mode 100644 cull/icon.icon/Assets/emoji_u1f52a 1.svg diff --git a/cull/CullApp.swift b/cull/CullApp.swift index 21efc96..b4a6c24 100644 --- a/cull/CullApp.swift +++ b/cull/CullApp.swift @@ -5,6 +5,7 @@ struct CullApp: App { @State private var session = CullSession() @State private var thumbnailCache = ThumbnailCache() @AppStorage("recentFolders") private var recentFoldersData: Data = Data() + @AppStorage("importRecursive") private var importRecursive: Bool = true private var recentFolders: [URL] { (try? JSONDecoder().decode([String].self, from: recentFoldersData))?.compactMap { URL(fileURLWithPath: $0) } ?? [] @@ -28,6 +29,9 @@ struct CullApp: App { addRecentFolder(url) } } + .onAppear { + session.importRecursive = importRecursive + } } .windowStyle(.automatic) .commands { @@ -39,8 +43,11 @@ struct CullApp: App { .keyboardShortcut("o") Toggle("Include Subfolders", isOn: Binding( - get: { session.importRecursive }, - set: { session.importRecursive = $0 } + get: { importRecursive }, + set: { newValue in + importRecursive = newValue + session.importRecursive = newValue + } )) Menu("Open Recent") { @@ -161,6 +168,10 @@ struct CullApp: App { } } + + Settings { + SettingsView() + } } private func openFolder() { diff --git a/cull/Models/CullSession.swift b/cull/Models/CullSession.swift index 0b9e099..893abc9 100644 --- a/cull/Models/CullSession.swift +++ b/cull/Models/CullSession.swift @@ -3,6 +3,11 @@ import SwiftUI @Observable final class CullSession { + /// Whether XMP sidecars are auto-written on rating/flag changes (mirrors @AppStorage) + var autoWriteXMP: Bool { + get { UserDefaults.standard.object(forKey: "autoWriteXMP") as? Bool ?? true } + set { UserDefaults.standard.set(newValue, forKey: "autoWriteXMP") } + } var sourceFolder: URL? var groups: [PhotoGroup] = [] var selectedGroupIndex: Int = 0 @@ -267,6 +272,9 @@ final class CullSession { } undoManager?.setActionName(actionName) scheduleSave() + if autoWriteXMP { + XMPSidecar.write(photo) + } } func setRating(_ rating: Int) { diff --git a/cull/Services/PhotoExporter.swift b/cull/Services/PhotoExporter.swift index 1c03947..2c23ae4 100644 --- a/cull/Services/PhotoExporter.swift +++ b/cull/Services/PhotoExporter.swift @@ -37,7 +37,8 @@ struct PhotoExporter { destination: URL, fileType: ExportFileType, mode: ExportMode, - folderStructure: ExportFolderStructure = .flat + folderStructure: ExportFolderStructure = .flat, + includeXMP: Bool = true ) async -> ExportResult { let fm = FileManager.default @@ -90,7 +91,23 @@ struct PhotoExporter { errors.append("\(sourceURL.lastPathComponent): \(error.localizedDescription)") } } - if photoExported { exported += 1 } + if photoExported { + exported += 1 + if includeXMP { + let primaryURL = urls.first! + let subfolder = subfolder(for: primaryURL, photo: photo, structure: folderStructure) + let destDir = subfolder.isEmpty ? destination : destination.appendingPathComponent(subfolder) + let xmpName = primaryURL.deletingPathExtension().lastPathComponent + ".xmp" + let xmpDest = destDir.appendingPathComponent(xmpName) + let xmpRating = photo.flag == .reject ? -1 : photo.rating + let xmpContent = XMPSidecar.freshXMP(rating: xmpRating) + do { + try xmpContent.data(using: .utf8)?.write(to: xmpDest) + } catch { + errors.append("\(xmpName): XMP write failed — \(error.localizedDescription)") + } + } + } } return ExportResult(exported: exported, skipped: skipped, errors: errors) diff --git a/cull/Services/PhotoImporter.swift b/cull/Services/PhotoImporter.swift index 65e814d..21f6f38 100644 --- a/cull/Services/PhotoImporter.swift +++ b/cull/Services/PhotoImporter.swift @@ -112,6 +112,13 @@ struct PhotoImporter { photos.sort { ($0.captureDate ?? .distantPast) < ($1.captureDate ?? .distantPast) } + // Read existing XMP sidecars + for photo in photos { + if let meta = XMPSidecar.read(for: photo.url) { + XMPSidecar.apply(meta, to: photo) + } + } + return ImportResult(photos: photos, paired: pairedCount) } diff --git a/cull/Services/XMPSidecar.swift b/cull/Services/XMPSidecar.swift new file mode 100644 index 0000000..b219d04 --- /dev/null +++ b/cull/Services/XMPSidecar.swift @@ -0,0 +1,136 @@ +import Foundation + +/// Reads and writes XMP sidecar files for photo metadata interoperability. +/// Uses `.xmp` naming (Lightroom/Bridge compatible). +struct XMPSidecar { + + /// Metadata stored in an XMP sidecar + struct Metadata { + var rating: Int = 0 // 0 = unrated, 1-5 = stars, -1 = rejected + var label: String? // color label: "Red", "Yellow", "Green", "Blue", "Purple" + } + + /// Returns the XMP sidecar URL for a given photo URL + static func sidecarURL(for photoURL: URL) -> URL { + photoURL.deletingPathExtension().appendingPathExtension("xmp") + } + + // MARK: - Read + + /// Read metadata from an existing XMP sidecar, if present + static func read(for photoURL: URL) -> Metadata? { + let url = sidecarURL(for: photoURL) + guard FileManager.default.fileExists(atPath: url.path) else { return nil } + guard let doc = try? XMLDocument(contentsOf: url, options: []) else { return nil } + + var meta = Metadata() + + // Try attribute form: xmp:Rating="3" on rdf:Description + if let ratingStr = try? doc.nodes(forXPath: "//@xmp:Rating").first?.stringValue, + let rating = Int(ratingStr) { + meta.rating = rating + } + // Try element form: 3 + else if let ratingStr = try? doc.nodes(forXPath: "//xmp:Rating").first?.stringValue, + let rating = Int(ratingStr) { + meta.rating = rating + } + + if let label = try? doc.nodes(forXPath: "//@xmp:Label").first?.stringValue { + meta.label = label + } else if let label = try? doc.nodes(forXPath: "//xmp:Label").first?.stringValue { + meta.label = label + } + + return meta + } + + // MARK: - Write + + /// Write metadata to an XMP sidecar, merging with existing content if present + static func write(_ photo: Photo) { + let url = sidecarURL(for: photo.url) + let xmpRating = xmpRating(for: photo) + + // If an existing XMP exists, try to merge + if FileManager.default.fileExists(atPath: url.path), + let doc = try? XMLDocument(contentsOf: url, options: [.nodePreserveWhitespace]) { + if mergeInto(doc, rating: xmpRating) { + try? doc.xmlData(options: [.nodePrettyPrint]).write(to: url) + return + } + } + + // Write fresh XMP + let xml = freshXMP(rating: xmpRating) + try? xml.data(using: .utf8)?.write(to: url) + } + + // MARK: - Mapping + + /// Map Cull's rating/flag to XMP rating value + private static func xmpRating(for photo: Photo) -> Int { + if photo.flag == .reject { return -1 } + return photo.rating // 0 = unrated, 1-5 = stars + } + + /// Map XMP metadata back to Cull's rating/flag + static func apply(_ meta: Metadata, to photo: Photo) { + if meta.rating == -1 { + photo.flag = .reject + } else if meta.rating >= 1 && meta.rating <= 5 { + photo.rating = meta.rating + } + // Don't overwrite existing state with "unrated" + } + + // MARK: - XMP Generation + + static func freshXMP(rating: Int) -> String { + """ + + + + + + + + + """ + } + + /// Merge rating into an existing XMLDocument's rdf:Description + private static func mergeInto(_ doc: XMLDocument, rating: Int) -> Bool { + // Find rdf:Description element + guard let descriptions = try? doc.nodes(forXPath: "//rdf:Description"), + let desc = descriptions.first as? XMLElement else { return false } + + // Update or add xmp:Rating attribute + let ns = "http://ns.adobe.com/xap/1.0/" + if let existing = desc.attribute(forLocalName: "Rating", uri: ns) { + existing.stringValue = "\(rating)" + } else { + let attr = XMLNode.attribute( + withName: "xmp:Rating", + uri: ns, + stringValue: "\(rating)" + ) as! XMLNode + desc.addAttribute(attr) + } + + // Ensure CreatorTool mentions Cull + if desc.attribute(forLocalName: "CreatorTool", uri: ns) == nil { + let attr = XMLNode.attribute( + withName: "xmp:CreatorTool", + uri: ns, + stringValue: "Cull" + ) as! XMLNode + desc.addAttribute(attr) + } + + return true + } +} diff --git a/cull/Views/ExportSheet.swift b/cull/Views/ExportSheet.swift index d0f7f6f..6be25ce 100644 --- a/cull/Views/ExportSheet.swift +++ b/cull/Views/ExportSheet.swift @@ -11,6 +11,7 @@ struct ExportSheet: View { @State private var isExporting: Bool = false @State private var result: ExportResult? @State private var useCurrentFilters: Bool = true + @State private var includeXMP: Bool = true private var eligiblePhotos: [Photo] { if useCurrentFilters { @@ -26,6 +27,7 @@ struct ExportSheet: View { Form { Toggle("Export only visible photos", isOn: $useCurrentFilters) + Toggle("Include XMP sidecars", isOn: $includeXMP) Picker("File Type", selection: $fileType) { ForEach(ExportFileType.allCases) { type in @@ -132,7 +134,8 @@ struct ExportSheet: View { destination: destination, fileType: fileType, mode: exportMode, - folderStructure: folderStructure + folderStructure: folderStructure, + includeXMP: includeXMP ) await MainActor.run { result = exportResult diff --git a/cull/Views/SettingsView.swift b/cull/Views/SettingsView.swift new file mode 100644 index 0000000..82c81e7 --- /dev/null +++ b/cull/Views/SettingsView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct SettingsView: View { + @AppStorage("autoWriteXMP") private var autoWriteXMP: Bool = true + @AppStorage("importRecursive") private var importRecursive: Bool = true + + var body: some View { + Form { + Section("Sidecars") { + Toggle("Automatically write XMP sidecars", isOn: $autoWriteXMP) + Text("Writes rating and flag changes to .xmp files alongside your photos for Lightroom/Bridge/darktable compatibility.") + .font(.caption) + .foregroundStyle(.secondary) + } + + Section("Import") { + Toggle("Include subfolders by default", isOn: $importRecursive) + } + } + .formStyle(.grouped) + .frame(width: 450) + .fixedSize() + } +} diff --git a/cull/cull.xcodeproj/project.pbxproj b/cull/cull.xcodeproj/project.pbxproj index 64d77c9..596e467 100644 --- a/cull/cull.xcodeproj/project.pbxproj +++ b/cull/cull.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 0B0EC2982F722491004523FA /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2812F722491004523FA /* ThumbnailCache.swift */; }; 0B0EC2A12F72570F004523FA /* icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A02F72570F004523FA /* icon.icon */; }; 0B0EC2A52F727789004523FA /* WorkspaceDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A42F727789004523FA /* WorkspaceDB.swift */; }; + 0B0EC2A72F7314F5004523FA /* XMPSidecar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A62F7314F5004523FA /* XMPSidecar.swift */; }; + 0B0EC2A92F73150B004523FA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A82F73150B004523FA /* SettingsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -47,6 +49,8 @@ 0B0EC2992F724FE5004523FA /* cull.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cull.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0B0EC2A02F72570F004523FA /* icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = icon.icon; sourceTree = ""; }; 0B0EC2A42F727789004523FA /* WorkspaceDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceDB.swift; sourceTree = ""; }; + 0B0EC2A62F7314F5004523FA /* XMPSidecar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMPSidecar.swift; sourceTree = ""; }; + 0B0EC2A82F73150B004523FA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -86,6 +90,7 @@ 0B0EC2822F722491004523FA /* Services */ = { isa = PBXGroup; children = ( + 0B0EC2A62F7314F5004523FA /* XMPSidecar.swift */, 0B0EC2A42F727789004523FA /* WorkspaceDB.swift */, 0B0EC27D2F722491004523FA /* PhotoExporter.swift */, 0B0EC27E2F722491004523FA /* PhotoImporter.swift */, @@ -99,6 +104,7 @@ 0B0EC2892F722491004523FA /* Views */ = { isa = PBXGroup; children = ( + 0B0EC2A82F73150B004523FA /* SettingsView.swift */, 0B0EC2832F722491004523FA /* ContentView.swift */, 0B0EC2842F722491004523FA /* ExportSheet.swift */, 0B0EC2852F722491004523FA /* GroupDetailView.swift */, @@ -185,6 +191,8 @@ 0B0EC28A2F722491004523FA /* ImportView.swift in Sources */, 0B0EC2A52F727789004523FA /* WorkspaceDB.swift in Sources */, 0B0EC28B2F722491004523FA /* ExportSheet.swift in Sources */, + 0B0EC2A92F73150B004523FA /* SettingsView.swift in Sources */, + 0B0EC2A72F7314F5004523FA /* XMPSidecar.swift in Sources */, 0B0EC28C2F722491004523FA /* ShotGrouper.swift in Sources */, 0B0EC28D2F722491004523FA /* Photo.swift in Sources */, 0B0EC28E2F722491004523FA /* GroupListView.swift in Sources */, @@ -348,6 +356,8 @@ ENABLE_RESOURCE_ACCESS_USB = NO; ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Cull; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -390,6 +400,8 @@ ENABLE_RESOURCE_ACCESS_USB = NO; ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Cull; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/cull/icon.icon/Assets/C.png b/cull/icon.icon/Assets/C.png new file mode 100644 index 0000000000000000000000000000000000000000..c65e1a87e47aa70ea659d1cb86d728cd5dde7922 GIT binary patch literal 10153 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 literal 0 HcmV?d00001 diff --git a/cull/icon.icon/Assets/Subtract.svg b/cull/icon.icon/Assets/Subtract.svg deleted file mode 100644 index 78a151c..0000000 --- a/cull/icon.icon/Assets/Subtract.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/cull/icon.icon/Assets/emoji_u1f39e 1 (1).svg b/cull/icon.icon/Assets/emoji_u1f39e 1 (1).svg deleted file mode 100644 index 29fdb92..0000000 --- a/cull/icon.icon/Assets/emoji_u1f39e 1 (1).svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cull/icon.icon/Assets/emoji_u1f52a 1.svg b/cull/icon.icon/Assets/emoji_u1f52a 1.svg deleted file mode 100644 index 99d1047..0000000 --- a/cull/icon.icon/Assets/emoji_u1f52a 1.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/cull/icon.icon/icon.json b/cull/icon.icon/icon.json index ec78206..2ce9233 100644 --- a/cull/icon.icon/icon.json +++ b/cull/icon.icon/icon.json @@ -1,9 +1,7 @@ { "fill-specializations" : [ { - "value" : { - "solid" : "display-p3:0.12880,0.13019,0.13434,1.00000" - } + "value" : "system-dark" }, { "appearance" : "dark", @@ -16,41 +14,29 @@ { "layers" : [ { - "blend-mode" : "normal", - "fill" : "none", - "glass" : true, - "hidden" : false, - "image-name" : "emoji_u1f52a 1.svg", - "name" : "emoji_u1f52a 1", - "opacity" : 1, + "fill" : { + "linear-gradient" : [ + "display-p3:1.00000,0.12046,0.22478,1.00000", + "display-p3:0.50511,0.67153,0.96783,1.00000" + ], + "orientation" : { + "start" : { + "x" : 0.5, + "y" : 0 + }, + "stop" : { + "x" : 0.5652511760752689, + "y" : 0.8858258928571429 + } + } + }, + "image-name" : "C.png", + "name" : "C", "position" : { "scale" : 1, "translation-in-points" : [ - -97.0625, - -105.04424700203003 - ] - } - }, - { - "image-name" : "emoji_u1f39e 1 (1).svg", - "name" : "emoji_u1f39e 1 (1)", - "position" : { - "scale" : 1.23, - "translation-in-points" : [ - -0.7779750000000831, - 23.765625 - ] - } - }, - { - "glass" : true, - "image-name" : "Subtract.svg", - "name" : "Subtract", - "position" : { - "scale" : 1.07, - "translation-in-points" : [ - 0.4913672408982279, - 106.2578125 + 0, + 0 ] } } -- 2.51.2