Rather than using some online Web services, use PowerShell scripts and Inkscape to generate icons of native mobile apps. And Continuous Integration could be smoother.
Introduction
This article is for developers who already have some experience in developing Android apps and iOS apps using Xamarin. While there are quite a few icon libraries around, free or commercial, sometimes, you may want to generate icon sets from an existing image file, ideally, a SVG file.
This article is to share my experience in generating icons of various sizes from SVG files when developing native mobile apps. While the primary developing tool that I had used was Xamarin, the principles and the tools mentioned here should be applicable even if you have been using other tools like XCode or Android Studio.
XCode 14+ supports single-size app icons. And Android Studio includes Image Asset Studio that generates app icons from material icons and custom images. However, if you prefer not to use such interactive way for the sake of Continuous Integration, or you are using Xamarin, please read on.
Background
A few years ago, I developed Visual Acuity Charts which tests distant visual acuity for checking early sign of myopia. There are some freely available SVG icons and I used some online transformation tools to produce icon sets needed. And I had been using "Material icons generator" created by Nika Nikabadze for Visual Sutido, and https://appicon.co/.
Android Icon Resources in an Android library project of Microsoft Visual Studio
iOS Icon Assets
However, the process of using these VS extension or online tools is rather troublesome:
- With the VS extension, too many user interactions troubled me.
- With the online tools: Upload SVG file -> Configuration -> Generate -> Wait -> Download zip -> Unzip to the project folders. And overall, such process is not friendly to Continuous Integration, thus I had written a series of PowerShell scripts to generate icon sets.
Overall, those two tools are fairly decent, however, if you feel the troubles that I had felt, you may try the approach introduced in this article.
Prerequisites
SVG Icon Libraries
There are many more out there, free or commercial.
Remarks
- Make sure the usage of the free icons conform to the license such as SIL Open Font License (OFL) that Google Material Icons library use.
- Google Material Icons site has provided downloads of image sets for Android and iOS in ZIPs.
"Inkscape is a Free and open source vector graphics editor for GNU/Linux, Windows and macOS."
As a software developer, I do casual and simple graphic design occasionally and I don't intend to buy an all-in-one, powerful and complex graphic design tool typically used by professional graphic designer. Inkscape satisfies what I need.
Composing Simple Icons
For example, rather than drawing everything from scratch, I used existing SVG files to compose the app launch icon.
The App Launcher Icon
The Visibility icon from Google Material Icons
Nevertheless, Inscape provides rich features for you to draw complex icons or images from scratch.
Adjust Existing Icons to Conform to Design Guidelines from Google and Apple
From time to time, you may find some candidates not so conforming to specific design guidelines and want to adjust the size and the margin.
References
Using the Code
The following PowerShell scripts utilize the command line ability of Inkscape, and the drafting of the scripts had referred to the icon design guidelines above.
Android
The following script generates icons for buttons and the app icon. Please read the in-source comment.
svgForAndroid.ps1
param([string]$svgFile, [int]$width, [int]$height, [boolean]$forAppIcon,
[string]$appIconName)
cd $PSScriptRoot
$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
function export([string]$subDir, [decimal]$ratio){
$dir= [System.IO.Path]::Combine($baseFileName,$subDir)
$exportedFileName=If ($forAppIcon) {If ($appIconName) {$appIconName+".png"}
Else {"5367533/ic_launcher.png"}} Else {$baseFileName+"_"+ $width + ".png"}
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$dwidth=[int]($width * $ratio)
$dheight=0
if ($height -gt 0){$dheight = $height * $ratio} Else {$dheight = $dwidth}
$arguments="$svgFile --export-filename=$exported -w $dwidth -h $dheight -d 96"
New-Item -ItemType Directory -Force -Path $dir
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
}
If ($forAppIcon){
export "mipmap-mdpi" 1
export "mipmap-hdpi" 1.5
export "mipmap-xhdpi" 2
export "mipmap-xxhdpi" 3
export "mipmap-xxxhdpi" 4
} Else {
export "drawable-mdpi" 1
export "drawable-hdpi" 1.5
export "drawable-xhdpi" 2
export "drawable-xxhdpi" 3
export "drawable-xxxhdpi" 4
}
iOS
Image Set
The following scripts generate three png files and Contents.json.
svgForIOSImageset.ps1
param([string]$svgFile, [Int32]$width, [Int32]$height, [boolean]$original)
cd $PSScriptRoot
$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
$dir= $baseFileName + "_" + $width + ".imageset"
New-Item -ItemType Directory -Force -Path $dir
function export([decimal]$ratio){
$exportedFileName=$baseFileName + "_" + $width +"pt_" + $ratio + "x.png"
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$dwidth=[int]($width * $ratio)
$dheight=0
if ($height -gt 0){$dheight = $height * $ratio} Else {$dheight = $dwidth}
$arguments="$svgFile --export-filename=$exported -w $dwidth -h $dheight -d 96"
Write-Host $arguments
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
return $exportedFileName
}
$f1=export 1
$f2=export 2
$f3=export 3
$intent=If ($original) {"original"} Else {"template"}
$contentsTemplate=
@"
{
"images": [
{
"filename": "$f1",
"idiom": "universal",
"scale": "1x"
},
{
"filename": "$f2",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "$f3",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "whocare",
"template-rendering-intent": "$intent",
"version": 1
}
}
"@
$jsonFile=[System.IO.Path]::Combine($dir, "Contents.json")
Set-Content $jsonFile $contentsTemplate
After running the scripts, you get these files in a folder like myicon.imageset:
App Icon
After running the scripts, you get 26 files including Contents.json in a folder like myAppIcon.appiconset:
svgForIOSAppIconSet.ps1
param([string]$svgFile)
cd $PSScriptRoot
$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
$dir= $baseFileName + ".appiconset"
New-Item -ItemType Directory -Force -Path $dir
function export([decimal]$size){
$exportedFileName=$size.ToString() + ".png"
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$arguments="$svgFile --export-filename $exported -w $size -h $size"
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
return $exportedFileName
}
function exportForWatch(){
$exportedFileName="watch.png"
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$arguments="$svgFile --export-filename $exported -w 55 -h 55"
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
return $exportedFileName
}
export 20
export 29
export 32
export 40
export 50
export 57
export 58
export 60
export 64
export 72
export 76
export 80
export 87
export 100
export 114
export 120
export 128
export 144
export 152
export 167
export 180
export 256
export 512
export 1024
exportForWatch
$contentsTemplate=
@"
{
"images": [
{
"size": "60x60",
"expected-size": "180",
"filename": "180.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "40x40",
"expected-size": "80",
"filename": "80.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "40x40",
"expected-size": "120",
"filename": "120.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "60x60",
"expected-size": "120",
"filename": "120.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "57x57",
"expected-size": "57",
"filename": "57.png",
"idiom": "iphone",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "29x29",
"expected-size": "29",
"filename": "29.png",
"idiom": "iphone",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "87",
"filename": "87.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "57x57",
"expected-size": "114",
"filename": "114.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "40",
"filename": "40.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "60",
"filename": "60.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "1024x1024",
"filename": "1024.png",
"expected-size": "1024",
"idiom": "ios-marketing",
"scale": "1x"
},
{
"size": "40x40",
"expected-size": "80",
"filename": "80.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "72x72",
"expected-size": "72",
"filename": "72.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "76x76",
"expected-size": "152",
"filename": "152.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "50x50",
"expected-size": "100",
"filename": "100.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "76x76",
"expected-size": "76",
"filename": "76.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "29",
"filename": "29.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "50x50",
"expected-size": "50",
"filename": "50.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "72x72",
"expected-size": "144",
"filename": "144.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "40x40",
"expected-size": "40",
"filename": "40.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "83.5x83.5",
"expected-size": "167",
"filename": "167.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "20",
"filename": "20.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "20x20",
"expected-size": "40",
"filename": "40.png",
"idiom": "ipad",
"scale": "2x"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "38mm",
"scale": "2x",
"size": "86x86",
"expected-size": "172",
"role": "quickLook"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "38mm",
"scale": "2x",
"size": "40x40",
"expected-size": "80",
"role": "appLauncher"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "42mm",
"scale": "2x",
"size": "98x98",
"expected-size": "196",
"role": "quickLook"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "38mm",
"scale": "2x",
"size": "24x24",
"expected-size": "48",
"role": "notificationCenter"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "42mm",
"scale": "2x",
"size": "27.5x27.5",
"expected-size": "55",
"role": "notificationCenter"
},
{
"size": "29x29",
"expected-size": "87",
"filename": "87.png",
"idiom": "watch",
"role": "companionSettings",
"scale": "3x"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "42mm",
"scale": "2x",
"size": "44x44",
"expected-size": "88",
"role": "longLook"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"idiom": "watch",
"role": "companionSettings",
"scale": "2x"
},
{
"size": "1024x1024",
"expected-size": "1024",
"filename": "1024.png",
"idiom": "watch-marketing",
"scale": "1x"
},
{
"size": "128x128",
"expected-size": "128",
"filename": "128.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "256x256",
"expected-size": "256",
"filename": "256.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "128x128",
"expected-size": "256",
"filename": "256.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "256x256",
"expected-size": "512",
"filename": "512.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "32x32",
"expected-size": "32",
"filename": "32.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "512x512",
"expected-size": "512",
"filename": "512.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "16x16",
"expected-size": "32",
"filename": "32.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "32x32",
"expected-size": "64",
"filename": "64.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "512x512",
"expected-size": "1024",
"filename": "1024.png",
"idiom": "mac",
"scale": "2x"
}
]
}
"@
$jsonFile=[System.IO.Path]::Combine($dir, "Contents.json")
Set-Content $jsonFile $contentsTemplate
Points of Interest
You may adjust the PowerShell scripts provided in this article according to your SDLC / CI process.
My experience in developing native mobile apps is limited to smartphone and tablet. Therefore, if you are developing native apps for smartwatches or smart TV, you may need to add a few more lines according to respective icon design specifications.
MAUI
For developing a native app for both Android and iOS, you need to create at least two platform specific app projects, one for Android and the other for iOS, while shared library codes are contained in Xamarin.Forms projects and .NET standard projects.
With MAUI, you just need one app project for Android, iOS, Windows and Mac, etc. And the icon sources can be vector images (SVG files). Apparently, MAUI will read the SVG files and generate PNG files according to the requirements of respective platforms, just as the script files in the article are doing. This sounds natural, promising and productive.
A few years ago, I had rewritten "Tour of Heroes" (the official tutorial app of Angular) for Xamarin, to demonstrate how to use WebApiClientGen in a real world project. In October 2023, I had migrated the app to MAUI. The deployable for Android built by Xamarin is around 22MB, however, the one by MAUI is around 33MB, while both are release build.
Such inflation in size is clearly not desirable. I will be interested in checking if Microsoft could fix this problem after .NET Conf 2023 and before the scheduled retirement of Xamarin in April 2024.
History
- 31st August, 2023: Initial version