Skip to content
This repository has been archived by the owner on Sep 15, 2022. It is now read-only.

Commit

Permalink
Merge pull request #14 from num42/feature/code_optimisation
Browse files Browse the repository at this point in the history
Feature/code optimisation
  • Loading branch information
Lutzifer authored Mar 8, 2019
2 parents b956e82 + 952384a commit fbfa31a
Show file tree
Hide file tree
Showing 13 changed files with 345 additions and 140 deletions.
37 changes: 30 additions & 7 deletions AppIconResizer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
716B0B012226DACD00C7A339 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716B0B002226DACD00C7A339 /* Device.swift */; };
7148B2172232AAA000ED54FC /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7148B2162232AAA000ED54FC /* DoubleExtensions.swift */; };
7148B2192232AB5500ED54FC /* CGImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7148B2182232AB5500ED54FC /* CGImageExtensions.swift */; };
7148B21B2232AB6400ED54FC /* CIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7148B21A2232AB6400ED54FC /* CIImageExtensions.swift */; };
7148B21D2232AC0300ED54FC /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7148B21C2232AC0300ED54FC /* DataExtensions.swift */; };
7148B21F2232ACAD00ED54FC /* AppIconEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7148B21E2232ACAD00ED54FC /* AppIconEntry.swift */; };
7148B2212232AD4900ED54FC /* ConvertibleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7148B2202232AD4900ED54FC /* ConvertibleExtensions.swift */; };
716B0B012226DACD00C7A339 /* Idiom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716B0B002226DACD00C7A339 /* Idiom.swift */; };
716B0B032226DAEF00C7A339 /* VirtualDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716B0B022226DAEF00C7A339 /* VirtualDevice.swift */; };
OBJ_100 /* CommandRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* CommandRunner.swift */; };
OBJ_101 /* CommandType.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* CommandType.swift */; };
Expand Down Expand Up @@ -142,7 +148,13 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
716B0B002226DACD00C7A339 /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = "<group>"; };
7148B2162232AAA000ED54FC /* DoubleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleExtensions.swift; sourceTree = "<group>"; };
7148B2182232AB5500ED54FC /* CGImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImageExtensions.swift; sourceTree = "<group>"; };
7148B21A2232AB6400ED54FC /* CIImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageExtensions.swift; sourceTree = "<group>"; };
7148B21C2232AC0300ED54FC /* DataExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = "<group>"; };
7148B21E2232ACAD00ED54FC /* AppIconEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconEntry.swift; sourceTree = "<group>"; };
7148B2202232AD4900ED54FC /* ConvertibleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertibleExtensions.swift; sourceTree = "<group>"; };
716B0B002226DACD00C7A339 /* Idiom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Idiom.swift; sourceTree = "<group>"; };
716B0B022226DAEF00C7A339 /* VirtualDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualDevice.swift; sourceTree = "<group>"; };
"AppIconResizer::AppIconResizer::Product" /* AppIconResizer */ = {isa = PBXFileReference; lastKnownFileType = text; path = AppIconResizer; sourceTree = BUILT_PRODUCTS_DIR; };
"AppIconResizer::AppIconResizerCore::Product" /* AppIconResizerCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AppIconResizerCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -220,8 +232,14 @@
isa = PBXGroup;
children = (
OBJ_11 /* AppIconResizer.swift */,
716B0B002226DACD00C7A339 /* Device.swift */,
716B0B002226DACD00C7A339 /* Idiom.swift */,
716B0B022226DAEF00C7A339 /* VirtualDevice.swift */,
7148B2162232AAA000ED54FC /* DoubleExtensions.swift */,
7148B2182232AB5500ED54FC /* CGImageExtensions.swift */,
7148B21A2232AB6400ED54FC /* CIImageExtensions.swift */,
7148B21C2232AC0300ED54FC /* DataExtensions.swift */,
7148B21E2232ACAD00ED54FC /* AppIconEntry.swift */,
7148B2202232AD4900ED54FC /* ConvertibleExtensions.swift */,
);
name = AppIconResizerCore;
path = Sources/AppIconResizerCore;
Expand Down Expand Up @@ -311,7 +329,7 @@
name = Products;
sourceTree = BUILT_PRODUCTS_DIR;
};
OBJ_5 /* */ = {
OBJ_5 = {
isa = PBXGroup;
children = (
OBJ_6 /* Package.swift */,
Expand All @@ -321,7 +339,6 @@
OBJ_18 /* Dependencies */,
OBJ_34 /* Products */,
);
name = "";
sourceTree = "<group>";
};
OBJ_7 /* Configs */ = {
Expand Down Expand Up @@ -490,7 +507,7 @@
knownRegions = (
en,
);
mainGroup = OBJ_5 /* */;
mainGroup = OBJ_5;
productRefGroup = OBJ_34 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -546,9 +563,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
7148B2172232AAA000ED54FC /* DoubleExtensions.swift in Sources */,
7148B21F2232ACAD00ED54FC /* AppIconEntry.swift in Sources */,
OBJ_60 /* AppIconResizer.swift in Sources */,
716B0B032226DAEF00C7A339 /* VirtualDevice.swift in Sources */,
716B0B012226DACD00C7A339 /* Device.swift in Sources */,
7148B2212232AD4900ED54FC /* ConvertibleExtensions.swift in Sources */,
7148B21B2232AB6400ED54FC /* CIImageExtensions.swift in Sources */,
7148B21D2232AC0300ED54FC /* DataExtensions.swift in Sources */,
7148B2192232AB5500ED54FC /* CGImageExtensions.swift in Sources */,
716B0B012226DACD00C7A339 /* Idiom.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
8 changes: 2 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ let package = Package(
.executable(name: "icon-resizer", targets: ["AppIconResizer"]),
],
dependencies: [
.package(
url: "https://github.com/JohnSundell/Files.git",
from: "2.2.1"
),
.package(
url: "https://github.com/kylef/Commander.git",
from: "0.8.0"
Expand All @@ -25,9 +21,9 @@ let package = Package(
name: "AppIconResizer",
dependencies: ["AppIconResizerCore"]),
.target(name: "AppIconResizerCore",
dependencies: ["Files", "Commander"]),
dependencies: ["Commander"]),
.testTarget(
name: "AppIconResizerTests",
dependencies: ["AppIconResizer", "Files", "Commander"]),
dependencies: ["AppIconResizer", "Commander"]),
]
)
50 changes: 50 additions & 0 deletions Sources/AppIconResizerCore/AppIconEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import CoreImage

struct AppIconEntry: Encodable, Hashable {
let size: CGFloat
let idiom: String
let scale: Int

var scaledSize: CGFloat {
return size * CGFloat(scale)
}

var fileName: String {
return "AppIcon-\(Int(scaledSize))x\(Int(scaledSize)).png"
}

private enum CodingKeys: String, CodingKey {
case size
case idiom
case fileName = "filename"
case scale
}

private var displaySize: String {
return Double(size).withDecimals(1).replacingOccurrences(of: ".0", with: "")
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("\(displaySize)x\(displaySize)", forKey: .size)
try container.encode("\(scale)x", forKey: .scale)
try container.encode(idiom, forKey: .idiom)
try container.encode(fileName, forKey: .fileName)
}
}

struct AppIconSetContents: Encodable {
let iconEntries: [AppIconEntry]?
let info: Info

private enum CodingKeys: String, CodingKey {
case iconEntries = "images"
case info
}
}

struct Info: Encodable {
let version: Int
let author: String
}

182 changes: 103 additions & 79 deletions Sources/AppIconResizerCore/AppIconResizer.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import Commander
import CoreImage
import Files
import Foundation

public final class AppIconResizer {

// Create one dimensional String array 'arguments'
private let arguments: [String]

Expand All @@ -14,58 +13,128 @@ public final class AppIconResizer {
}

public func run() throws {

let resizingCommand = command(
Option("device", default: "all"),
Option("badge", default:"test"),
Argument<String>("filename")
) { [weak self] device, badgeFileName, fileName in
guard let device = Device(rawValue: device.lowercased()) else {
print("Error: Entered device is not a valid device! Valid devices are \(Device.allCases.map { $0.rawValue }.joined(separator: ", "))")
return
VariadicOption("device", default: ["all"]),
Option<String?>("badge", default: nil),
Option("targetPath", default: FileManager.default.currentDirectoryPath),
Argument<String>("inputPath")
) { [weak self] idiomStrings, badgeFilePath, targetPath, filePath in
let idioms = Set(idiomStrings).map { idiomString -> Idiom in
guard let idiom = Idiom(rawValue: idiomString.lowercased()) else{
fatalError("\(idiomString) is an unknown value. Valid values are \(Idiom.allCases.map { $0.rawValue }.joined(separator: ", "))")
}
return idiom
}

let currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
let inputFileURL : URL
let badgeFileURL : URL?

if let badgeFilePath = badgeFilePath {
if badgeFilePath.starts(with: "/") {
badgeFileURL = URL(fileURLWithPath: badgeFilePath)
} else {
badgeFileURL = URL(fileURLWithPath: badgeFilePath, relativeTo: currentDirectoryURL)
}
} else {
badgeFileURL = nil
}
self?.render(device: device, fileName: fileName, badgeFileName: badgeFileName)

if filePath.starts(with: "/") {
inputFileURL = URL(fileURLWithPath: filePath)
} else {
inputFileURL = URL(fileURLWithPath: filePath, relativeTo: currentDirectoryURL)
}

try self?.render(idioms: idioms, inputFileURL: inputFileURL, targetPath: targetPath, badgeFileURL: badgeFileURL)
}

resizingCommand.run()
}

public func render(device: Device, fileName: String, badgeFileName: String) {
device.sizes
.forEach { size in
let currentPath = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

guard let inputImage = CIImage(contentsOf: URL(fileURLWithPath: fileName, relativeTo: currentPath)) else {
print("Error: Input image with name \(fileName) is not valid in current path!")
return
}

let badgeImage = CIImage(contentsOf: URL(fileURLWithPath: badgeFileName, relativeTo: currentPath))
public func render(idioms: [Idiom], inputFileURL: URL, targetPath: String, badgeFileURL: URL?) throws {

let fileManager = FileManager.default

let targetURL = URL(fileURLWithPath: targetPath)
let xcAssetsURL = targetURL.appendingPathComponent("AppIcon.xcassets", isDirectory: true)
let xcAssetsJsonURL = URL(fileURLWithPath: "Contents.json", relativeTo: xcAssetsURL)
let iconSetURL = xcAssetsURL.appendingPathComponent("AppIcon.appiconset", isDirectory: true)
let iconSetJsonURL = URL(fileURLWithPath: "Contents.json", relativeTo: iconSetURL)

do {
try fileManager.createDirectory(atPath: iconSetURL.path, withIntermediateDirectories: true, attributes: nil)
} catch {
fatalError(error.localizedDescription)
}

// Get app icon entries
let appIconEntriesWithDuplicates = idioms.flatMap { $0.appIconEntries }
let appIconEntries = Array(Set<AppIconEntry>(appIconEntriesWithDuplicates))

// Write app icon entries to contents json
let info = Info(version: 1, author: "AppIconResizer")
let outerContents = AppIconSetContents(iconEntries: nil, info: info)
let contents = AppIconSetContents(iconEntries: appIconEntries, info: info)
do {
let jsonData = try JSONEncoder().encode(contents)
let jsonInfoData = try JSONEncoder().encode(outerContents)
let jsonString = jsonData.prettyPrintedJSONString
let jsonInfoString = jsonInfoData.prettyPrintedJSONString
try jsonInfoString?.write(to: xcAssetsJsonURL, atomically: true, encoding: String.Encoding.utf8.rawValue)
try jsonString?.write(to: iconSetJsonURL, atomically: true, encoding: String.Encoding.utf8.rawValue)
} catch {
fatalError(error.localizedDescription)
}

// write png files
let sizes = Set(appIconEntries.map{ $0.scaledSize }).sorted()

guard let inputImage = CIImage(contentsOf: inputFileURL) else {
print("Error: Input image at path \(inputFileURL) is not valid in current path!")
return
}

let badgeImage: CIImage?

if let badgeFileURL = badgeFileURL {
guard let nonOptionalBadgeImage = CIImage(contentsOf: badgeFileURL) else {
print("Error: Badge image at path \(badgeFileURL.path) could not be found!")
return
}

badgeImage = nonOptionalBadgeImage
} else {
badgeImage = nil
}

let bgColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 1.0])

let options : [AnyHashable:Any] = [
kCGImageDestinationBackgroundColor: bgColor as Any
]

sizes.forEach { width in
let size = CGSize(width: width, height: width)

guard let image = inputImage.cgImage?.resize(to: size, badgedBy: badgeImage?.cgImage) else {
print("Error: Input image couldn't be resized")
return
}

let targetImageFileURL = URL(fileURLWithPath: "AppIcon-\(Int(size.height))x\(Int(size.width)).png", relativeTo: iconSetURL)

let url = URL(fileURLWithPath: "\(Int(size.height)).png", relativeTo: currentPath) as CFURL

guard let destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, nil) else {
guard let destination = CGImageDestinationCreateWithURL(URL(fileURLWithPath: targetImageFileURL.path) as CFURL, kUTTypePNG, 1, nil) else {
print("Error: Image couldn't be written in current directory")
return
}

let bgColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 1.0])

let options : [AnyHashable:Any] = [
kCGImageDestinationBackgroundColor: bgColor as Any
]

CGImageDestinationAddImage(destination, image, options as CFDictionary)
CGImageDestinationFinalize(destination)
print("Created AppIcon from \(fileName) in \(size)")
print("Created AppIcon from file \(inputFileURL.path) at \(targetImageFileURL.path) in \(size)")
}
}


}

public extension AppIconResizer {
Expand All @@ -74,48 +143,3 @@ public extension AppIconResizer {
case failedToCreateFile
}
}

extension CGImage {
func resize(to newSize: CGSize, badgedBy badge: CGImage? = nil) -> CGImage? {
let height = Int(newSize.height)

guard let colorSpace = self.colorSpace else {
return nil
}

guard let context = CGContext(
data: nil,
width: height,
height: height,
bitsPerComponent: self.bitsPerComponent,
bytesPerRow: self.bytesPerRow,
space: colorSpace,
bitmapInfo: self.alphaInfo.rawValue
) else {
return nil
}

// draw image to context (resizing it)
context.interpolationQuality = .high
context.setFillColor(CGColor.white)
context.fill(CGRect(x: 0, y: 0, width: height, height: height))
context.draw(self, in: CGRect(x: 0, y: 0, width: height, height: height))

if let badge = badge {
context.draw(badge, in: CGRect(x: 0, y: 0, width: height, height: height))
}

// extract resulting image from context
return context.makeImage()
}
}

extension CIImage {
var cgImage: CGImage? {
let context = CIContext(options: nil)
guard let cgImage = context.createCGImage(self, from: self.extent) else {
return nil
}
return cgImage
}
}
Loading

0 comments on commit fbfa31a

Please sign in to comment.