diff --git a/apps/ios/Branding/README.md b/apps/ios/Branding/README.md index 740cd86..a23b75e 100644 --- a/apps/ios/Branding/README.md +++ b/apps/ios/Branding/README.md @@ -1,23 +1,32 @@ # Branding -`logo.svg` is the source artwork: the Monero monogram in black and white — -two outer legs, a central V descending onto a full-width base bar, one uniform -stroke width (118 of 1024) so it stays legible at 60 px. +The app icon is the **official Monero mark**, monochrome. -iOS cannot use SVG for app icons, so the PNGs in -`Koins/Assets.xcassets/AppIcon.appiconset` are rendered from the same geometry: +`logo-official.svg` is `meta/safari-pinned-tab.svg` from +[monero-project/monero-site](https://github.com/monero-project/monero-site) — +the Monero Project's own traced, single-colour version of the logo, which is +exactly what a black-and-white icon needs. `logo-official.png` is that file +rasterised to 1024×1024: + +```sh +qlmanage -t -s 1024 -o /tmp Branding/logo-official.svg +``` + +iOS cannot use SVG for app icons, so `make-icons.swift` produces the PNG the +asset catalog needs — the mark as published (black badge, white M) inset to 90% +so the platform's squircle mask does not clip it: ```sh cd apps/ios swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset ``` -That writes the 1024×1024 icon (black ground, white mark, no alpha channel — -the App Store rejects icons with alpha) plus `Branding/logo-light.png`, the -inverted variant. +That writes the opaque 1024×1024 app icon (no alpha channel — the App Store +rejects icons with alpha) and `Branding/logo-inverted.png`, the same mark +inverted for dark surfaces (white badge, black M, black ground). The asset catalog uses the single-size format, so Xcode derives every size the device needs from the 1024. -Note: the Monero name and mark are the Monero Project's; check their brand +Note: the Monero name and mark belong to the Monero Project — check their brand guidance before shipping this publicly. diff --git a/apps/ios/Branding/logo-inverted.png b/apps/ios/Branding/logo-inverted.png new file mode 100644 index 0000000..2577411 Binary files /dev/null and b/apps/ios/Branding/logo-inverted.png differ diff --git a/apps/ios/Branding/logo-light.png b/apps/ios/Branding/logo-light.png deleted file mode 100644 index c227282..0000000 Binary files a/apps/ios/Branding/logo-light.png and /dev/null differ diff --git a/apps/ios/Branding/logo-official.png b/apps/ios/Branding/logo-official.png new file mode 100644 index 0000000..bddee2c Binary files /dev/null and b/apps/ios/Branding/logo-official.png differ diff --git a/apps/ios/Branding/logo-official.svg b/apps/ios/Branding/logo-official.svg new file mode 100644 index 0000000..42341a3 --- /dev/null +++ b/apps/ios/Branding/logo-official.svg @@ -0,0 +1,22 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/apps/ios/Branding/logo.svg b/apps/ios/Branding/logo.svg deleted file mode 100644 index 120ccdf..0000000 --- a/apps/ios/Branding/logo.svg +++ /dev/null @@ -1,17 +0,0 @@ - - Koins — Monero mark (monochrome) - - - - - - - - diff --git a/apps/ios/Branding/make-icons.swift b/apps/ios/Branding/make-icons.swift index 25be929..4634320 100644 --- a/apps/ios/Branding/make-icons.swift +++ b/apps/ios/Branding/make-icons.swift @@ -1,81 +1,134 @@ -// Regenerates the app icon PNGs from the same geometry as Branding/logo.svg. +// Regenerates the app icon PNGs from the official Monero mark. // // cd apps/ios // swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset // -// iOS cannot use SVG for app icons, so the SVG is the source artwork and this -// script produces the raster the asset catalog needs. It also writes -// Branding/logo-light.png (white ground, black mark). +// 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 -// Draws the same geometry as Branding/logo.svg with CoreGraphics, because iOS -// app icons must be raster images. -func drawIcon(size: CGFloat, inverted: Bool = false) -> CGImage? { - let scale = size / 1024.0 - let colorSpace = CGColorSpaceCreateDeviceRGB() - guard let context = CGContext(data: nil, - width: Int(size), - height: Int(size), - bitsPerComponent: 8, - bytesPerRow: 0, - space: colorSpace, - bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else { return nil } +struct Bitmap { + let width: Int + let height: Int + var pixels: [UInt8] // RGBA, premultiplied - let background = inverted ? NSColor.white : NSColor.black - let foreground = inverted ? NSColor.black : NSColor.white - context.setFillColor(background.cgColor) - context.fill(CGRect(x: 0, y: 0, width: size, height: size)) - - context.setStrokeColor(foreground.cgColor) - context.setLineWidth(118 * scale) - context.setLineCap(.butt) - context.setLineJoin(.miter) - - func point(_ x: CGFloat, _ y: CGFloat) -> CGPoint { - // SVG y grows downwards, CoreGraphics grows upwards. - CGPoint(x: x * scale, y: size - y * scale) + func luminance(_ offset: Int) -> Int { + (Int(pixels[offset]) * 299 + Int(pixels[offset + 1]) * 587 + Int(pixels[offset + 2]) * 114) / 1000 } - - // Left leg continuing into the central V (one path, so the top corner is a - // clean miter rather than two overlapping strokes). - context.beginPath() - context.move(to: point(279, 758)) - context.addLine(to: point(279, 280)) - context.addLine(to: point(512, 640)) - context.strokePath() - - // Right leg, mirrored. - context.beginPath() - context.move(to: point(745, 758)) - context.addLine(to: point(745, 280)) - context.addLine(to: point(512, 640)) - context.strokePath() - - // Full-width bar across the base. - context.beginPath() - context.move(to: point(220, 699)) - context.addLine(to: point(804, 699)) - context.strokePath() - - return context.makeImage() } -func write(_ image: CGImage, to path: String) { - let rep = NSBitmapImageRep(cgImage: image) - guard let data = rep.representation(using: .png, properties: [:]) else { return } +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 outputDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "/tmp" -try? FileManager.default.createDirectory(atPath: outputDir, withIntermediateDirectories: true) +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) -if let icon = drawIcon(size: 1024) { - write(icon, to: "\(outputDir)/AppIcon-1024.png") -} -// A light variant, handy for other surfaces (white background, black mark). -if let light = drawIcon(size: 1024, inverted: true) { - write(light, to: "\(outputDir)/AppIcon-light-1024.png") +guard let source = loadBitmap(sourcePath) else { + print("cannot read source artwork at \(sourcePath)") + exit(1) } + +let inset: CGFloat = 0.9 +// 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") diff --git a/apps/ios/Koins/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/apps/ios/Koins/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png index 0ce8d77..838b194 100644 Binary files a/apps/ios/Koins/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png and b/apps/ios/Koins/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png differ