├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── Document └── Images │ └── appicon.gif ├── INSTALL_RECEIPT.json ├── LICENSE.md ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── AppIcon │ └── main.swift └── AppIconCore │ ├── Command │ └── Command.swift │ ├── Data │ ├── Icon │ │ ├── AppIcon.swift │ │ ├── AppIconSet.swift │ │ ├── IosAppIconSet.swift │ │ ├── MacAppIconSet.swift │ │ ├── Scale.swift │ │ └── WatchAppIconSet.swift │ ├── Json │ │ └── ContentsJson.swift │ └── Platform.swift │ ├── Error │ └── LocalError.swift │ └── Extractor │ ├── ImageExtractor.swift │ └── JsonExtractor.swift └── Tests └── AppIconTests └── AppIconTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | 3 | .DS_Store 4 | 5 | ### Swift ### 6 | 7 | /.build 8 | /DerivedData 9 | /Packages 10 | /*.xcodeproj 11 | /*.tar.gz 12 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode14 3 | 4 | install: 5 | - make build 6 | 7 | script: 8 | - make test 9 | -------------------------------------------------------------------------------- /Document/Images/appicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nonchalant/AppIcon/ddc4662a854dbd3f6f6b2159c914d90d2dfbbf51/Document/Images/appicon.gif -------------------------------------------------------------------------------- /INSTALL_RECEIPT.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEAD": null, 3 | "built_as_bottle": true, 4 | "compiler": "clang", 5 | "poured_from_bottle": false, 6 | "stdlib": null, 7 | "tapped_from": "nonchalant/appicon", 8 | "time": 0, 9 | "unused_options": [], 10 | "used_options": [] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Takeshi Ihara 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY?=appicon 2 | BUILD_FOLDER?=.build 3 | OS?=ventura 4 | PREFIX?=/usr/local 5 | PROJECT?=AppIcon 6 | RELEASE_BINARY_FOLDER?=$(BUILD_FOLDER)/release/$(PROJECT) 7 | VERSION?=1.0.6 8 | 9 | debug: 10 | swift build 11 | 12 | build: 13 | swift build -c release --disable-sandbox --manifest-cache none 14 | 15 | test: 16 | swift test 17 | 18 | clean: 19 | swift package clean 20 | rm -rf DerivedData 21 | rm -rf $(BUILD_FOLDER) 22 | 23 | install: build 24 | mkdir -p $(PREFIX)/bin 25 | cp -f $(RELEASE_BINARY_FOLDER) $(PREFIX)/bin/$(BINARY) 26 | 27 | bottle: clean build 28 | mkdir -p $(BINARY)/$(VERSION)/bin 29 | cp README.md $(BINARY)/$(VERSION)/README.md 30 | cp LICENSE.md $(BINARY)/$(VERSION)/LICENSE.md 31 | cp INSTALL_RECEIPT.json $(BINARY)/$(VERSION)/INSTALL_RECEIPT.json 32 | cp -f $(RELEASE_BINARY_FOLDER) $(BINARY)/$(VERSION)/bin/$(BINARY) 33 | tar cfvz $(BINARY)-$(VERSION).$(OS).bottle.tar.gz --exclude='*/.*' $(BINARY) 34 | shasum -a 256 $(BINARY)-$(VERSION).$(OS).bottle.tar.gz 35 | rm -rf $(BINARY) 36 | 37 | sha256: 38 | wget https://github.com/Nonchalant/$(PROJECT)/archive/$(VERSION).tar.gz -O $(PROJECT)-$(VERSION).tar.gz 39 | shasum -a 256 $(PROJECT)-$(VERSION).tar.gz 40 | rm $(PROJECT)-$(VERSION).tar.gz 41 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 7 | "state": { 8 | "branch": null, 9 | "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff", 10 | "version": "0.3.1" 11 | } 12 | }, 13 | { 14 | "package": "SwiftShell", 15 | "repositoryURL": "https://github.com/kareman/SwiftShell.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "99680b2efc7c7dbcace1da0b3979d266f02e213c", 19 | "version": "5.1.0" 20 | } 21 | }, 22 | { 23 | "package": "Tablier", 24 | "repositoryURL": "https://github.com/akkyie/Tablier", 25 | "state": { 26 | "branch": null, 27 | "revision": "1e8e0e0d92b5c1412cfec30cd3c4c761333133b3", 28 | "version": "0.3.3" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppIcon", 7 | products: [ 8 | .executable(name: "AppIcon", targets: ["AppIcon"]) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.1"), 12 | .package(url: "https://github.com/kareman/SwiftShell.git", from: "5.1.0"), 13 | .package(url: "https://github.com/akkyie/Tablier", from: "0.2.0") 14 | ], 15 | targets: [ 16 | .executableTarget( 17 | name: "AppIcon", 18 | dependencies: [ 19 | "AppIconCore" 20 | ] 21 | ), 22 | .target( 23 | name: "AppIconCore", 24 | dependencies: [ 25 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 26 | "SwiftShell", 27 | ] 28 | ), 29 | .testTarget( 30 | name: "AppIconTests", 31 | dependencies: [ 32 | "AppIconCore", 33 | "Tablier" 34 | ] 35 | ) 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppIcon 2 | 3 | [![Build Status](https://travis-ci.com/Nonchalant/AppIcon.svg?branch=master)](https://travis-ci.com/Nonchalant/AppIcon) 4 | ![platforms](https://img.shields.io/badge/platforms-iOS-333333.svg) 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/Nonchalant/AppIcon/master/LICENSE.md) 6 | [![GitHub release](https://img.shields.io/github/release/Nonchalant/AppIcon.svg)](https://github.com/Nonchalant/AppIcon/releases) 7 | ![Xcode](https://img.shields.io/badge/Xcode-14.2-brightgreen.svg) 8 | ![Swift](https://img.shields.io/badge/Swift-5.7.2-brightgreen.svg) 9 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 10 | 11 | `AppIcon` generates `*.appiconset` contains each resolution image for iOS, MacOS. 12 | 13 | ``` 14 | AppIcon.appiconset 15 | ├── Contents.json 16 | ├── AppIcon-20.0x20.0@2x.png 17 | ├── AppIcon-20.0x20.0@3x.png 18 | ├── AppIcon-29.0x29.0@2x.png 19 | ├── AppIcon-29.0x29.0@3x.png 20 | ├── AppIcon-40.0x40.0@2x.png 21 | ├── AppIcon-40.0x40.0@3x.png 22 | ├── AppIcon-60.0x60.0@2x.png 23 | ├── AppIcon-60.0x60.0@3x.png 24 | └── AppIcon-1024.0x1024.0@1x.png 25 | ``` 26 | 27 | ## Demo 28 | 29 | ![](Document/Images/appicon.gif) 30 | 31 | ## Installation 32 | 33 | ### Homebrew 34 | 35 | ``` 36 | $ brew install Nonchalant/appicon/appicon 37 | ``` 38 | 39 | ### [Mint](https://github.com/yonaskolb/Mint) 40 | 41 | ```bash 42 | $ mint run nonchalant/appicon 43 | ``` 44 | 45 | ### Manual 46 | 47 | Clone the master branch of the repository, then run make install. 48 | 49 | ``` 50 | $ git clone https://github.com/Nonchalant/AppIcon.git 51 | $ make install 52 | ``` 53 | 54 | ## Usage 55 | 56 | `AppIcon` needs path of base image(`.png`). The size of base image is 1024x1024 pixel preferably. 57 | 58 | ``` 59 | $ appicon iTunesIcon-1024x1024.png 60 | ``` 61 | 62 | ## Option 63 | 64 | You can see options by `appicon --help`. 65 | 66 | #### --icon-name 67 | 68 | Default: `AppIcon` 69 | 70 | #### --output-path 71 | 72 | Default: `./AppIcon.appiconset` 73 | 74 | #### --mac 75 | 76 | Default: false 77 | 78 | #### --watch 79 | 80 | Default: false 81 | 82 | ## Develop 83 | 84 | ### Runs debug build 85 | 86 | ``` 87 | $ make debug 88 | ``` 89 | 90 | ### Runs release build 91 | 92 | ``` 93 | $ make build 94 | ``` 95 | 96 | ### Runs tests 97 | 98 | ``` 99 | $ make test 100 | ``` 101 | 102 | ## Author 103 | 104 | Takeshi Ihara 105 | 106 | ## License 107 | 108 | Appicon is available under the MIT license. See the LICENSE file for more info. 109 | -------------------------------------------------------------------------------- /Sources/AppIcon/main.swift: -------------------------------------------------------------------------------- 1 | import AppIconCore 2 | import ArgumentParser 3 | import Foundation 4 | 5 | struct AppIcon: ParsableCommand { 6 | static let configuration = CommandConfiguration( 7 | commandName: "appicon", 8 | abstract: "AppIcon generates *.appiconset contains each resolution image for iOS", 9 | version: "1.0.6" 10 | ) 11 | 12 | @Argument(help: "The path to the base image (1024x1024.png)", completion: .file(extensions: ["png"])) 13 | var image: String 14 | 15 | @Option(help: "The name of the generated image") 16 | var iconName = "AppIcon" 17 | 18 | @Option(help: "The path of the generated appiconset") 19 | var outputPath = "AppIcon" 20 | 21 | @Flag(help: "Generate also Mac icons") 22 | var mac = false 23 | 24 | @Flag(help: "Generate also Apple Watch icons") 25 | var watch = false 26 | 27 | func run() throws { 28 | guard let `extension` = image.split(separator: ".").last, 29 | `extension`.caseInsensitiveCompare("png") == .orderedSame else { 30 | throw ValidationError("image path should have .png extension") 31 | } 32 | 33 | guard FileManager.default.fileExists(atPath: image) else { 34 | throw ValidationError("an input image is not exists") 35 | } 36 | 37 | let outputExpansion = ".appiconset" 38 | let outputPath = self.outputPath.hasSuffix(outputExpansion) ? self.outputPath : "\(self.outputPath)\(outputExpansion)" 39 | let platform = Platform(mac: mac, watch: watch) 40 | 41 | do { 42 | try ImageExtractor.extract(base: image, output: outputPath, iconName: iconName, platform: platform) 43 | } catch { 44 | print("Image Extraction Error has occurred 😱") 45 | throw ExitCode(1) 46 | } 47 | 48 | do { 49 | try JsonExtractor.extract(output: outputPath, iconName: iconName, platform: platform) 50 | } catch { 51 | print("Json Extraction Error has occurred 😱") 52 | throw ExitCode(1) 53 | } 54 | 55 | print("\(outputPath) has been generated 🎉") 56 | } 57 | } 58 | 59 | AppIcon.main() 60 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Command/Command.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | enum Command { 5 | case createDirectory(output: String) 6 | case extractImage(base: String, output: String, size: Float) 7 | case createJSON(json: String, output: String) 8 | 9 | func execute() throws { 10 | switch self { 11 | case .createDirectory(let output): 12 | try execute("/bin/mkdir", "-p", output) 13 | case .extractImage(let base, let output, let size): 14 | try execute("/usr/bin/sips", "-Z", "\(size)", base, "--out", output) 15 | case .createJSON(let json, let output): 16 | let writefile = try open(forWriting: output, overwrite: true) 17 | writefile.write(json) 18 | writefile.close() 19 | } 20 | } 21 | 22 | private func execute(_ executable: String, _ args: String...) throws { 23 | let runOutput = run(executable, args) 24 | 25 | guard runOutput.succeeded else { 26 | throw LocalError.execution 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Icon/AppIcon.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AppIcon: Hashable { 4 | let name: String 5 | let platform: Platform 6 | let scale: Scale 7 | let size: Float 8 | } 9 | 10 | extension AppIcon { 11 | var scaleSize: Float { 12 | size * scale.magnification 13 | } 14 | 15 | var scaleForJson: String? { 16 | platform.isSingleTrimmed && (scale == ._1x) ? nil : scale.rawValue 17 | } 18 | 19 | var baseSizeStr: String { 20 | (size == round(size)) ? String(format: "%.f", size) : "\(size)" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Icon/AppIconSet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol AppIconSet { 4 | var size: Float { get } 5 | var platform: Platform { get } 6 | var scales: Set { get } 7 | } 8 | 9 | extension AppIconSet { 10 | func icons(iconName: String) -> [AppIcon] { 11 | scales.map { scale in 12 | AppIcon( 13 | name: "\(iconName)-\(size)x\(size)@\(scale.rawValue).png", 14 | platform: platform, 15 | scale: scale, 16 | size: size 17 | ) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Icon/IosAppIconSet.swift: -------------------------------------------------------------------------------- 1 | enum IosAppIconSet: CaseIterable { 2 | case _20 3 | case _29 4 | case _38 5 | case _40 6 | case _60 7 | case _64 8 | case _68 9 | case _76 10 | case _83_5 11 | case _1024 12 | } 13 | 14 | extension IosAppIconSet: AppIconSet { 15 | var size: Float { 16 | switch self { 17 | case ._20: 18 | return 20 19 | case ._29: 20 | return 29 21 | case ._38: 22 | return 38 23 | case ._40: 24 | return 40 25 | case ._60: 26 | return 60 27 | case ._64: 28 | return 64 29 | case ._68: 30 | return 68 31 | case ._76: 32 | return 76 33 | case ._83_5: 34 | return 83.5 35 | case ._1024: 36 | return 1024 37 | } 38 | } 39 | 40 | var platform: Platform { 41 | .ios 42 | } 43 | 44 | var scales: Set { 45 | switch self { 46 | case ._20, ._29, ._38, ._40, ._60, ._64: 47 | return Set([._2x, ._3x]) 48 | case ._68, ._76, ._83_5: 49 | return Set([._2x]) 50 | case ._1024: 51 | return Set([._1x]) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Icon/MacAppIconSet.swift: -------------------------------------------------------------------------------- 1 | enum MacAppIconSet: CaseIterable { 2 | case _16 3 | case _32 4 | case _128 5 | case _256 6 | case _512 7 | } 8 | 9 | extension MacAppIconSet: AppIconSet { 10 | var size: Float { 11 | switch self { 12 | case ._16: 13 | return 16 14 | case ._32: 15 | return 32 16 | case ._128: 17 | return 128 18 | case ._256: 19 | return 256 20 | case ._512: 21 | return 512 22 | } 23 | } 24 | 25 | var platform: Platform { 26 | .mac 27 | } 28 | 29 | var scales: Set { 30 | Set([._1x, ._2x]) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Icon/Scale.swift: -------------------------------------------------------------------------------- 1 | enum Scale: Hashable { 2 | case _1x 3 | case _2x 4 | case _3x 5 | } 6 | 7 | extension Scale { 8 | var rawValue: String { 9 | switch self { 10 | case ._1x: 11 | return "1x" 12 | case ._2x: 13 | return "2x" 14 | case ._3x: 15 | return "3x" 16 | } 17 | } 18 | 19 | var magnification: Float { 20 | switch self { 21 | case ._1x: 22 | return 1.0 23 | case ._2x: 24 | return 2.0 25 | case ._3x: 26 | return 3.0 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Icon/WatchAppIconSet.swift: -------------------------------------------------------------------------------- 1 | enum WatchAppIconSet: CaseIterable { 2 | case _22 3 | case _24 4 | case _27_5 5 | case _29 6 | case _30 7 | case _32 8 | case _33 9 | case _40 10 | case _43_5 11 | case _44 12 | case _46 13 | case _50 14 | case _51 15 | case _54 16 | case _86 17 | case _98 18 | case _108 19 | case _117 20 | case _129 21 | case _1024 22 | } 23 | 24 | extension WatchAppIconSet: AppIconSet { 25 | var size: Float { 26 | switch self { 27 | case ._22: 28 | return 22 29 | case ._24: 30 | return 24 31 | case ._27_5: 32 | return 27.5 33 | case ._29: 34 | return 29 35 | case ._30: 36 | return 30 37 | case ._32: 38 | return 32 39 | case ._33: 40 | return 33 41 | case ._40: 42 | return 40 43 | case ._43_5: 44 | return 43.5 45 | case ._44: 46 | return 44 47 | case ._46: 48 | return 46 49 | case ._50: 50 | return 50 51 | case ._51: 52 | return 51 53 | case ._54: 54 | return 54 55 | case ._86: 56 | return 86 57 | case ._98: 58 | return 98 59 | case ._108: 60 | return 108 61 | case ._117: 62 | return 117 63 | case ._129: 64 | return 129 65 | case ._1024: 66 | return 1024 67 | } 68 | } 69 | 70 | var platform: Platform { 71 | .watch 72 | } 73 | 74 | var scales: Set { 75 | switch self { 76 | case ._22, 77 | ._24, 78 | ._27_5, 79 | ._29, 80 | ._30, 81 | ._32, 82 | ._33, 83 | ._40, 84 | ._43_5, 85 | ._44, 86 | ._46, 87 | ._50, 88 | ._51, 89 | ._54, 90 | ._86, 91 | ._98, 92 | ._108, 93 | ._117, 94 | ._129: 95 | return Set([._2x]) 96 | case ._1024: 97 | return Set([._1x]) 98 | } 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Json/ContentsJson.swift: -------------------------------------------------------------------------------- 1 | struct ContentsJson: Encodable { 2 | let images: [Image] 3 | let info: Info 4 | } 5 | 6 | extension ContentsJson { 7 | struct Image: Encodable { 8 | let filename: String 9 | let idiom: String 10 | let platform: String? 11 | let scale: String? 12 | let size: String 13 | } 14 | 15 | struct Info: Encodable { 16 | let version: Int 17 | let author: String 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Data/Platform.swift: -------------------------------------------------------------------------------- 1 | public enum Platform: Equatable { 2 | case ios 3 | case mac 4 | case watch 5 | } 6 | 7 | extension Platform { 8 | public init(mac: Bool, watch: Bool) { 9 | if mac { 10 | self = .mac 11 | } else if watch { 12 | self = .watch 13 | } else { 14 | self = .ios 15 | } 16 | } 17 | 18 | func icons(iconName: String) -> Set { 19 | let icons: [AppIconSet] = { 20 | switch self { 21 | case .ios: 22 | return IosAppIconSet.allCases 23 | case .mac: 24 | return MacAppIconSet.allCases 25 | case .watch: 26 | return WatchAppIconSet.allCases 27 | } 28 | }() 29 | 30 | return Set( 31 | icons 32 | .map { $0.icons(iconName: iconName) } 33 | .flatMap { $0 } 34 | ) 35 | } 36 | } 37 | 38 | extension Platform { 39 | var idiom: String { 40 | switch self { 41 | case .ios, .watch: 42 | return "universal" 43 | case .mac: 44 | return "mac" 45 | } 46 | } 47 | 48 | var rawValue: String? { 49 | switch self { 50 | case .ios: 51 | return "ios" 52 | case .mac: 53 | return nil 54 | case .watch: 55 | return "watchos" 56 | } 57 | } 58 | 59 | var isSingleTrimmed: Bool { 60 | switch self { 61 | case .ios, .mac: 62 | return false 63 | case .watch: 64 | return true 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Error/LocalError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum LocalError: Error { 4 | case execution 5 | case extraction 6 | } 7 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Extractor/ImageExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ImageExtractor { 4 | public static func extract( 5 | base: String, 6 | output: String, 7 | iconName: String, 8 | platform: Platform 9 | ) throws { 10 | do { 11 | try Command.createDirectory(output: output).execute() 12 | 13 | let icons = platform.icons(iconName: iconName) 14 | try extract(base: base, output: output, icons: icons) 15 | } catch { 16 | throw LocalError.extraction 17 | } 18 | } 19 | 20 | private static func extract( 21 | base: String, 22 | output: String, 23 | icons: Set 24 | ) throws { 25 | for icon in icons { 26 | try Command 27 | .extractImage( 28 | base: base, 29 | output: "\(output)/\(icon.name)", 30 | size: icon.scaleSize 31 | ) 32 | .execute() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/AppIconCore/Extractor/JsonExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum JsonExtractor { 4 | public static func extract( 5 | output: String, 6 | iconName: String, 7 | platform: Platform 8 | ) throws { 9 | do { 10 | let icons = platform.icons(iconName: iconName) 11 | try Command 12 | .createJSON( 13 | json: generate(icons: icons), 14 | output: "\(output)/Contents.json" 15 | ) 16 | .execute() 17 | } catch { 18 | throw LocalError.extraction 19 | } 20 | } 21 | 22 | private static func generate(icons: Set) -> String { 23 | let images = icons.map { icon in 24 | ContentsJson.Image( 25 | filename: icon.name, 26 | idiom: icon.platform.idiom, 27 | platform: icon.platform.rawValue, 28 | scale: icon.scaleForJson, 29 | size: "\(icon.baseSizeStr)x\(icon.baseSizeStr)" 30 | ) 31 | } 32 | 33 | let json = ContentsJson( 34 | images: images, 35 | info: ContentsJson.Info( 36 | version: 1, 37 | author: "appicon" 38 | ) 39 | ) 40 | 41 | do { 42 | let data = try JSONEncoder().encode(json) 43 | return String(data: data, encoding: .utf8) ?? "" 44 | } catch { 45 | return "" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/AppIconTests/AppIconTests.swift: -------------------------------------------------------------------------------- 1 | import Tablier 2 | import XCTest 3 | @testable import AppIconCore 4 | 5 | class AppIconTests: XCTestCase { 6 | 7 | func test_scale() { 8 | let recipe = Recipe { input in 9 | input.scaleForJson 10 | } 11 | 12 | recipe.assert(with: self) { 13 | $0.when(.init(name: "", platform: .ios, scale: ._1x, size: 0)) 14 | .expect("1x") 15 | 16 | $0.when(.init(name: "", platform: .watch, scale: ._1x, size: 0)) 17 | .expect(nil) 18 | 19 | $0.when(.init(name: "", platform: .watch, scale: ._2x, size: 0)) 20 | .expect("2x") 21 | } 22 | } 23 | 24 | func test_size() { 25 | let recipe = Recipe { input in 26 | input.baseSizeStr 27 | } 28 | 29 | recipe.assert(with: self) { 30 | $0.when(.init(name: "", platform: .ios, scale: ._1x, size: 10.0)) 31 | .expect("10") 32 | 33 | $0.when(.init(name: "", platform: .watch, scale: ._1x, size: 10.1)) 34 | .expect("10.1") 35 | } 36 | } 37 | } 38 | --------------------------------------------------------------------------------