IconPackager turns PNG, BMP and SVG artwork into multi-resolution Windows icons (.ico) at whatever
sizes you ask for, driven by a short text project file so that it can run unattended in a CI/CD pipeline.
It exists to replace the aged icon tooling that stopped at 16-colour favicons: modern icons need frames
from 16 to 256 pixels with full alpha, and rebuilding them by hand every time the artwork changes does
not scale. You list the frames each icon needs and where the artwork comes from; the tool renders every
frame and packs them into one .ico. An SVG can supply a single named element, so one Inkscape drawing
can hold the artwork for several icons.
It ships as two NuGet packages:
| Package | Kind | Purpose |
|---|---|---|
PatTech.IconPackager |
Build step | Builds the .ico files named in icons.ini before your project compiles. Adds no assembly to your project and copies nothing to its output. |
PatTech.IcoNet |
.NET Standard 2.1 and .NET 10 library | Assembles .ico files from encoded frames, for applications that produce icons themselves. |
Most projects want only the first. The second is there for tools that write icons of their own.
Active development. Windows only, because rendering uses System.Drawing.
Working today:
- Frames from raster images (
.png,.bmpand anything else System.Drawing opens), resized to each frame size. - Frames from
.svgfiles: the whole page, one element chosen by id or Inkscape label (use), or a region of the page (snip). - 256 px frames stored PNG-compressed; smaller frames stored as bitmaps with a transparency mask at 1-bit
black and white (
bw), 8-bit palette (pal), 24-bit RGB (rgb) or 32-bit ARGB (true). - Single PNG images at any frame size from the same sources, for NuGet package icons and the like.
- Outputs rebuilt only when the artwork or the project file changed, or on every run, or skipped (
output=). - Colour keys for legacy artwork: a transparent colour (
mask) and a screen-inverting colour (invert), with the classic magenta and teal defaults for images that have no alpha channel. - A montage of every frame beside each icon, for checking the result at a glance, and an existing
.icoexploded back into one PNG per frame.
- Windows, on the machine that builds the icons
- .NET 10 SDK. The build step runs on the .NET 10 runtime whatever framework your project targets.
- Visual Studio 2026 or VS Code, if you want an IDE
Add the build step to the project that needs the icon:
dotnet add package PatTech.IconPackager
Create icons.ini anywhere under the project folder, next to your artwork:
[app.ico]
source=assets
pack=logo.svg|use AppIcon
48-true=logo.svg|use AppIcon
32-true=logo.svg|use AppIcon
16-true=logo.svg|use AppIconSmallBuild the project. app.ico is written under obj before the compiler runs, so the project can embed
it by naming it:
<PropertyGroup>
<IconPackagerApplicationIcon>app.ico</IconPackagerApplicationIcon>
</PropertyGroup>Nothing lands in the source tree: the icons are rebuilt whenever the artwork or icons.ini changes,
skipped when neither has, and removed by dotnet clean. Next to each icon is app.montage.png, every
frame on one sheet, for checking what a change to the artwork did.
The PatTech.IconPackager package is a development dependency: it adds one MSBuild target and nothing else. No
assembly is referenced, nothing is copied to your output folder, and the package does not flow to projects
that reference yours. Before CoreCompile in every project that references it, the target:
- builds every
icons.iniunder the project folder, ignoringbinandobj, intoobj\<Configuration>\<TargetFramework>\icons\, with a montage of every frame beside each icon; - reports problems as build errors that name the
icons.iniline concerned, so they show in the error list and jump to the line; - fails the build when any icon could not be built in full. An icon whose remaining frames rendered is still written, but it is rebuilt and the error repeated on every build until it is fixed.
| In the project file | Effect |
|---|---|
<EnableDefaultIconProjects>false</EnableDefaultIconProjects> |
Stop picking up icons.ini files automatically. |
<IconProject Include="art\tray.ini" /> |
Build this project file as well, or instead. |
<IconPackagerApplicationIcon>app.ico</IconPackagerApplicationIcon> |
Embed this icon, from the output folder, as the application icon. |
<IconPackagerOutputDir>$(MSBuildProjectDirectory)\icons\</IconPackagerOutputDir> |
Write the icons somewhere else. |
<IconPackagerMontage>false</IconPackagerMontage> |
Skip the montages. |
IconPackagerOutputDir is set by the package's targets, after the project file has been read, so a
project can use it inside a target or in a Directory.Build.targets, for a Content or EmbeddedResource
item say, but not in its own body. That is why the application icon has a property of its own.
Rendering uses System.Drawing, so the step only works on Windows and is an error elsewhere. A project
that also builds on Linux or macOS should condition its IconProject items and EnableDefaultIconProjects
on $([MSBuild]::IsOSPlatform('Windows')), and check in the icon, or condition ApplicationIcon the same way.
The tool can also run by hand, for scripts outside MSBuild or for looking inside an icon. Build this repository and run:
IconPackager [--out <folder>] [--montage] [--explode] <icons.ini | app.ico> ...
Project files are built, with their outputs next to them or in the --out folder. An .ico file is
pictured instead: --montage writes app.montage.png with every frame on one sheet, --explode writes
each frame as app.<size>-<depth>.png, and an icon named with neither gets both. The same two switches
apply to the icons a project file builds. The exit code is 0 only when everything was built, 1 when a
project file, frame or icon failed, 2 when the command line was not understood and 3 when the tool itself
failed.
A project file has one section per output file: [name.ico] for an icon, or [name.png] for a single
PNG image such as the 128 px package icon NuGet recommends. Inside a section:
| Line | Meaning |
|---|---|
source=folder |
Folder the frame files are looked up in, relative to the project file. |
output=policy |
When to build the output: newest (the default) when it is missing or a source or the project file is newer, overwrite every run, none skips the section. |
pack=frame |
A 256 px frame, stored PNG-compressed. |
<size>-<depth>=frame |
A frame of that size. Sizes: 16, 24, 32, 48, 64, 128, 256. Depths: bw, pal, rgb, true. |
A frame is a file name followed by options separated by |:
| Option | Example | Effect |
|---|---|---|
use |
logo.svg|use Wordsmith |
Render only this SVG element, found by id or Inkscape label, scaled to fill the frame. |
snip |
tray.svg|snip 15mm,0,12mm,12mm |
Render only this region: x, y, width, height in mm (default), in or px. |
mask |
old.bmp|mask #ff00ff |
Colour drawn as transparent. Defaults to magenta for artwork without an alpha channel; none disables it. |
invert |
old.bmp|invert #008080 |
Colour drawn as screen-inverting pixels at bw, pal and rgb. Defaults to teal for artwork without an alpha channel; none disables it. |
The full format, including rendering rules and error behaviour, is in docs/project-format.md.
For an application that writes icons itself, reference the library instead of the build step:
dotnet add package PatTech.IcoNet
IcoNet writes the icon; you supply each frame as encoded bytes. Frames below 256 px go in as bitmaps
with a mask (GetBmpData, at 32 bits or with a bit count of 1, 4, 8 or 24, and optionally a colour whose
pixels invert the screen), and the 256 px frame goes in
as a PNG (GetPngData). The builder reads each frame's depth and palette size from the data itself.
using System.Drawing;
using System.IO;
using PatTech.IcoNet;
using var source = (Bitmap)Image.FromFile("logo.png");
var icon = new IconBuilder();
foreach (var size in new[] { 16, 32, 48 }) {
using var frame = source.Resize(new Size(size, size));
icon.Add(size, frame.GetBmpData());
}
using var large = source.Resize(new Size(256, 256));
icon.Add(256, large.GetPngData());
using var writer = new BinaryWriter(File.Create("app.ico"));
icon.Write(writer);Frames are written in the order they are added, and sizes run from 1 to 256 pixels. The public types carry XML documentation, so IntelliSense describes each member.
IconReader goes the other way: it reads an .ico into its frames, each with its size, depth and encoded
data, and ToBitmap decodes a frame to a 32-bit bitmap, honouring the palette and the transparency mask.
Pixels that would invert the screen can be painted a colour of your choice or left transparent.
| Path | Contents |
|---|---|
IconPackager/ |
The command-line tool. ProjectParser reads project files, IconProject renders and writes the outputs, FrameLoader, Artwork and SvgRenderer produce each frame, and IconInspector and Montage picture existing icons. |
IconPackager/build/ |
The .props and .targets the package adds to a consuming project. |
IconPackager/Properties/project.ini |
A sample project file showing the syntax. Its assets are not included. |
IcoNet/ |
The library: IconBuilder writes .ico files, IconReader reads them, BitmapExt prepares frames. |
IconPackager.Tests/ |
xunit tests for the parser, rendering, the atomic write and the library. |
Directory.Build.props |
Version, author and licence shared by both packages. |
docs/ |
The project file format reference. |
eel.svg |
A sample Inkscape drawing with a laughing-eel element to try use on. |
icon/ |
The project's own icon: icon.svg, the icons.ini that renders it, and the rendered icon.png and icon.ico that the packages and the tool embed. |
dotnet test runs the suite in IconPackager.Tests/. Then:
dotnet pack -c Release -o artifacts
PatTech.IcoNet.<version>.nupkg is an ordinary library package. PatTech.IconPackager.<version>.nupkg holds the
published tool under tools/net10.0/ and the MSBuild files under build/, with no lib/ folder and no
dependencies, which is what keeps it out of a consuming project's output. The version is set once in
Directory.Build.props.
Both packages carry icon/icon.png as their package icon and the tool embeds icon/icon.ico. Those two
rendered images are checked in, unlike the icons of a project that uses the build step, because the build
needs them before the tool that renders them exists. After editing icon/icon.svg or icon/icons.ini,
run the built tool on icon\icons.ini and commit the new images with the drawing.
MIT. See LICENSE.txt.