Publish v1.0
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit

Reviewed-on: #1
This commit is contained in:
2022-10-17 11:24:27 +02:00
parent a99466f258
commit 6203700b0c
87 changed files with 3112 additions and 1223 deletions

View File

@ -0,0 +1,56 @@
//
// FileManagerExtensions.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
extension FileManager {
func getAllRegularFileIn(directory: String) -> [String] {
var files = [String]()
guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
let error = ImagesError.unknown("Cannot enumerate file in \(directory)")
print(error.localizedDescription)
Images.exit(withError: error)
}
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey])
if fileAttributes.isRegularFile! {
files.append(fileURL.relativePath)
}
} catch {
let error = ImagesError.getFileAttributed(fileURL.relativePath, error.localizedDescription)
print(error.localizedDescription)
Images.exit(withError: error)
}
}
return files
}
func getAllImageSetFolderIn(directory: String) -> [String] {
var files = [String]()
guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
let error = ImagesError.unknown("Cannot enumerate imageset directory in \(directory)")
print(error.localizedDescription)
Images.exit(withError: error)
}
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isDirectoryKey])
if fileAttributes.isDirectory! && fileURL.lastPathComponent.hasSuffix(".imageset") {
files.append(fileURL.lastPathComponent)
}
} catch {
let error = ImagesError.getFileAttributed(fileURL.relativePath, error.localizedDescription)
print(error.localizedDescription)
Images.exit(withError: error)
}
}
return files
}
}

View File

@ -0,0 +1,77 @@
//
// ImageExtensionGenerator.swift
//
//
// Created by Thibaut Schmitt on 14/02/2022.
//
import ToolCore
import Foundation
class ImageExtensionGenerator {
// MARK: - pragm
static func generateExtensionFile(images: [ParsedImage],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionFilePath: String) {
// Create extension conten1t
let extensionContent = Self.getExtensionContent(images: images,
staticVar: staticVar,
extensionName: extensionName,
inputFilename: inputFilename)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription)
print(error.localizedDescription)
Images.exit(withError: error)
}
}
// MARK: - Extension content
static func getExtensionContent(images: [ParsedImage], staticVar: Bool, extensionName: String, inputFilename: String) -> String {
[
Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName),
Self.getProperties(images: images, staticVar: staticVar),
Self.getFooter()
]
.joined(separator: "\n")
}
// MARK: - Extension part
private static func getHeader(inputFilename: String, extensionClassname: String) -> String {
"""
// Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion)
// Images from \(inputFilename)
import UIKit
extension \(extensionClassname) {
"""
}
private static func getProperties(images: [ParsedImage], staticVar: Bool) -> String {
if staticVar {
return images
.map { "\n\($0.getStaticImageProperty())" }
.joined(separator: "\n")
}
return images
.map { "\n\($0.getImageProperty())" }
.joined(separator: "\n")
}
private static func getFooter() -> String {
"""
}
"""
}
}

View File

@ -0,0 +1,171 @@
//
// XcassetsGenerator.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
import ToolCore
class XcassetsGenerator {
static let outputImageExtension = "png"
let forceGeneration: Bool
// MARK: - Init
init(forceGeneration: Bool) {
self.forceGeneration = forceGeneration
}
// MARK: - Assets generation
func generateXcassets(inputPath: String, imagesToGenerate: [ParsedImage], xcassetsPath: String) {
let fileManager = FileManager()
let svgConverter = Images.getSvgConverterPath()
let allSubFiles = fileManager.getAllRegularFileIn(directory: inputPath)
var generatedAssetsPaths = [String]()
// Generate new assets
imagesToGenerate.forEach { parsedImage in
// Get image path
let imageData: (path: String, ext: String) = {
for subfile in allSubFiles {
if subfile.hasSuffix("/" + parsedImage.name + ".svg") {
return (subfile, "svg")
}
if subfile.hasSuffix("/" + parsedImage.name + ".png") {
return (subfile, "png")
}
if subfile.hasSuffix("/" + parsedImage.name + ".jpg") {
return (subfile, "jpg")
}
if subfile.hasSuffix("/" + parsedImage.name + ".jepg") {
return (subfile, "jepg")
}
}
let error = ImagesError.unknownImageExtension(parsedImage.name)
print(error.localizedDescription)
Images.exit(withError: error)
}()
// Create imageset folder name
let imagesetName = "\(parsedImage.name).imageset"
let imagesetPath = "\(xcassetsPath)/\(imagesetName)"
// Store managed images path
generatedAssetsPaths.append(imagesetName)
// Generate output images path
let output1x = "\(imagesetPath)/\(parsedImage.name).\(XcassetsGenerator.outputImageExtension)"
let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(XcassetsGenerator.outputImageExtension)"
let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(XcassetsGenerator.outputImageExtension)"
// Check if we need to convert image
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x) else {
//print("\(parsedImage.name) -> Not regenerating")
return
}
// Create imageset folder
if fileManager.fileExists(atPath: imagesetPath) == false {
do {
try fileManager.createDirectory(atPath: imagesetPath,
withIntermediateDirectories: true)
} catch {
let error = ImagesError.createAssetFolder(imagesetPath)
print(error.localizedDescription)
Images.exit(withError: error)
}
}
// Convert image
let convertArguments = parsedImage.convertArguments
if imageData.ext == "svg" {
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png
var command1x = ["\(svgConverter)", "\(imageData.path)"]
var command2x = ["\(svgConverter)", "\(imageData.path)"]
var command3x = ["\(svgConverter)", "\(imageData.path)"]
self.addConvertArgument(command: &command1x, convertArgument: convertArguments.x1)
self.addConvertArgument(command: &command2x, convertArgument: convertArguments.x2)
self.addConvertArgument(command: &command3x, convertArgument: convertArguments.x3)
command1x.append(contentsOf: ["-o", output1x])
command2x.append(contentsOf: ["-o", output2x])
command3x.append(contentsOf: ["-o", output3x])
Shell.shell(command1x)
Shell.shell(command2x)
Shell.shell(command3x)
} else {
// convert path/to/image.png -resize 200x300 path/to/output.png
// convert path/to/image.png -resize 200x path/to/output.png
// convert path/to/image.png -resize x300 path/to/output.png
Shell.shell(["convert", "\(imageData.path)",
"-resize", "\(convertArguments.x1.width ?? "")x\(convertArguments.x1.height ?? "")",
output1x])
Shell.shell(["convert", "\(imageData.path)",
"-resize", "\(convertArguments.x2.width ?? "")x\(convertArguments.x2.height ?? "")",
output2x])
Shell.shell(["convert", "\(imageData.path)",
"-resize", "\(convertArguments.x3.width ?? "")x\(convertArguments.x3.height ?? "")",
output3x])
}
// Write Content.json
let imagesetContentJson = parsedImage.contentJson
let contentJsonFilePath = "\(imagesetPath)/Contents.json"
let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath)
try! imagesetContentJson.write(to: contentJsonFilePathURL, atomically: false, encoding: .utf8)
print("\(parsedImage.name) -> Generated")
}
// Success info
let generatedAssetsCount = generatedAssetsPaths.count
print("Images generated: \(generatedAssetsCount)")
// Delete old assets
let allImagesetName = Set(fileManager.getAllImageSetFolderIn(directory: xcassetsPath))
let imagesetToRemove = allImagesetName.subtracting(Set(generatedAssetsPaths))
imagesetToRemove.forEach {
print("Will remove: \($0)")
}
imagesetToRemove.forEach { itemToRemove in
try! fileManager.removeItem(atPath: "\(xcassetsPath)/\(itemToRemove)")
}
print("Removed \(imagesetToRemove.count) images")
}
// MARK: - Helpers: SVG command
private func addConvertArgument(command: inout [String], convertArgument: ConvertArgument) {
if let width = convertArgument.width, width.isEmpty == false {
command.append("-w")
command.append("\(width)")
}
if let height = convertArgument.height, height.isEmpty == false {
command.append("-h")
command.append("\(height)")
}
}
// MARK: - Helpers: bypass generation
private func shouldGenerate(inputImagePath: String, xcassetImagePath: String) -> Bool {
if forceGeneration {
return true
}
return GeneratorChecker.isFile(inputImagePath, moreRecenThan: xcassetImagePath)
}
}

View File

@ -0,0 +1,108 @@
//
// Images.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import ToolCore
import Foundation
import ArgumentParser
struct Images: ParsableCommand {
// MARK: - CommandConfiguration
static var configuration = CommandConfiguration(
abstract: "A utility for generate images and an extension to access them easily.",
version: ResgenSwiftVersion
)
// MARK: - Static
static let toolName = "Images"
static let defaultExtensionName = "UIImage"
// MARK: - Command Options
@OptionGroup var options: ImagesOptions
// MARK: - Run
mutating func run() {
print("[\(Self.toolName)] Starting images generation")
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate images in xcassets \(options.xcassetsPath)")
// Check requirements
guard checkRequirements() else { return }
print("[\(Self.toolName)] Will generate images")
// Parse input file
let imagesToGenerate = ImageFileParser.parse(options.inputFile, platform: PlatormTag.ios)
// Generate xcassets files
let inputFolder = URL(fileURLWithPath: options.inputFile)
.deletingLastPathComponent()
.relativePath
let xcassetsGenerator = XcassetsGenerator(forceGeneration: options.forceExecutionAndGeneration)
xcassetsGenerator.generateXcassets(inputPath: inputFolder,
imagesToGenerate: imagesToGenerate,
xcassetsPath: options.xcassetsPath)
// Generate extension
ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate,
staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
print("[\(Self.toolName)] Images generated")
}
// MARK: - Requirements
private func checkRequirements() -> Bool {
guard options.forceExecutionAndGeneration == false else {
return true
}
let fileManager = FileManager()
// Input file
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = ImagesError.fileNotExists(options.inputFile)
print(error.localizedDescription)
Images.exit(withError: error)
}
// RSVG-Converter
_ = Images.getSvgConverterPath()
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(force: options.forceExecution,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
print("[\(Self.toolName)] Images are already up to date :) ")
return false
}
return true
}
// MARK: - Helpers
@discardableResult
static func getSvgConverterPath() -> String {
let taskSvgConverter = Shell.shell(["which", "rsvg-convert"])
if taskSvgConverter.terminationStatus == 0 {
return taskSvgConverter.output!.removeCharacters(from: CharacterSet.whitespacesAndNewlines)
}
let error = ImagesError.rsvgConvertNotFound
print(error.localizedDescription)
Images.exit(withError: error)
}
}

View File

@ -0,0 +1,47 @@
//
// ImagesError.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
enum ImagesError: Error {
case inputFolderNotFound(String)
case fileNotExists(String)
case unknownImageExtension(String)
case getFileAttributed(String, String)
case rsvgConvertNotFound
case writeFile(String, String)
case createAssetFolder(String)
case unknown(String)
var localizedDescription: String {
switch self {
case .inputFolderNotFound(let inputFolder):
return " error:[\(Images.toolName)] Input folder not found: \(inputFolder)"
case .fileNotExists(let filename):
return " error:[\(Images.toolName)] File \(filename) does not exists"
case .unknownImageExtension(let filename):
return " error:[\(Images.toolName)] File \(filename) have an unhandled file extension. Cannot generate image."
case .getFileAttributed(let filename, let errorDescription):
return " error:[\(Images.toolName)] Getting file attributes of \(filename) failed with error: \(errorDescription)"
case .rsvgConvertNotFound:
return " error:[\(Images.toolName)] Can't find rsvg-convert (can be installed with 'brew remove imagemagick && brew install imagemagick --with-librsvg')"
case .writeFile(let subErrorDescription, let filename):
return " error:[\(Images.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
case .createAssetFolder(let folder):
return "error:[\(Colors.toolName)] An error occured while creating folder `\(folder)`"
case .unknown(let errorDescription):
return " error:[\(Images.toolName)] Unknown error: \(errorDescription)"
}
}
}

View File

@ -0,0 +1,56 @@
//
// ImagiumOptions.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
import ArgumentParser
struct ImagesOptions: ParsableArguments {
@Flag(name: .customShort("f"), help: "Should force script execution")
var forceExecution = false
@Flag(name: .customShort("F"), help: "Regenerate all images")
var forceExecutionAndGeneration = false
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var inputFile: String
@Option(help: "Xcassets path where to generate images.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var xcassetsPath: String
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var extensionOutputPath: String
@Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIImage extension. Using default extension name will generate static property.")
var extensionName: String = Images.defaultExtensionName
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift")
var extensionSuffix: String?
}
// MARK: - Computed var
extension ImagesOptions {
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
}
return "\(extensionName).swift"
}
var extensionFilePath: String {
"\(extensionOutputPath)/\(extensionFileName)"
}
var inputFilenameWithoutExt: String {
URL(fileURLWithPath: inputFile)
.deletingPathExtension()
.lastPathComponent
}
}

View File

@ -0,0 +1,13 @@
//
// ConvertArgument.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
struct ConvertArgument {
let width: String?
let height: String?
}

View File

@ -0,0 +1,90 @@
//
// ParsedImage.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
struct ParsedImage {
let name: String
let tags: String
let width: Int
let height: Int
// MARK: - Convert
var convertArguments: (x1: ConvertArgument, x2: ConvertArgument, x3: ConvertArgument) {
var width1x = ""
var height1x = ""
var width2x = ""
var height2x = ""
var width3x = ""
var height3x = ""
if width != -1 {
width1x = "\(width)"
width2x = "\(width * 2)"
width3x = "\(width * 3)"
}
if height != -1 {
height1x = "\(height)"
height2x = "\(height * 2)"
height3x = "\(height * 3)"
}
return (x1: ConvertArgument(width: width1x, height: height1x),
x2: ConvertArgument(width: width2x, height: height2x),
x3: ConvertArgument(width: width3x, height: height3x))
}
// MARK: - Assets
var contentJson: String {
"""
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x",
"filename" : "\(name).\(XcassetsGenerator.outputImageExtension)"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "\(name)@2x.\(XcassetsGenerator.outputImageExtension)"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "\(name)@3x.\(XcassetsGenerator.outputImageExtension)"
}
],
"info" : {
"version" : 1,
"author" : "ResgenSwift-Imagium"
}
}
"""
}
// MARK: - Extension property
func getImageProperty() -> String {
"""
var \(name): UIImage {
UIImage(named: "\(name)")!
}
"""
}
func getStaticImageProperty() -> String {
"""
static var \(name): UIImage {
UIImage(named: "\(name)")!
}
"""
}
}

View File

@ -0,0 +1,13 @@
//
// PlatormTag.swift
//
//
// Created by Thibaut Schmitt on 29/08/2022.
//
import Foundation
enum PlatormTag: String {
case droid = "d"
case ios = "i"
}

View File

@ -0,0 +1,50 @@
//
// ImageFileParser.swift
//
//
// Created by Thibaut Schmitt on 24/01/2022.
//
import Foundation
class ImageFileParser {
static func parse(_ inputFile: String, platform: PlatormTag) -> [ParsedImage] {
let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8)
let stringsByLines = inputFileContent.components(separatedBy: .newlines)
return Self.parseLines(stringsByLines, platform: platform)
}
static func parseLines(_ lines: [String], platform: PlatormTag) -> [ParsedImage] {
var imagesToGenerate = [ParsedImage]()
lines.forEach {
guard $0.removeLeadingTrailingWhitespace().isEmpty == false, $0.first != "#" else {
return
}
let splittedLine = $0.split(separator: " ")
let width: Int = {
if splittedLine[2] == "?" {
return -1
}
return Int(splittedLine[2])!
}()
let height: Int = {
if splittedLine[3] == "?" {
return -1
}
return Int(splittedLine[3])!
}()
let image = ParsedImage(name: String(splittedLine[1]), tags: String(splittedLine[0]), width: width, height: height)
imagesToGenerate.append(image)
}
return imagesToGenerate.filter {
$0.tags.contains(platform.rawValue)
}
}
}