Skip to content

Instantly share code, notes, and snippets.

@sombraguerrero
Created January 26, 2026 22:35
Show Gist options
  • Select an option

  • Save sombraguerrero/cf7c521d33987975f648cf11311ac9f8 to your computer and use it in GitHub Desktop.

Select an option

Save sombraguerrero/cf7c521d33987975f648cf11311ac9f8 to your computer and use it in GitHub Desktop.
Icon Encoder (Windows)
<#
.SYNOPSIS
Convert a bitmap (BMP/PNG/JPG etc.) into a valid .ico file with proper ICO headers.
.DESCRIPTION
Builds a correct ICO structure:
- ICONDIR (6 bytes)
- N x ICONDIRENTRY (16 bytes each)
- Image blobs, each consisting of:
* BITMAPINFOHEADER (40 bytes, biHeight = height*2)
* XOR bitmap (32bpp BGRA, bottom-up)
* AND mask (1bpp, bottom-up, rows padded to 4-byte boundaries)
If -Sizes is present, the script rescales the input image to multiple sizes and
packs all sizes into a single .ico. If -Sizes is omitted, the original image
size is used (single-entry ICO).
.PARAMETER InputPath
Path to the source bitmap (e.g., .bmp). Any format supported by System.Drawing is fine.
.PARAMETER OutputPath
Path to the output .ico.
.PARAMETER Sizes
Optional list of sizes (e.g., 16,32,48,64,128,256). If omitted, uses original image size.
.EXAMPLE
.\New-IcoFromBitmap.ps1 -InputPath .\logo.bmp -OutputPath .\logo.ico
.EXAMPLE
.\New-IcoFromBitmap.ps1 -InputPath .\logo.bmp -OutputPath .\logo_multi.ico -Sizes 16,32,48,64,128,256
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$InputPath,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[int[]]$Sizes
)
# --- Helpers to ensure System.Drawing is available on Windows
try {
Add-Type -AssemblyName System.Drawing -ErrorAction Stop
} catch {
throw "Failed to load System.Drawing. This script requires Windows with .NET Framework / System.Drawing."
}
# Dispose-safety helper
function Use-Object([IDisposable]$obj, [ScriptBlock]$script) {
try { & $script $obj } finally { if ($obj) { $obj.Dispose() } }
}
# Create a high-quality resized clone at 32bpp ARGB
function New-ResizedBitmap32([System.Drawing.Bitmap]$src, [int]$size) {
$bmp = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
try {
$g.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$g.DrawImage($src, 0, 0, $size, $size)
} finally {
$g.Dispose()
}
return $bmp
}
# Build a DIB blob for ICO (BITMAPINFOHEADER + XOR + AND)
function Get-IcoDibBytes([System.Drawing.Bitmap]$bmp32) {
# Ensure 32bpp surface
if ($bmp32.PixelFormat -ne [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) {
$tmp = New-Object System.Drawing.Bitmap($bmp32.Width, $bmp32.Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($tmp)
try {
$g.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
$g.DrawImage($bmp32, 0, 0, $bmp32.Width, $bmp32.Height)
} finally { $g.Dispose() }
$bmp32.Dispose()
$bmp32 = $tmp
}
$w = $bmp32.Width
$h = $bmp32.Height
if ($w -gt 256 -or $h -gt 256) {
throw "ICO entries must be <= 256x256 (got ${w}x${h}). Resize or use -Sizes."
}
$rect = [System.Drawing.Rectangle]::new(0,0,$w,$h)
$lock = $bmp32.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
try {
$stride = [Math]::Abs($lock.Stride)
$srcBytes = New-Object byte[] ($stride * $h)
[System.Runtime.InteropServices.Marshal]::Copy($lock.Scan0, $srcBytes, 0, $srcBytes.Length)
} finally {
$bmp32.UnlockBits($lock)
}
# XOR: 32bpp BGRA bottom-up, width*4 bytes per row (no extra padding beyond stride cropping)
$xorRowLen = $w * 4
$xorBytes = New-Object byte[] ($xorRowLen * $h)
for ($y = 0; $y -lt $h; $y++) {
$srcRowOff = $y * $stride
$dstRowOff = ($h - 1 - $y) * $xorRowLen
[Array]::Copy($srcBytes, $srcRowOff, $xorBytes, $dstRowOff, $xorRowLen)
}
# AND: 1bpp mask bottom-up, 1 bit per pixel, rows padded to 32-bit boundary
$andRowBytes = [int]([Math]::Ceiling($w / 32.0) * 4) # ((w + 31) / 32) * 4
$andBytes = New-Object byte[] ($andRowBytes * $h)
for ($y = 0; $y -lt $h; $y++) {
$dstRowOff = ($h - 1 - $y) * $andRowBytes
# Pack bits: MSB first per byte, 1 = transparent, 0 = opaque
for ($x = 0; $x -lt $w; $x++) {
$srcIdx = ($y * $stride) + ($x * 4)
$a = $srcBytes[$srcIdx + 3] # BGRA -> A
$bit = if ($a -eq 0) { 1 } else { 0 }
$byteIndex = [int]([Math]::Floor($x / 8))
$bitPos = 7 - ($x % 8)
if ($bit -eq 1) {
$andBytes[$dstRowOff + $byteIndex] = $andBytes[$dstRowOff + $byteIndex] -bor (1 -shl $bitPos)
}
}
# Remaining bits in last byte are left as 0; overall row already padded to 4 bytes
}
# BITMAPINFOHEADER (40 bytes), with biHeight = imageHeight * 2 (XOR + AND)
$ms = New-Object System.IO.MemoryStream
$bw = New-Object System.IO.BinaryWriter($ms)
# biSize
$bw.Write([int]40)
# biWidth
$bw.Write([int]$w)
# biHeight = h * 2 (ICO DIB: XOR+AND vertical stack)
$bw.Write([int]($h * 2))
# biPlanes
$bw.Write([int16]1)
# biBitCount
$bw.Write([int16]32)
# biCompression = BI_RGB (0)
$bw.Write([int]0)
# biSizeImage (XOR only; can be 0 for BI_RGB, but we set it)
$bw.Write([int]($xorBytes.Length))
# biXPelsPerMeter
$bw.Write([int]0)
# biYPelsPerMeter
$bw.Write([int]0)
# biClrUsed
$bw.Write([int]0)
# biClrImportant
$bw.Write([int]0)
# Append XOR then AND
$bw.Write($xorBytes)
$bw.Write($andBytes)
$bw.Flush()
$dib = $ms.ToArray()
$bw.Dispose()
$ms.Dispose()
# Return DIB blob
return ,@($dib, $w, $h)
}
# Load input image
if (-not (Test-Path -LiteralPath $InputPath)) {
throw "Input file not found: $InputPath"
}
$baseBmp = $null
try {
$baseBmp = [System.Drawing.Image]::FromFile($InputPath)
} catch {
throw "Unable to load image: $InputPath. Ensure it's a valid bitmap."
}
# Prepare list of bitmaps to pack into ICO
$bitmaps = New-Object System.Collections.Generic.List[System.Drawing.Bitmap]
try {
if ($Sizes -and $Sizes.Count -gt 0) {
$unique = $Sizes | Sort-Object -Unique
foreach ($s in $unique) {
if ($s -lt 1 -or $s -gt 256) {
throw "Invalid size $s. ICO entries must be between 1 and 256."
}
$resized = New-ResizedBitmap32 -src $baseBmp -size $s
$bitmaps.Add($resized) | Out-Null
}
} else {
# Use original size, but ensure <=256
$w = $baseBmp.Width
$h = $baseBmp.Height
if ($w -gt 256 -or $h -gt 256) {
Write-Warning "Source is ${w}x${h}. ICO limits entries to max 256. Will resize down to 256x256."
$max = [Math]::Min([Math]::Max($w, $h), 256)
$resized = New-ResizedBitmap32 -src $baseBmp -size $max
$bitmaps.Add($resized) | Out-Null
} else {
# Ensure 32bpp clone
$clone = New-ResizedBitmap32 -src $baseBmp -size $w
$bitmaps.Add($clone) | Out-Null
}
}
} finally {
$baseBmp.Dispose()
}
# Build all DIBs
$entries = @()
$totalCount = $bitmaps.Count
foreach ($bmp in $bitmaps) {
$dibInfo = Get-IcoDibBytes -bmp32 $bmp
$dib = $dibInfo[0]
$w = [int]$dibInfo[1]
$h = [int]$dibInfo[2]
$entries += [PSCustomObject]@{
Width = $w
Height = $h
DibBytes = $dib
}
}
# Write ICO: ICONDIR + N*ICONDIRENTRY + DIB blobs
$fs = [System.IO.File]::Open($OutputPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
$bw = New-Object System.IO.BinaryWriter($fs)
try {
# ICONDIR
$bw.Write([int16]0) # Reserved
$bw.Write([int16]1) # Type = 1 (icon)
$bw.Write([int16]$totalCount) # Count
# Precompute offsets
$offset = 6 + (16 * $totalCount) # header + all entries
# Write ICONDIRENTRY for each
foreach ($e in $entries) {
$w = [byte]([Math]::Min($e.Width, 255))
$h = [byte]([Math]::Min($e.Height, 255))
# ICO stores 256 as 0
$bWidth = if ($e.Width -eq 256) { [byte]0 } else { $w }
$bHeight = if ($e.Height -eq 256) { [byte]0 } else { $h }
$bw.Write([byte]$bWidth) # bWidth
$bw.Write([byte]$bHeight) # bHeight
$bw.Write([byte]0) # bColorCount (0 for >= 8bpp)
$bw.Write([byte]0) # bReserved
$bw.Write([int16]1) # wPlanes
$bw.Write([int16]32) # wBitCount
$bw.Write([int]$e.DibBytes.Length) # dwBytesInRes
$bw.Write([int]$offset) # dwImageOffset
$offset += $e.DibBytes.Length
}
# Write all DIB blobs
foreach ($e in $entries) {
$bw.Write($e.DibBytes)
}
$bw.Flush()
} finally {
$bw.Dispose()
$fs.Dispose()
# Dispose bitmaps
foreach ($b in $bitmaps) { $b.Dispose() }
}
Write-Host "ICO created: $OutputPath"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment