├── .gitignore ├── AppIconGen.podspec.tmp ├── LICENSE ├── Makefile ├── Meta ├── banner.png └── movie.gif ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── AppIconGen │ └── main.swift └── AppIconGenCore │ ├── AppIconGenCore.swift │ ├── Factory │ └── ContentsFactory.swift │ ├── Model │ └── Models.swift │ └── Options.swift ├── Tests ├── AppIconGenTests │ ├── AppIconGenTests.swift │ ├── Resources │ │ └── iTunesArtwork@2x.png │ └── XCTestManifests.swift └── LinuxMain.swift └── release.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | *.zip 6 | portable_appicongen 7 | -------------------------------------------------------------------------------- /AppIconGen.podspec.tmp: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AppIconGen' 3 | s.version = 'LATEST_RELEASE_VERSION_NUMBER' 4 | s.summary = 'A app icon generator for iOS applications' 5 | s.homepage = 'https://github.com/noppefoxwolf/AppIconGen' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = 'noppefoxwolf' 8 | s.source = { :http => "#{s.homepage}/releases/download/#{s.version}/portable_appicongen.zip" } 9 | s.preserve_paths = '*' 10 | s.exclude_files = '**/file.zip' 11 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 noppefoxwolf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | 3 | TEMPORARY_FOLDER=./tmp_app_icon_gen 4 | 5 | build: 6 | swift build --disable-sandbox -c release -Xswiftc -static-stdlib 7 | 8 | test: 9 | swift test 10 | 11 | lint: 12 | swiftlint 13 | 14 | clean: 15 | swift package clean 16 | 17 | xcode: 18 | swift package generate-xcodeproj 19 | 20 | install: build 21 | mkdir -p "$(PREFIX)/bin" 22 | cp -f ".build/release/AppIconGen" "$(PREFIX)/bin/appicongen" 23 | 24 | portable_zip: build 25 | mkdir -p "$(TEMPORARY_FOLDER)" 26 | cp -f ".build/release/AppIconGen" "$(TEMPORARY_FOLDER)/appicongen" 27 | cp -f "LICENSE" "$(TEMPORARY_FOLDER)" 28 | (cd $(TEMPORARY_FOLDER); zip -r - LICENSE appicongen) > "./portable_appicongen.zip" 29 | rm -r "$(TEMPORARY_FOLDER)" 30 | -------------------------------------------------------------------------------- /Meta/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppefoxwolf/AppIconGen/92006363ce2fa8385acd409ae41b5bd560e29914/Meta/banner.png -------------------------------------------------------------------------------- /Meta/movie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppefoxwolf/AppIconGen/92006363ce2fa8385acd409ae41b5bd560e29914/Meta/movie.gif -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Cgd", 6 | "repositoryURL": "https://github.com/twostraws/Cgd.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "2e370814715c3557bb1cbdcd96e610ce29e83e45", 10 | "version": "0.2.0" 11 | } 12 | }, 13 | { 14 | "package": "Commander", 15 | "repositoryURL": "https://github.com/kylef/Commander.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "e5b50ad7b2e91eeb828393e89b03577b16be7db9", 19 | "version": "0.8.0" 20 | } 21 | }, 22 | { 23 | "package": "Spectre", 24 | "repositoryURL": "https://github.com/kylef/Spectre.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 28 | "version": "0.9.0" 29 | } 30 | }, 31 | { 32 | "package": "SwiftGD", 33 | "repositoryURL": "https://github.com/twostraws/SwiftGD.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "b038d13ef40ce6d11aa3599181e45f099814a053", 37 | "version": "2.2.0" 38 | } 39 | }, 40 | { 41 | "package": "SwiftHash", 42 | "repositoryURL": "https://github.com/onmyway133/SwiftHash.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "a900fc3f33c67eb33c9e650651edf6e23a410b3c", 46 | "version": "2.0.2" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppIconGen", 7 | products: [ 8 | .library( 9 | name: "AppIconGen", 10 | targets: ["AppIconGen"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/twostraws/SwiftGD.git", 14 | from: "2.0.0"), 15 | .package(url: "https://github.com/kylef/Commander.git", 16 | from: "0.8.0"), 17 | .package(url: "https://github.com/onmyway133/SwiftHash.git", 18 | from: "2.0.2") 19 | ], 20 | targets: [ 21 | .target( 22 | name: "AppIconGen", 23 | dependencies: ["AppIconGenCore", "Commander"]), 24 | .target( 25 | name: "AppIconGenCore", 26 | dependencies: ["SwiftGD", "SwiftHash"]), 27 | .testTarget( 28 | name: "AppIconGenTests", 29 | dependencies: ["AppIconGen"]), 30 | ] 31 | ) 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/noppefoxwolf/AppIconGen/blob/master/Meta/banner.png) 2 | 3 | # Demo 4 | 5 | ![](https://github.com/noppefoxwolf/AppIconGen/blob/master/Meta/movie.gif) 6 | 7 | # Installation 8 | 9 | `brew install gb` 10 | 11 | ## cocoapods 12 | 13 | ``` 14 | pod 'AppIconGen' 15 | ``` 16 | 17 | # Usage 18 | 19 | build phase. 20 | 21 | ``` 22 | $PODS_ROOT/AppIconGen/appicongen --input $SRCROOT/$PRODUCT_NAME/Artwork.png --xcassets $SRCROOT/$PRODUCT_NAME/Assets.xcassets/ 23 | ``` 24 | 25 | # Author 26 | 27 | noppefoxwolf, noppelabs@gmail.com 28 | 29 | # LICENSE 30 | 31 | AppIconGen is available under the MIT license. See the LICENSE file for more info. 32 | 33 | -------------------------------------------------------------------------------- /Sources/AppIconGen/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppIconGenCore 3 | import Commander 4 | 5 | let inputPathOption = Option("input", default: "iTunesArtwork.png") 6 | let xcassetsLocation = Option("xcassets", default: "Assets.xcassets") 7 | let appIconNameOption = Option("app-icon-name", default: "AppIcon") 8 | 9 | let main = command(inputPathOption, xcassetsLocation, appIconNameOption) { (input, xcassetsLocation, appIconName) in 10 | let options = Options(inputFilePath: URL(fileURLWithPath: input), 11 | xcassetsLocation: URL(fileURLWithPath: xcassetsLocation), 12 | appIconName: appIconName) 13 | let tool = AppIconGen(options: options) 14 | tool.process() 15 | } 16 | main.run() 17 | -------------------------------------------------------------------------------- /Sources/AppIconGenCore/AppIconGenCore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftGD 3 | import SwiftHash 4 | 5 | public struct AppIconGen { 6 | private let options: Options 7 | private let fileManager = FileManager.default 8 | 9 | public init(options: Options) { 10 | self.options = options 11 | } 12 | 13 | public func process() { 14 | #warning("TODO: gd入ってるかチェック") 15 | guard isSourceFilePathExists else { 16 | print("ERROR: Source file not found.") 17 | exit(0) 18 | } 19 | guard isUpdatedNeeded() else { 20 | print("SUCCESS: Already updated.") 21 | exit(0) 22 | } 23 | makeAppIconDirectory() 24 | let contents = defaultContentsFactory.make() 25 | writeContentJSON(contents: contents) 26 | writeImages(contents: contents) 27 | updateMd5() 28 | } 29 | 30 | var isSourceFilePathExists: Bool { 31 | return fileManager.fileExists(atPath: options.inputFilePath.path) 32 | } 33 | 34 | var defaultContentsFactory: ContentsFactory { 35 | return ContentsFactory(xcassets: options.inputFilePath.lastPathComponent, appIconName: options.appIconName) 36 | } 37 | 38 | private func makeAppIconDirectory(withIntermediateDirectories: Bool = true) { 39 | var xcassets = options.xcassetsLocation 40 | xcassets.appendPathComponent("\(options.appIconName).appiconset", isDirectory: true) 41 | try! fileManager.createDirectory(atPath: xcassets.path, 42 | withIntermediateDirectories: withIntermediateDirectories, 43 | attributes: nil) 44 | } 45 | 46 | private func writeContentJSON(contents: Contents) { 47 | let jsonData = try! JSONEncoder().encode(contents) 48 | let json = String(data: jsonData, encoding: .utf8)! 49 | let url = options.filePathInAppIcon(fileName: "Contents.json") 50 | try! json.write(to: url, atomically: true, encoding: .utf8) 51 | } 52 | 53 | private func writeImages(contents: Contents) { 54 | let image = Image(url: options.inputFilePath)! 55 | 56 | contents.images.forEach { (content) in 57 | self.writeImage(image, as: content) 58 | } 59 | } 60 | 61 | private func writeImage(_ image: Image, as content: Content) { 62 | let resizedImage = image.resizedTo(width: content.expectedSizeNumber, height: content.expectedSizeNumber) 63 | resizedImage?.write(to: options.filePathInAppIcon(fileName: content.filename)) 64 | } 65 | 66 | private func isUpdatedNeeded() -> Bool { 67 | let image = md5(ofImage: options.inputFilePath) 68 | let saved = md5(ofSaved: options.filePathInAppIcon(fileName: ".md5")) 69 | return image != saved 70 | } 71 | 72 | private func md5(ofImage url: URL) -> String { 73 | let imageData = try! Data(contentsOf: url, options: .alwaysMapped) 74 | let imageString = String(data: imageData, encoding: .ascii)! 75 | return MD5(imageString) 76 | } 77 | 78 | private func md5(ofSaved url: URL) -> String? { 79 | guard let imageString = try? String(contentsOf: url, encoding: .utf8) else { return nil } 80 | return imageString 81 | } 82 | 83 | private func updateMd5() { 84 | let image = md5(ofImage: options.inputFilePath) 85 | let url = options.filePathInAppIcon(fileName: ".md5") 86 | try! image.write(to: url, atomically: true, encoding: .utf8) 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /Sources/AppIconGenCore/Factory/ContentsFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentsFactory.swift 3 | // AppIconGen 4 | // 5 | // Created by Tomoya Hirano on 2018/10/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ContentsFactory { 11 | private let folder: String 12 | 13 | init(xcassets: String = "Assets.xcassets", appIconName: String = "AppIcon") { 14 | let folder = "\(xcassets)/\(appIconName).appiconset/" 15 | self.folder = folder 16 | } 17 | 18 | func make() -> Contents { 19 | let contents = Contents(images: [ 20 | Content(size: "60x60", expectedSize: 180, folder: folder, idiom: .iPhone, scale: "3x"), 21 | Content(size: "40x40", expectedSize: 80, folder: folder, idiom: .iPhone, scale: "2x"), 22 | Content(size: "40x40", expectedSize: 120, folder: folder, idiom: .iPhone, scale: "3x"), 23 | Content(size: "60x60", expectedSize: 120, folder: folder, idiom: .iPhone, scale: "2x"), 24 | Content(size: "57x57", expectedSize: 57, folder: folder, idiom: .iPhone, scale: "1x"), 25 | Content(size: "29x29", expectedSize: 58, folder: folder, idiom: .iPhone, scale: "2x"), 26 | Content(size: "29x29", expectedSize: 29, folder: folder, idiom: .iPhone, scale: "1x"), 27 | Content(size: "29x29", expectedSize: 87, folder: folder, idiom: .iPhone, scale: "3x"), 28 | Content(size: "57x57", expectedSize: 114, folder: folder, idiom: .iPhone, scale: "2x"), 29 | Content(size: "20x20", expectedSize: 40, folder: folder, idiom: .iPhone, scale: "2x"), 30 | Content(size: "20x20", expectedSize: 60, folder: folder, idiom: .iPhone, scale: "3x"), 31 | Content(size: "40x40", expectedSize: 80, folder: folder, idiom: .iPad, scale: "2x"), 32 | Content(size: "72x72", expectedSize: 72, folder: folder, idiom: .iPad, scale: "1x"), 33 | Content(size: "76x76", expectedSize: 152, folder: folder, idiom: .iPad, scale: "2x"), 34 | Content(size: "50x50", expectedSize: 100, folder: folder, idiom: .iPad, scale: "2x"), 35 | Content(size: "29x29", expectedSize: 58, folder: folder, idiom: .iPad, scale: "2x"), 36 | Content(size: "76x76", expectedSize: 76, folder: folder, idiom: .iPad, scale: "1x"), 37 | Content(size: "29x29", expectedSize: 29, folder: folder, idiom: .iPad, scale: "1x"), 38 | Content(size: "50x50", expectedSize: 50, folder: folder, idiom: .iPad, scale: "1x"), 39 | Content(size: "72x72", expectedSize: 144, folder: folder, idiom: .iPad, scale: "2x"), 40 | Content(size: "40x40", expectedSize: 40, folder: folder, idiom: .iPad, scale: "1x"), 41 | Content(size: "83.5x83.5", expectedSize: 167, folder: folder, idiom: .iPad, scale: "2x"), 42 | Content(size: "20x20", expectedSize: 20, folder: folder, idiom: .iPad, scale: "1x"), 43 | Content(size: "20x20", expectedSize: 40, folder: folder, idiom: .iPad, scale: "2x"), 44 | Content(size: "1024x1024", expectedSize: 1024, folder: folder, idiom: .iosMarketing, scale: "1x")]) 45 | 46 | return contents 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/AppIconGenCore/Model/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // AppIconGen 4 | // 5 | // Created by Tomoya Hirano on 2018/10/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Contents: Encodable { 11 | let images: [Content] 12 | } 13 | 14 | struct Content: Encodable { 15 | let size: String 16 | let expectedSize: String 17 | let expectedSizeNumber: Int 18 | let filename: String 19 | let folder: String 20 | let idiom: Idiom 21 | let scale: String 22 | 23 | 24 | init(size: String, expectedSize: Int, folder: String, idiom: Idiom, scale: String) { 25 | self.size = size 26 | self.expectedSize = String(expectedSize) 27 | self.expectedSizeNumber = expectedSize 28 | self.filename = "\(String(expectedSize)).png" 29 | self.folder = folder 30 | self.idiom = idiom 31 | self.scale = scale 32 | } 33 | } 34 | 35 | enum Idiom: String, Encodable { 36 | case iPhone = "iphone" 37 | case iPad = "ipad" 38 | case iosMarketing = "ios-marketing" 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AppIconGenCore/Options.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Options.swift 3 | // AppIconGen 4 | // 5 | // Created by Tomoya Hirano on 2018/10/03. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Options { 11 | let inputFilePath: URL 12 | let xcassetsLocation: URL 13 | let appIconName: String 14 | var appIconPath: URL { 15 | var url = xcassetsLocation 16 | url.appendPathComponent("\(appIconName).appiconset") 17 | return url 18 | } 19 | func filePathInAppIcon(fileName: String) -> URL { 20 | var url = appIconPath 21 | url.appendPathComponent(fileName) 22 | return url 23 | } 24 | 25 | public init(inputFilePath: URL, xcassetsLocation: URL, appIconName: String) { 26 | self.inputFilePath = inputFilePath 27 | self.xcassetsLocation = xcassetsLocation 28 | self.appIconName = appIconName 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/AppIconGenTests/AppIconGenTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftGD 3 | @testable import AppIconGenCore 4 | 5 | final class AppIconGenTests: XCTestCase { 6 | func testExample() { 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Tests/AppIconGenTests/Resources/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppefoxwolf/AppIconGen/92006363ce2fa8385acd409ae41b5bd560e29914/Tests/AppIconGenTests/Resources/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /Tests/AppIconGenTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(AppIconGenTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import AppIconGenTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += AppIconGenTests.allTests() 7 | XCTMain(tests) -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -eq 0 ]; then 4 | echo "A tag and token argument is needed!(ex: ./release.sh 1.2.3)" 5 | exit 1 6 | fi 7 | 8 | lib_name="appicongen" 9 | tag=$1 10 | 11 | podspec_name="AppIconGen.podspec" 12 | cat "$podspec_name.tmp" | sed s/LATEST_RELEASE_VERSION_NUMBER/$tag/ > "$podspec_name" 13 | pod trunk push $podspec_name 14 | rm $podspec_name --------------------------------------------------------------------------------