├── Images ├── earth-01.gif ├── wallpaper.png └── cyberpunk-01.gif ├── Sources ├── WallpapperLib │ ├── WallpapperError.swift │ ├── ConsoleOutput │ │ ├── OutputType.swift │ │ └── ConsoleIO.swift │ ├── SunPositions │ │ ├── Models │ │ │ ├── SunPosition.swift │ │ │ ├── SunCoordinates.swift │ │ │ └── WallpapperItem.swift │ │ └── SunCalculations.swift │ ├── ImageMetadataReaders │ │ ├── Models │ │ │ └── ImageLocation.swift │ │ ├── Errors │ │ │ ├── MetadataExtractorError.swift │ │ │ └── ExifExtractorError.swift │ │ ├── LocationExtractor.swift │ │ └── DynamicWallpaperExtractor.swift │ ├── DynamicWallpaperGenerator │ │ ├── Models │ │ │ ├── TimeInfo.swift │ │ │ ├── Apperance.swift │ │ │ ├── PictureInfo.swift │ │ │ ├── SequenceItem.swift │ │ │ └── SequenceInfo.swift │ │ ├── Errors │ │ │ └── ImageMetadataGeneratorError.swift │ │ ├── WallpaperGenerator.swift │ │ └── ImageMetadataGenerator.swift │ └── Extensions │ │ └── NSImage.swift ├── Wallpapper │ ├── main.swift │ ├── OptionType.swift │ └── Program.swift └── WallpapperExif │ ├── main.swift │ ├── OptionType.swift │ └── Program.swift ├── .github └── workflows │ └── build.yml ├── Package.swift ├── LICENSE.md ├── README.md └── .gitignore /Images/earth-01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mczachurski/wallpapper/HEAD/Images/earth-01.gif -------------------------------------------------------------------------------- /Images/wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mczachurski/wallpapper/HEAD/Images/wallpaper.png -------------------------------------------------------------------------------- /Images/cyberpunk-01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mczachurski/wallpapper/HEAD/Images/cyberpunk-01.gif -------------------------------------------------------------------------------- /Sources/WallpapperLib/WallpapperError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public protocol WallpapperError: Error { 10 | var message: String { get } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Wallpapper/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | let program = Program() 10 | let result = program.run() 11 | 12 | exit(result ? EXIT_SUCCESS : EXIT_FAILURE) 13 | -------------------------------------------------------------------------------- /Sources/WallpapperExif/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | let program = Program() 10 | let result = program.run() 11 | 12 | exit(result ? EXIT_SUCCESS : EXIT_FAILURE) 13 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/ConsoleOutput/OutputType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public enum OutputType { 10 | case error 11 | case standard 12 | case debug 13 | } 14 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/SunPositions/Models/SunPosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public struct SunPosition { 10 | public let azimuth: Double 11 | public let altitude: Double 12 | } 13 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/SunPositions/Models/SunCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public struct SunCoordinates { 10 | public let declination: Double 11 | public let rightAscension: Double 12 | } 13 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/ImageMetadataReaders/Models/ImageLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public struct ImageLocation { 10 | public let latitude: Double 11 | public let longitude: Double 12 | public let createDate: Date 13 | } 14 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/Models/TimeInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public class TimeItem : Codable { 10 | enum CodingKeys: String, CodingKey { 11 | case time = "t" 12 | case imageIndex = "i" 13 | } 14 | 15 | var time: Double = 0.0 16 | var imageIndex: Int = 0 17 | } 18 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/Models/Apperance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public class Apperance: Codable { 10 | enum CodingKeys: String, CodingKey { 11 | case darkIndex = "d" 12 | case lightIndex = "l" 13 | } 14 | 15 | var darkIndex: Int = 0 16 | var lightIndex: Int = 0 17 | } 18 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/Models/PictureInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public struct PictureInfo: Decodable { 10 | var fileName: String 11 | var isPrimary: Bool? 12 | var isForLight: Bool? 13 | var isForDark: Bool? 14 | var altitude: Double? 15 | var azimuth: Double? 16 | var time: Date? 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Build 13 | run: swift build --configuration release --arch arm64 --arch x86_64 14 | 15 | - uses: actions/upload-artifact@v3 16 | with: 17 | name: wallpaper 18 | path: | 19 | .build/apple/Products/Release/wallpapper 20 | .build/apple/Products/Release/wallpapper-exif 21 | -------------------------------------------------------------------------------- /Sources/WallpapperExif/OptionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | enum OptionType: String { 10 | case help = "-h" 11 | case version = "-v" 12 | case unknown 13 | 14 | init(value: String) { 15 | switch value { 16 | case "-h": self = .help 17 | case "-v": self = .version 18 | default: self = .unknown 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/Models/SequenceItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public class SequenceItem : Codable { 10 | enum CodingKeys: String, CodingKey { 11 | case altitude = "a" 12 | case azimuth = "z" 13 | case imageIndex = "i" 14 | } 15 | 16 | var altitude: Double = 0.0 17 | var azimuth: Double = 0.0 18 | var imageIndex: Int = 0 19 | } 20 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/Models/SequenceInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public class SequenceInfo : Codable { 10 | enum CodingKeys: String, CodingKey { 11 | case sequenceItems = "si" 12 | case timeItems = "ti" 13 | case apperance = "ap" 14 | } 15 | 16 | var sequenceItems: [SequenceItem]? 17 | var timeItems: [TimeItem]? 18 | var apperance: Apperance? 19 | } 20 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/SunPositions/Models/WallpapperItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public struct WallpapperItem: Encodable { 10 | public let fileName: String 11 | public let altitude: Double 12 | public let azimuth: Double 13 | 14 | public init(fileName: String, altitude: Double, azimuth: Double) { 15 | self.fileName = fileName 16 | self.altitude = altitude 17 | self.azimuth = azimuth 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/Extensions/NSImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import AppKit 9 | 10 | extension NSImage { 11 | @objc var CGImage: CGImage? { 12 | get { 13 | guard let imageData = self.tiffRepresentation else { return nil } 14 | guard let sourceData = CGImageSourceCreateWithData(imageData as CFData, nil) else { return nil } 15 | return CGImageSourceCreateImageAtIndex(sourceData, 0, nil) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "wallpapper", 6 | products: [ 7 | .library(name: "WallpapperLib", targets: ["WallpapperLib"]), 8 | .executable(name: "wallpapper", targets: ["Wallpapper"]), 9 | .executable(name: "wallpapper-exif", targets: ["WallpapperExif"]) 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target(name: "WallpapperLib", dependencies: []), 14 | .target(name: "Wallpapper", dependencies: ["WallpapperLib"]), 15 | .target(name: "WallpapperExif", dependencies: ["WallpapperLib"]) 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/ConsoleOutput/ConsoleIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public class ConsoleIO { 10 | 11 | public init() { 12 | } 13 | 14 | public func writeMessage(_ message: String, to: OutputType = .standard) { 15 | switch to { 16 | case .standard: 17 | print(message) 18 | case .debug: 19 | fputs("\(message)", stdout) 20 | fflush(stdout) 21 | case .error: 22 | fputs("Error: \(message)\n", stderr) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/ImageMetadataReaders/Errors/MetadataExtractorError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | import Foundation 7 | 8 | public enum MetadataExtractorError: WallpapperError { 9 | case imageSourceNotCreated 10 | case imageMetadataNotCreated 11 | 12 | public var message: String { 13 | switch self { 14 | case .imageSourceNotCreated: 15 | return "CGImageSource object cannot be created." 16 | case .imageMetadataNotCreated: 17 | return "CGImageMetadata object cannot be created." 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Wallpapper/OptionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | enum OptionType: String { 10 | case help = "-h" 11 | case version = "-v" 12 | case input = "-i" 13 | case output = "-o" 14 | case extract = "-e" 15 | case unknown 16 | 17 | init(value: String) { 18 | switch value { 19 | case "-h": self = .help 20 | case "-v": self = .version 21 | case "-i": self = .input 22 | case "-o": self = .output 23 | case "-e": self = .extract 24 | default: self = .unknown 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/Errors/ImageMetadataGeneratorError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public enum ImageMetadataGeneratorError: WallpapperError { 10 | case addTagIntoImageFailed 11 | case imageNotFinalized 12 | case namespaceNotRegistered 13 | case notSupportedSystem 14 | 15 | public var message: String { 16 | switch self { 17 | case .addTagIntoImageFailed: 18 | return "Add tag into image failed." 19 | case .imageNotFinalized: 20 | return "Image has not be finilized." 21 | case .namespaceNotRegistered: 22 | return "Namespave cannot be registered." 23 | case .notSupportedSystem: 24 | return "Not supported operating system." 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Marcin Czachurski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Sources/WallpapperLib/ImageMetadataReaders/Errors/ExifExtractorError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | 9 | public enum ExifExtractorError: WallpapperError { 10 | case imageSourceNotCreated 11 | case imageMetadataNotCreated 12 | case missingCreationDate 13 | case missingLongitude 14 | case missingLatitude 15 | case notSupportedCreationDate(date: String?) 16 | case notSupportedLongitude(longitiude: String?) 17 | case notSupportedLatitude(latitude: String?) 18 | 19 | public var message: String { 20 | switch self { 21 | case .imageSourceNotCreated: 22 | return "CGImageSource object cannot be created." 23 | case .imageMetadataNotCreated: 24 | return "CGImageMetadata object cannot be created." 25 | case .missingCreationDate: 26 | return "Missing 'xmp:CreateDate' exif metadata." 27 | case .missingLongitude: 28 | return "Missing 'exif:GPSLongitude' exif metadata." 29 | case .missingLatitude: 30 | return "Missing 'exif:GPSLatitude' exif metadata." 31 | case .notSupportedCreationDate(let date): 32 | return "Not supported format of creation date: '\(date ?? "")'." 33 | case .notSupportedLongitude(let longitiude): 34 | return "Not supported format of longitiude: '\(longitiude ?? "")'." 35 | case .notSupportedLatitude(let latitude): 36 | return "Not supported format of latitude: '\(latitude ?? "")'." 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/WallpaperGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import AppKit 9 | import AVFoundation 10 | 11 | public class WallpaperGenerator { 12 | 13 | public init() { 14 | } 15 | 16 | public func generate(pictureInfos: [PictureInfo], baseURL: URL, outputFileName: String) throws { 17 | let consoleIO = ConsoleIO() 18 | let options = [kCGImageDestinationLossyCompressionQuality: 1.0] 19 | 20 | if #available(OSX 10.13, *) { 21 | 22 | let imageMetadataGenerator = ImageMetadataGenerator(pictureInfos: pictureInfos) 23 | let imageMetadata = try imageMetadataGenerator.getImageMetadata() 24 | let images = imageMetadataGenerator.images 25 | 26 | let destinationData = NSMutableData() 27 | if let destination = CGImageDestinationCreateWithData(destinationData, AVFileType.heic as CFString, images.count, nil) { 28 | for (index, fileName) in images.enumerated() { 29 | let fileURL = URL(fileURLWithPath: fileName, relativeTo: baseURL) 30 | 31 | consoleIO.writeMessage("Reading image file: '\(fileURL.absoluteString)'...", to: .debug) 32 | guard let orginalImage = NSImage(contentsOf: fileURL) else { 33 | consoleIO.writeMessage("ERROR.\n", to: .debug) 34 | return 35 | } 36 | 37 | consoleIO.writeMessage("OK.\n", to: .debug) 38 | 39 | if let cgImage = orginalImage.CGImage { 40 | if index == 0 { 41 | consoleIO.writeMessage("Adding image and metadata...", to: .debug) 42 | CGImageDestinationAddImageAndMetadata(destination, cgImage, imageMetadata, options as CFDictionary) 43 | consoleIO.writeMessage("OK.\n", to: .debug) 44 | } else { 45 | consoleIO.writeMessage("Adding image...", to: .debug) 46 | CGImageDestinationAddImage(destination, cgImage, options as CFDictionary) 47 | consoleIO.writeMessage("OK.\n", to: .debug) 48 | } 49 | } 50 | } 51 | 52 | consoleIO.writeMessage("Finalizing image container...", to: .debug) 53 | guard CGImageDestinationFinalize(destination) else { 54 | throw ImageMetadataGeneratorError.imageNotFinalized 55 | } 56 | consoleIO.writeMessage("OK.\n", to: .debug) 57 | 58 | let outputURL = URL(fileURLWithPath: outputFileName) 59 | consoleIO.writeMessage("Saving data to file '\(outputURL.absoluteString)'...", to: .debug) 60 | let imageData = destinationData as Data 61 | try imageData.write(to: outputURL) 62 | consoleIO.writeMessage("OK.\n", to: .debug) 63 | } 64 | } else { 65 | throw ImageMetadataGeneratorError.notSupportedSystem 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/SunPositions/SunCalculations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Code created based on library: https://github.com/mourner/suncalc 4 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 5 | // Licensed under the MIT License. 6 | // 7 | 8 | import Foundation 9 | 10 | public class SunCalculations { 11 | 12 | private let imageLocation: ImageLocation 13 | 14 | private let rad = Double.pi / 180.0 15 | private let daySec = 60 * 60 * 24.0 16 | private let J1970 = 2440588.0 17 | private let J2000 = 2451545.0 18 | 19 | // obliquity of the Earth 20 | private let e = (Double.pi / 180.0) * 23.4397 21 | 22 | public init(imageLocation: ImageLocation) { 23 | self.imageLocation = imageLocation 24 | } 25 | 26 | public func getSunPosition() -> SunPosition { 27 | let lw = self.rad * (-self.imageLocation.longitude) 28 | let phi = self.rad * self.imageLocation.latitude 29 | let days = self.toDays(date: self.imageLocation.createDate) 30 | 31 | let c = self.sunCoords(days: days); 32 | let h = self.siderealTime(days, lw) - c.rightAscension 33 | 34 | let radAzimuth = self.azimuth(h, phi, c.declination) 35 | let radAltitude = self.altitude(h, phi, c.declination) 36 | 37 | let altitude = (radAltitude * 180) / Double.pi 38 | let azimuthFromSouth = (radAzimuth * 180) / Double.pi 39 | 40 | var azimuth = azimuthFromSouth + 180 41 | if azimuth > 360 { 42 | azimuth = azimuth - 360 43 | } 44 | 45 | return SunPosition(azimuth: azimuth, altitude: altitude) 46 | } 47 | 48 | private func sunCoords(days: Double) -> SunCoordinates { 49 | 50 | let m = self.solarMeanAnomaly(days: days) 51 | let l = self.eclipticLongitude(solarMeanAnomaly: m); 52 | 53 | let declination = self.declination(l, 0) 54 | let rightAscension = self.rightAscension(l, 0) 55 | 56 | return SunCoordinates(declination: declination, rightAscension: rightAscension); 57 | } 58 | 59 | private func toDays(date: Date) -> Double { 60 | let julianDate = self.toJulian(date: date) 61 | return julianDate - self.J2000 62 | } 63 | 64 | private func toJulian(date: Date) -> Double { 65 | return (date.timeIntervalSince1970 / self.daySec) - 0.5 + self.J1970 66 | } 67 | 68 | private func solarMeanAnomaly(days: Double) -> Double { 69 | return self.rad * (357.5291 + 0.98560028 * days) 70 | } 71 | 72 | private func eclipticLongitude(solarMeanAnomaly m: Double) -> Double{ 73 | let c = self.rad * (1.9148 * sin(m) + 0.02 * sin(2 * m) + 0.0003 * sin(3 * m)); // equation of center 74 | let p = rad * 102.9372; // perihelion of the Earth 75 | 76 | return m + c + p + Double.pi; 77 | } 78 | 79 | private func declination(_ l: Double, _ b: Double) -> Double { 80 | return asin(sin(b) * cos(e) + cos(b) * sin(self.e) * sin(l)) 81 | } 82 | 83 | private func rightAscension(_ l: Double, _ b: Double) -> Double { 84 | return atan2(sin(l) * cos(self.e) - tan(b) * sin(self.e), cos(l)) 85 | } 86 | 87 | private func siderealTime(_ days: Double, _ lw: Double) -> Double { 88 | return self.rad * (280.16 + 360.9856235 * days) - lw 89 | } 90 | 91 | private func azimuth(_ h: Double, _ phi: Double, _ dec: Double) -> Double { 92 | return atan2(sin(h), cos(h) * sin(phi) - tan(dec) * cos(phi)) 93 | } 94 | 95 | private func altitude(_ h: Double, _ phi: Double, _ dec: Double) -> Double { 96 | return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(h)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/ImageMetadataReaders/LocationExtractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import AppKit 9 | import AVFoundation 10 | 11 | public class LocationExtractor { 12 | 13 | public init() { 14 | } 15 | 16 | public func extract(imageData: Data) throws -> ImageLocation { 17 | let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) 18 | guard let imageSourceValue = imageSource else { 19 | throw MetadataExtractorError.imageSourceNotCreated 20 | } 21 | 22 | let imageMetadata = CGImageSourceCopyMetadataAtIndex(imageSourceValue, 0, nil) 23 | guard let imageMetadataValue = imageMetadata else { 24 | throw MetadataExtractorError.imageMetadataNotCreated 25 | } 26 | 27 | var latRaw: String? 28 | var lat: Double? 29 | 30 | var lngRaw: String? 31 | var lng: Double? 32 | 33 | var datRaw: String? 34 | var dat: Date? 35 | 36 | CGImageMetadataEnumerateTagsUsingBlock(imageMetadataValue, nil, nil) { (value, metadataTag) -> Bool in 37 | 38 | let valueString = value as String 39 | let tag = CGImageMetadataTagCopyValue(metadataTag) 40 | 41 | switch valueString { 42 | case "exif:GPSLatitude": 43 | guard let valueTag = tag as? String else { 44 | return false 45 | } 46 | 47 | latRaw = valueTag 48 | lat = self.toDecimaDegrees(value: valueTag) 49 | break 50 | case "exif:GPSLongitude": 51 | guard let valueTag = tag as? String else { 52 | return false 53 | } 54 | 55 | lngRaw = valueTag 56 | lng = self.toDecimaDegrees(value: valueTag) 57 | break 58 | case "xmp:CreateDate": 59 | guard let valueTag = tag as? String else { 60 | return false 61 | } 62 | 63 | datRaw = valueTag 64 | dat = self.toDate(value: valueTag) 65 | break 66 | default: 67 | break 68 | } 69 | 70 | return true 71 | } 72 | 73 | guard latRaw != nil else { 74 | throw ExifExtractorError.missingLatitude 75 | } 76 | 77 | guard lngRaw != nil else { 78 | throw ExifExtractorError.missingLongitude 79 | } 80 | 81 | guard datRaw != nil else { 82 | throw ExifExtractorError.missingCreationDate 83 | } 84 | 85 | guard let latitude = lat else { 86 | throw ExifExtractorError.notSupportedLatitude(latitude: latRaw) 87 | } 88 | 89 | guard let longitude = lng else { 90 | throw ExifExtractorError.notSupportedLongitude(longitiude: lngRaw) 91 | } 92 | 93 | guard let createDate = dat else { 94 | throw ExifExtractorError.notSupportedCreationDate(date: datRaw) 95 | } 96 | 97 | return ImageLocation(latitude: latitude, longitude: longitude, createDate: createDate) 98 | } 99 | 100 | func toDate(value: String) -> Date? { 101 | let dateFormatter = DateFormatter() 102 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" 103 | return dateFormatter.date(from: value) 104 | } 105 | 106 | func toDecimaDegrees(value: String) -> Double? { 107 | let parts = value.split(separator: ",") 108 | guard parts.count == 2 else { 109 | return nil 110 | } 111 | 112 | let deg = Double(parts[0]) 113 | 114 | let minParts = parts[1].split(separator: ".") 115 | guard minParts.count >= 1 else { 116 | return nil 117 | } 118 | 119 | let min = Double(minParts[0]) 120 | 121 | guard let degrees = deg else { 122 | return nil 123 | } 124 | 125 | guard let minutes = min else { 126 | return nil 127 | } 128 | 129 | return degrees + (minutes / 60) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/WallpapperExif/Program.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import WallpapperLib 9 | 10 | class Program { 11 | let consoleIO = ConsoleIO() 12 | var inputFileNames: [String] = [] 13 | var wallpapperItems: [WallpapperItem] = [] 14 | 15 | func run() -> Bool { 16 | 17 | let (shouldBreak, resultCode) = self.proceedCommandLineArguments() 18 | if shouldBreak { 19 | return resultCode 20 | } 21 | 22 | return self.generateSunPositions() 23 | } 24 | 25 | private func generateSunPositions() -> Bool { 26 | do { 27 | for inputFileName in inputFileNames { 28 | let fileURL = try getPathToInputFile(inputFileName: inputFileName) 29 | self.consoleIO.writeMessage("Reading file: '\(fileURL.absoluteString)'...", to: .debug) 30 | let inputFileContents = try Data(contentsOf: fileURL) 31 | self.consoleIO.writeMessage("OK.\n", to: .debug) 32 | 33 | self.consoleIO.writeMessage("Extracting Exif information...", to: .debug) 34 | let locationExtractor = LocationExtractor() 35 | let imageLocation = try locationExtractor.extract(imageData: inputFileContents) 36 | self.consoleIO.writeMessage("OK.\n", to: .debug) 37 | 38 | self.consoleIO.writeMessage("Calculating Sun position...", to: .debug) 39 | let sunCalculations = SunCalculations(imageLocation: imageLocation) 40 | let position = sunCalculations.getSunPosition() 41 | self.consoleIO.writeMessage("OK.\n", to: .debug) 42 | 43 | let wallpapperItem = WallpapperItem(fileName: inputFileName, altitude: position.altitude, azimuth: position.azimuth) 44 | wallpapperItems.append(wallpapperItem) 45 | } 46 | 47 | return try self.printJsonString(wallpapperItems: wallpapperItems) 48 | } catch (let error as WallpapperError) { 49 | self.consoleIO.writeMessage("Unexpected error occurs: \(error.message)", to: .error) 50 | return false 51 | } catch { 52 | self.consoleIO.writeMessage("Unexpected error occurs: \(error)", to: .error) 53 | return false 54 | } 55 | } 56 | 57 | private func printJsonString(wallpapperItems: [WallpapperItem]) throws -> Bool { 58 | let encoder = JSONEncoder() 59 | encoder.outputFormatting = .prettyPrinted 60 | 61 | let jsonData = try encoder.encode(wallpapperItems) 62 | let jsonOptionslString = String(bytes: jsonData, encoding: .utf8) 63 | 64 | guard let jsonString = jsonOptionslString else { 65 | self.consoleIO.writeMessage("Error during converting object into string", to: .error) 66 | return false 67 | } 68 | 69 | self.consoleIO.writeMessage("Output:", to: .standard) 70 | self.consoleIO.writeMessage(jsonString, to: .standard) 71 | return true 72 | } 73 | 74 | private func getPathToInputFile(inputFileName: String) throws -> URL { 75 | return URL(fileURLWithPath: inputFileName) 76 | } 77 | 78 | private func proceedCommandLineArguments() -> (Bool, Bool) { 79 | if CommandLine.arguments.count == 1 { 80 | self.printUsage() 81 | return (true, false) 82 | } 83 | 84 | var optionIndex = 1 85 | while optionIndex < CommandLine.arguments.count { 86 | 87 | let option = CommandLine.arguments[optionIndex] 88 | let optionType = OptionType(value: option) 89 | 90 | switch optionType { 91 | case .help: 92 | self.printUsage() 93 | return (true, true) 94 | case .version: 95 | self.printVersion() 96 | return (true, true) 97 | default: 98 | let fileName = CommandLine.arguments[optionIndex] 99 | inputFileNames.append(fileName) 100 | break; 101 | } 102 | 103 | optionIndex = optionIndex + 1 104 | } 105 | 106 | if self.inputFileNames.count == 0 { 107 | self.consoleIO.writeMessage("unknown input file names.", to: .error) 108 | return (true, false) 109 | } 110 | 111 | return (false, false) 112 | } 113 | 114 | private func printVersion() { 115 | self.consoleIO.writeMessage("1.7.4") 116 | } 117 | 118 | private func printUsage() { 119 | 120 | let executableName = (CommandLine.arguments[0] as NSString).lastPathComponent 121 | 122 | self.consoleIO.writeMessage("\(executableName): [file1] [file2]") 123 | self.consoleIO.writeMessage("Command options are:") 124 | self.consoleIO.writeMessage(" -h\t\t\tshow this message and exit") 125 | self.consoleIO.writeMessage(" -v\t\t\tshow program version and exit") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/DynamicWallpaperGenerator/ImageMetadataGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import ImageIO 9 | 10 | public class ImageMetadataGenerator { 11 | private let pictureInfos: [PictureInfo] 12 | 13 | public lazy var images: [String] = { 14 | let sordedPictureInfors = pictureInfos.sorted(by: { (left, right) -> Bool in 15 | return left.isPrimary == true 16 | }) 17 | 18 | let sortedFileNames = sordedPictureInfors.map { pictureInfo -> String in 19 | pictureInfo.fileName 20 | } 21 | var addedDict = [String: Bool]() 22 | return sortedFileNames.filter { addedDict.updateValue(true, forKey: $0) == nil } 23 | }() 24 | 25 | init(pictureInfos: [PictureInfo]) { 26 | self.pictureInfos = pictureInfos 27 | } 28 | 29 | public func getImageMetadata() throws -> CGMutableImageMetadata { 30 | let imageMetadata = CGImageMetadataCreateMutable() 31 | let sequenceInfo = self.createPropertyList() 32 | 33 | if sequenceInfo.sequenceItems != nil { 34 | try self.appendDesktopProperties(to: imageMetadata, withKey: "solar", value: sequenceInfo) 35 | } else if sequenceInfo.timeItems != nil { 36 | try self.appendDesktopProperties(to: imageMetadata, withKey: "h24", value: sequenceInfo) 37 | } else { 38 | try self.appendDesktopProperties(to: imageMetadata, withKey: "apr", value: sequenceInfo.apperance) 39 | } 40 | 41 | return imageMetadata 42 | } 43 | 44 | private func appendDesktopProperties(to imageMetadata: CGMutableImageMetadata, withKey key: String, value: T) throws where T: Codable { 45 | guard CGImageMetadataRegisterNamespaceForPrefix(imageMetadata, 46 | "http://ns.apple.com/namespace/1.0/" as CFString, 47 | "apple_desktop" as CFString, 48 | nil) else { 49 | throw ImageMetadataGeneratorError.namespaceNotRegistered 50 | } 51 | 52 | let base64PropertyList = try self.createBase64PropertyList(value: value) 53 | let imageMetadataTag = CGImageMetadataTagCreate("http://ns.apple.com/namespace/1.0/" as CFString, 54 | "apple_desktop" as CFString, 55 | key as CFString, 56 | CGImageMetadataType.string, 57 | base64PropertyList as CFTypeRef) 58 | 59 | guard CGImageMetadataSetTagWithPath(imageMetadata, nil, "apple_desktop:\(key)" as CFString, imageMetadataTag!) else { 60 | throw ImageMetadataGeneratorError.addTagIntoImageFailed 61 | } 62 | } 63 | 64 | private func createPropertyList() -> SequenceInfo { 65 | 66 | let sequenceInfo = SequenceInfo() 67 | 68 | for (index, item) in self.pictureInfos.enumerated() { 69 | 70 | if item.isForLight != nil || item.isForDark != nil { 71 | if sequenceInfo.apperance == nil { 72 | sequenceInfo.apperance = Apperance() 73 | } 74 | } 75 | 76 | if item.isForLight ?? false { 77 | sequenceInfo.apperance?.lightIndex = index 78 | } 79 | 80 | if item.isForDark ?? false { 81 | sequenceInfo.apperance?.darkIndex = index 82 | } 83 | 84 | if let altitude = item.altitude, let azimuth = item.azimuth { 85 | let sequenceItem = SequenceItem() 86 | sequenceItem.altitude = altitude 87 | sequenceItem.azimuth = azimuth 88 | sequenceItem.imageIndex = self.getImageIndex(fileName: item.fileName) 89 | 90 | if sequenceInfo.sequenceItems == nil { 91 | sequenceInfo.sequenceItems = [] 92 | } 93 | 94 | sequenceInfo.sequenceItems?.append(sequenceItem) 95 | } 96 | 97 | if let time = item.time { 98 | let timeItem = TimeItem() 99 | timeItem.imageIndex = self.getImageIndex(fileName: item.fileName) 100 | let hour = Calendar.current.component(.hour, from: time) 101 | let min = Calendar.current.component(.minute, from: time) 102 | timeItem.time = (Double(hour) / 24.0) + (Double(min) / 60.0 / 24.0) 103 | 104 | if sequenceInfo.timeItems == nil { 105 | sequenceInfo.timeItems = [] 106 | } 107 | 108 | sequenceInfo.timeItems?.append(timeItem) 109 | } 110 | } 111 | 112 | return sequenceInfo 113 | } 114 | 115 | private func createBase64PropertyList(value: T) throws -> String where T: Codable { 116 | 117 | let encoder = PropertyListEncoder() 118 | encoder.outputFormat = .binary 119 | let plistData = try encoder.encode(value) 120 | 121 | let base64PropertyList = plistData.base64EncodedString() 122 | return base64PropertyList 123 | } 124 | 125 | private func getImageIndex(fileName: String) -> Int { 126 | return self.images.firstIndex(of: fileName) ?? 0 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/WallpapperLib/ImageMetadataReaders/DynamicWallpaperExtractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import AppKit 9 | import AVFoundation 10 | 11 | public class DynamicWallpaperExtractor { 12 | let consoleIO = ConsoleIO() 13 | 14 | public init() { 15 | } 16 | 17 | public func extract(imageData: Data, outputFileName: String?) throws { 18 | let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) 19 | guard let imageSourceValue = imageSource else { 20 | throw MetadataExtractorError.imageSourceNotCreated 21 | } 22 | 23 | let imageMetadata = CGImageSourceCopyMetadataAtIndex(imageSourceValue, 0, nil) 24 | guard let imageMetadataValue = imageMetadata else { 25 | throw MetadataExtractorError.imageMetadataNotCreated 26 | } 27 | 28 | CGImageMetadataEnumerateTagsUsingBlock(imageMetadataValue, nil, nil) { (value, metadataTag) -> Bool in 29 | 30 | let valueString = value as String 31 | self.consoleIO.writeMessage("---------------------------------------------------") 32 | self.consoleIO.writeMessage("Metadata key: \(valueString)") 33 | 34 | let tag = CGImageMetadataTagCopyValue(metadataTag) 35 | 36 | guard let valueTag = tag as? String else { 37 | self.consoleIO.writeMessage("\tError during convert tag into string") 38 | return true 39 | } 40 | 41 | if valueString.starts(with: "apple_desktop:solar") || valueString.starts(with: "apple_desktop:h24") { 42 | guard let decodedData = Data(base64Encoded: valueTag) else { 43 | self.consoleIO.writeMessage("\tError during convert tag into binary data") 44 | return true 45 | } 46 | 47 | if let outputFileName = outputFileName { 48 | self.saveImage(decodedData: decodedData, outputFileName: outputFileName) 49 | } 50 | 51 | let decoder = PropertyListDecoder() 52 | guard let sequenceInfo = try? decoder.decode(SequenceInfo.self, from: decodedData) else { 53 | self.consoleIO.writeMessage("\tError during convert tag into object") 54 | return true 55 | } 56 | 57 | if let apperance = sequenceInfo.apperance { 58 | self.consoleIO.writeMessage("[APPERANCE]") 59 | self.consoleIO.writeMessage("\timage index: \(apperance.darkIndex), dark") 60 | self.consoleIO.writeMessage("\timage index: \(apperance.lightIndex), light") 61 | } 62 | 63 | if let solarItems = sequenceInfo.sequenceItems { 64 | self.consoleIO.writeMessage("[SOLAR]") 65 | for solarItem in solarItems { 66 | self.consoleIO.writeMessage("\timage index: \(solarItem.imageIndex), azimuth: \(solarItem.azimuth), altitude: \(solarItem.altitude)") 67 | } 68 | } 69 | 70 | if let timeItems = sequenceInfo.timeItems { 71 | self.consoleIO.writeMessage("[TIME]") 72 | for timeItem in timeItems { 73 | self.consoleIO.writeMessage("\timage index: \(timeItem.imageIndex), time: \(timeItem.time)") 74 | } 75 | } 76 | } else if valueString.starts(with: "apple_desktop:apr") { 77 | guard let decodedData = Data(base64Encoded: valueTag) else { 78 | self.consoleIO.writeMessage("\tError during convert tag into binary data") 79 | return false 80 | } 81 | 82 | if let outputFileName = outputFileName { 83 | self.saveImage(decodedData: decodedData, outputFileName: outputFileName) 84 | } 85 | 86 | let decoder = PropertyListDecoder() 87 | guard let apperance = try? decoder.decode(Apperance.self, from: decodedData) else { 88 | self.consoleIO.writeMessage("\tError during convert tag into object") 89 | return false 90 | } 91 | 92 | self.consoleIO.writeMessage("[APPERANCE]") 93 | self.consoleIO.writeMessage("\timage index: \(apperance.darkIndex), dark") 94 | self.consoleIO.writeMessage("\timage index: \(apperance.lightIndex), light") 95 | } else { 96 | self.consoleIO.writeMessage("\tvalue: \(valueTag)") 97 | } 98 | 99 | return true 100 | } 101 | } 102 | 103 | private func saveImage(decodedData: Data, outputFileName: String) { 104 | let path = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) 105 | let plistFile = path.appendingPathComponent(outputFileName) 106 | do { 107 | try decodedData.write(to: plistFile) 108 | self.consoleIO.writeMessage("\tSaved plist file: \(plistFile)") 109 | } 110 | catch { 111 | self.consoleIO.writeMessage("\tError during writing plist file: \(plistFile)") 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/Wallpapper/Program.swift: -------------------------------------------------------------------------------- 1 | // 2 | // https://mczachurski.dev 3 | // Copyright © 2021 Marcin Czachurski and the repository contributors. 4 | // Licensed under the MIT License. 5 | // 6 | 7 | import Foundation 8 | import WallpapperLib 9 | 10 | class Program { 11 | 12 | let consoleIO = ConsoleIO() 13 | var inputFileName = "" 14 | var outputFileName: String? = nil 15 | var shouldExtract = false 16 | 17 | func run() -> Bool { 18 | 19 | let (shouldBreak, resultCode) = self.proceedCommandLineArguments() 20 | if shouldBreak { 21 | return resultCode 22 | } 23 | 24 | if self.shouldExtract { 25 | return self.extractMetadata() 26 | } else { 27 | return self.generateImage() 28 | } 29 | } 30 | 31 | private func generateImage() -> Bool { 32 | do { 33 | let fileURL = try self.getPathToInputFile() 34 | self.consoleIO.writeMessage("Reading JSON file: '\(fileURL.absoluteString)'...", to: .debug) 35 | let inputFileContents = try Data(contentsOf: fileURL) 36 | self.consoleIO.writeMessage("OK.\n", to: .debug) 37 | 38 | let decoder = JSONDecoder() 39 | let formatter = DateFormatter() 40 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 41 | decoder.dateDecodingStrategy = .formatted(formatter) 42 | 43 | self.consoleIO.writeMessage("Decoding JSON file...", to: .debug) 44 | let pictureInfos = try decoder.decode([PictureInfo].self, from: inputFileContents) 45 | self.consoleIO.writeMessage("OK (\(pictureInfos.count) pictures).\n", to: .debug) 46 | 47 | let baseURL = fileURL.deletingLastPathComponent() 48 | let wallpaperGenerator = WallpaperGenerator() 49 | let fileName = self.outputFileName ?? "output.heic" 50 | try wallpaperGenerator.generate(pictureInfos: pictureInfos, baseURL: baseURL, outputFileName: fileName); 51 | } catch (let error as WallpapperError) { 52 | self.consoleIO.writeMessage("Unexpected error occurs: \(error.message)", to: .error) 53 | return false 54 | } catch { 55 | self.consoleIO.writeMessage("Unexpected error occurs: \(error)", to: .error) 56 | return false 57 | } 58 | 59 | return true 60 | } 61 | 62 | private func extractMetadata() -> Bool { 63 | do { 64 | let fileURL = try self.getPathToInputFile() 65 | self.consoleIO.writeMessage("Reading HEIC file: '\(fileURL.absoluteString)'...", to: .debug) 66 | let inputFileContents = try Data(contentsOf: fileURL) 67 | 68 | let dynamicWallpaperExtractor = DynamicWallpaperExtractor() 69 | try dynamicWallpaperExtractor.extract(imageData: inputFileContents, outputFileName: self.outputFileName) 70 | } catch { 71 | self.consoleIO.writeMessage("Error occurs during metadata extraction: \(error)", to: .error) 72 | return false 73 | } 74 | 75 | return true 76 | } 77 | 78 | private func getPathToInputFile() throws -> URL { 79 | return URL(fileURLWithPath: inputFileName) 80 | } 81 | 82 | private func proceedCommandLineArguments() -> (Bool, Bool) { 83 | if CommandLine.arguments.count == 1 { 84 | self.printUsage() 85 | return (true, false) 86 | } 87 | 88 | var optionIndex = 1 89 | while optionIndex < CommandLine.arguments.count { 90 | 91 | let option = CommandLine.arguments[optionIndex] 92 | let optionType = OptionType(value: option) 93 | 94 | switch optionType { 95 | case .help: 96 | self.printUsage() 97 | return (true, true) 98 | case .version: 99 | self.printVersion() 100 | return (true, true) 101 | case .input: 102 | let inputNameOptionIndex = optionIndex + 1 103 | if inputNameOptionIndex < CommandLine.arguments.count { 104 | self.inputFileName = CommandLine.arguments[inputNameOptionIndex] 105 | } 106 | 107 | optionIndex = inputNameOptionIndex 108 | case .output: 109 | let outputNameOptionIndex = optionIndex + 1 110 | if outputNameOptionIndex < CommandLine.arguments.count { 111 | self.outputFileName = CommandLine.arguments[outputNameOptionIndex] 112 | } 113 | 114 | optionIndex = outputNameOptionIndex 115 | case .extract: 116 | let inputNameOptionIndex = optionIndex + 1 117 | if inputNameOptionIndex < CommandLine.arguments.count { 118 | self.inputFileName = CommandLine.arguments[inputNameOptionIndex] 119 | } 120 | 121 | optionIndex = inputNameOptionIndex 122 | 123 | self.shouldExtract = true 124 | default: 125 | break; 126 | } 127 | 128 | optionIndex = optionIndex + 1 129 | } 130 | 131 | if self.inputFileName == "" { 132 | self.consoleIO.writeMessage("unknown input file name.", to: .error) 133 | return (true, false) 134 | } 135 | 136 | return (false, false) 137 | } 138 | 139 | private func printVersion() { 140 | self.consoleIO.writeMessage("1.7.4") 141 | } 142 | 143 | private func printUsage() { 144 | 145 | let executableName = (CommandLine.arguments[0] as NSString).lastPathComponent 146 | 147 | self.consoleIO.writeMessage("\(executableName): [command_option] [-i jsonFile] [-e heicFile]") 148 | self.consoleIO.writeMessage("Command options are:") 149 | self.consoleIO.writeMessage(" -h\t\t\tshow this message and exit") 150 | self.consoleIO.writeMessage(" -v\t\t\tshow program version and exit") 151 | self.consoleIO.writeMessage(" -o\t\t\toutput file name (default is 'output.heic')") 152 | self.consoleIO.writeMessage(" -i\t\t\tinput .json file with wallpaper description") 153 | self.consoleIO.writeMessage(" -e\t\t\tinput .heic file to extract metadata") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💻 wallpapper / wallpapper-exif 2 | 3 | ![Build Status](https://github.com/mczachurski/wallpapper/workflows/Build/badge.svg) 4 | [![Swift 5.2](https://img.shields.io/badge/Swift-5.2-orange.svg?style=flat)](ttps://developer.apple.com/swift/) 5 | [![Swift Package Manager](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/) 6 | [![Platforms OS X | Linux](https://img.shields.io/badge/Platforms-macOS%20-lightgray.svg?style=flat)](https://developer.apple.com/swift/) 7 | 8 | ![wallpaper](Images/wallpaper.png) 9 | 10 | This is simple console application for macOS to create dynamic wallpapers introduced in macOS Mojave. [Here](https://www.youtube.com/watch?v=TVqfPzdsbzY) you can watch how dynamic wallpapers works. Also you can read more about dynamic wallpapers in following articles: 11 | 12 | - [macOS Mojave dynamic wallpaper](https://itnext.io/macos-mojave-dynamic-wallpaper-fd26b0698223) 13 | - [macOS Mojave dynamic wallpapers (II)](https://itnext.io/macos-mojave-dynamic-wallpapers-ii-f8b1e55c82f) 14 | - [macOS Mojave dynamic wallpapers (III)](https://itnext.io/macos-mojave-wallpaper-iii-c747c30935c4) 15 | 16 | ## Examples 17 | 18 | Below you can download prepared dynamic wallpapers: 19 | 20 | - Earth view ([download](https://www.dropbox.com/s/kd2g59qswchsd0v/Earth%20View.heic?dl=0)) 21 | ![earth](Images/earth-01.gif) 22 | 23 | - Cyberpunk 2077 ([download](https://www.dropbox.com/s/54iupz5kmveh61j/cyberpunk-01.heic?dl=0)) 24 | ![cyberpunk](Images/cyberpunk-01.gif) 25 | 26 | ## Build and install 27 | 28 | You need to have latest XCode (10.2) and Swift 5 installed. 29 | 30 | ### Homebrew 31 | 32 | Open your terminal and run following commands. 33 | 34 | ```bash 35 | brew tap mczachurski/wallpapper 36 | brew install wallpapper 37 | ``` 38 | 39 | ### Manually 40 | 41 | Open your terminal and run following commands. 42 | 43 | ```bash 44 | $ git clone https://github.com/mczachurski/wallpapper.git 45 | $ cd wallpapper 46 | $ swift build --configuration release 47 | $ sudo cp .build/release/wallpapper /usr/local/bin 48 | $ sudo cp .build/release/wallpapper-exif /usr/local/bin 49 | ``` 50 | 51 | If you are using swift in version 4.1, please edit `Package.swift` file and put there your version of swift (in first line). 52 | 53 | Also you can build using `build.sh` script (it uses `swiftc` instead Swift CLI). 54 | 55 | ```bash 56 | $ git clone https://github.com/mczachurski/wallpapper.git 57 | $ cd wallpapper 58 | $ ./build.sh 59 | $ sudo cp .output/wallpapper /usr/local/bin 60 | $ sudo cp .output/wallpapper-exif /usr/local/bin 61 | ``` 62 | 63 | Now in the console you can run `wallpapper -h` and you should got a response similar to the following one. 64 | 65 | ```bash 66 | wallpapper: [command_option] [-i jsonFile] [-e heicFile] 67 | Command options are: 68 | -h show this message and exit 69 | -v show program version and exit 70 | -o output file name (default is 'output.heic') 71 | -i input .json file with wallpaper description 72 | -e input .heic file to extract metadata 73 | ``` 74 | 75 | That's all. Now you can build your own dynamic wallpappers. 76 | 77 | ### Troubleshooting 78 | 79 | If you get an error during the Swift build portion of install, try downloading the entire Xcode IDE (not just the tools) from the app store. Then run 80 | 81 | ```bash 82 | sudo xcode-select -s /Applications/Xcode.app/Contents/Developer 83 | ``` 84 | 85 | and run the installation command again. 86 | 87 | ## Getting started 88 | 89 | If you have done above commands now you can build dynamic wallpaper. It's really easy. First you have to put all you pictures into one folder and in the same folder create `json` file with picture's description. Application support three kinds of dynamic wallpapers. 90 | 91 | ### Solar 92 | 93 | For wallpaper which based on solar coordinates `json` file have to have structure like on below snippet. 94 | 95 | ```json 96 | [ 97 | { 98 | "fileName": "1.png", 99 | "isPrimary": true, 100 | "isForLight": true, 101 | "altitude": 27.95, 102 | "azimuth": 279.66 103 | }, 104 | { 105 | "fileName": "2.png", 106 | "altitude": -31.05, 107 | "azimuth": 4.16 108 | }, 109 | ... 110 | { 111 | "fileName": "16.png", 112 | "isForDark": true, 113 | "altitude": -28.63, 114 | "azimuth": 340.41 115 | } 116 | ] 117 | ``` 118 | 119 | Properties: 120 | 121 | - `fileName` - name of picture file name (you can use same file for few nodes). 122 | - `isPrimary` - information about image which is primary image (it will be visible after creating `heic` file). Only one of the file can be primary. 123 | - `isForLight` - if `true` picture will be displayed when user chose "Light (static)" wallpaper 124 | - `isForDark` - if `true` picture will be displayed when user chose "Dark (static)" wallpaper 125 | - `altitude` - is the angle between the Sun and the observer's local horizon. 126 | - `azimuth` - that is the angle of the Sun around the horizon. 127 | 128 | To calculate proper altitude and azimuth you can use `wallpapper-exif` application or web page: [https://keisan.casio.com/exec/system/1224682277](https://keisan.casio.com/exec/system/1224682277). In web page you have to put place where you take a photo and the date. Then system generate for you altitude and azimuth of the Sun during whole day. 129 | 130 | ### Time 131 | 132 | For wallpaper which based on OS time `json` file have to have structure like on below snippet. 133 | 134 | ```json 135 | [ 136 | { 137 | "fileName": "1.png", 138 | "isPrimary": true, 139 | "isForLight": true, 140 | "time": "2012-04-23T10:25:43Z" 141 | }, 142 | { 143 | "fileName": "2.png", 144 | "time": "2012-04-23T14:32:12Z" 145 | }, 146 | { 147 | "fileName": "3.png", 148 | "time": "2012-04-23T18:12:01Z" 149 | }, 150 | { 151 | "fileName": "4.png", 152 | "isForDark": true, 153 | "time": "2012-04-23T20:10:45Z" 154 | } 155 | ] 156 | ``` 157 | 158 | Properties: 159 | 160 | - `fileName` - name of picture file name (you can use same file for few nodes). 161 | - `isPrimary` - information about image which is primary image (it will be visible after creating `heic` file). Only one of the file can be primary. 162 | - `isForLight` - if `true` picture will be displayed when user chose "Light (static)" wallpaper 163 | - `isForDark` - if `true` picture will be displayed when user chose "Dark (static)" wallpaper 164 | - `time` - time when wallpaper will be changed (most important is hour). 165 | 166 | ### Apperance 167 | 168 | For wallpapers based on OS apperance settings (light/dark) we have to prepare much simpler JSON file, and we have to use only two images (one for light and one for dark theme). 169 | 170 | ```json 171 | [ 172 | { 173 | "fileName": "1.png", 174 | "isPrimary": true, 175 | "isForLight": true 176 | }, 177 | { 178 | "fileName": "2.png", 179 | "isForDark": true 180 | } 181 | ] 182 | ``` 183 | 184 | Properties: 185 | 186 | - `fileName` - name of picture file name. 187 | - `isPrimary` - information about image which is primary image (it will be visible after creating `heic` file). Only one of the file can be primary. 188 | - `isForLight` - if `true` picture will be displayed when user uses light theme 189 | - `isForDark` - if `true` picture will be displayed when user uses dark theme 190 | 191 | ### Preparing wallpapers 192 | 193 | When you have `json` file and all pictures then you can generate `heic` file. You have to run following command: 194 | 195 | ```bash 196 | wallpapper -i wallpapper.json 197 | ``` 198 | 199 | You should got a new file: `output.heic`. Set this file as a new wallpaper and enjoy you own dynamic wallpaper! 200 | 201 | ### Extracting metadata 202 | 203 | You can extract metadata from existing `heic` file. You have to run following command: 204 | 205 | ```bash 206 | wallpapper -e Catalina.heic 207 | ``` 208 | 209 | Metadata should be printed as output on the console. 210 | 211 | Also it's possible to extract and save whole `plist` file: 212 | 213 | ```bash 214 | wallpapper -e Catalina.heic -o output.plist 215 | ``` 216 | 217 | ### Calculating sun position 218 | 219 | If your photos contains GPS Exif metadata and creation time you can use `wallpapper-exif` application to generate `json` file with Sun `altitude` and `azimuth`. Example application usage: 220 | 221 | ```bash 222 | $ wallpapper-exif 1.jpeg 2.jpeg 3.jpeg 223 | ``` 224 | 225 | `json` should be produced as output on the console. 226 | 227 | Sun calculations has been created based on the [JavaScript library](https://github.com/mourner/suncalc) created by [Vladimir Agafonkin](http://agafonkin.com/en) ([@mourner](https://github.com/mourner)). 228 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,xcode,linux,vapor,swift,swiftpm,windows,visualstudio,visualstudiocode,swiftpackagemanager 3 | # Edit at https://www.gitignore.io/?templates=macos,xcode,linux,vapor,swift,swiftpm,windows,visualstudio,visualstudiocode,swiftpackagemanager 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Swift ### 49 | # Xcode 50 | # 51 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 52 | 53 | ## Build generated 54 | build/ 55 | DerivedData/ 56 | output/ 57 | 58 | ## Various settings 59 | *.pbxuser 60 | !default.pbxuser 61 | *.mode1v3 62 | !default.mode1v3 63 | *.mode2v3 64 | !default.mode2v3 65 | *.perspectivev3 66 | !default.perspectivev3 67 | xcuserdata/ 68 | 69 | ## Other 70 | *.moved-aside 71 | *.xccheckout 72 | *.xcscmblueprint 73 | 74 | ## Obj-C/Swift specific 75 | *.hmap 76 | *.ipa 77 | *.dSYM.zip 78 | *.dSYM 79 | 80 | ## Playgrounds 81 | timeline.xctimeline 82 | playground.xcworkspace 83 | 84 | # Swift Package Manager 85 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 86 | Packages/ 87 | Package.pins 88 | Package.resolved 89 | .build/ 90 | # Add this line if you want to avoid checking in Xcode SPM integration. 91 | .swiftpm/xcode 92 | 93 | # CocoaPods 94 | # We recommend against adding the Pods directory to your .gitignore. However 95 | # you should judge for yourself, the pros and cons are mentioned at: 96 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 97 | # Pods/ 98 | # Add this line if you want to avoid checking in source code from the Xcode workspace 99 | # *.xcworkspace 100 | 101 | # Carthage 102 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 103 | # Carthage/Checkouts 104 | 105 | Carthage/Build 106 | 107 | # Accio dependency management 108 | Dependencies/ 109 | .accio/ 110 | 111 | # fastlane 112 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 113 | # screenshots whenever they are needed. 114 | # For more information about the recommended setup visit: 115 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 116 | 117 | fastlane/report.xml 118 | fastlane/Preview.html 119 | fastlane/screenshots/**/*.png 120 | fastlane/test_output 121 | 122 | # Code Injection 123 | # After new code Injection tools there's a generated folder /iOSInjectionProject 124 | # https://github.com/johnno1962/injectionforxcode 125 | 126 | iOSInjectionProject/ 127 | 128 | ### SwiftPackageManager ### 129 | Packages 130 | xcuserdata 131 | *.xcodeproj 132 | 133 | 134 | ### SwiftPM ### 135 | 136 | 137 | ### Vapor ### 138 | Config/secrets 139 | 140 | ### Vapor Patch ### 141 | 142 | 143 | ### VisualStudioCode ### 144 | .vscode/* 145 | !.vscode/settings.json 146 | !.vscode/tasks.json 147 | !.vscode/launch.json 148 | !.vscode/extensions.json 149 | 150 | ### VisualStudioCode Patch ### 151 | # Ignore all local history of files 152 | .history 153 | 154 | ### Windows ### 155 | # Windows thumbnail cache files 156 | Thumbs.db 157 | Thumbs.db:encryptable 158 | ehthumbs.db 159 | ehthumbs_vista.db 160 | 161 | # Dump file 162 | *.stackdump 163 | 164 | # Folder config file 165 | [Dd]esktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msix 174 | *.msm 175 | *.msp 176 | 177 | # Windows shortcuts 178 | *.lnk 179 | 180 | ### Xcode ### 181 | # Xcode 182 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 183 | 184 | ## User settings 185 | 186 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 187 | 188 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 189 | 190 | ## Xcode Patch 191 | *.xcodeproj/* 192 | !*.xcodeproj/project.pbxproj 193 | !*.xcodeproj/xcshareddata/ 194 | !*.xcworkspace/contents.xcworkspacedata 195 | /*.gcno 196 | 197 | ### Xcode Patch ### 198 | **/xcshareddata/WorkspaceSettings.xcsettings 199 | 200 | ### VisualStudio ### 201 | ## Ignore Visual Studio temporary files, build results, and 202 | ## files generated by popular Visual Studio add-ons. 203 | ## 204 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 205 | 206 | # User-specific files 207 | *.rsuser 208 | *.suo 209 | *.user 210 | *.userosscache 211 | *.sln.docstates 212 | 213 | # User-specific files (MonoDevelop/Xamarin Studio) 214 | *.userprefs 215 | 216 | # Mono auto generated files 217 | mono_crash.* 218 | 219 | # Build results 220 | [Dd]ebug/ 221 | [Dd]ebugPublic/ 222 | [Rr]elease/ 223 | [Rr]eleases/ 224 | x64/ 225 | x86/ 226 | [Aa][Rr][Mm]/ 227 | [Aa][Rr][Mm]64/ 228 | bld/ 229 | [Bb]in/ 230 | [Oo]bj/ 231 | [Ll]og/ 232 | 233 | # Visual Studio 2015/2017 cache/options directory 234 | .vs/ 235 | # Uncomment if you have tasks that create the project's static files in wwwroot 236 | #wwwroot/ 237 | 238 | # Visual Studio 2017 auto generated files 239 | Generated\ Files/ 240 | 241 | # MSTest test Results 242 | [Tt]est[Rr]esult*/ 243 | [Bb]uild[Ll]og.* 244 | 245 | # NUnit 246 | *.VisualState.xml 247 | TestResult.xml 248 | nunit-*.xml 249 | 250 | # Build Results of an ATL Project 251 | [Dd]ebugPS/ 252 | [Rr]eleasePS/ 253 | dlldata.c 254 | 255 | # Benchmark Results 256 | BenchmarkDotNet.Artifacts/ 257 | 258 | # .NET Core 259 | project.lock.json 260 | project.fragment.lock.json 261 | artifacts/ 262 | 263 | # StyleCop 264 | StyleCopReport.xml 265 | 266 | # Files built by Visual Studio 267 | *_i.c 268 | *_p.c 269 | *_h.h 270 | *.ilk 271 | *.obj 272 | *.iobj 273 | *.pch 274 | *.pdb 275 | *.ipdb 276 | *.pgc 277 | *.pgd 278 | *.rsp 279 | *.sbr 280 | *.tlb 281 | *.tli 282 | *.tlh 283 | *.tmp 284 | *.tmp_proj 285 | *_wpftmp.csproj 286 | *.log 287 | *.vspscc 288 | *.vssscc 289 | .builds 290 | *.pidb 291 | *.svclog 292 | *.scc 293 | 294 | # Chutzpah Test files 295 | _Chutzpah* 296 | 297 | # Visual C++ cache files 298 | ipch/ 299 | *.aps 300 | *.ncb 301 | *.opendb 302 | *.opensdf 303 | *.sdf 304 | *.cachefile 305 | *.VC.db 306 | *.VC.VC.opendb 307 | 308 | # Visual Studio profiler 309 | *.psess 310 | *.vsp 311 | *.vspx 312 | *.sap 313 | 314 | # Visual Studio Trace Files 315 | *.e2e 316 | 317 | # TFS 2012 Local Workspace 318 | $tf/ 319 | 320 | # Guidance Automation Toolkit 321 | *.gpState 322 | 323 | # ReSharper is a .NET coding add-in 324 | _ReSharper*/ 325 | *.[Rr]e[Ss]harper 326 | *.DotSettings.user 327 | 328 | # JustCode is a .NET coding add-in 329 | .JustCode 330 | 331 | # TeamCity is a build add-in 332 | _TeamCity* 333 | 334 | # DotCover is a Code Coverage Tool 335 | *.dotCover 336 | 337 | # AxoCover is a Code Coverage Tool 338 | .axoCover/* 339 | !.axoCover/settings.json 340 | 341 | # Visual Studio code coverage results 342 | *.coverage 343 | *.coveragexml 344 | 345 | # NCrunch 346 | _NCrunch_* 347 | .*crunch*.local.xml 348 | nCrunchTemp_* 349 | 350 | # MightyMoose 351 | *.mm.* 352 | AutoTest.Net/ 353 | 354 | # Web workbench (sass) 355 | .sass-cache/ 356 | 357 | # Installshield output folder 358 | [Ee]xpress/ 359 | 360 | # DocProject is a documentation generator add-in 361 | DocProject/buildhelp/ 362 | DocProject/Help/*.HxT 363 | DocProject/Help/*.HxC 364 | DocProject/Help/*.hhc 365 | DocProject/Help/*.hhk 366 | DocProject/Help/*.hhp 367 | DocProject/Help/Html2 368 | DocProject/Help/html 369 | 370 | # Click-Once directory 371 | publish/ 372 | 373 | # Publish Web Output 374 | *.[Pp]ublish.xml 375 | *.azurePubxml 376 | # Note: Comment the next line if you want to checkin your web deploy settings, 377 | # but database connection strings (with potential passwords) will be unencrypted 378 | *.pubxml 379 | *.publishproj 380 | 381 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 382 | # checkin your Azure Web App publish settings, but sensitive information contained 383 | # in these scripts will be unencrypted 384 | PublishScripts/ 385 | 386 | # NuGet Packages 387 | *.nupkg 388 | # NuGet Symbol Packages 389 | *.snupkg 390 | # The packages folder can be ignored because of Package Restore 391 | **/[Pp]ackages/* 392 | # except build/, which is used as an MSBuild target. 393 | !**/[Pp]ackages/build/ 394 | # Uncomment if necessary however generally it will be regenerated when needed 395 | #!**/[Pp]ackages/repositories.config 396 | # NuGet v3's project.json files produces more ignorable files 397 | *.nuget.props 398 | *.nuget.targets 399 | 400 | # Microsoft Azure Build Output 401 | csx/ 402 | *.build.csdef 403 | 404 | # Microsoft Azure Emulator 405 | ecf/ 406 | rcf/ 407 | 408 | # Windows Store app package directories and files 409 | AppPackages/ 410 | BundleArtifacts/ 411 | Package.StoreAssociation.xml 412 | _pkginfo.txt 413 | *.appx 414 | *.appxbundle 415 | *.appxupload 416 | 417 | # Visual Studio cache files 418 | # files ending in .cache can be ignored 419 | *.[Cc]ache 420 | # but keep track of directories ending in .cache 421 | !?*.[Cc]ache/ 422 | 423 | # Others 424 | ClientBin/ 425 | ~$* 426 | *.dbmdl 427 | *.dbproj.schemaview 428 | *.jfm 429 | *.pfx 430 | *.publishsettings 431 | orleans.codegen.cs 432 | 433 | # Including strong name files can present a security risk 434 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 435 | #*.snk 436 | 437 | # Since there are multiple workflows, uncomment next line to ignore bower_components 438 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 439 | #bower_components/ 440 | 441 | # RIA/Silverlight projects 442 | Generated_Code/ 443 | 444 | # Backup & report files from converting an old project file 445 | # to a newer Visual Studio version. Backup files are not needed, 446 | # because we have git ;-) 447 | _UpgradeReport_Files/ 448 | Backup*/ 449 | UpgradeLog*.XML 450 | UpgradeLog*.htm 451 | ServiceFabricBackup/ 452 | *.rptproj.bak 453 | 454 | # SQL Server files 455 | *.mdf 456 | *.ldf 457 | *.ndf 458 | 459 | # Business Intelligence projects 460 | *.rdl.data 461 | *.bim.layout 462 | *.bim_*.settings 463 | *.rptproj.rsuser 464 | *- [Bb]ackup.rdl 465 | *- [Bb]ackup ([0-9]).rdl 466 | *- [Bb]ackup ([0-9][0-9]).rdl 467 | 468 | # Microsoft Fakes 469 | FakesAssemblies/ 470 | 471 | # GhostDoc plugin setting file 472 | *.GhostDoc.xml 473 | 474 | # Node.js Tools for Visual Studio 475 | .ntvs_analysis.dat 476 | node_modules/ 477 | 478 | # Visual Studio 6 build log 479 | *.plg 480 | 481 | # Visual Studio 6 workspace options file 482 | *.opt 483 | 484 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 485 | *.vbw 486 | 487 | # Visual Studio LightSwitch build output 488 | **/*.HTMLClient/GeneratedArtifacts 489 | **/*.DesktopClient/GeneratedArtifacts 490 | **/*.DesktopClient/ModelManifest.xml 491 | **/*.Server/GeneratedArtifacts 492 | **/*.Server/ModelManifest.xml 493 | _Pvt_Extensions 494 | 495 | # Paket dependency manager 496 | .paket/paket.exe 497 | paket-files/ 498 | 499 | # FAKE - F# Make 500 | .fake/ 501 | 502 | # CodeRush personal settings 503 | .cr/personal 504 | 505 | # Python Tools for Visual Studio (PTVS) 506 | __pycache__/ 507 | *.pyc 508 | 509 | # Cake - Uncomment if you are using it 510 | # tools/** 511 | # !tools/packages.config 512 | 513 | # Tabs Studio 514 | *.tss 515 | 516 | # Telerik's JustMock configuration file 517 | *.jmconfig 518 | 519 | # BizTalk build output 520 | *.btp.cs 521 | *.btm.cs 522 | *.odx.cs 523 | *.xsd.cs 524 | 525 | # OpenCover UI analysis results 526 | OpenCover/ 527 | 528 | # Azure Stream Analytics local run output 529 | ASALocalRun/ 530 | 531 | # MSBuild Binary and Structured Log 532 | *.binlog 533 | 534 | # NVidia Nsight GPU debugger configuration file 535 | *.nvuser 536 | 537 | # MFractors (Xamarin productivity tool) working folder 538 | .mfractor/ 539 | 540 | # Local History for Visual Studio 541 | .localhistory/ 542 | 543 | # BeatPulse healthcheck temp database 544 | healthchecksdb 545 | 546 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 547 | MigrationBackup/ 548 | 549 | # End of https://www.gitignore.io/api/macos,xcode,linux,vapor,swift,swiftpm,windows,visualstudio,visualstudiocode,swiftpackagemanager 550 | --------------------------------------------------------------------------------