#!/usr/bin/env swift import Foundation import CoreGraphics import ImageIO // Programmatic Redline app icon. // Draws a 1024×1024 master, writes AppIcon.iconset (16…1024 incl. @2x), // and compiles Resources/AppIcon.icns via iconutil. // // Usage: // swift scripts/make-icon.swift // swift scripts/make-icon.swift --preview /path/to/icon-512.png private let masterSize = 1024 private enum Palette { static let charcoalTop = CGColor(srgbRed: 44.0 / 255.0, green: 44.0 / 255.0, blue: 46.0 / 255.0, alpha: 1) static let charcoalBottom = CGColor(srgbRed: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, alpha: 1) // #1C1C1E static let white = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) static let redline = CGColor(srgbRed: 229.0 / 255.0, green: 72.0 / 255.0, blue: 63.0 / 255.0, alpha: 1) // #E5483F } private struct IconsetEntry { let filename: String let pixels: Int } private let iconsetEntries: [IconsetEntry] = [ IconsetEntry(filename: "icon_16x16.png", pixels: 16), IconsetEntry(filename: "icon_16x16@2x.png", pixels: 32), IconsetEntry(filename: "icon_32x32.png", pixels: 32), IconsetEntry(filename: "icon_32x32@2x.png", pixels: 64), IconsetEntry(filename: "icon_128x128.png", pixels: 128), IconsetEntry(filename: "icon_128x128@2x.png", pixels: 256), IconsetEntry(filename: "icon_256x256.png", pixels: 256), IconsetEntry(filename: "icon_256x256@2x.png", pixels: 512), IconsetEntry(filename: "icon_512x512.png", pixels: 512), IconsetEntry(filename: "icon_512x512@2x.png", pixels: 1024), ] // MARK: - Geometry /// Apple-style continuous-corner rounded square (squircle-like). /// Superellipse |x/a|^n + |y/b|^n = 1 with n≈5, inset 1px so antialiased /// edge pixels are not clipped by the bitmap. private func continuousRoundedSquare(size: CGFloat, exponent n: CGFloat = 5.0, segments: Int = 256) -> CGPath { let inset: CGFloat = 1 let a = (size - inset * 2) / 2 let cx = size / 2 let cy = size / 2 let twoOverN = 2 / n let path = CGMutablePath() for i in 0...segments { let theta = CGFloat(i) / CGFloat(segments) * 2 * .pi let ct = cos(theta) let st = sin(theta) let x = cx + (ct < 0 ? -1 : 1) * pow(abs(ct), twoOverN) * a let y = cy + (st < 0 ? -1 : 1) * pow(abs(st), twoOverN) * a if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } } path.closeSubpath() return path } // MARK: - Drawing private func drawMasterIcon(size: Int) -> CGImage { let s = CGFloat(size) let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue guard let ctx = CGContext( data: nil, width: size, height: size, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo ) else { fputs("error: failed to create \(size)×\(size) bitmap context\n", stderr) exit(1) } ctx.setShouldAntialias(true) ctx.setAllowsAntialiasing(true) ctx.interpolationQuality = .high // Flip to top-left origin so "top-to-bottom gradient" is literal. ctx.translateBy(x: 0, y: s) ctx.scaleBy(x: 1, y: -1) ctx.clear(CGRect(x: 0, y: 0, width: s, height: s)) let squircle = continuousRoundedSquare(size: s) ctx.saveGState() ctx.addPath(squircle) ctx.clip() let gradient = CGGradient( colorsSpace: colorSpace, colors: [Palette.charcoalTop, Palette.charcoalBottom] as CFArray, locations: [0, 1] )! ctx.drawLinearGradient( gradient, start: CGPoint(x: s / 2, y: 0), end: CGPoint(x: s / 2, y: s), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation] ) ctx.restoreGState() // Viewfinder corner brackets: thick L-strokes, rounded caps/joins, inset ~18%. let inset = s * 0.18 let bracketWidth = s * 0.095 let arm = s * 0.155 let centerline = inset + bracketWidth / 2 ctx.saveGState() ctx.addPath(squircle) ctx.clip() ctx.setStrokeColor(Palette.white) ctx.setLineWidth(bracketWidth) ctx.setLineCap(.round) ctx.setLineJoin(.round) func strokeBracket(cornerX: CGFloat, cornerY: CGFloat, dirX: CGFloat, dirY: CGFloat) { let path = CGMutablePath() path.move(to: CGPoint(x: cornerX + dirX * arm, y: cornerY)) path.addLine(to: CGPoint(x: cornerX, y: cornerY)) path.addLine(to: CGPoint(x: cornerX, y: cornerY + dirY * arm)) ctx.addPath(path) ctx.strokePath() } // Top-left, top-right, bottom-left, bottom-right. strokeBracket(cornerX: centerline, cornerY: centerline, dirX: 1, dirY: 1) strokeBracket(cornerX: s - centerline, cornerY: centerline, dirX: -1, dirY: 1) strokeBracket(cornerX: centerline, cornerY: s - centerline, dirX: 1, dirY: -1) strokeBracket(cornerX: s - centerline, cornerY: s - centerline, dirX: -1, dirY: -1) // Bold redline slash over the frame, lower-left bracket area → upper-right. let slashWidth = s * 0.078 ctx.setStrokeColor(Palette.redline) ctx.setLineWidth(slashWidth) ctx.setLineCap(.round) ctx.setLineJoin(.round) let slashInset = centerline + arm * 0.12 let slash = CGMutablePath() slash.move(to: CGPoint(x: slashInset, y: s - slashInset)) slash.addLine(to: CGPoint(x: s - slashInset, y: slashInset)) ctx.addPath(slash) ctx.strokePath() ctx.restoreGState() guard let image = ctx.makeImage() else { fputs("error: failed to materialize master CGImage\n", stderr) exit(1) } return image } private func scaledImage(_ image: CGImage, pixels: Int) -> CGImage { if image.width == pixels && image.height == pixels { return image } let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue guard let ctx = CGContext( data: nil, width: pixels, height: pixels, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo ) else { fputs("error: failed to create \(pixels)×\(pixels) scale context\n", stderr) exit(1) } ctx.interpolationQuality = .high ctx.setShouldAntialias(true) ctx.draw(image, in: CGRect(x: 0, y: 0, width: pixels, height: pixels)) guard let out = ctx.makeImage() else { fputs("error: failed to scale image to \(pixels)px\n", stderr) exit(1) } return out } private func writePNG(_ image: CGImage, to url: URL) { let dir = url.deletingLastPathComponent() try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) if FileManager.default.fileExists(atPath: url.path) { try? FileManager.default.removeItem(at: url) } guard let dest = CGImageDestinationCreateWithURL(url as CFURL, "public.png" as CFString, 1, nil) else { fputs("error: cannot create PNG destination at \(url.path)\n", stderr) exit(1) } CGImageDestinationAddImage(dest, image, nil) if !CGImageDestinationFinalize(dest) { fputs("error: failed to write PNG \(url.path)\n", stderr) exit(1) } } // MARK: - Paths / CLI private func repoRoot() -> URL { let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) let arg0 = URL(fileURLWithPath: CommandLine.arguments[0]) let scriptURL: URL if arg0.path.hasPrefix("/") { scriptURL = arg0.standardizedFileURL } else { scriptURL = cwd.appendingPathComponent(arg0.path).standardizedFileURL } let fromScript = scriptURL.deletingLastPathComponent().deletingLastPathComponent() if FileManager.default.fileExists(atPath: fromScript.appendingPathComponent("Package.swift").path) { return fromScript } if FileManager.default.fileExists(atPath: cwd.appendingPathComponent("Package.swift").path) { return cwd } fputs("error: could not find repo root (Package.swift)\n", stderr) exit(1) } private func parsePreviewPath() -> String? { let args = CommandLine.arguments var i = 1 var preview: String? while i < args.count { let arg = args[i] if arg == "--preview" { i += 1 guard i < args.count else { fputs("error: --preview requires a path\n", stderr) exit(1) } preview = args[i] } else if arg.hasPrefix("--preview=") { preview = String(arg.dropFirst("--preview=".count)) } else if arg == "--help" || arg == "-h" { fputs("Usage: swift scripts/make-icon.swift [--preview PATH]\n", stderr) exit(0) } else { fputs("error: unknown argument \(arg)\n", stderr) fputs("Usage: swift scripts/make-icon.swift [--preview PATH]\n", stderr) exit(1) } i += 1 } return preview } private func runIconutil(iconset: URL, icns: URL) { let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil") proc.arguments = ["-c", "icns", iconset.path, "-o", icns.path] proc.standardOutput = FileHandle.standardOutput proc.standardError = FileHandle.standardError do { try proc.run() proc.waitUntilExit() } catch { fputs("error: failed to launch iconutil: \(error)\n", stderr) exit(1) } if proc.terminationStatus != 0 { fputs("error: iconutil exited \(proc.terminationStatus)\n", stderr) exit(1) } } // MARK: - Main let root = repoRoot() let resources = root.appendingPathComponent("Resources", isDirectory: true) let icnsURL = resources.appendingPathComponent("AppIcon.icns") let previewPath = parsePreviewPath() let fm = FileManager.default print("==> Drawing \(masterSize)×\(masterSize) master") let master = drawMasterIcon(size: masterSize) let iconset = fm.temporaryDirectory.appendingPathComponent("Redline-AppIcon-\(UUID().uuidString).iconset", isDirectory: true) do { try fm.createDirectory(at: iconset, withIntermediateDirectories: true) try fm.createDirectory(at: resources, withIntermediateDirectories: true) print("==> Writing AppIcon.iconset") for entry in iconsetEntries { let img = scaledImage(master, pixels: entry.pixels) writePNG(img, to: iconset.appendingPathComponent(entry.filename)) } if fm.fileExists(atPath: icnsURL.path) { try fm.removeItem(at: icnsURL) } print("==> Compiling \(icnsURL.path)") runIconutil(iconset: iconset, icns: icnsURL) try? fm.removeItem(at: iconset) } catch { try? fm.removeItem(at: iconset) fputs("error: \(error)\n", stderr) exit(1) } if let previewPath { let previewURL = URL(fileURLWithPath: previewPath) print("==> Writing 512px preview \(previewURL.path)") writePNG(scaledImage(master, pixels: 512), to: previewURL) } print("App icon: \(icnsURL.path)")