返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
C#

SharpIco: A zero-dependency .ico icon generator built with pure C#, supporting .NET9 and AOT compilation.

SharpIco is a cross-platform icon generation tool implemented in pure C#, supporting AOT compilation without external dependencies. It can decompose PNG images into multiple sizes (from 16x16 to 512x512) to synthesize ICO icons and supports icon structure analysis. The tool provides a command-line interface, which can be installed in one step via dotnet tool, and has been published to NuGet and Scoop. The project aims to simplify icon generation and verification, pursuing a lightweight, pure, and easy-to-integrate user experience.

曦远 (DealiAxy)约 66 分钟
目录27 个章节

Preface

Recently, I've been continuously refining my two desktop software projects this year: the video editing tool Clipify and the AI article creation tool StarBlogPublisher.

Although the interfaces are basically polished, they still have default icons, which doesn't look very professional.

So, I decided to replace the icons for both applications with better-looking ones.

Back in the VB6.0 era, I used an open-source ICO icon creation tool, but I can no longer find it now.

Many ICO creation tools found online are designed for favicons.

Others are either too heavyweight or require payment, so I turned my attention back to open-source tools.

I found one implemented in Node.js with zero dependencies (most others rely on Magick, a C++ image library). Unfortunately, this tool does not work on my computer...

Goal

At this point, I recalled that I had previously developed an image format conversion tool in C#, utilizing the AOT feature of .Net8, which allowed me to create cross-platform single executable files like Go does.

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

  • Pure C# implementation, no external dependencies, cross-platform, single executable file, AOT
  • Decompose a PNG image into multiple smaller images of different sizes (side lengths of 16, 32, 48, 64, 128, 256, 512), then combine them into a single ICO icon to provide an optimal visual experience for screens of various sizes
  • Support an Inspect feature that can read and analyze ICO icons
  • Convenient distribution methods, supporting one-click installation via tools like dotnet tool, scoop, brew, etc.

This has been completed and published to NuGet and Scoop. Next, I will look into how to publish it to Brew.

Project homepage: https://github.com/star-plan/sharp-ico

Implementation

In SharpIco, the generation of .ico files does not rely on ImageMagick or any external image processing tools at all. Instead, it manually assembles the ICO binary structure compliant with specifications through pure C# code.

The core class for this part is IcoGenerator. I won't paste the exact code here since it's available in the project. Let me just highlight a few key points~

Generating Multi-size Images

Using ImageSharp to generate multi-size images

var clone = original.Clone(ctx => ctx.Resize(size, size));
clone.SaveAsPng(ms);
  • Use ImageSharp's Resize and Clone functions to generate multiple target sizes (e.g., 16x16, 32x32, 256x256) from the original high-resolution PNG.

  • Save as PNG format to a memory stream for subsequent writing to .ico.

💡 ICO files support embedded PNG images (since Vista), allowing for a smaller file size and better transparency effects.

Manually Constructing the ICO File Header

Follow the ICO file format to manually construct ICONDIR and ICONDIRENTRY.

The ICO file header consists of 3 parts:

  • ICONDIR (6 bytes): A fixed structure.
  • ICONDIRENTRY × N (16 bytes each): Describes the size and offset of each embedded image.
  • Image Data × N: The actual binary image data.
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 to write the description information for each image (width, height, bit depth, data offset, etc.).
writer.Write((byte)(img.Width == 256 ? 0 : img.Width)); // 256 is represented as 0
writer.Write((ushort)32); // bits per pixel
writer.Write(image.Length);
writer.Write(offset);

In an ICO file, if the width/height fields are 0, it represents 256. This is a special specification of the ICO format.

The ICO file format has a limitation when representing image dimensions: the width and height fields are each only one byte, with a value range of 0-255. When these fields are 0, according to the specification, it represents 256 pixels. For dimensions larger than 256 (e.g., 512×512 or 1024×1024), the file header will still show 0 (i.e., 256), but the actual image data can contain images of larger dimensions.

Concatenating Images

Concatenate all PNG image data

foreach (var image in images) {
    writer.Write(image);
}
  • After all descriptor information has been written, the image data body is written immediately afterward.

  • Since offsets are calculated in advance, it ensures that each image data can be correctly identified and read by the system.

Extension

Supports custom sizes

public static void GenerateIcon(string sourcePng, string outputIco, int[] sizes)
  • Default support for common sizes from 16 to 512

  • Allows flexible specification of size combinations through parameters (e.g., only packaging 32/256)

Inspect Functionality

In addition to icon generation, SharpIco also includes a built-in icon content analysis tool, IcoInspector, which helps developers deeply understand the internal structure of .ico files. It can verify the actual dimensions and bit-depth information of the contained layered images, solving the issue of many icon tools on the market generating non-standard .ico files.

Here's a brief overview of the implementation approach~

Reading the ICO File Header

Manually reading the ICONDIR + ICONDIRENTRY structures

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

  • ICONDIR (6 bytes): Marks it as an icon and records the number of images.
  • ICONDIRENTRY × N (16 bytes each): Records the metadata for each image (width, height, bit depth, data offset, etc.).
  • Image Data Block: The actual PNG or BMP image.
ushort reserved = reader.ReadUInt16(); // Must be 0
ushort type = reader.ReadUInt16();     // 1 indicates an icon
ushort count = reader.ReadUInt16();    // Number of images

Then, read the image entries one by one and save them 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();

By the way, as mentioned earlier, the space in the ICO header is limited and can only store 8 bits. Therefore, if the width/height fields are 0, according to the specification, it represents 256, or greater than 256.

Extracting & Parsing Image Data

Extract image data and verify the true resolution.

The width and height recorded in an ICO file may not be accurate, especially when PNG format is embedded. Therefore, use ImageSharp to truly parse each image.

fs.Seek(entry.ImageOffset, SeekOrigin.Begin);
fs.Read(imageData, 0, dataSize);
Image.Load(imageData) → Obtain the true Width and Height
  • Use the GetImageDimensions() method to determine if it is PNG and load it with ImageSharp.

  • If the format is incorrect or reading fails, fall back to the dimensions declared in the header.

This can be used to detect some issues with "counterfeit ICOs" (for example, dimensions inconsistent with the image content).

Complete and Output Analysis Results

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

Checking ICO file: logo.ico
Number of icons: 7
- Image 1: 16x16, 32bpp, size: 840 bytes, offset: 118
- Image 2: 32x32, 32bpp, size: 1939 bytes, offset: 958
- Image 3: 48x48, 32bpp, size: 3375 bytes, offset: 2897
- Image 4: 64x64, 32bpp, size: 4951 bytes, offset: 6272
- Image 5: 128x128, 32bpp, size: 13782 bytes, offset: 11223
- Image 6: 256x256, 32bpp, size: 37823 bytes, offset: 25005
- Image 7: 512x512, 32bpp, size: 114655 bytes, offset: 62828
  Note: The size specified in the file header is 256x256, but the actual image size is 512x512

This allows you to quickly confirm:

  • How many layers are contained within a single .ico file.
  • The true dimensions and bit depth of each layer.
  • Whether there are any format issues (size mismatch, abnormal size, etc.).

Command Line Interface Design

SharpIco is not just a code library; it also provides a complete command-line tool, making it convenient to quickly invoke in any development scenario, whether for manual use or integration into build scripts without any hassle.

This section utilizes the modern CLI building library System.CommandLine from .NET, implementing two main commands:

  • generate: Converts PNG images to ICO icons
  • inspect: Inspects the structure and layer information of ICO files

Command-line usage examples:

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

🛠️ The generate Command

This command allows you to control the input and output paths as well as the icon sizes through parameters:

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

Parameter Description:

ParameterShorthandDescription
--input-iSource PNG image path (required)
--output-oOutput ICO file path (required)
--sizes-sList of icon sizes to generate, supports multiple (defaults to seven common sizes)

Supports generating multiple sizes simultaneously, internally calls IcoGenerator.GenerateIcon() to create multi-layer .ico files.

For example:

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

The output will contain three resolution layers.

🔍 Check Command inspect

You can perform structural inspection and validation on any .ico file:

sharpico inspect icon.ico

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

  • Dimensions (declared vs. actual size)
  • Bit depth (bpp)
  • Data offset and size
  • Whether there is a potential issue where the header size does not match the actual image size

Example Output:

Checking ICO file: logo.ico
Number of icons: 7
- Image 1: 16x16, 32bpp, Size: 840 bytes, Offset: 118
- Image 2: 32x32, 32bpp, Size: 1939 bytes, Offset: 958
- Image 3: 48x48, 32bpp, Size: 3375 bytes, Offset: 2897
- Image 4: 64x64, 32bpp, Size: 4951 bytes, Offset: 6272
- Image 5: 128x128, 32bpp, Size: 13782 bytes, Offset: 11223
- Image 6: 256x256, 32bpp, Size: 37823 bytes, Offset: 25005
- Image 7: 512x512, 32bpp, Size: 114655 bytes, Offset: 62828
  Note: The specified size in the file header is 256x256, but the actual image size is 512x512

Release

SharpIco is not just a library, nor is it a small tool that can only be used via source code. It is a professional icon tool that can be installed and run cross-platform via dotnet tool and supports AOT compilation to optimize performance and size.

This time, to support both AOT and traditional publishing, some configurations were made to the project file.

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

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net9.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <!-- AOT Compilation settings moved to conditional property group -->
        <InvariantGlobalization>true</InvariantGlobalization>

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

        <!-- Package Information -->
        <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 Publication Specific Settings -->
    <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 a CLI Tool (dotnet tool)

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

This allows SharpIco to be installed globally like any other .NET CLI tool:

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

After installation, simply run:

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

Support for AOT Compilation Publishing (.NET 9 Native Support)

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

By setting PublishAot=true, SharpIco supports compilation into native executable files.

No .NET runtime support is required, startup speed is extremely fast, making it suitable for build toolchains or integrated environments.

Example of AOT build command:

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

The generated sharpico.exe is a purely native Windows executable that does not require .NET installation!

In addition to Windows, it can also be published for cross-platform targets such as linux-x64, osx-arm64, etc.

Automatic NuGet Publishing

SharpIco utilizes a complete GitHub Actions CI/CD pipeline to achieve fully automated cross-platform builds with a single tag push.

Simply push a tag that conforms to a semantic format (e.g., v1.0.0), and the system will automatically:

  1. Build and publish the NuGet tool package.
  2. Compile native AOT executables for Windows / Linux / macOS.
  3. Automatically upload all artifacts to the GitHub Release page.

I have previously written articles on how to publish NuGet packages. For details, see:

This part is straightforward.

However, this time it's a bit different. Previously, I was publishing class libraries and project templates; this time, it's a dotnet tool.

This concept is similar to npx scripts, pip tools, etc.

It can be installed and invoked using the dotnet tool command.

But with this method, AOT cannot be used; it must be published as a framework-dependent deployment.

NuGet publishing is simpler, also using the dotnet command.

dotnet pack -c Release

The key is that I want to use GitHub Actions to automate the build and publishing. This process is similar to what I've done before.

name: Publish SharpIco
run-name: ${{ github.actor }} is publishing SharpIco 🚀

on:
  push:
    tags:
      - "v*.*.*"  # More precise version format matching

Setting Permissions for the Entire Workflow

permissions: contents: write id-token: write issues: write

jobs:

Step 1: Publish the NuGet Package

publish-nuget: runs-on: ubuntu-latest

steps:
  - uses: actions/checkout@v3
    with:
      fetch-depth: 0  # Fetch all history for version calculation
  
  - name: Setup .NET
    uses: actions/setup-dotnet@v3
    with:
      dotnet-version: 9.0.x
  
  - name: Cache NuGet Packages
    uses: actions/cache@v3
    with:
      path: ~/.nuget/packages
      key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
      restore-keys: |
        ${{ runner.os }}-nuget-
  
  - name: Extract Version Number
    id: get_version
    shell: bash
    run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
  
  - name: Restore Dependencies
    run: dotnet restore ./SharpIco/SharpIco.csproj
  
  - name: Run Tests
    run: dotnet test --no-restore

  - name: Build Project
    run: dotnet build --no-restore -c Release --nologo ./SharpIco/SharpIco.csproj -p:Version=${{ steps.get_version.outputs.VERSION }}
  
  - name: Create NuGet Package
    run: dotnet pack -c Release ./SharpIco/SharpIco.csproj -p:PackageVersion=${{ steps.get_version.outputs.VERSION }} --no-build --output ./nupkg
    
  - name: Publish to NuGet Gallery
    run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_GALLERY_TOKEN }} --source https://api.nuget.org/v3/index.json --skip-duplicate
## Automatic Publishing to Github Release

This is the most troublesome part, I had to adjust it over ten times before it finally worked 😂

I have to say, debugging Github Actions is really unfriendly.

Here is the configuration I ultimately succeeded with:

```yaml
name: PublishSharpIco
run-name: ${{ github.actor }} is publishing SharpIco 🚀

on:
  push:
    tags:
      - "v*.*.*"  # More explicit version format matching

Set permissions for the entire workflow

permissions: contents: write id-token: write issues: write

jobs:

Step 2: Build executables for each platform

build-executables: needs: publish-nuget # Ensure it runs after NuGet package is published 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  # Fetch all history for version calculation
  
  - name: Extract version number
    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: Cache NuGet packages
    uses: actions/cache@v3
    with:
      path: ~/.nuget/packages
      key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
      restore-keys: |
        ${{ runner.os }}-nuget-
  
  - name: Install Linux dependencies
    if: matrix.kind == 'linux'
    run: |
      sudo apt-get update
      sudo apt-get install -y clang zlib1g-dev libkrb5-dev

  - name: Set up Windows environment
    if: matrix.kind == 'windows'
    shell: pwsh
    run: |
      Write-Host "Setting up Windows build environment..."
      # Ensure latest developer tools are available
      choco install visualstudio2022buildtools -y --no-progress
  
  - name: Restore dependencies
    run: dotnet restore ./SharpIco/SharpIco.csproj
  
  - name: AOT compilation
    run: |
      echo "Performing AOT compilation for ${{ matrix.kind }} platform..."
      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: Package Windows executable
    if: matrix.kind == 'windows'
    run: |
      cd ./publish/${{ matrix.kind }}
      7z a -tzip ../../SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }} *
  
  - name: Package Linux/macOS executable
    if: matrix.kind != 'windows'
    run: |
      cd ./publish/${{ matrix.kind }}
      tar -czvf ../../SharpIco-${{ matrix.kind }}-${{ steps.get_version.outputs.VERSION }}${{ matrix.extension }} *
  
  # Upload build artifacts as workflow artifacts
  - name: Upload build artifacts
    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

Step 3: Upload all platform executables to GitHub Release

upload-to-release: needs: build-executables runs-on: ubuntu-latest

steps:
  - name: Extract version number
    id: get_version
    shell: bash
    run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
  
  # Download build artifacts for all platforms
  - name: Download Windows build artifact
    uses: actions/download-artifact@v4
    with:
      name: SharpIco-windows-${{ steps.get_version.outputs.VERSION }}
      path: ./artifacts
  
  - name: Download Linux build artifact
    uses: actions/download-artifact@v4
    with:
      name: SharpIco-linux-${{ steps.get_version.outputs.VERSION }}
      path: ./artifacts
  
  - name: Download macOS build artifact
    uses: actions/download-artifact@v4
    with:
      name: SharpIco-macOS-${{ steps.get_version.outputs.VERSION }}
      path: ./artifacts
  
  # List downloaded files for confirmation
  - name: List downloaded files
    run: ls -la ./artifacts
  
  # Upload all files to GitHub Release
  - name: Upload all files to 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 version ${{ steps.get_version.outputs.VERSION }}
      generate_release_notes: true
### Workflow Overview

The entire workflow consists of three major Jobs:

| Stage               | Description                                                       |
| ------------------- | ----------------------------------------------------------------- |
| `publish-nuget`     | Compile the project and publish the `.nupkg` package to NuGet.org |
| `build-executables` | Compile AOT native executables for three platforms                |
| `upload-to-release` | Upload all artifacts to the GitHub Release corresponding to the current tag |

The NuGet publishing part is relatively straightforward, so it won't be repeated here.
### Building Three-Platform AOT Executables (build-executables)

Utilizing a GitHub Matrix build strategy to target the following platforms:

- `win-x64` (Windows executable, packaged as ZIP)
- `linux-x64` (Linux ELF, executable, packaged as tar.gz)
- `osx-x64` (macOS executable, packaged as tar.gz)

```bash
dotnet publish -c Release -r ${{ matrix.target }} --self-contained true -p:PublishAot=true

A corresponding archive is generated for each platform. After packaging, the files are uploaded to staging using upload-artifact.

Upload to GitHub Release (upload-to-release)

This stage automatically downloads the previously built artifacts and packages them into the current version's release page:

uses: softprops/action-gh-release@v1

The release page automatically generates update notes (generate_release_notes: true) for users to view version changes.

The final result is shown in the following diagram:

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

Pitfalls

The biggest pitfall here was that using actions/upload-artifact@v3 kept throwing errors. Later, after checking the issues, I discovered it had been deprecated.

Upgrading to v4 resolved the problem.

Additionally, there were constant conflicts when publishing GitHub Releases, repeatedly stating that it already existed.

Setting append didn't work either...

Eventually, I changed the approach to first upload to a staging area and then publish everything in one go, which finally succeeded 😂

Summary

SharpIco is a pure C# icon tool that I built based on .NET 9 and its AOT compilation capabilities, aiming to replace cumbersome dependencies and simplify the icon generation and validation process.

From ImageSharp image processing to manually assembling the ICO format using BinaryWriter; from crafting a command-line experience with System.CommandLine to fully automated publishing via GitHub Actions—SharpIco is not just a practical utility but also a small yet elegant engineering exploration.

It represents my pursuit of the ideal tool: lightweight, pure, easy to integrate, cross-platform, and ready to use out of the box.

  • No need to install Python, Node, or ImageMagick
  • Supports AOT compilation, generating native executables
  • Provides both icon generation and structure analysis functionality
  • A single release automatically produces build results for all platforms

Feel free to Star / Fork / Issue / PR, and welcome to integrate it into your build systems.

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

_EOF_

SharpIco: A zero-dependency .ico icon generator built with pure C#, supporting .NET9 and AOT compilation.

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…