1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
// Cuts pardes.app's icon out of a committed drawing and hands the result to
// iconutil.
//
// The mark is still GLENDA, the Plan 9 rabbit — pardes is an acme, and acme is
// Plan 9's, so the bunny is the lineage stated in one shape. What changed is
// where she comes from: she used to be drawn here out of the terminal's own
// palette, four ellipses and a tag bar, and she is now `icon.png` beside this
// file. A drawing is not derivable from a palette, so the old argument for
// generating her ("a checked-in .icns stops matching the app the first time a
// colour moves") no longer applies to the artwork — but it still applies to
// the ICNS, which is why this file did not become a committed binary. What is
// committed is the source picture, in one format, reviewable as an image; what
// is generated is the ten-size container macOS actually reads.
//
// So the work here is no longer drawing. It is the part a designer's PNG never
// has: Apple's icon grid, the rounded-square mask, and ten exact sizes.
//
// build.zig compiles this file alone into a cached binary and runs it with the
// source PNG as argv[1] and the bundle's Resources directory as argv[2];
// Info.plist's CFBundleIconFile names the pardes.icns that comes out. Compiled
// alone is also what makes top-level code legal here: one file, one module.
//
// Byte-identical output for byte-identical input is a requirement, not a
// nicety — an icns that churns on every build is a bundle that churns on every
// build, and Launch Services notices. Hence a pinned sRGB colour space,
// integer geometry, and nothing read from the clock or the environment.
import CoreGraphics
import Foundation
import ImageIO
import UniformTypeIdentifiers
/// Every failure path lands here. A build that ships the generic blank-page
/// icon looks like an app nobody finished, so half-drawn is worse than absent
/// and build.zig has this exit code waiting: a bundle whose icon did not draw
/// should not have built.
func die(_ message: String) -> Never {
fputs("icon.swift: \(message)\n", stderr)
exit(1)
}
// Apple's icon grid rather than the whole square: the artwork is a rounded
// square floating in a transparent margin, 824 of 1024 with a 185.4 corner
// radius in the template — 80.47% of the canvas, and 22.37% of the SQUARE, not
// of the canvas, which would over-round it by a quarter. Filling the canvas
// edge to edge is the loudest tell that an app was not built on a Mac.
let squareFraction: CGFloat = 0.8047
let cornerFraction: CGFloat = 0.2237
/// Where the crop comes off when the source is not square.
///
/// The drawing is 1204x1306 — taller than wide — so filling a square throws
/// away 8% of its height, and WHICH 8% is the whole decision. Anchoring the
/// top keeps the sun, which is the only warm thing in the picture and sits in
/// the top-right corner, and spends the loss on the bottom band of grass,
/// which is texture and repeats. Anchoring the centre would clip the sun's
/// rays to buy back grass, which is the trade backwards. 0 is top, 1 is
/// bottom; a square source ignores this entirely.
let cropAnchor: CGFloat = 0
/// sRGB in the bitmap, so the paper white in the drawing is the paper white in
/// the icon rather than whatever DeviceRGB would make of it.
func sRGB() -> CGColorSpace {
guard let space = CGColorSpace(name: CGColorSpace.sRGB) else { die("sRGB colour space unavailable") }
return space
}
func loadSource(_ url: URL) -> CGImage {
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
die("cannot read \(url.path)")
}
guard let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
die("\(url.lastPathComponent) holds no decodable image")
}
return image
}
func renderIcon(_ art: CGImage, pixels: Int) -> CGImage {
guard
let ctx = CGContext(
data: nil, width: pixels, height: pixels,
bitsPerComponent: 8, bytesPerRow: 0, space: sRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
else { die("CGContext \(pixels)x\(pixels) failed") }
// Round the MARGIN and derive the square from it. Rounding the square
// instead leaves an odd remainder to split, and at 16 pixels the artwork
// lands a pixel off centre. At 1024 this is Apple's 100/824/100 exactly.
let canvas = CGFloat(pixels)
let inset = (canvas * (1 - squareFraction) / 2).rounded()
let side = canvas - 2 * inset
let body = CGRect(x: inset, y: inset, width: side, height: side)
let corner = side * cornerFraction
ctx.addPath(CGPath(roundedRect: body, cornerWidth: corner, cornerHeight: corner, transform: nil))
ctx.clip()
// Aspect FILL, not fit. Fitting would letterbox the rounded square with a
// flat band beside textured paper, and the seam between the two is visible
// at every size the band is wide enough to see. Filling overflows the clip
// instead, and the clip is already exact.
let artWidth = CGFloat(art.width)
let artHeight = CGFloat(art.height)
guard artWidth > 0, artHeight > 0 else { die("source image is empty") }
let scale = max(side / artWidth, side / artHeight)
let drawWidth = artWidth * scale
let drawHeight = artHeight * scale
// CoreGraphics is bottom-left origin, so `cropAnchor` 0 (the TOP of the
// picture) means the drawing's top edge meets the body's top edge and the
// overflow hangs off the bottom, into the clip.
let overflowY = drawHeight - side
let overflowX = drawWidth - side
ctx.interpolationQuality = .high
ctx.draw(
art,
in: CGRect(
x: body.minX - overflowX / 2,
y: body.maxY - drawHeight + overflowY * cropAnchor,
width: drawWidth, height: drawHeight))
guard let image = ctx.makeImage() else { die("CGContext.makeImage failed at \(pixels)") }
return image
}
func writePNG(_ image: CGImage, to url: URL) {
guard
let sink = CGImageDestinationCreateWithURL(
url as CFURL, UTType.png.identifier as CFString, 1, nil)
else { die("cannot open \(url.path) for writing") }
CGImageDestinationAddImage(sink, image, nil)
guard CGImageDestinationFinalize(sink) else { die("encoding \(url.lastPathComponent) failed") }
}
// The ten names iconutil demands, spelled out rather than derived: the set is
// fixed by the tool, and a loop that generated them would be a loop to read
// before believing the list. 32, 256 and 512 appear twice under two names and
// are simply drawn twice — a third of a megapixel, against the clarity of not
// caching anything in a build tool.
let variants: [(name: String, pixels: Int)] = [
("icon_16x16.png", 16),
("[email protected]", 32),
("icon_32x32.png", 32),
("[email protected]", 64),
("icon_128x128.png", 128),
("[email protected]", 256),
("icon_256x256.png", 256),
("[email protected]", 512),
("icon_512x512.png", 512),
("[email protected]", 1024),
]
let arguments = CommandLine.arguments
guard arguments.count == 3 else {
die("usage: \(URL(fileURLWithPath: arguments.first ?? "icon").lastPathComponent) <source.png> <output-directory>")
}
let files = FileManager.default
let art = loadSource(URL(fileURLWithPath: arguments[1]))
let outputDir = URL(fileURLWithPath: arguments[2], isDirectory: true)
let output = outputDir.appendingPathComponent("pardes.icns")
// A fixed scratch path, cleared before use rather than a unique one: a run that
// died half way leaves a partial iconset behind, and iconutil would happily
// fold the stale sizes into the next icns without a word.
let scratch = files.temporaryDirectory.appendingPathComponent("pardes-icon", isDirectory: true)
let iconset = scratch.appendingPathComponent("pardes.iconset", isDirectory: true)
try? files.removeItem(at: scratch)
do {
try files.createDirectory(at: iconset, withIntermediateDirectories: true)
try files.createDirectory(at: outputDir, withIntermediateDirectories: true)
} catch {
die("cannot create \(iconset.path): \(error.localizedDescription)")
}
for variant in variants {
writePNG(renderIcon(art, pixels: variant.pixels), to: iconset.appendingPathComponent(variant.name))
}
let iconutil = Process()
iconutil.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
iconutil.arguments = ["-c", "icns", "-o", output.path, iconset.path]
do {
try iconutil.run()
} catch {
die("cannot run /usr/bin/iconutil: \(error.localizedDescription)")
}
iconutil.waitUntilExit()
guard iconutil.terminationStatus == 0 else {
die("iconutil exited \(iconutil.terminationStatus) over \(iconset.path)")
}
try? files.removeItem(at: scratch)
|