// 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), ("icon_16x16@2x.png", 32), ("icon_32x32.png", 32), ("icon_32x32@2x.png", 64), ("icon_128x128.png", 128), ("icon_128x128@2x.png", 256), ("icon_256x256.png", 256), ("icon_256x256@2x.png", 512), ("icon_512x512.png", 512), ("icon_512x512@2x.png", 1024), ] let arguments = CommandLine.arguments guard arguments.count == 3 else { die("usage: \(URL(fileURLWithPath: arguments.first ?? "icon").lastPathComponent) ") } 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)