135 lines
6.0 KiB
Swift
135 lines
6.0 KiB
Swift
// 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: "\(outputDirectory)/AppIcon-inverted-1024.png")
|