Packages
mob_dev
0.3.3
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/icon_generator.ex
defmodule MobDev.IconGenerator do
# Image and Avatarz are optional deps — only needed for custom/random icon generation.
# When they're absent we fall back to the bundled pre-built mob_logo PNGs.
@compile {:no_warn_undefined, [Image, Avatarz, Avatarz.Sets.Robot]}
@moduledoc """
Generates app icons for Android and iOS from either a random robot avatar
(using Avatarz) or a provided source image (using Image).
When the `image` dep is not available, falls back to the bundled Mob logo
(pre-built PNGs shipped with mob_dev, no system tools required).
## Android sizes (mipmap buckets)
| Bucket | px |
|-------------|-----|
| mdpi | 48 |
| hdpi | 72 |
| xhdpi | 96 |
| xxhdpi | 144 |
| xxxhdpi | 192 |
## iOS sizes (AppIcon.appiconset subset)
| Usage | px |
|---------------------------|------|
| iPhone Notification 2x | 40 |
| iPhone Notification 3x | 60 |
| iPhone Settings 2x | 58 |
| iPhone Settings 3x | 87 |
| iPhone Spotlight 2x | 80 |
| iPhone Spotlight 3x | 120 |
| iPhone App 2x | 120 |
| iPhone App 3x | 180 |
| iPad Notification 1x | 20 |
| iPad Notification 2x | 40 |
| iPad Settings 1x | 29 |
| iPad Settings 2x | 58 |
| iPad Spotlight 1x | 40 |
| iPad Spotlight 2x | 80 |
| iPad App 1x | 76 |
| iPad App 2x | 152 |
| iPad Pro App 2x | 167 |
| App Store |1024 |
"""
@mob_logo_dir :code.priv_dir(:mob_dev) |> Path.join("mob_logo")
@android_sizes %{
"mipmap-mdpi" => 48,
"mipmap-hdpi" => 72,
"mipmap-xhdpi" => 96,
"mipmap-xxhdpi" => 144,
"mipmap-xxxhdpi" => 192
}
@ios_sizes [20, 29, 40, 58, 60, 76, 80, 87, 120, 152, 167, 180, 1024]
@doc """
Generates a random robot avatar and writes platform icons into `output_dir`.
Creates:
- `output_dir/android/app/src/main/res/<bucket>/ic_launcher.png` for each Android bucket
- `output_dir/ios/Assets.xcassets/AppIcon.appiconset/icon_<px>.png` for each iOS size
- `output_dir/icon_source.png` — the 1024×1024 master
Returns `:ok` on success.
"""
@spec generate_random(output_dir :: String.t()) :: :ok
def generate_random(output_dir) do
if image_available?() do
renders_path = Path.join(output_dir, ".icon_renders")
File.mkdir_p!(renders_path)
seed = Path.basename(output_dir)
avatar = Avatarz.render(seed, Avatarz.Sets.Robot, :robot, renders_path)
source_png = Path.join(output_dir, "icon_source.png")
Image.write!(avatar.image, source_png)
resize_for_platforms(source_png, output_dir)
else
Mix.shell().info("""
\nNote: the `image` dependency is not available so a random icon could not
be generated. Using the Mob logo as a placeholder instead.
Run `mix mob.icon` after adding `{:image, "~> 0.54"}` to your deps to
replace it with a custom or generated icon.\n
""")
use_mob_logo(output_dir)
end
end
@doc """
Copies the bundled Mob logo (pre-built PNGs) to all platform icon directories
in `output_dir`. Used as the default placeholder icon by `mix mob.install`.
No extra dependencies or system tools required.
Returns `:ok`.
"""
@spec use_mob_logo(output_dir :: String.t()) :: :ok
def use_mob_logo(output_dir) do
write_android_icons_from_priv(output_dir)
write_ios_icons_from_priv(output_dir)
:ok
end
@doc """
Resizes an existing image at `source_path` to all platform icon sizes,
writing them into `output_dir`.
Returns `:ok`.
"""
@spec generate_from_source(source_path :: String.t(), output_dir :: String.t()) :: :ok
def generate_from_source(source_path, output_dir) do
if image_available?() do
resize_for_platforms(source_path, output_dir)
else
Mix.raise("""
The `image` dependency is required to generate icons from a source file.
Add `{:image, "~> 0.54"}` to your deps and run `mix deps.get`.
""")
end
end
@doc """
Returns the map of Android mipmap bucket names to pixel dimensions.
"""
@spec android_sizes() :: %{String.t() => pos_integer()}
def android_sizes, do: @android_sizes
@doc """
Returns the list of iOS icon pixel dimensions.
"""
@spec ios_sizes() :: [pos_integer()]
def ios_sizes, do: @ios_sizes
# ── private ──────────────────────────────────────────────────────────────────
defp image_available? do
Code.ensure_loaded?(Image)
end
defp resize_for_platforms(source_png, output_dir) do
source = Image.open!(source_png)
write_android_icons(source, output_dir)
write_ios_icons(source, output_dir)
:ok
end
defp write_android_icons(source, output_dir) do
Enum.each(@android_sizes, fn {bucket, px} ->
dest_dir = Path.join([output_dir, "android", "app", "src", "main", "res", bucket])
File.mkdir_p!(dest_dir)
dest = Path.join(dest_dir, "ic_launcher.png")
source |> Image.thumbnail!(px) |> Image.write!(dest)
end)
end
defp write_android_icons_from_priv(output_dir) do
Enum.each(@android_sizes, fn {bucket, px} ->
dest_dir = Path.join([output_dir, "android", "app", "src", "main", "res", bucket])
File.mkdir_p!(dest_dir)
File.cp!(Path.join(@mob_logo_dir, "#{px}.png"), Path.join(dest_dir, "ic_launcher.png"))
end)
end
defp write_ios_icons(source, output_dir) do
dest_dir = Path.join([output_dir, "ios", "Assets.xcassets", "AppIcon.appiconset"])
File.mkdir_p!(dest_dir)
Enum.each(@ios_sizes, fn px ->
dest = Path.join(dest_dir, "icon_#{px}.png")
source |> Image.thumbnail!(px) |> Image.write!(dest)
end)
write_ios_contents_json(dest_dir)
end
defp write_ios_icons_from_priv(output_dir) do
dest_dir = Path.join([output_dir, "ios", "Assets.xcassets", "AppIcon.appiconset"])
File.mkdir_p!(dest_dir)
Enum.each(@ios_sizes, fn px ->
File.cp!(Path.join(@mob_logo_dir, "#{px}.png"), Path.join(dest_dir, "icon_#{px}.png"))
end)
write_ios_contents_json(dest_dir)
end
defp write_ios_contents_json(dest_dir) do
images =
[
%{idiom: "iphone", scale: "2x", size: "20x20", px: 40},
%{idiom: "iphone", scale: "3x", size: "20x20", px: 60},
%{idiom: "iphone", scale: "2x", size: "29x29", px: 58},
%{idiom: "iphone", scale: "3x", size: "29x29", px: 87},
%{idiom: "iphone", scale: "2x", size: "40x40", px: 80},
%{idiom: "iphone", scale: "3x", size: "40x40", px: 120},
%{idiom: "iphone", scale: "2x", size: "60x60", px: 120},
%{idiom: "iphone", scale: "3x", size: "60x60", px: 180},
%{idiom: "ipad", scale: "1x", size: "20x20", px: 20},
%{idiom: "ipad", scale: "2x", size: "20x20", px: 40},
%{idiom: "ipad", scale: "1x", size: "29x29", px: 29},
%{idiom: "ipad", scale: "2x", size: "29x29", px: 58},
%{idiom: "ipad", scale: "1x", size: "40x40", px: 40},
%{idiom: "ipad", scale: "2x", size: "40x40", px: 80},
%{idiom: "ipad", scale: "1x", size: "76x76", px: 76},
%{idiom: "ipad", scale: "2x", size: "76x76", px: 152},
%{idiom: "ipad", scale: "2x", size: "83.5x83.5", px: 167},
%{idiom: "ios-marketing", scale: "1x", size: "1024x1024", px: 1024}
]
|> Enum.map(fn %{idiom: idiom, scale: scale, size: size, px: px} ->
%{
"filename" => "icon_#{px}.png",
"idiom" => idiom,
"scale" => scale,
"size" => size
}
end)
contents = %{"images" => images, "info" => %{"author" => "mob", "version" => 1}}
json_path = Path.join(dest_dir, "Contents.json")
File.write!(json_path, Jason.encode!(contents, pretty: true))
end
end