Module: Everywhere::Icon

Defined in:
lib/everywhere/icon.rb

Overview

Turns one source PNG into platform-native app icons — no Tauri, no sips, no iconutil, no ImageMagick. Pure Ruby, so it runs the same locally and on the cloud build runners.

The important part is macОS shaping. macOS renders an .icns literally: it does not round the corners or add margins the way iOS does. A full-bleed square PNG therefore shows up as a hard square, out of place next to every native app. So we bake the platform's look into the artwork ourselves — inset the icon to the Big Sur grid (~824px inside a 1024px canvas) and clip it to a superellipse ("squircle") with transparent corners.

Windows (.ico) and Linux (hicolor PNGs) don't get the squircle — those desktops present square icons — so they're generated straight from the full-bleed source.

Constant Summary collapse

CANVAS =

Big Sur icon-grid geometry, all as fractions of the 1024px canvas so it scales to any master size.

1024
INSET =

icon body is 824/1024 of the canvas

0.8047
CORNER_RATIO =

corner radius as a fraction of the body's side

0.2237
EXPONENT =

superellipse power — the iOS/macOS "squircle" corner

5.0
ICNS_TYPES =

.icns type codes -> pixel size. Mirrors what iconutil emits from a standard .iconset (retina variants included), so the result is indistinguishable from an Apple-toolchain icns.

[
  ["icp4", 16], ["icp5", 32], ["ic07", 128], ["ic08", 256], ["ic09", 512],
  ["ic11", 32], ["ic12", 64], ["ic13", 256], ["ic14", 512], ["ic10", 1024]
].freeze
ICO_SIZES =
[16, 32, 48, 64, 128, 256].freeze
LINUX_SIZES =
[16, 32, 48, 64, 128, 256, 512].freeze
IOS_APPICON_CONTENTS =

Modern (Xcode 14+) single-size asset catalog entry: one 1024px universal image, iOS derives every size itself.

<<~JSON
  {
    "images" : [
      {
        "filename" : "AppIcon.png",
        "idiom" : "universal",
        "platform" : "ios",
        "size" : "1024x1024"
      }
    ],
    "info" : {
      "author" : "xcode",
      "version" : 1
    }
  }
JSON

Class Method Summary collapse

Class Method Details

.hex_rgb(hex) ⇒ Object

"#RRGGBB" (or "RRGGBBAA" — alpha ignored) -> [r, g, b] bytes, nil if unparseable. For resolving appearance colors into flatten backdrops.



165
166
167
168
# File 'lib/everywhere/icon.rb', line 165

def hex_rgb(hex)
  m = hex.to_s.strip.match(/\A#?(\h{2})(\h{2})(\h{2})/) or return nil
  m.captures.map { |c| c.to_i(16) }
end

.macos_master(source, dest, canvas: CANVAS) ⇒ Object

Write a macOS-shaped 1024px RGBA master PNG (padding + squircle) from a source PNG. Returns true, or false if the source PNG can't be read.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/everywhere/icon.rb', line 63

def macos_master(source, dest, canvas: CANVAS)
  raster = Raster.decode_png(source) or return false

  body = (canvas * INSET).round
  offset = (canvas - body) / 2

  # Fit the artwork inside the body square preserving aspect ratio (square
  # sources fill it exactly; non-square ones are centred, not stretched).
  fit = [body.to_f / raster.width, body.to_f / raster.height].min
  sw = (raster.width * fit).round.clamp(1, body)
  sh = (raster.height * fit).round.clamp(1, body)
  scaled = raster.resample(sw, sh)

  out = Raster.transparent(canvas, canvas)
  out.paste!(scaled, offset + (body - sw) / 2, offset + (body - sh) / 2)

  centre = canvas / 2.0
  half = body / 2.0
  radius = [body * CORNER_RATIO, half].min
  straight = half - radius # half-extent of the straight edges
  out.mask_region!(offset, offset, offset + body, offset + body) do |x, y|
    squircle_coverage(x, y, centre, half, straight, radius, EXPONENT)
  end

  out.write_png(dest)
  true
end

.sized_png(master, size) ⇒ Object

PNG bytes of master at sizexsize (no-op resample when already sized).



171
172
173
# File 'lib/everywhere/icon.rb', line 171

def sized_png(master, size)
  (size == master.width && size == master.height ? master : master.resample(size, size)).encode_png
end

.squircle_coverage(x, y, centre, half, straight, radius, exponent) ⇒ Object

Coverage (0.0..1.0) of pixel (x, y) by the squircle: a rounded rectangle with continuous (superelliptical) corners. Interior pixels return 1.0, exterior 0.0, and boundary pixels are antialiased by 4x4 supersampling.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/everywhere/icon.rb', line 178

def squircle_coverage(x, y, centre, half, straight, radius, exponent)
  c0 = squircle_inside?(x,     y,     centre, half, straight, radius, exponent)
  c1 = squircle_inside?(x + 1, y,     centre, half, straight, radius, exponent)
  c2 = squircle_inside?(x,     y + 1, centre, half, straight, radius, exponent)
  c3 = squircle_inside?(x + 1, y + 1, centre, half, straight, radius, exponent)
  return 1.0 if c0 && c1 && c2 && c3
  return 0.0 unless c0 || c1 || c2 || c3

  hit = 0
  4.times do |i|
    4.times do |j|
      hit += 1 if squircle_inside?(x + (i + 0.5) / 4.0, y + (j + 0.5) / 4.0,
                                   centre, half, straight, radius, exponent)
    end
  end
  hit / 16.0
end

.squircle_inside?(sx, sy, centre, half, straight, radius, exponent) ⇒ Boolean

Is the continuous point (sx, sy) inside the squircle? Straight edges up to straight from centre, superelliptical corners of radius beyond that.

Returns:

  • (Boolean)


198
199
200
201
202
203
204
205
206
207
# File 'lib/everywhere/icon.rb', line 198

def squircle_inside?(sx, sy, centre, half, straight, radius, exponent)
  u = (sx - centre).abs
  v = (sy - centre).abs
  return false if u > half || v > half
  return true if u <= straight || v <= straight

  du = (u - straight) / radius
  dv = (v - straight) / radius
  (du**exponent) + (dv**exponent) <= 1.0
end

.write_icns(master_png, dest) ⇒ Object

Build a macOS .icns from an already-shaped master PNG (ideally 1024px). Returns true on success.



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/everywhere/icon.rb', line 117

def write_icns(master_png, dest)
  master = Raster.decode_png(master_png) or return false

  body = ICNS_TYPES.map do |type, size|
    png = sized_png(master, size)
    type.b + [png.bytesize + 8].pack("N") + png
  end.join

  File.binwrite(dest, "icns".b + [body.bytesize + 8].pack("N") + body)
  true
end

.write_ico(source, dest, sizes: ICO_SIZES) ⇒ Object

Build a Windows .ico (PNG-compressed entries, Vista+) from a full-bleed source PNG. Returns true on success.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/everywhere/icon.rb', line 131

def write_ico(source, dest, sizes: ICO_SIZES)
  master = Raster.decode_png(source) or return false

  images = sizes.map { |s| [s, sized_png(master, s)] }
  offset = 6 + images.size * 16
  entries = +"".b
  blob = +"".b
  images.each do |size, png|
    dim = size >= 256 ? 0 : size # 0 means 256 in the ICONDIRENTRY
    entries << [dim, dim, 0, 0, 1, 32, png.bytesize, offset].pack("CCCCvvVV")
    blob << png
    offset += png.bytesize
  end

  header = [0, 1, images.size].pack("vvv") # reserved, type=icon, count
  File.binwrite(dest, header + entries + blob)
  true
end

.write_ios_appiconset(source, dest_dir, background: nil) ⇒ Object

Write an iOS AppIcon.appiconset (Contents.json + one 1024px PNG) from a full-bleed source PNG. Unlike macOS, iOS masks the icon itself, so the artwork ships as a plain square — no inset, no squircle. The App Store rejects alpha channels, so the source is flattened onto background ([r, g, b] bytes, default white). Returns true, false if unreadable.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/everywhere/icon.rb', line 96

def write_ios_appiconset(source, dest_dir, background: nil)
  raster = Raster.decode_png(source) or return false

  # Fit the artwork inside the canvas preserving aspect ratio (square
  # sources fill it exactly), then flatten onto the opaque backdrop.
  fit = [CANVAS.to_f / raster.width, CANVAS.to_f / raster.height].min
  sw = (raster.width * fit).round.clamp(1, CANVAS)
  sh = (raster.height * fit).round.clamp(1, CANVAS)

  canvas = Raster.transparent(CANVAS, CANVAS)
  canvas.paste!(raster.resample(sw, sh), (CANVAS - sw) / 2, (CANVAS - sh) / 2)
  out = canvas.flatten_onto(Array(background || [255, 255, 255])[0, 3])

  FileUtils.mkdir_p(dest_dir)
  out.write_png(File.join(dest_dir, "AppIcon.png"))
  File.write(File.join(dest_dir, "Contents.json"), IOS_APPICON_CONTENTS)
  true
end

.write_png_set(source, dest_dir, sizes: LINUX_SIZES) ⇒ Object

Write a set of square PNGs (Linux/desktop use) named "x.png" into dest_dir from a full-bleed source. Returns the written paths.



152
153
154
155
156
157
158
159
160
161
# File 'lib/everywhere/icon.rb', line 152

def write_png_set(source, dest_dir, sizes: LINUX_SIZES)
  master = Raster.decode_png(source) or return []

  FileUtils.mkdir_p(dest_dir)
  sizes.map do |size|
    path = File.join(dest_dir, "#{size}x#{size}.png")
    File.binwrite(path, sized_png(master, size))
    path
  end
end