preface
最近一直在完善我今年的两款桌面软件:视频剪辑工具 Clipify 和 AI 文章创作工具 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),系统将自动:
- Build and release the NuGet toolkit
- Compile native AOT executables for Windows/Linux/macOS
- Automatically upload all products to GitHub Release page
I have previously written an article on how to publish nuget. See:
- 开发现代化的.NET Core控制台程序 (3) 将nuget包发布到GitHubPackages
- 开发现代化的.NET Core控制台程序 (4) 使用GitHubAction自动构建以及发布nuget包
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