Skip to content

Commit d2fa6fd

Browse files
authored
port iconify.jxa to Swift (#43)
* remove ye olde JXA iconify * github: remove Applescript image-events authorization hacks fixes #42
1 parent 1eff996 commit d2fa6fd

File tree

7 files changed

+237
-173
lines changed

7 files changed

+237
-173
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ jobs:
2323
uses: actions/checkout@v1
2424
- name: Build All Apps
2525
run: |
26-
sqlite3 "$HOME/Library/Application Support/com.apple.TCC/TCC.db" "INSERT OR IGNORE INTO access VALUES('kTCCServiceAppleEvents','/bin/bash',1,2,0,1,NULL,NULL,0,'com.apple.imageevents',NULL,NULL,1591532620)"
2726
make allapps
2827
env:
2928
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
@@ -43,7 +42,6 @@ jobs:
4342
uses: actions/checkout@v1
4443
- name: Build All Apps
4544
run: |
46-
sqlite3 "$HOME/Library/Application Support/com.apple.TCC/TCC.db" "insert into access (service, client, client_type, allowed, prompt_count, indirect_object_identifier_type, indirect_object_identifier) values ('kTCCServiceAppleEvents','/bin/bash', 1, 2, 0, 1, 'com.apple.imageevents')"
4745
make allapps
4846
env:
4947
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ gen_apps = $(patsubst $(macpin_sites)/%,$(appdir)/%.app,$(wildcard $(macpin_si
3333

3434
template_bundle_id := com.github.kfix.MacPin
3535
xcassets := $(builddir)/xcassets/$(platform)
36-
icontypes := imageset
36+
icontypes := --imageset
3737

3838
ifeq ($(platform),macos)
39-
icontypes += iconset
39+
icontypes += --iconset
4040
else
41-
icontypes += appiconset
41+
icontypes += --appiconset
4242
endif
4343

4444
# http://www.objc.io/issue-17/inside-code-signing.html
@@ -133,13 +133,13 @@ GH_RELEASE_JSON = '{"tag_name": "v$(VERSION)","target_commitish": "master","name
133133
#####
134134

135135
$(xcassets)/%.xcassets: $(macpin_sites)/%/icon.png
136-
osascript -l JavaScript iconify.jxa $< $@ $(icontypes)
136+
$(swiftrun_mac) iconify $(icontypes) $< $@
137137

138138
$(xcassets)/%.xcassets: templates/xcassets/$(platform)/%/*.png
139-
for i in $^; do osascript -l JavaScript iconify.jxa $$i $@ imageset; done
139+
for i in $^; do $(swiftrun_mac) iconify --imageset $$i $@; done
140140

141141
$(xcassets)/%.xcassets: templates/xcassets/%/*.png
142-
for i in $^; do osascript -l JavaScript iconify.jxa $$i $@ imageset; done
142+
for i in $^; do $(swiftrun_mac) iconify --imageset $$i $@; done
143143

144144
#$(appdir): ; install -d $@
145145

Package.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ let package = Package(
1010
.library(name: "MacPin", type: .dynamic, targets: ["MacPin"]),
1111
.executable(name: "MacPin_static", targets: ["MacPin_static"]),
1212
.executable(name: "MacPin_stub", targets: ["MacPin_stub"]),
13+
.executable(name: "iconify", targets: ["iconify"]),
1314
],
1415
dependencies: [
1516
.package(path: "modules/Linenoise"),
1617
.package(path: "modules/UTIKit"),
18+
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
1719
],
1820
targets: [
1921
.systemLibrary(
@@ -53,6 +55,13 @@ if let iosvar = ProcessInfo.processInfo.environment["MACPIN_IOS"], !iosvar.isEmp
5355
)
5456
} else {
5557
package.targets.append(contentsOf: [
58+
.executableTarget(
59+
name: "iconify",
60+
dependencies: [
61+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
62+
],
63+
path: "Tools/iconify"
64+
),
5665
.target(name: "MacPin",
5766
dependencies: [
5867
"WebKitPrivates",

Tools/iconify/iconify.swift

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import ArgumentParser
2+
import Cocoa
3+
import Foundation
4+
5+
func warn(_ msg: String = String(), function: StaticString = #function, file: StaticString = #file, line: UInt = #line, column: UInt = #column) {
6+
FileHandle.standardError.write((msg + "\n").data(using: String.Encoding.utf8)!)
7+
}
8+
9+
extension String {
10+
var fileURL: URL {
11+
return URL(fileURLWithPath: self)
12+
}
13+
var pathExtension: String {
14+
return fileURL.pathExtension
15+
}
16+
var lastPathComponent: String {
17+
return fileURL.lastPathComponent
18+
}
19+
var basename: String {
20+
return fileURL.deletingPathExtension().lastPathComponent
21+
}
22+
}
23+
24+
extension Dictionary {
25+
var jsonData: Data? {
26+
return try? JSONSerialization.data(withJSONObject: self, options: [.prettyPrinted])
27+
}
28+
29+
func toJSONString() -> String? {
30+
if let jsonData = jsonData {
31+
let jsonString = String(data: jsonData, encoding: .utf8)
32+
return jsonString
33+
}
34+
35+
return nil
36+
}
37+
}
38+
39+
struct RuntimeError: Error, CustomStringConvertible {
40+
var description: String
41+
42+
init(_ description: String) {
43+
self.description = description
44+
}
45+
}
46+
47+
enum IconIdiom: String {
48+
case mac
49+
case universal
50+
}
51+
52+
struct IconOptions {
53+
var idiom: IconIdiom
54+
var scaleUp: Bool
55+
var basename: String
56+
var sizes: [Int: String]
57+
var scales: Array<Int>
58+
}
59+
60+
enum OutputType: String, EnumerableFlag {
61+
case appiconset, iconset, imageset
62+
}
63+
64+
// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html#//apple_ref/doc/uid/TP40012302-CH7-SW4i
65+
let iconsetOpts = IconOptions(
66+
idiom: .mac,
67+
scaleUp: true,
68+
basename: "icon_",
69+
sizes: [
70+
16: "16x16",
71+
32: "32x32",
72+
128: "128x128",
73+
256: "256x256",
74+
512: "512x512"
75+
],
76+
scales: [1, 2]
77+
)
78+
79+
// https://developer.apple.com/library/ios/recipes/xcode_help-image_catalog-1.0/Recipe.html
80+
// https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
81+
let appIconsetOpts = IconOptions(
82+
idiom: .universal,
83+
scaleUp: true,
84+
basename: "Icon-",
85+
sizes: [ // https://developer.apple.com/library/ios/qa/qa1686/_index.html
86+
// the TN says filenames don't matter but Xcode and actool disagree
87+
25: "Small-50",
88+
29: "Small",
89+
40: "Small-40",
90+
60: "60",
91+
76: "76"
92+
//512: 'iTunesArtwork' // needs to be this with no basename or extension
93+
],
94+
scales: [1, 2, 3]
95+
)
96+
97+
var imagesetOpts = IconOptions(
98+
idiom: .universal,
99+
scaleUp: false,
100+
basename: "image", //doesn't matter
101+
sizes: [:],
102+
scales: []
103+
)
104+
105+
func setScalesFromImageSize(src: NSImage, options: inout IconOptions) {
106+
// find the scales that can be evenly-derived from the source image's nativeSize
107+
let nativeSize = src.size.width
108+
for scale in [3, 2, 1] {
109+
if ((nativeSize.truncatingRemainder(dividingBy: CGFloat(scale))) == 0) {
110+
let pts = Int(nativeSize / CGFloat(scale))
111+
options.sizes[pts] = "\(pts)"
112+
for i in 1...scale { options.scales.append(i) }
113+
break
114+
}
115+
}
116+
}
117+
118+
func imageExplode(src: NSImage, outdir: String, options: IconOptions) -> Array<[String:String]> {
119+
warn("generating \(outdir)")
120+
try? FileManager.default.createDirectory(atPath: outdir, withIntermediateDirectories: true)
121+
var images: Array<[String:String]> = []
122+
for size in options.sizes.keys.sorted() {
123+
for scale in options.scales {
124+
let pxs = size * scale
125+
if !options.scaleUp && (Float(src.size.width) < Float(pxs)) {
126+
continue
127+
}
128+
// do rescaling
129+
var imageName = options.basename
130+
let sizeVal = options.sizes[size] ?? "\(size)x\(size)"
131+
imageName.append(sizeVal)
132+
if (scale > 1) {
133+
imageName.append("@\(scale)x")
134+
}
135+
let imagePath = outdir + "/" + imageName + ".png"
136+
warn("\(size) @ \(scale)x => \(pxs)px => \(imagePath)")
137+
let newSize = NSSize(width: Int(pxs), height: Int(pxs))
138+
let frame = NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height)
139+
guard let representation = src.bestRepresentation(for: frame, context: nil, hints: nil) else { continue }
140+
let newImage = NSImage(size: newSize, flipped: false, drawingHandler: { (_) -> Bool in
141+
return representation.draw(in: frame)
142+
})
143+
guard let newTiff = newImage.tiffRepresentation, let bmp = NSBitmapImageRep(data: newTiff) else { continue }
144+
bmp.hasAlpha = true
145+
guard let data = bmp.representation(using: .png, properties: [.compressionFactor: 0.85]) else { continue }
146+
try? data.write(to: imagePath.fileURL)
147+
images.append([
148+
"filename": imagePath.lastPathComponent,
149+
"idiom": options.idiom.rawValue,
150+
"size": "\(size)x\(size)",
151+
"scale": "\(scale)x"
152+
])
153+
}
154+
}
155+
return images
156+
}
157+
158+
@main
159+
struct Iconify: ParsableCommand {
160+
static let configuration = CommandConfiguration(abstract: "generate an .icns, .iconset, .xcasset, and .car from a single big bitmap icon.")
161+
162+
@Flag(help: "types of icons to generate in the xcassetdir")
163+
var outputTypes: [OutputType]
164+
165+
@Argument(help: "path of the source .png file")
166+
var sourcePath: String
167+
168+
@Argument(help: "path of the output .xcassets directory")
169+
var xcassetPath: String
170+
171+
mutating func run() throws {
172+
let iconName = sourcePath.basename
173+
guard let sourceImage = NSImage(contentsOfFile: sourcePath) else {
174+
throw RuntimeError("\(sourcePath) is unparseable as an NSImage")
175+
}
176+
for outputType in outputTypes {
177+
var outputPath: String
178+
var iconOptions: IconOptions
179+
switch (outputType) {
180+
case .appiconset:
181+
outputPath = "\(xcassetPath)/AppIcon.appiconset"
182+
iconOptions = appIconsetOpts
183+
case .iconset:
184+
outputPath = "\(xcassetPath)/Icon.iconset"
185+
iconOptions = iconsetOpts
186+
case .imageset:
187+
outputPath = "\(xcassetPath)/\(iconName).imageset"
188+
iconOptions = imagesetOpts
189+
setScalesFromImageSize(src: sourceImage, options: &iconOptions)
190+
}
191+
let imagesinfo = imageExplode(src: sourceImage, outdir: outputPath, options: iconOptions)
192+
193+
switch (outputType) {
194+
case .iconset:
195+
// OSX icon, needs nothing
196+
break;
197+
case .appiconset, .imageset:
198+
// write imagesinfo
199+
var info: [String:Any] = [
200+
"info": [
201+
"version": 1,
202+
"author": "iconify"
203+
],
204+
]
205+
info["images"] = imagesinfo
206+
let manifest = "\(outputPath)/Contents.json"
207+
guard let jsonString = info.toJSONString() else {
208+
throw RuntimeError("could not serialize JSON for \(manifest)")
209+
}
210+
//warn("============= \(manifest) ==========\n\(jsonString)")
211+
try? jsonString.write(to: manifest.fileURL, atomically: true, encoding: String.Encoding.utf8)
212+
}
213+
}
214+
}
215+
}
216+
// vim: filetype=swift

Tools/iconify/null.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// empty file to workaround https://bugs.swift.org/browse/SR-12683

eXcode.mk

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ sdks_macos ?= macosx
1414
sdks_ios ?= iphoneos iphonesimulator
1515
sdk ?= macosx
1616

17-
archs_macosx ?= x86_64
17+
archs_macosx ?= x86_64 arm64
1818
archs_iphonesimulator ?= $(archs_macosx)
1919
archs_iphoneos ?= arm64
2020
arch ?= $(shell uname -m)
@@ -34,7 +34,7 @@ target := $(target_ios)
3434
endif
3535

3636
outdir := $(builddir)/$(sdk)-$(arch)-$(target_$(platform))
37-
37+
outdir_mac := $(builddir)/macosx-$(arch)-$(target_macos)
3838
#-> platform, sdk, arch, target
3939
#$(info $(0) $(1) $(2) $(3) $(4))
4040
define REMAKE_template
@@ -83,3 +83,6 @@ swiftbuild := swift build --configuration release \
8383
--build-path $(outdir)/swiftpm \
8484
-Xswiftc "-sdk" -Xswiftc $(sdkpath) \
8585
-Xswiftc "-target" -Xswiftc $(arch)-$(target_$(platform))
86+
87+
swiftrun_mac := swift run --configuration release \
88+
--build-path $(outdir_mac)/swiftpm

0 commit comments

Comments
 (0)