// gen-icon.awk: generate a program icon for xM in the Apple icon format // // Copyright (c) 2023, Přemysl Eric Janouch // SPDX-License-Identifier: 0BSD // // NSGraphicsContext mostly just weirdly wraps over Quartz, // so we do it all in Quartz directly. import CoreGraphics import Foundation import ImageIO import UniformTypeIdentifiers // XXX: Not even AppKit provides real superelliptic squircles; screw it. func drawSquircle(context: CGContext, bounds: CGRect, radius: CGFloat) { context.move(to: CGPointMake(bounds.minX, bounds.maxY - radius)) context.addArc( center: CGPointMake(bounds.minX + radius, bounds.maxY - radius), radius: radius, startAngle: .pi, endAngle: .pi / 2, clockwise: true) context.addLine(to: CGPointMake(bounds.maxX - radius, bounds.maxY)) context.addArc( center: CGPointMake(bounds.maxX - radius, bounds.maxY - radius), radius: radius, startAngle: .pi / 2, endAngle: 0, clockwise: true) context.addLine(to: CGPointMake(bounds.maxX, bounds.maxY - radius)) context.addArc( center: CGPointMake(bounds.maxX - radius, bounds.minY + radius), radius: radius, startAngle: 0, endAngle: .pi / -2, clockwise: true) context.addLine(to: CGPointMake(bounds.minX + radius, bounds.minY)) context.addArc( center: CGPointMake(bounds.minX + radius, bounds.minY + radius), radius: radius, startAngle: .pi / -2, endAngle: .pi, clockwise: true) context.closePath() } func drawIcon(scale: CGFloat) -> CGImage? { let size = CGSizeMake(1024, 1024) let colorspace = CGColorSpaceCreateDeviceRGB() let context = CGContext(data: nil, width: Int(size.width * scale), height: Int(size.height * scale), bitsPerComponent: 8, bytesPerRow: 0, space: colorspace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! context.scaleBy(x: scale, y: scale) let bounds = CGRectMake(100, 100, size.width - 200, size.height - 200) // The radius was something like size.{width,height}/6.4, at least for iOS. drawSquircle(context: context, bounds: bounds, radius: 180) let squircle = context.path! // Gradients don't draw shadows, so draw it separately. context.saveGState() context.setShadow(offset: CGSizeMake(0, -12).applying(context.ctm), blur: 28 * scale, color: CGColor(gray: 0, alpha: 0.5)) context.setFillColor(CGColor(red: 1, green: 0x55p-8, blue: 0, alpha: 1)) context.fillPath() context.restoreGState() context.saveGState() context.addPath(squircle) context.clip() context.drawLinearGradient( CGGradient(colorsSpace: colorspace, colors: [ CGColor(red: 1, green: 0x00p-8, blue: 0, alpha: 1), CGColor(red: 1, green: 0xaap-8, blue: 0, alpha: 1) ] as CFArray, locations: [0, 1])!, start: CGPointMake(0, 100), end: CGPointMake(0, size.height - 100), options: CGGradientDrawingOptions(rawValue: 0)) context.restoreGState() context.move(to: CGPoint(x: size.width * 0.30, y: size.height * 0.30)) context.addLine(to: CGPoint(x: size.width * 0.30, y: size.height * 0.70)) context.addLine(to: CGPoint(x: size.width * 0.575, y: size.height * 0.425)) context.move(to: CGPoint(x: size.width * 0.70, y: size.height * 0.30)) context.addLine(to: CGPoint(x: size.width * 0.70, y: size.height * 0.70)) context.addLine(to: CGPoint(x: size.width * 0.425, y: size.height * 0.425)) context.setLineWidth(80) context.setLineCap(.round) context.setLineJoin(.round) context.setStrokeColor(CGColor.white) context.strokePath() return context.makeImage() } if CommandLine.arguments.count != 2 { print("Usage: \(CommandLine.arguments.first!) OUTPUT.icns") exit(EXIT_FAILURE) } let filename = CommandLine.arguments[1] let macOSSizes: Array = [16, 32, 128, 256, 512] let icns = CGImageDestinationCreateWithURL( URL(fileURLWithPath: filename) as CFURL, UTType.icns.identifier as CFString, macOSSizes.count * 2, nil)! for size in macOSSizes { CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0)!, nil) CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0 * 2)!, [ kCGImagePropertyDPIWidth: 144, kCGImagePropertyDPIHeight: 144, ] as CFDictionary) } if !CGImageDestinationFinalize(icns) { print("ICNS finalization failed.") exit(EXIT_FAILURE) }