SharpIco: Use pure C#to create a zero-dependency.ico icon generator, support. NET9 and AOT compilation

SharpIco: Use pure C#to create a zero-dependency.ico icon generator, support. NET9 and AOT compilation

Many of the ICO production tools I found online were targeted at favicon. Others were either not too heavy or charged, so I turned my attention back to open source tools.

最后更新 5/27/2025 8:27 PM
程序设计实验室
预计阅读 21 分钟
分类
.NET
专题
C# AOT
标签
.NET C# open source AOT ICO

preface

最近一直在完善我今年的两款桌面软件:视频剪辑工具 ClipifyAI 文章创作工具 StarBlogPublisher

Although the interface is basically complete, the icons are still default, which seems very unprofessional.

So I plan to change these two apps with a nice icon

Back in the VB6.0 era, I used an open source ICO icon making tool, but now I can't find it

Many ICO production tools found online are targeted at favicon

Others were either not too heavy or charged, so I turned my attention back to open source tools

I found a zero-dependency image library based on nodejs implementation (most of them rely on Magick, a C++ implementation). However, unfortunately, this tool cannot be used on my computer...

Target

At this time, I remembered that I had developed an image format conversion tool using c#before. Using the AOT function of. Net8, I could develop cross-platform single executable files like the go language.

So I decided to continue using C#to develop an icon generation tool that can achieve:

  • Pure C#implementation, no external dependencies, cross-platform, single executable file, AOT
  • Decompose the png picture into multiple small pictures of different size (side lengths 16, 32, 48, 64, 128, 256, 512), and then synthesize an ICO icon to achieve a good visual experience on screens of different size sizes
  • Support the Inspect function to read and analyze ICO icons
  • Convenient distribution method, supporting one-click installation of dotnet tool, scoop, brew and other tools

It has been completed and has also been released to nuget and scoop. Next, we will study how to publish it to brew

Project home page: www.example.com

achieve

在 SharpIco 中,.ico 文件的生成完全不依赖 ImageMagick 或任何图像处理外部工具,而是通过纯 C# 代码手工拼接符合规范的 ICO 二进制结构。

这部分的核心类是 IcoGenerator,具体代码我就不贴了,在项目里有,挑几个要点介绍吧~

Generating multiple size images

Generating multiple size images using ImageSharp

var clone = original.Clone(ctx => ctx.Resize(size, size));
clone.SaveAsPng(ms);
      • Use ImageSharp's Resize and Clone ** functions to generate raw high-resolution PNG into multiple target size (such as 16x16, 32x32, 256x256)
  • PNG 格式保存到内存流中,用于后续写入 .ico

💡 ICO files support embedding PNG images (starting with Vista) to maintain a smaller size and better transparency.

Manually build ICO file headers

Manually build ICONDIR and ICONDIRERARY according to ICO file format

The header of the ICO file consists of three parts:

  • ICONDIR(6 字节):固定结构
  • ICONDIRENTRY × N(每个 16 字节):描述每一张嵌入图像的尺寸、偏移
  • Image Data × N:实际图像二进制数据
writer.Write((ushort)0); // Reserved
writer.Write((ushort)1); // Type = icon
writer.Write((ushort)images.Count); // Image count
  • First write the ICO file header ICONDIR
  • Then loop the descriptive information of each picture (width and height, bit depth, data offset, etc.)
writer.Write((byte)(img.Width == 256 ? 0 : img.Width)); // 256 用 0 表示
writer.Write((ushort)32); // bits per pixel
writer.Write(image.Length);
writer.Write(offset);
    • If the width/height field in the ICO file is 0, it means 256 **. This is a special provision of the ICO format.

The ICO file format has a limitation when expressing image size: the width and height fields each have only one byte, and the value range is 0 - 255. When these fields are 0, 256 pixels are represented as per the specification. For size larger than 256 (such as 512 × 512 or 1024 × 1024), 0 (i.e. 256) will still be displayed in the file header, but the actual image data can contain images of a larger size.

stitched image

Stitching all PNG image data

foreach (var image in images) {
    writer.Write(image);
}
  • After all description information is written, it is written into the image data ontology
  • Because the offset is calculated in advance, it is ensured that each image data can be correctly recognized and read by the system

extended

Support custom size

public static void GenerateIcon(string sourcePng, string outputIco, int[] sizes)
  • Common size from 16 to 512 is supported by default
  • The size combination can be flexibly specified by passing parameters (for example, only 32/256 is packed)

Inspect function

除了图标生成,SharpIco 还内置了一个图标内容分析工具 IcoInspector,可以帮助开发者深入理解 .ico 文件内部结构,并验证实际包含的图层图像尺寸与位深信息,解决市面上不少图标工具生成不规范 .ico 文件的问题。

Here is a brief introduction to the implementation ideas~

Read ICO file header

Manually read the ICONDIR + ICONDIRERARY structure

The header structure of an ICO file consists of three parts:

  • ICONDIR(6 字节):标记为图标、记录图像数量
  • ICONDIRENTRY × N(每个 16 字节):记录每张图片的元信息(宽、高、位深、数据偏移等)
  • Image data block: actual PNG or BMP image
ushort reserved = reader.ReadUInt16(); // 必须为0
ushort type = reader.ReadUInt16();     // 1表示图标
ushort count = reader.ReadUInt16();    // 图像数量

The image entries are then read one by one and saved to memory for subsequent processing.

byte width = reader.ReadByte();
byte height = reader.ReadByte();
ushort bitCount = reader.ReadUInt16();
int sizeInBytes = reader.ReadInt32();
int imageOffset = reader.ReadInt32();

对了,前面介绍过,ICO 头部的空间有限,只能存 8 位,所以如果 width/height 字段为 0,根据规范表示 256,或者是超过 256

Extract and parse image data

Extract image data and verify true resolution

ICO 文件中记录的宽高未必真实,尤其是嵌入 PNG 格式的情况。因此,使用 ImageSharp 对每张图像进行真正解析。

fs.Seek(entry.ImageOffset, SeekOrigin.Begin);
fs.Read(imageData, 0, dataSize);
Image.Load(imageData) → 获取真实 Width 和 Height
  • 通过 GetImageDimensions() 方法判断是否为 PNG,并用 ImageSharp 加载
  • If format error or read fails, fall back to the size declared in the header

This can be used to detect certain "counterfeit ICO" issues (such as inconsistency between size and image content)

Complete and output analysis results

After all entries are read and parsed, the content is output as structured information:

正在检查ICO文件: logo.ico
图标数量: 7
- 第1张图像: 16x16, 32bpp, 大小: 840字节, 偏移: 118
- 第2张图像: 32x32, 32bpp, 大小: 1939字节, 偏移: 958
- 第3张图像: 48x48, 32bpp, 大小: 3375字节, 偏移: 2897
- 第4张图像: 64x64, 32bpp, 大小: 4951字节, 偏移: 6272
- 第5张图像: 128x128, 32bpp, 大小: 13782字节, 偏移: 11223
- 第6张图像: 256x256, 32bpp, 大小: 37823字节, 偏移: 25005
- 第7张图像: 512x512, 32bpp, 大小: 114655字节, 偏移: 62828
  注意: 文件头中指定的尺寸为256x256,但实际图像尺寸为512x512

This allows you to quickly confirm:

  • 一个 .ico 文件中包含多少图层
  • True size and depth of each layer
  • Whether there is a format problem (incorrect size, abnormal size, etc.)

Command line interface design

SharpIco is not just a code base, but also provides a complete command-line tool that is easy to call quickly in any development scenario, and is stress-free whether it is used manually or integrated into build scripts.

这一部分使用了 .NET 的现代 CLI 构建库 System.CommandLine,实现了两个主命令:

  • generate:将 PNG 图像转换为 ICO 图标
  • inspect:检查 ICO 文件的结构与图层信息

Command line usage method:

sharpico generate -i logo.png -o icon.ico --sizes 16 32 48 256
sharpico inspect icon.ico

🛠️ 生成命令 generate

This command supports controlling the input, output paths and generated icon size through parameters:

sharpico generate --input logo.png --output app.ico --sizes 16 32 64 256

Parameter description:

parameters shorthand description
--input -i Source PNG image path (required)
--output -o Output ICO file path (required)
--sizes -s List of icon size to be generated, multiple ones are supported (the default is seven common size)

支持多个尺寸同时生成,内部调用 IcoGenerator.GenerateIcon() 实现多图层 .ico 文件创建。

An example:

sharpico generate -i logo.png -o icon.ico -s 32 64 256

The output will contain three resolution layers.

🔍 检查命令 inspect

可以对任意 .ico 文件进行结构检查与验证:

sharpico inspect icon.ico

This will output the following for each layer in the icon:

  • Width and height (declared and actual size)
  • Bit depth (bpp)
  • Data Offset and Size
  • Is there a potential problem where the head is inconsistent with the actual image size

Example output:

正在检查ICO文件: logo.ico
图标数量: 7
- 第1张图像: 16x16, 32bpp, 大小: 840字节, 偏移: 118
- 第2张图像: 32x32, 32bpp, 大小: 1939字节, 偏移: 958
- 第3张图像: 48x48, 32bpp, 大小: 3375字节, 偏移: 2897
- 第4张图像: 64x64, 32bpp, 大小: 4951字节, 偏移: 6272
- 第5张图像: 128x128, 32bpp, 大小: 13782字节, 偏移: 11223
- 第6张图像: 256x256, 32bpp, 大小: 37823字节, 偏移: 25005
- 第7张图像: 512x512, 32bpp, 大小: 114655字节, 偏移: 62828
  注意: 文件头中指定的尺寸为256x256,但实际图像尺寸为512x512

released

SharpIco 不只是一个库,也不是一个只能源码使用的小工具,而是一个可以 通过 dotnet tool 全平台安装运行支持 AOT 编译优化性能和体积 的专业图标工具。

This time we need to support both AOT and traditional releases, so we have made some configurations to the project files.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net9.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <!-- AOT编译设置移至条件属性组 -->
        <InvariantGlobalization>true</InvariantGlobalization>

        <!-- .NET Tool 配置 -->
        <PackAsTool>true</PackAsTool>
        <ToolCommandName>sharpico</ToolCommandName>
        <PackageOutputPath>./nupkg</PackageOutputPath>

        <!-- 包信息 -->
        <PackageId>SharpIco</PackageId>
        <Version>1.0.0</Version>
        <Authors>StarPlan</Authors>
        <Description>SharpIco是一个纯 C# AOT 实现的轻量级图标生成工具,用于生成和检查ICO图标文件。可将一张高分辨率 PNG 图片一键生成标准的 Windows .ico 图标文件,内含多种尺寸(16x16 到 512x512),还可以自定义尺寸。除了图标生成,SharpIco 还内置图标结构分析功能,助你轻松验证 .ico 文件中包含的图层与尺寸。</Description>
        <PackageTags>icon;ico;png;converter,DealiAxy,cli,tool,dotnet-tool,imagesharp</PackageTags>
        <PackageProjectUrl>https://github.com/star-plan/sharp-ico</PackageProjectUrl>
        <RepositoryUrl>https://github.com/star-plan/sharp-ico</RepositoryUrl>
        <PackageLicenseExpression>MIT</PackageLicenseExpression>
        <PackageReadmeFile>README.md</PackageReadmeFile>
    </PropertyGroup>

    <!-- AOT发布专用设置 -->
    <PropertyGroup Condition="'$(PublishAot)' == 'true'">
        <PublishAot>true</PublishAot>
        <TrimMode>full</TrimMode>
        <InvariantGlobalization>true</InvariantGlobalization>
        <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
        <IlcOptimizationPreference>Size</IlcOptimizationPreference>
        <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
        <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
    </PropertyGroup>

    <ItemGroup>
        <None Include="README.md" Pack="true" PackagePath="\" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
        <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
    </ItemGroup>

</Project>

Several key points:

Defined as CLI tool (dotnet tool)

<PackAsTool>true</PackAsTool>
<ToolCommandName>sharpico</ToolCommandName>

这使得 SharpIco 可以像任何其他 .NET CLI 工具 一样被全局安装:

dotnet tool install -g SharpIco --add-source ./nupkg

After installation, just run:

sharpico generate -i logo.png -o icon.ico

Support AOT compilation and release (. NET 9 native support)

<PublishAot>true</PublishAot>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<TrimMode>full</TrimMode>

SharpIco supports compilation to native executables by setting PublishAot = true

No need for. NET runtime support, extremely fast startup, suitable for building tool chains or integrated environments

Example AOT build command:

dotnet publish -c Release -r win-x64 /p:PublishAot=true

生成的 sharpico.exe 是一个纯原生的 Windows 可执行文件,无需安装 .NET!

除 Windows 外,也可发布为 linux-x64, osx-arm64 等跨平台目标。

Automatically publish nuget

SharpIco adopts a complete GitHub Actions CI/CD pipeline to achieve ** one-time tagging and the entire platform construction is automatically completed **.

只需推送一个符合语义化格式的标签(如 v1.0.0),系统将自动:

  1. Build and release the NuGet toolkit
  2. Compile native AOT executables for Windows/Linux/macOS
  3. Automatically upload all products to GitHub Release page

I have previously written an article on how to publish nuget. See:

This part is not a big problem

But this time is a little different. Previously, libraries and project templates were released, but this time it is dotnet tool

This is similar to concepts such as npx scripts, pip tools, etc.

You can use the dotnet tool command to install and call

However, AOT cannot be used in this method, and you can only use the framework dependent method to publish it

Nuget publishing is relatively simple and also uses the dotnet command

dotnet pack -c Release

But the key is that I will use GitHub Action to automate build and publish, which is similar to the previous process

name: 发布SharpIco
run-name: ${{ github.actor }} 正在发布SharpIco 🚀

on:
  push:
    tags:
      - "v*.*.*"  # 更明确的版本格式匹配

# 为整个工作流设置权限
permissions:
  contents: write
  id-token: write
  issues: write

jobs:
  # 第一步:发布NuGet包
  publish-nuget:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # 获取所有历史记录用于版本号计算
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 9.0.x
      
      - name: 缓存NuGet包
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
          restore-keys: |
            ${{ runner.os }}-nuget-
      
      - name: 提取版本号
        id: get_version
        shell: bash
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      - name: 恢复依赖
        run: dotnet restore ./SharpIco/SharpIco.csproj
      
      - name: 运行测试
        run: dotnet test --no-restore

      - name: 构建项目
        run: dotnet build --no-restore -c Release --nologo ./SharpIco/SharpIco.csproj -p:Version=${{ steps.get_version.outputs.VERSION }}
      
      - name: 创建NuGet包
        run: dotnet pack -c Release ./SharpIco/SharpIco.csproj -p:PackageVersion=${{ steps.get_version.outputs.VERSION }} --no-build --output ./nupkg
        
      - name: 发布到NuGet Gallery
        run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_GALLERY_TOKEN }} --source https://api.nuget.org/v3/index.json --skip-duplicate

Automatically publish to GitHub Release

This is the most troublesome one. I had to debug it more than a dozen times before it was successful.😂

I have to say that debugging on GitHub Action is too unfriendly

Just post my last successful configuration

name: 发布SharpIco
run-name: ${{ github.actor }} 正在发布SharpIco 🚀

on:
  push:
    tags:
      - "v*.*.*"  # 更明确的版本格式匹配

# 为整个工作流设置权限
permissions:
  contents: write
  id-token: write
  issues: write

jobs:
  # 第二步:编译各平台可执行文件
  build-executables:
    needs: publish-nuget  # 确保在NuGet包发布后运行
    strategy:
      fail-fast: false
      matrix:
        kind: ['windows', 'linux', 'macOS']
        include:
          - kind: windows
            os: windows-latest
            target: win-x64
            extension: '.zip'
          - kind: linux
            os: ubuntu-latest
            target: linux-x64
            extension: '.tar.gz'
          - kind: macOS
            os: macos-latest
            target: osx-x64
            extension: '.tar.gz'

    runs-on: ${{ matrix.os }}
    
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # 获取所有历史记录用于版本号计算
      
      - name: 提取版本号
        id: get_version
        shell: bash
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 9.0.x
      
      - name: 缓存NuGet包
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
          restore-keys: |
            ${{ runner.os }}-nuget-
      
      - name: 安装Linux依赖
        if: matrix.kind == 'linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y clang zlib1g-dev libkrb5-dev

      - name: 设置Windows环境
        if: matrix.kind == 'windows'
        shell: pwsh
        run: |
          Write-Host "设置Windows编译环境..."
          # 确保有最新的开发者工具
          choco install visualstudio2022buildtools -y --no-progress
      
      - name: 恢复依赖
        run: dotnet restore ./SharpIco/SharpIco.csproj
      
      - name: AOT编译
        run: |
          echo "正在为 ${{ matrix.kind }} 平台进行AOT编译..."
          dotnet publish ./SharpIco/SharpIco.csproj -c Release -r ${{ matrix.target }} --self-contained true -p:PublishAot=true -p:Version=${{ steps.get_version.outputs.VERSION }} -o ./publish/${{ matrix.kind }}
      
      - name: 打包Windows可执行文件
        if: matrix.kind == 'windows'
        run: |
          cd ./publish/${{ matrix.kind }}
          7z a -tzip ../../SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }} *
      
      - name: 打包Linux/macOS可执行文件
        if: matrix.kind != 'windows'
        run: |
          cd ./publish/${{ matrix.kind }}
          tar -czvf ../../SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }} *
      
      # 上传构建产物作为工作流构件(artifacts)
      - name: 上传构建产物
        uses: actions/upload-artifact@v4
        with:
          name: SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}
          path: ./SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }}
          retention-days: 1

  # 第三步:统一上传所有平台可执行文件到GitHub Release
  upload-to-release:
    needs: build-executables
    runs-on: ubuntu-latest
    
    steps:
      - name: 提取版本号
        id: get_version
        shell: bash
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      # 下载所有平台构建产物
      - name: 下载Windows构建产物
        uses: actions/download-artifact@v4
        with:
          name: SharpIco-windows-${{ steps.get_version.outputs.VERSION }}
          path: ./artifacts
      
      - name: 下载Linux构建产物
        uses: actions/download-artifact@v4
        with:
          name: SharpIco-linux-${{ steps.get_version.outputs.VERSION }}
          path: ./artifacts
      
      - name: 下载macOS构建产物
        uses: actions/download-artifact@v4
        with:
          name: SharpIco-macOS-${{ steps.get_version.outputs.VERSION }}
          path: ./artifacts
      
      # 列出下载的文件以确认
      - name: 列出下载的文件
        run: ls -la ./artifacts
      
      # 统一上传到GitHub Release
      - name: 上传所有文件到GitHub Release
        uses: softprops/action-gh-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          files: ./artifacts/*
          tag_name: ${{ github.ref }}
          fail_on_unmatched_files: false
          draft: false
          name: SharpIco 版本 ${{ steps.get_version.outputs.VERSION }}
          generate_release_notes: true 

Workflow overview

The entire workflow consists of three major jobs:

stage described
publish-nuget 编译项目并发布 .nupkg 包到 NuGet.org
build-executables Compile AOT native executables for three major platforms
upload-to-release Upload all products to the GitHub Release corresponding to the current tag

Publishing the nuget part is easier, so I won't repeat it here

Compile three-platform AOT executables

Adopt GitHub Matrix construction strategies, which target:

  • win-x64(Windows 可执行文件,ZIP 打包)
  • linux-x64(Linux ELF,可执行,tar.gz 打包)
  • osx-x64(macOS 可执行,tar.gz 打包)
dotnet publish -c Release -r ${{ matrix.target }} --self-contained true -p:PublishAot=true

每个平台生成对应压缩包,打包后使用 upload-artifact 上传中转。

Upload to GitHub Release (upload-to-release)

At this stage, previously built products will be automatically downloaded and packaged into the Release page of the current version:

uses: softprops/action-gh-release@v1

发布页面自动生成更新说明(generate_release_notes: true),方便用户查看版本变更。

The final effect is as shown in the following figure:

🔖 v1.0.0
├── SharpIco-windows-v1.0.0.zip
├── SharpIco-linux-v1.0.0.tar.gz
└── SharpIco-macOS-v1.0.0.tar.gz

pit

这里最大的坑就是原本使用 actions/upload-artifact@v3 一直报错,后面查了 issue 发现是已经 deprecate 了

Upgrading to v4 problem solving

In addition, when releasing GitHub releases, there are always conflicts and always say that they already exist

It doesn't work even if you set append...

Later, I changed it to upload it first and transfer it in one go, and then I released it in one go. Only then did I finally succeed?😂

summary

SharpIco 是我基于 .NET 9 和 AOT 编译能力打造的一款纯 C# 图标工具,目标是替代繁琐依赖、简化图标生成与验证流程

ImageSharp 图像处理,到 BinaryWriter 手工拼装 ICO 格式;从 System.CommandLine 打造命令行体验,到 GitHub Actions 全流程自动化发布;SharpIco 不仅是一个实用工具,也是一场小而美的工程探索。

It represents my pursuit of tool ideals: ** Lightweight, pure, easy to integrate, cross-platform, and ready-to-use **.

  • No need to install Python, Node or ImageMagick
  • Support AOT compilation and generate native executable files
  • It also has icon generation and structural analysis functions
  • One-time release automatically produces full-platform construction results

Welcome Star/Fork/Issue/PR and welcome integrating it into your build system.

📦 GitHub project address: 👉 https://github.com/star-plan/sharp-ico

Keep Exploring

延伸阅读

更多文章
同分类 / 同专题 2/7/2026

Summary of experience in using AOT

From the very beginning of project creation, you should develop a good habit of conducting AOT release testing in a timely manner whenever new features are added or newer syntax is used.

继续阅读
同分类 / 同专题 8/29/2023

.NET 8.0 AOT DebugView

Debugview is an application that allows you to monitor debug output on your local system or any computer on a network accessible via TCP/IP.

继续阅读