Skip to content

Commit

Permalink
Support multiple output suffixes (#23)
Browse files Browse the repository at this point in the history
Changes will be detailed in release notes
  • Loading branch information
henrik-dmg authored Aug 28, 2020
1 parent 8e754c9 commit cdef5e9
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 54 deletions.
5 changes: 2 additions & 3 deletions Example/example.config
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
"fontFile": "/System/Library/Fonts/HelveticaNeue.ttc",
"textColor": "#FFF",
"outputWholeImage": true,
"locales": "de",
"deviceData": [
{
"outputSuffix": "iPhone X 5.8 inches",
"outputSuffixes": ["iPhone X 5.8 inches"],
"screenshots": "Example/Screenshots/iPhone X/",
"templateFile": "Example/Template Files/iPhone X TemplateFile.png",
"screenshotData": [
Expand Down Expand Up @@ -91,7 +90,7 @@
]
},
{
"outputSuffix": "iPad Pro 12.9 inch",
"outputSuffixes": ["ipadPro", "ipadPro129"],
"screenshots": "Example/Screenshots/iPad Pro/",
"templateFile": "Example/Template Files/iPad Pro TemplateFile.png",
"screenshotData": [
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "7255fd547f70468e19abbac5f7964f1ef309ad92",
"version": "0.2.1"
"revision": "15351c1cd009eba0b6e438bfef55ea9847a8dc4a",
"version": "0.3.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let package = Package(
.executable(name: "swiftframe", targets: ["SwiftFrame"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.2.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0")
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ To use SwiftFrame, you need to pass it a configuration file (which is a plain JS
* `clearDirectories`: **optional (default: true)** a boolean telling the application whether or not to clear the specified output directories before writing new files to it. This prevents random screenshots from being used in case you update your template file to include one less screenshot for example
* `locales`: **optional** a regular expression that can be used to exlude (or include) certain locales during rendering. To only include `fr` and `de` locale for example, use `"fr|de"`. To exclude `ru` and `fr`, use something like `"^(?!ru|fr$)\\w*$"`
* `deviceData`: an array containing device specific data about screenshot and text coordinates (this way you can frame screenshots for more than one device per config file)
* `outputSuffix`: a suffix to apply to the output files in addition to the locale identifier and index
* `outputSuffixes`: an array of suffixes to apply to the output files in addition to the locale identifier and index. Multiple suffixes can be used to render the same screenshots for different target devices (for example 2nd and 3rd 12.9 inch iPad Pro)
* `screenshots`: a folder path containing a subfolder for each locale, which in turn contains all the screenshots for that device
* `templateFile`: an image file that will be rendered above the screenshots to overlay device frames (e.g. see `Example/Template Files/iPhone X/TemplateFile.png`) **Note:** places where screenshots should go need to be transparent
* `sliceSizeOverride`: **optional** A custom slice size override in cases where you want to use different size screenshots (for example iPhone X screenshots with a iPhone 8 template file)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFrame/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct SwiftFrame: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "swiftframe",
abstract: "CLI application for speedy screenshot framing",
version: "3.1.1",
version: "4.0.0",
helpNames: .shortAndLong)

@Argument(help: "Read configuration values from the specified file", completion: .list(["config", "json", "yml", "yaml"]))
Expand Down
22 changes: 13 additions & 9 deletions Sources/SwiftFrameCore/Config/DeviceData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public struct DeviceData: Decodable, ConfigValidatable {

private let kScreenshotExtensions = Set(["png", "jpg", "jpeg"])

let outputSuffix: String
let outputSuffixes: [String]
let templateImagePath: FileURL
private let screenshotsPath: FileURL
let sliceSizeOverride: DecodableSize?
Expand All @@ -19,10 +19,14 @@ public struct DeviceData: Decodable, ConfigValidatable {
private(set) var screenshotData = [ScreenshotData]()
private(set) var textData = [TextData]()

private var suffixesStringRepresentation: String {
outputSuffixes.map { "\"\($0)\"" }.joined(separator: ", ")
}

// MARK: - Coding Keys

enum CodingKeys: String, CodingKey {
case outputSuffix
case outputSuffixes
case screenshotsPath = "screenshots"
case templateImagePath = "templateFile"
case sliceSizeOverride
Expand All @@ -34,7 +38,7 @@ public struct DeviceData: Decodable, ConfigValidatable {
// MARK: - Init

internal init(
outputSuffix: String,
outputSuffixes: [String],
templateImagePath: FileURL,
screenshotsPath: FileURL,
sliceSizeOverride: DecodableSize? = nil,
Expand All @@ -44,7 +48,7 @@ public struct DeviceData: Decodable, ConfigValidatable {
textData: [TextData] = [TextData](),
gapWidth: Int = 0)
{
self.outputSuffix = outputSuffix
self.outputSuffixes = outputSuffixes
self.templateImagePath = templateImagePath
self.screenshotsPath = screenshotsPath
self.sliceSizeOverride = sliceSizeOverride
Expand Down Expand Up @@ -79,7 +83,7 @@ public struct DeviceData: Decodable, ConfigValidatable {
.sorted { $0.zIndex < $1.zIndex }

return DeviceData(
outputSuffix: outputSuffix,
outputSuffixes: outputSuffixes,
templateImagePath: templateImagePath,
screenshotsPath: screenshotsPath,
sliceSizeOverride: sliceSizeOverride,
Expand Down Expand Up @@ -115,7 +119,7 @@ public struct DeviceData: Decodable, ConfigValidatable {
// plus specified gap width in between
if let screenshotSize = sliceSizeOverride?.cgSize ?? NSBitmapImageRep.ky_loadFromURL(screenshotsGroupedByLocale.first?.value.first?.value)?.ky_nativeSize {
guard let templateImageSize = templateImage?.ky_nativeSize else {
throw NSError(description: "Template image for output suffix \"\(outputSuffix)\" could not be loaded for validation")
throw NSError(description: "Template image for output suffixes \(suffixesStringRepresentation) could not be loaded for validation")
}
try validateSize(templateImageSize, screenshotSize: screenshotSize)
}
Expand All @@ -138,23 +142,23 @@ public struct DeviceData: Decodable, ConfigValidatable {
if gapWidth == 0 {
guard remainingPixels == 0 else {
throw NSError(
description: "Template image for output suffix \"\(outputSuffix)\" is not a multiple in width as associated screenshot width",
description: "Template image for output suffixes \(suffixesStringRepresentation) is not a multiple in width as associated screenshot width",
expectation: "Width should be multiple of \(Int(screenshotSize.width))px",
actualValue: "\(Int(screenshotSize.width))px")
}
} else {
// Make sure there's at least one gap
guard remainingPixels.truncatingRemainder(dividingBy: CGFloat(gapWidth)) == 0 && remainingPixels != 0 else {
throw NSError(
description: "Template image for output suffix \"\(outputSuffix)\" is not a multiple in width as associated screenshot width",
description: "Template image for output suffixes \(suffixesStringRepresentation) is not a multiple in width as associated screenshot width",
expectation: "Template image width should be = (x * screenshot width) + (x - 1) * gap width",
actualValue: "Template image width: \(templateSize.width)px, screenshot width: \(screenshotSize.width), gap width: \(gapWidth)")
}
}
}

func printSummary(insetByTabs tabs: Int) {
CommandLineFormatter.printKeyValue("Ouput suffix", value: outputSuffix, insetBy: tabs)
CommandLineFormatter.printKeyValue("Ouput suffixes", value: outputSuffixes.joined(separator: ", "), insetBy: tabs)
CommandLineFormatter.printKeyValue("Template file path", value: templateImagePath.path, insetBy: tabs)
CommandLineFormatter.printKeyValue("Gap Width", value: gapWidth, insetBy: tabs)
CommandLineFormatter.printKeyValue(
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftFrameCore/Workers/ConfigProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,12 @@ public class ConfigProcessor {
gapWidth: deviceData.gapWidth,
outputWholeImage: data.outputWholeImage,
locale: locale,
suffix: deviceData.outputSuffix,
suffixes: deviceData.outputSuffixes,
format: data.outputFormat)

print("Finished \(locale)-\(deviceData.outputSuffix)")
deviceData.outputSuffixes.forEach { suffix in
print("Finished \(locale)-\(suffix)")
}

group.leave()
}
Expand Down
91 changes: 66 additions & 25 deletions Sources/SwiftFrameCore/Workers/ImageWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,34 @@ public final class ImageWriter {
gapWidth: Int,
outputWholeImage: Bool,
locale: String,
suffix: String,
suffixes: [String],
format: FileFormat) throws
{
guard let image = context.cg.makeImage() else {
throw NSError(description: "Could not render output image")
}
let slices = try sliceImage(image, with: sliceSize, gapWidth: gapWidth)
let fileNameConfiguration = OutputConfiguration(
outputPaths: outputPaths,
locale: locale,
suffixes: suffixes,
format: format
)

let workGroup = DispatchGroup()

// Writing images asynchronously gave a big performance boost, what a surprise
// Also, since we checked beforehand if the directory is writable, we can safely put of the rendering work to a different queue
workGroup.enter()
DispatchQueue.global(qos: .userInitiated).ky_asyncOrExit {
try ImageWriter.write(images: slices, to: outputPaths, locale: locale, suffix: suffix, format: format)
try ImageWriter.writeSlices(slices, with: fileNameConfiguration)
workGroup.leave()
}

if outputWholeImage {
workGroup.enter()
DispatchQueue.global(qos: .userInitiated).ky_asyncOrExit {
try outputPaths.forEach {
try ImageWriter.write(image, to: $0.absoluteURL.appendingPathComponent(locale), fileName: "\(locale)-\(suffix)-big", format: format)
}
try ImageWriter.writeBigImage(image, with: fileNameConfiguration)
workGroup.leave()
}
}
Expand All @@ -60,35 +64,72 @@ public final class ImageWriter {

// MARK: - Writing Images

static func write(images: [CGImage], to outputPaths: [FileURL], locale: String, suffix: String, format: FileFormat) throws {
try outputPaths.forEach { url in
try images.enumerated().forEach { tuple in
try write(tuple.element, to: url.path, locale: locale, deviceID: suffix, index: tuple.offset, format: format)
}
static func writeSlices(_ images: [CGImage], with configuration: OutputConfiguration) throws {
try images.enumerated().forEach { value in
let outputPaths = configuration.makeOutputPaths(for: value.offset)
try writeImage(value.element, to: outputPaths, format: configuration.format)
}
}

static func write(_ image: CGImage, to directoryPath: String, locale: String, deviceID: String, index: Int? = nil, format: FileFormat) throws {
let fileName: String
if let index = index {
fileName = "\(locale)-\(deviceID)-\(index)"
} else {
fileName = "\(locale)-\(deviceID)"
}
let directory = URL(fileURLWithPath: directoryPath).appendingPathComponent(locale)
try write(image, to: directory, fileName: fileName, format: format)
static func writeBigImage(_ image: CGImage, with configuration: OutputConfiguration) throws {
let outputPaths = configuration.makeBigImageOutputPaths()
try writeImage(image, to: outputPaths, format: configuration.format)
}

static func write(_ image: CGImage, to directoryPath: URL, fileName: String, format: FileFormat) throws {
static func writeImage(_ image: CGImage, to urls: [URL], format: FileFormat) throws {
let rep = NSBitmapImageRep(cgImage: image)
guard let data = rep.representation(using: format, properties: [:]) else {
throw NSError(description: "Failed to convert composed image to PNG")
throw NSError(description: "Failed to convert image to \(format.fileExtension.uppercased())")
}

try urls.forEach {
try data.ky_write(to: $0, options: .atomicWrite)
}
}

}

// MARK: - OutputConfiguration

extension ImageWriter {

struct OutputConfiguration {

// MARK: - Properties

let outputPaths: [FileURL]
let locale: String
let suffixes: [String]
let format: FileFormat

// MARK: - Methods

func makeBigImageOutputPaths() -> [URL] {
var urls = [URL]()
makeBasePaths().forEach { basePath in
suffixes.forEach { suffix in
let url = basePath.appendingPathComponent("\(locale)-\(suffix)-big").appendingPathExtension(format.fileExtension)
urls.append(url)
}
}
return urls
}

func makeOutputPaths(for sliceIndex: Int) -> [URL] {
var urls = [URL]()
makeBasePaths().forEach { basePath in
suffixes.forEach { suffix in
let url = basePath.appendingPathComponent("\(locale)-\(suffix)-\(sliceIndex)").appendingPathExtension(format.fileExtension)
urls.append(url)
}
}
return urls
}

private func makeBasePaths() -> [URL] {
outputPaths.map { $0.absoluteURL.appendingPathComponent(locale) }
}

let targetURL = directoryPath
.appendingPathComponent(fileName)
.appendingPathExtension(format.fileExtension)
try data.ky_write(to: targetURL, options: .atomicWrite)
}

}
4 changes: 3 additions & 1 deletion Tests/SwiftFrameTests/ImageLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ class ImageLoaderTests: BaseTest {
let rep = context.cg.makePlainWhiteImageRep()
let cgImage = try ky_unwrap(rep.cgImage)

try ImageWriter.write(cgImage, to: "testing/", locale: "en", deviceID: "testing_device", format: .png)
let url = URL(fileURLWithPath: "testing/en/en-testing_device.png")

try ImageWriter.writeImage(cgImage, to: [url], format: .png)
XCTAssertNoThrow(try ImageLoader().loadImage(atPath: "testing/en/en-testing_device.png"))
}

Expand Down
10 changes: 5 additions & 5 deletions Tests/SwiftFrameTests/Utility/DeviceDataFixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,37 @@ import Foundation
extension DeviceData {

static let goodData = DeviceData(
outputSuffix: "iPhone X",
outputSuffixes: ["iPhone X"],
templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"),
screenshotsPath: FileURL(path: "testing/screenshots/"),
screenshotData: [.goodData],
textData: [.goodData])

static let gapData = DeviceData(
outputSuffix: "iPhone X",
outputSuffixes: ["iPhone X"],
templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"),
screenshotsPath: FileURL(path: "testing/screenshots/"),
screenshotData: [.goodData],
textData: [.goodData],
gapWidth: 16)

static let invalidData = DeviceData(
outputSuffix: "iPhone X",
outputSuffixes: ["iPhone X"],
templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"),
screenshotsPath: FileURL(path: "testing/screenshots/"),
screenshotData: [.goodData],
textData: [.invalidData])

static let mismatchingDeviceSizeData = DeviceData(
outputSuffix: "iPhone X",
outputSuffixes: ["iPhone X"],
templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"),
screenshotsPath: FileURL(path: "testing/screenshots/"),
sliceSizeOverride: DecodableSize(width: 50, height: 100),
screenshotData: [.goodData],
textData: [.goodData])

static let faultyMismatchingDeviceSizeData = DeviceData(
outputSuffix: "iPhone X",
outputSuffixes: ["iPhone X"],
templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"),
screenshotsPath: FileURL(path: "testing/screenshots/"),
sliceSizeOverride: DecodableSize(width: 50, height: 100),
Expand Down
11 changes: 7 additions & 4 deletions Tests/SwiftFrameTests/Utility/TestingUtility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ struct TestingUtility {
throw NSError(description: "Could not make CGImage from Bitmap")
}

let url = URL(fileURLWithPath: "testing/screenshots/").appendingPathComponent(locale)
try ImageWriter.write(cgImage, to: url, fileName: deviceSuffix, format: .png)
let url = URL(fileURLWithPath: "testing/screenshots/")
.appendingPathComponent(locale)
.appendingPathComponent(deviceSuffix)
.appendingPathExtension(FileFormat.png.fileExtension)
try ImageWriter.writeImage(cgImage, to: [url], format: .png)
}

static func writeMockTemplateFile(deviceSuffix: String, gapWidth: Int) throws {
Expand All @@ -21,8 +24,8 @@ struct TestingUtility {
throw NSError(description: "Could not make CGImage from Bitmap")
}

let url = URL(fileURLWithPath: "testing/")
try ImageWriter.write(cgImage, to: url, fileName: "templatefile-\(deviceSuffix)", format: .png)
let url = URL(fileURLWithPath: "testing/templatefile-\(deviceSuffix).png")
try ImageWriter.writeImage(cgImage, to: [url], format: .png)
}

static func setupMockDirectoryWithScreenshots(gapWidth: Int = 0) throws {
Expand Down

0 comments on commit cdef5e9

Please sign in to comment.