// Regenerates the app icon PNGs from the official Monero mark. // // cd apps/ios // swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset // // Input: Branding/logo-official.png — the monochrome mark published in // monero-project/monero-site (meta/safari-pinned-tab.svg), rasterised. // It is a black badge with the M knocked out, drawn on white. // Output: AppIcon-1024.png - the inverse: white badge, black M, on black // (reads well on the app's dark UI) // AppIcon-light-1024.png - the mark as published: black badge, white M, // on white // // The artwork is inset to 90% of the canvas so the iOS squircle mask does not // clip the badge's edges. iOS cannot use SVG for app icons, which is why this // rasterises instead of linking the SVG. import AppKit import CoreGraphics import Foundation struct Bitmap { let width: Int let height: Int var pixels: [UInt8] // RGBA, premultiplied func luminance(_ offset: Int) -> Int { (Int(pixels[offset]) * 299 + Int(pixels[offset + 1]) * 587 + Int(pixels[offset + 2]) * 114) / 1000 } } func loadBitmap(_ path: String) -> Bitmap? { guard let image = NSImage(contentsOfFile: path), let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } let width = cgImage.width let height = cgImage.height var pixels = [UInt8](repeating: 0, count: width * height * 4) guard let context = CGContext(data: &pixels, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil } context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) return Bitmap(width: width, height: height, pixels: pixels) } func makeImage(_ bitmap: Bitmap) -> CGImage? { guard let provider = CGDataProvider(data: Data(bitmap.pixels) as CFData) else { return nil } return CGImage(width: bitmap.width, height: bitmap.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bitmap.width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) } func inverted(_ bitmap: Bitmap) -> Bitmap { var pixels = bitmap.pixels for offset in stride(from: 0, to: pixels.count, by: 4) { let value = UInt8(255 - bitmap.luminance(offset)) pixels[offset] = value pixels[offset + 1] = value pixels[offset + 2] = value } return Bitmap(width: bitmap.width, height: bitmap.height, pixels: pixels) } /// Composes the mark onto a square background, inset so the platform mask has /// room, and writes an opaque PNG (App Store icons must not carry alpha). func writeIcon(_ source: Bitmap, size: Int, inset: CGFloat, background: NSColor, to path: String) { guard let image = makeImage(source) else { return } var pixels = [UInt8](repeating: 0, count: size * size * 4) guard let context = CGContext(data: &pixels, width: size, height: size, bitsPerComponent: 8, bytesPerRow: size * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return } context.setFillColor(background.cgColor) context.fill(CGRect(x: 0, y: 0, width: size, height: size)) let side = CGFloat(size) * inset let offset = (CGFloat(size) - side) / 2 context.interpolationQuality = .high context.draw(image, in: CGRect(x: offset, y: offset, width: side, height: side)) var rgb = [UInt8](repeating: 0, count: size * size * 3) for index in 0..<(size * size) { rgb[index * 3] = pixels[index * 4] rgb[index * 3 + 1] = pixels[index * 4 + 1] rgb[index * 3 + 2] = pixels[index * 4 + 2] } guard let provider = CGDataProvider(data: Data(rgb) as CFData), let flattened = CGImage(width: size, height: size, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: size * 3, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue), provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) else { return } let representation = NSBitmapImageRep(cgImage: flattened) guard let data = representation.representation(using: .png, properties: [:]) else { return } try? data.write(to: URL(fileURLWithPath: path)) print("wrote \(path)") } let arguments = CommandLine.arguments let outputDirectory = arguments.count > 1 ? arguments[1] : "/tmp" let sourcePath = arguments.count > 2 ? arguments[2] : "Branding/logo-official.png" try? FileManager.default.createDirectory(atPath: outputDirectory, withIntermediateDirectories: true) guard let source = loadBitmap(sourcePath) else { print("cannot read source artwork at \(sourcePath)") exit(1) } let inset: CGFloat = 0.78 // Primary: the mark exactly as Monero publishes it (black badge, white M). writeIcon(source, size: 1024, inset: inset, background: .white, to: "\(outputDirectory)/AppIcon-1024.png") // Alternate: inverted, for dark surfaces (white badge, black M, black ground). writeIcon(inverted(source), size: 1024, inset: inset, background: .black, to: "Branding/logo-inverted.png")