diff --git a/JPG to HEIF Converter.xcodeproj/project.pbxproj b/JPG to HEIF Converter.xcodeproj/project.pbxproj index 926d2c1..ce9f581 100644 --- a/JPG to HEIF Converter.xcodeproj/project.pbxproj +++ b/JPG to HEIF Converter.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 2407286A20B313F000E704B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2407286920B313F000E704B8 /* Assets.xcassets */; }; 2407286D20B313F000E704B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2407286B20B313F000E704B8 /* Main.storyboard */; }; 2407287720B34D5500E704B8 /* MainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2407287620B34D5500E704B8 /* MainWindowController.swift */; }; + 24F4F3B325836E6C00FF15A9 /* URL+contains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F4F3B225836E6C00FF15A9 /* URL+contains.swift */; }; CD181F4A21407A9000D58033 /* UserDefaultsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD181F4921407A9000D58033 /* UserDefaultsManager.swift */; }; CDF981C92139FB6500EBC90E /* FileTypeEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF981C82139FB6500EBC90E /* FileTypeEnum.swift */; }; /* End PBXBuildFile section */ @@ -30,6 +31,7 @@ 2407286F20B313F000E704B8 /* JPG_to_HEIF_Converter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = JPG_to_HEIF_Converter.entitlements; sourceTree = ""; }; 2407287520B31FF300E704B8 /* JPG to HEIF Converter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "JPG to HEIF Converter.entitlements"; sourceTree = ""; }; 2407287620B34D5500E704B8 /* MainWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindowController.swift; sourceTree = ""; }; + 24F4F3B225836E6C00FF15A9 /* URL+contains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+contains.swift"; sourceTree = ""; }; CD181F4921407A9000D58033 /* UserDefaultsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsManager.swift; sourceTree = ""; }; CDF981C82139FB6500EBC90E /* FileTypeEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypeEnum.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -64,6 +66,7 @@ 2407286420B313EE00E704B8 /* JPG to HEIF Converter */ = { isa = PBXGroup; children = ( + 24F4F3B125836E5C00FF15A9 /* extensions */, 2429921F232E7C6500477607 /* controllers */, 24299221232E7CD000477607 /* managers */, 24299220232E7CC700477607 /* models */, @@ -103,6 +106,14 @@ path = managers; sourceTree = ""; }; + 24F4F3B125836E5C00FF15A9 /* extensions */ = { + isa = PBXGroup; + children = ( + 24F4F3B225836E6C00FF15A9 /* URL+contains.swift */, + ); + path = extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -130,7 +141,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0930; - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1220; ORGANIZATIONNAME = "Sergey Armodin"; TargetAttributes = { 2407286120B313EE00E704B8 = { @@ -183,6 +194,7 @@ files = ( CDF981C92139FB6500EBC90E /* FileTypeEnum.swift in Sources */, CD181F4A21407A9000D58033 /* UserDefaultsManager.swift in Sources */, + 24F4F3B325836E6C00FF15A9 /* URL+contains.swift in Sources */, 2407286820B313EE00E704B8 /* ViewController.swift in Sources */, 2407286620B313EE00E704B8 /* AppDelegate.swift in Sources */, 2407287720B34D5500E704B8 /* MainWindowController.swift in Sources */, @@ -240,6 +252,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -301,6 +314,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -335,7 +349,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = 345VKAJA5N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "JPG to HEIF Converter/Info.plist"; @@ -344,7 +358,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.05; + MARKETING_VERSION = 1.06; PRODUCT_BUNDLE_IDENTIFIER = "me.spaceinbox.JPG-to-HEIF-Converter"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -358,7 +372,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = 345VKAJA5N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "JPG to HEIF Converter/Info.plist"; @@ -367,7 +381,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.05; + MARKETING_VERSION = 1.06; PRODUCT_BUNDLE_IDENTIFIER = "me.spaceinbox.JPG-to-HEIF-Converter"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/JPG to HEIF Converter/Base.lproj/Main.storyboard b/JPG to HEIF Converter/Base.lproj/Main.storyboard index 4af970c..763dd8d 100644 --- a/JPG to HEIF Converter/Base.lproj/Main.storyboard +++ b/JPG to HEIF Converter/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -160,14 +160,14 @@ - + - + - + @@ -175,7 +175,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -212,6 +259,7 @@ + @@ -219,6 +267,7 @@ + @@ -242,6 +291,8 @@ + + diff --git a/JPG to HEIF Converter/controllers/ViewController.swift b/JPG to HEIF Converter/controllers/ViewController.swift index e4c0bbb..49066c0 100644 --- a/JPG to HEIF Converter/controllers/ViewController.swift +++ b/JPG to HEIF Converter/controllers/ViewController.swift @@ -30,18 +30,21 @@ class ViewController: NSViewController { /// Open files button @IBOutlet fileprivate weak var openFilesButton: NSButtonCell! - /// Indicator @IBOutlet fileprivate weak var progressIndicator: NSProgressIndicator! - /// Complete label @IBOutlet fileprivate weak var completeLabel: NSTextField! - /// Keep Originals checkbox - @IBOutlet fileprivate weak var keepOriginalsCheckbox: NSButton! + @IBOutlet fileprivate weak var keepOriginalsCheckbox: NSButton! + /// Quality value + @IBOutlet fileprivate weak var qualityValueLabel: NSTextField! + /// Quality slider + @IBOutlet fileprivate weak var qualitySlider: NSSlider! + - // MARK: - Properties + // MARK: - Private properties + private var quality: Double = 0.9 /// Processed images number fileprivate var processedImages: Int = 0 { @@ -88,6 +91,16 @@ class ViewController: NSViewController { self.converterState = .launched keepOriginalsCheckbox.state = UserDefaultsManager.preferToRemoveOriginals ? .off : .on + + var preferredQuality = UserDefaultsManager.qualityPreference ?? 0.9 + if preferredQuality <= 0 { + preferredQuality = 0.9 + } + qualityValueLabel.stringValue = "\(preferredQuality)" + qualitySlider.maxValue = 1 // lossless compression + qualitySlider.minValue = 0 // maximum compression + qualitySlider.doubleValue = preferredQuality + quality = preferredQuality } override var representedObject: Any? { @@ -95,13 +108,16 @@ class ViewController: NSViewController { // Update the view, if already loaded. } } - - } // MARK: - Actions extension ViewController { + @IBAction func sliderChanged(_ sender: Any) { + quality = round(qualitySlider.doubleValue * 100) / 100 + UserDefaultsManager.qualityPreference = quality + qualityValueLabel.stringValue = "\(quality)" + } /// Keep Original Files checkbox checked/unchecked /// @@ -157,7 +173,7 @@ extension ViewController { } group.notify(queue: .main, execute: { [weak self] in - guard let `self` = self else { return } + guard let self = self else { return } self.converterState = .complete }) @@ -240,7 +256,7 @@ extension ViewController { group.enter() queue.async { [weak self] in - guard let `self` = self else { return } + guard let self = self else { return } guard case .image = FileType(imageUrl) else { return } guard let source = CGImageSourceCreateWithURL(imageUrl as CFURL, nil) else { return } @@ -258,7 +274,8 @@ extension ViewController { fatalError("unable to create CGImageDestination") } - CGImageDestinationAddImageAndMetadata(destination, image, imageMetadata, nil) + let options = [kCGImageDestinationLossyCompressionQuality: self.quality] + CGImageDestinationAddImageAndMetadata(destination, image, imageMetadata, options as CFDictionary) CGImageDestinationFinalize(destination) if deletingOriginals { @@ -276,65 +293,56 @@ extension ViewController { } -extension ViewController { - - /// Update the contents.json file in an imageset to reflect new file type - /// - /// - Parameter url: the file path to be processed - /// - Parameter group: the DispatchGroup managing conversion work - /// - Parameter queue: the serial queue to contain conversion work - func updateContentsFile(_ url: URL, group: DispatchGroup, queue: DispatchQueue) { - guard case .json = FileType(url) else { return } - - group.enter() - queue.async { [weak self] in - - do { - try self?.updateJSONContents(url) - } catch let error { - print(error) - } - - group.leave() - } - - } - - /// Attempt to translate a file path into JSON, process it, and overwrite the file with the result - private func updateJSONContents(_ url: URL) throws { - guard let json = try JSONSerialization.jsonObject(with: Data(contentsOf: url), options: .mutableLeaves) as? JSON else { return } - let processed = try JSONSerialization.data(withJSONObject: processJSON(json), options: .prettyPrinted) - try processed.write(to: url) - - } - - /// Traverse a json object, changing only the path extension of values keyed for filename - private func processJSON(_ json: JSON) -> JSON { - var json = json - for (k, v) in json { - if k == "filename", let value = v as? String { - for type in FileType.allowedImageTypes { - let newValue = value.replacingOccurrences(of: ".\(type)", with: ".heic") - if newValue != value { - json[k] = newValue - } - } - } else if let value = v as? JSON { - json[k] = processJSON(value) - } else if let values = v as? [JSON] { - json[k] = values.compactMap({ return processJSON($0) }) - } - } - - return json - } - -} - -extension URL { - public func contains(_ other: URL) -> Bool { - return autoreleasepool { - return resolvingSymlinksInPath().absoluteString.lowercased().contains(other.resolvingSymlinksInPath().absoluteString.lowercased()) - } - } +// MARK: - Private methods +private extension ViewController { + /// Update the contents.json file in an imageset to reflect new file type + /// + /// - Parameter url: the file path to be processed + /// - Parameter group: the DispatchGroup managing conversion work + /// - Parameter queue: the serial queue to contain conversion work + func updateContentsFile(_ url: URL, group: DispatchGroup, queue: DispatchQueue) { + guard case .json = FileType(url) else { return } + + group.enter() + queue.async { [weak self] in + + do { + try self?.updateJSONContents(url) + } catch let error { + print(error) + } + + group.leave() + } + + } + + /// Attempt to translate a file path into JSON, process it, and overwrite the file with the result + func updateJSONContents(_ url: URL) throws { + guard let json = try JSONSerialization.jsonObject(with: Data(contentsOf: url), options: .mutableLeaves) as? JSON else { return } + let processed = try JSONSerialization.data(withJSONObject: processJSON(json), options: .prettyPrinted) + try processed.write(to: url) + + } + + /// Traverse a json object, changing only the path extension of values keyed for filename + func processJSON(_ json: JSON) -> JSON { + var json = json + for (k, v) in json { + if k == "filename", let value = v as? String { + for type in FileType.allowedImageTypes { + let newValue = value.replacingOccurrences(of: ".\(type)", with: ".heic") + if newValue != value { + json[k] = newValue + } + } + } else if let value = v as? JSON { + json[k] = processJSON(value) + } else if let values = v as? [JSON] { + json[k] = values.compactMap({ return processJSON($0) }) + } + } + + return json + } } diff --git a/JPG to HEIF Converter/extensions/URL+contains.swift b/JPG to HEIF Converter/extensions/URL+contains.swift new file mode 100644 index 0000000..ff5ab96 --- /dev/null +++ b/JPG to HEIF Converter/extensions/URL+contains.swift @@ -0,0 +1,17 @@ +// +// URL+contains.swift +// JPG to HEIF Converter +// +// Created by Sergei Armodin on 11.12.2020. +// Copyright © 2020 Sergey Armodin. All rights reserved. +// + +import Foundation + +extension URL { + public func contains(_ other: URL) -> Bool { + return autoreleasepool { + return resolvingSymlinksInPath().absoluteString.lowercased().contains(other.resolvingSymlinksInPath().absoluteString.lowercased()) + } + } +} diff --git a/JPG to HEIF Converter/managers/UserDefaultsManager.swift b/JPG to HEIF Converter/managers/UserDefaultsManager.swift index a6ef2ef..a69d7bb 100644 --- a/JPG to HEIF Converter/managers/UserDefaultsManager.swift +++ b/JPG to HEIF Converter/managers/UserDefaultsManager.swift @@ -9,9 +9,11 @@ import Foundation struct UserDefaultsManager { - + // MARK: - Private properties private static let removeOriginalImagePreferenceKey = "removeOriginalImagePreferenceKey" + private static let qualityPreferenceKey = "qualityPreferenceKey" + // MARK: - Public properties static var preferToRemoveOriginals: Bool { get { return UserDefaults.standard.bool(forKey: removeOriginalImagePreferenceKey) @@ -19,7 +21,15 @@ struct UserDefaultsManager { set { UserDefaults.standard.set(newValue, forKey: removeOriginalImagePreferenceKey) } - + } + + static var qualityPreference: Double? { + get { + return UserDefaults.standard.double(forKey: qualityPreferenceKey) + } + set { + UserDefaults.standard.set(newValue, forKey: qualityPreferenceKey) + } } } diff --git a/JPG to HEIF Converter/models/FileTypeEnum.swift b/JPG to HEIF Converter/models/FileTypeEnum.swift index c0227ba..9e6d4b5 100644 --- a/JPG to HEIF Converter/models/FileTypeEnum.swift +++ b/JPG to HEIF Converter/models/FileTypeEnum.swift @@ -32,7 +32,7 @@ enum FileType { } static var allowedImageTypes: [String] { - return ["jpg", "jpeg", "png", "nef", "cr2"] + return ["jpg", "jpeg", "png", "nef", "cr2", "sr2", "arw", "dng"] } static var directoryTypes: [String] {