Image to Icon Tool Development Practice - From Requirements Analysis to Code Implementation

Image to Icon Tool Development Practice - From Requirements Analysis to Code Implementation

This article introduces how to use C# and Avalonia to develop an image to icon tool, including requirements analysis, core code implementation, UI design, and application of MVVM pattern.

Last updated 3/10/2025 6:14 AM
沙漠尽头的狼
11 min read
Category
.NET
Tags
.NET C# Avalonia UI MVVM UI Design

1. Requirements Analysis and Solution Design

In development work, we often need to convert images into Icon files of different sizes. Whether it's creating a favicon.ico for a website or designing icons for an application, this is a common requirement. Although there are many image-to-icon tools available on the market, they often suffer from issues like limited functionality, excessive ads, or complex operations.

This article will introduce how to develop a simple and efficient image-to-icon tool using C# and Avalonia, implementing the following features:

  1. Support converting common image formats (such as PNG, JPG, etc.) to ICO format
  2. Support generating icons of multiple sizes (16x16, 32x32, 48x48, 64x64, 128x128, 256x256)
  3. Provide two conversion modes:
    • Merge mode: Combine icons of multiple sizes into a single ICO file
    • Separate mode: Generate individual ICO files for each size
  4. Support drag-and-drop operations to enhance user experience

2. Core Conversion Code

First, let's look at the core image-to-icon conversion logic. This part of the code is encapsulated in the ImageHelper class:

using ImageMagick;
using System.IO;
using System.Threading.Tasks;

// ReSharper disable once CheckNamespace
namespace CodeWF.Tools;

public static class ImageHelper
{
    public static async Task MergeGenerateIcon(string sourceImagePath, string destIconPath, uint[] sizes)
    {
        var baseImage = new MagickImage(sourceImagePath);
        var collection = new MagickImageCollection();

        foreach (var size in sizes)
        {
            var resizedImage = baseImage.Clone();
            resizedImage.Resize(size, size);
            collection.Add(resizedImage);
        }

        await collection.WriteAsync(destIconPath);
    }

    public static async Task SeparateGenerateIcon(string sourceImagePath, string destIconFolder, uint[] sizes)
    {
        var fileName = Path.GetFileNameWithoutExtension(sourceImagePath);

        var baseImage = new MagickImage(sourceImagePath);

        foreach (var size in sizes)
        {
            var resizedImage = baseImage.Clone();
            resizedImage.Resize(size, size);

            var savePath = Path.Combine(destIconFolder, $"{fileName}-{size}x{size}.ico");
            await resizedImage.WriteAsync(savePath);
        }
    }
}

The code above uses the NuGet package Magick.NET-Q16-AnyCPU. Magick.NET is a .NET wrapper library for ImageMagick, providing powerful image processing capabilities. Q16 indicates 16-bit quantization during image processing, and AnyCPU means support for multiple processor architectures. With this library, we can easily resize images and save them in ICO format.

The core code provides two main methods:

  • MergeGenerateIcon: Converts a single source image into one ICO file containing multiple sizes
  • SeparateGenerateIcon: Converts a single source image into multiple ICO files of different sizes

3. User Interface Design

3.1 Basic Interface Layout

The user interface is designed using the Avalonia framework and is defined in the ImageToIconView.axaml file:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:u="https://irihi.tech/ursa"
             xmlns:i18n="https://codewf.com"
             xmlns:vm="clr-namespace:CodeWF.Modules.Converter.ViewModels"
             xmlns:language="clr-namespace:Localization"
             xmlns:local="clr-namespace:CodeWF.Modules.Converter.Models"
             prism:ViewModelLocator.AutoWireViewModel="True"
             x:DataType="vm:ImageToIconViewModel"
             x:CompileBindings="True"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="CodeWF.Modules.Converter.ImageToIconView">
    <StackPanel>
        <TextBlock Text="{i18n:I18n {x:Static language:ImageToIconView.ChoiceSourceImageDescription}}" />
        <StackPanel Orientation="Horizontal" Margin="0 10">
            <TextBox VerticalAlignment="Center" Margin="10 0" Width="400" Classes="Small"
                     Text="{Binding NeedConvertImagePath}"
                     DragDrop.AllowDrop="True" DragDrop.Drop="RaiseDropSourceImagePath"/>
            <Button Content="..." Classes="Small" Command="{Binding RaiseChoiceNeedConvertImageHandler}" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{i18n:I18n {x:Static language:ImageToIconView.DestImageSize}}" />

            <ItemsControl ItemsSource="{Binding IconSizes}" Margin="0 10">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Content}"
                                  VerticalAlignment="Center" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
            <Button Margin="10" Classes="Small"
                    Content="{i18n:I18n {x:Static language:ImageToIconView.MergeGenerateButtonContent}}"
                    Command="{Binding RaiseMergeGenerateIconHandler}" />
            <Button Classes="Small"
                    Content="{i18n:I18n {x:Static language:ImageToIconView.SeparateGenerateButtonContent}}"
                    Command="{Binding RaiseSeparateGenerateIconHandler}" />
        </StackPanel>

        <TextBlock Margin="0 40 0 0" Classes="H4" Theme="{StaticResource TitleTextBlock}"
                   Text="{i18n:I18n {x:Static language:ImageToIconView.MemoTitle}}" />
        <TextBlock Margin="0 5 0 3" Text="{i18n:I18n {x:Static language:ImageToIconView.MemoContent1}}" />
        <TextBlock Text="{i18n:I18n {x:Static language:ImageToIconView.MemoContent2}}" />
        <Border Margin="0,16" Classes="CodeBlock">
            <SelectableTextBlock FontFamily="Consolas"
                                 Text="&lt;link rel=&quot;shortcut icon&quot; href=&quot;/favicon.ico&quot; type=&quot;image/x-icon&quot; /&gt;" />
        </Border>
    </StackPanel>
</UserControl>

To briefly describe the code above, our interface mainly includes the following parts:

  1. Source image selection area (supports text input and file selection)
  2. Target icon size selection area (via checkboxes)
  3. Two action buttons (merge generation and separate generation)
  4. Notes area (provides usage instructions and HTML reference example)

The implementation effect is as follows:

Interface Screenshot

3.2 Drag-and-Drop Functionality

To enhance user experience, we support two ways to select a source image:

  1. Click the "..." button to choose from a file picker
  2. Directly drag and drop an image file onto the input box

The drag-and-drop handling is implemented in ImageToIconView.axaml.cs:

using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using CodeWF.Modules.Converter.ViewModels;

namespace CodeWF.Modules.Converter;

public partial class ImageToIconView : UserControl
{
    public ImageToIconView()
    {
        InitializeComponent();
    }

    public void RaiseDropSourceImagePath(object? sender, DragEventArgs e)
    {
        if (this.DataContext is not ImageToIconViewModel vm)
        {
            return;
        }

        var files = e.Data.GetFiles();
        var file = files?.FirstOrDefault();
        if (file == null)
        {
            return;
        }

        vm.NeedConvertImagePath = file.TryGetLocalPath();
        e.Handled = true;
    }
}

With the above code, when a file is dragged onto the text box, the file path is automatically obtained:

Drag-and-Drop Demo

4. ViewModel Implementation

The business logic is implemented in ImageToIconViewModel.cs:

using Avalonia.Platform.Storage;
using AvaloniaXmlTranslator;
using CodeWF.Core.IServices;
using CodeWF.Modules.Converter.Models;
using CodeWF.Tools;
using CodeWF.Tools.FileExtensions;
using ReactiveUI;
using System.Collections.ObjectModel;
using Ursa.Controls;

namespace CodeWF.Modules.Converter.ViewModels;

public class ImageToIconViewModel : ReactiveObject
{
    private readonly IFileChooserService _fileChooserService;
    private readonly INotificationService _notificationService;

    private readonly FilePickerFileType _icoFilePickerFileType =
        new("Icon file") { Patterns = ["*.ico"] };

    public ImageToIconViewModel(IFileChooserService fileChooserService, INotificationService notificationService)
    {
        _fileChooserService = fileChooserService;
        _notificationService = notificationService;
        IconSizes.AddRange(Enum.GetValues<IconSize>()
            .Select(size => new IconSizeItem(size)));
    }

    #region Properties

    public ObservableCollection<IconSizeItem> IconSizes { get; } = new();

    private string? _needConvertImagePath;

    public string? NeedConvertImagePath
    {
        get => _needConvertImagePath;
        set => this.RaiseAndSetIfChanged(ref _needConvertImagePath, value);
    }

    #endregion

    #region Command's handler

    public async Task RaiseChoiceNeedConvertImageHandler()
    {
        var files = await _fileChooserService.OpenFileAsync(
            I18nManager.Instance.GetResource(Localization.ImageToIconView.ChoiceSourceImageDescription)!,
            true,
            [FilePickerFileTypes.All]);
        if (!(files?.Count > 0))
        {
            return;
        }

        NeedConvertImagePath = files[0];
    }

    public async Task RaiseMergeGenerateIconHandler()
    {
        (bool isSuccess, uint[]? sizes) = await GetGenerateInfo();
        if (!isSuccess)
        {
            return;
        }

        var folder = Path.GetDirectoryName(NeedConvertImagePath);
        var fileName = Path.GetFileNameWithoutExtension(NeedConvertImagePath);
        var saveIconPath = Path.Combine(folder, $"{fileName}.ico");
        try
        {
            await ImageHelper.MergeGenerateIcon(NeedConvertImagePath, saveIconPath, sizes);
        }
        catch (Exception ex)
        {
            await MessageBox.ShowOverlayAsync(ex.Message);
        }

        FileHelper.OpenFolderAndSelectFile(saveIconPath);
    }

    public async Task RaiseSeparateGenerateIconHandler()
    {
        (bool isSuccess, uint[]? sizes) = await GetGenerateInfo();
        if (!isSuccess)
        {
            return;
        }

        var saveIconFolder = Path.GetDirectoryName(NeedConvertImagePath);
        try
        {
            await ImageHelper.SeparateGenerateIcon(NeedConvertImagePath, saveIconFolder, sizes);
        }
        catch (Exception ex)
        {
            await MessageBox.ShowOverlayAsync(ex.Message);
        }

        FileHelper.OpenFolder(saveIconFolder);
    }

    private async Task<(bool IsSuccess, uint[]? Sizes)> GetGenerateInfo()
    {
        if (string.IsNullOrWhiteSpace(NeedConvertImagePath)
            || !File.Exists(NeedConvertImagePath))
        {
            await MessageBox.ShowOverlayAsync(
                I18nManager.Instance.GetResource(Localization.ImageToIconView.ChoiceSourceImageDialogTitle)!);
            return (false, null);
        }

        var selectedSize = IconSizes.Where(item => item.IsSelected).ToList();
        if (selectedSize.Count <= 0)
        {
            await MessageBox.ShowOverlayAsync(
                I18nManager.Instance.GetResource(Localization.ImageToIconView.DestImageSize)!);
            return (false, null);
        }

        var destSizes = selectedSize.Select(size => (uint)(size.Size)).ToArray();

        return (true, destSizes);
    }

    #endregion
}

The ViewModel follows the MVVM design pattern and is mainly responsible for:

  1. Managing UI data and state
  2. Handling user operations (selecting files, executing conversions, etc.)
  3. Validating input data
  4. Calling core business logic
  5. Handling exceptional cases

The effects of the two conversion modes are as follows:

Multi-size Merge Conversion

Convert to Multiple Sizes

5. Data Model Design

To manage icon size options, we define the following data model:

using CodeWF.Tools.Extensions;
using ReactiveUI;
using System.ComponentModel;

namespace CodeWF.Modules.Converter.Models;

public enum IconSize
{
    [Description("16x16")] Size16 = 16,
    [Description("24x24")] Size24 = 24,
    [Description("32x32")] Size32 = 32,
    [Description("48x48")] Size48 = 48,
    [Description("64x64")] Size64 = 64,
    [Description("128x128")] Size128 = 128,
    [Description("256x256")] Size256 = 256
}

public class IconSizeItem(IconSize size) : ReactiveObject
{
    private bool _isSelected = true;

    public bool IsSelected
    {
        get => _isSelected;
        set => this.RaiseAndSetIfChanged(ref _isSelected, value);
    }

    public string Content { get; set; } = size.GetDescription();
    public IconSize Size { get; set; } = size;
}

6. Online Icon Conversion Tool

In addition to the desktop application version, I have also developed a Blazor-based online icon conversion tool, allowing users to convert images to icons without installing any software.

6.1 Features of the Online Converter

Online access address: https://dotnet9.com/tool/ico

Compared to the desktop version, the online version has the following features:

  1. No installation required: Use it directly through a browser
  2. Cross-platform compatibility: Supports any modern browser, including mobile devices
  3. Temporary file storage: Converted files are temporarily saved on the server; users need to download them promptly
  4. Simplified interface: Optimized for web usage, with more straightforward operations

Online Converter Interface

6.2 Conversion Workflow

The workflow of the online conversion tool is simple and intuitive:

  1. Select an image file (supports PNG, JPG, JPEG, WEBP formats)
  2. Choose the desired icon sizes
  3. Select the conversion mode (merge generation or separate generation)
  4. Click the button, and the system uploads the image to the server for conversion
  5. After conversion completes, click the "Download" button to get the generated file

The online version also uses Magick.NET for image processing, with the same core conversion logic as the desktop version, but with added functionality for file upload, temporary storage, and cleanup. Readers interested in the specific implementation details can directly view the source code:

7. Summary and Application Scenarios

Through this article, we have implemented both desktop and online versions of an image-to-icon tool, meeting the needs of different users. They have the following features:

  1. Simple user interface: Intuitive operations with drag-and-drop support
  2. Rich conversion options: Supports multiple sizes to meet different application requirements
  3. Flexible conversion modes: Can generate a single multi-size ICO file or multiple single-size ICO files
  4. Good code structure: Adopts MVVM design pattern, with clear code that is easy to maintain and extend

This tool can be applied in the following scenarios:

  • Generating favicon.ico for website development
  • Creating application icons for application development
  • Designers quickly generating icon files of different sizes

Additionally, this project demonstrates how to use the powerful image processing library Magick.NET in C# applications, and how to build cross-platform desktop applications with Avalonia. These knowledge points can be applied to other similar development projects.

I hope this article is helpful to you. If you have any questions, please leave a comment to discuss!

Source Code References

Keep Exploring

Related Reading

More Articles
Same category / Same tag 3/10/2025

Parking Move QR Code Generation Tool Development Practice

This article introduces how to develop a parking move QR code generation tool, including a desktop version implemented with C# and Avalonia, as well as an online version implemented with Blazor frontend and .NET Web API, covering requirements analysis, core code implementation, UI design, and application of MVVM pattern.

Continue Reading
Same category / Same tag 2/25/2025

.NET 10 Preview 1 Released

Today .NET 10 Preview 1 was released. I downloaded it immediately, upgraded the Avalonia UI project and blog website. The former passed functional testing and AOT publishing successfully, the latter debugging went fine, but Docker has not been successful yet.

Continue Reading