How to Convert PNG to ICO Using PowerShell (Multi-Size Icons)

Tom Baker, IT lead at a Denver clinic, needed a sharp icon for an internal tool, and all he had was a PNG logo. PowerShell has no cmdlet to convert PNG to ICO, but a short .NET function builds a real multi-size icon:

. .\ConvertTo-Ico.ps1
ConvertTo-Ico -Path .\fieldnotes.png     # creates fieldnotes.ico, 16 to 256 px

In this tutorial, I’ll show you how to convert PNG to ICO using PowerShell step by step: build the converter, check the file byte by byte, and see why the popular one-line Save trick fails.

What you need to convert PNG to ICO in PowerShell

You need Windows, PowerShell 7 or 5.1, and a square PNG of at least 256 x 256 pixels. System.Drawing, the .NET wrapper around the Windows GDI+ graphics library, does the work.

PowerShell 7 loads System.Drawing at startup. In 5.1, [System.Drawing.Bitmap] fails with “Unable to find type” until you run Add-Type -AssemblyName System.Drawing, so every script here starts with it.

System.Drawing is Windows-only in modern .NET. If you’re unsure which shell you have, check your PowerShell version.

Step 1: Create a sample PNG logo

Let’s draw a Field Notes logo instead of using a real company file. This saves a 512 x 512 PNG with a transparent background:

Add-Type -AssemblyName System.Drawing
$folder = New-Item -Path .\png2ico -ItemType Directory -Force
$logo = New-Object System.Drawing.Bitmap 512, 512
$g = [System.Drawing.Graphics]::FromImage($logo)
$g.SmoothingMode = 'AntiAlias'
$g.FillEllipse([System.Drawing.Brushes]::SteelBlue, 16, 16, 480, 480)
$font = New-Object System.Drawing.Font 'Segoe UI', 190, 'Bold', 'Pixel'
$center = New-Object System.Drawing.StringFormat
$center.Alignment = 'Center'
$center.LineAlignment = 'Center'
$box = New-Object System.Drawing.RectangleF 0, 0, 512, 512
$g.DrawString('FN', $font, [System.Drawing.Brushes]::White, $box, $center)
$g.Dispose()

$logo.Save("$($folder.FullName)\fieldnotes.png", [System.Drawing.Imaging.ImageFormat]::Png)
"Saved fieldnotes.png: $($logo.Width) x $($logo.Height), $($logo.PixelFormat)"
$logo.Dispose()

Output:

Saved fieldnotes.png: 512 x 512, Format32bppArgb

Format32bppArgb includes an alpha (transparency) channel. Save() gets a full path because .NET resolves relative paths against the process folder, not your PowerShell location.

Image.FromFile also reads JPG, BMP and GIF logos, the same way you’d load one to convert a PNG to JPG with PowerShell.

Why doesn’t Image.Save with ImageFormat.Icon create an icon?

Many scripts, including an earlier version of this post, save the bitmap with [ImageFormat]::Icon. Here’s what that writes:

Add-Type -AssemblyName System.Drawing
$png = Join-Path $PWD 'png2ico\fieldnotes.png'
$ico = Join-Path $PWD 'png2ico\quick.ico'

$image = [System.Drawing.Image]::FromFile($png)
$image.Save($ico, [System.Drawing.Imaging.ImageFormat]::Icon)
$image.Dispose()

$bytes = [System.IO.File]::ReadAllBytes($ico)
'First 4 bytes: ' + (($bytes[0..3] | ForEach-Object { $_.ToString('X2') }) -join ' ')
$encoders = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders()
'GDI+ encoders: ' + ($encoders.FormatDescription -join ', ')
try   { $null = New-Object System.Drawing.Icon $ico; 'Loads as an icon' }
catch { "Failed. $($_.Exception.GetBaseException().Message)" }

Output:

First 4 bytes: 89 50 4E 47
GDI+ encoders: BMP, JPEG, GIF, TIFF, PNG
Failed. Argument 'picture' must be a picture that can be used as a Icon.
PowerShell 7 console showing Image.Save with ImageFormat.Icon writes PNG bytes 89 50 4E 47 and the Icon class rejects the file
The .ico file starts with the PNG signature, and System.Drawing.Icon refuses it (PowerShell 7)

GDI+ has no ICO encoder, so Save() falls back to PNG. You get a PNG file with an .ico name, in 7 and 5.1 alike. Extensions can lie, which is worth remembering when you find files by extension.

What about GetHicon() and Icon.FromHandle()?