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 @@
+
+
+
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 @@
-
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