Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Last active August 8, 2026 12:53
Show Gist options
  • Select an option

  • Save scriptingstudio/2a610d68b36b193cb90757922a8e8402 to your computer and use it in GitHub Desktop.

Select an option

Save scriptingstudio/2a610d68b36b193cb90757922a8e8402 to your computer and use it in GitHub Desktop.
ICO Reader with export capability demo
<#
.SYNOPSIS
ICO file reader.
.DESCRIPTION
An AIO function to read and export icons stored within .ICO files. Read-Ico can export high-quality icons of all sizes, and all color formats to a number of formats, including ico, png, jpg, and gif.
Key features:
- Multisize high-quality ICO
- Ability to create a separate ICO for each size at once
- ICO integrity check
- Ability interactively select image file
- Ability to fetch files from internet
- Ability to see basic image information before export
- Ability to model an output file name
- All sizes are native, no scaling, except custom ones
- Control over ICO background color
- Control over image anti-aliasing
- A number of filters to adjust ICO sizes, pixel format
.PARAMETER Path
Mandatory. Position is 0. Specifies a filepath to the icon resource. To open interactive file selection dialog specify "*" or just a folder name. Besides Path can be a web-URL and in this case the file from internet is saved in the user's downloads.
.PARAMETER Output
Specifies a directory where the exports should be stored. If no directory is specified, all icons will be exported to the user's TEMP directory. To select the source's directory specify "*".
.PARAMETER Rename
Specifies an alternative name for the output file. If a new name starts with "+" it will join the input file base name.
.PARAMETER Type
Specifies the type of file you would like to export to. The default is "ico".
Valid values are ico, png, jpg, jpeg, and gif.
.PARAMETER Sizes
Primary size filter. Specifies a pixel size list of the exported icons. All icons will be squares, so if you want a 16x16 export, it would be -Sizes 16.
Standard values are 16, 20, 24, 32, 40, 48, 64, 72, 96, 128, 192, and 256. To select the standard size range specify -1. To disable this filter specify 0 or leave it behind.
.PARAMETER Series
Indicates to create a separate ICO for each size. One size, one ICO. Note, that multisize ICO will not create.
.PARAMETER Background
Specifies the background color for the icons to be exported. The default is "Transparent",
but images like JPEG don't have the alpha channel and their background cannot be transparent, by default it is black, therefore you may need to set a desired color. For JPEG, if not specified white will set.
You can specify the value in RGB(A) notation, i.e. "#123456EE"
Predefined colors you can find at https://learn.microsoft.com/en-us/dotnet/api/system.drawing.color
.PARAMETER Noaliasing
Experimental. Indicates to disable anti-aliasing that smoothes jagged edges on curves and diagonal lines.
.PARAMETER Exclude
Secondary size filter. Specifies a list of sizes to exclude values from the standard or native ranges. The default is 8. To disable this filter specify 0.
.PARAMETER Colors
Indicates that the pixel format is N bits per pixel. The default is 32. You can specify a list of values. To disable the color filter specify 0.
.PARAMETER Export
Indicates to extract icons according to -Sizes value. If Sizes' value equals to Path's icon sizes nothing will export.
.NOTES
Requires : PowerShell 5+
Version : 2.0
LastUpdate : 2026-August-8
.LINK
https://gist.github.com/scriptingstudio/2a610d68b36b193cb90757922a8e8402
.EXAMPLE
Read-Ico $icoPath | ft
.EXAMPLE
Read-Ico $icoPath -Sizes 256,128 -Output * -Export
.EXAMPLE
Read-Ico $icoPath -Sizes 256 -Output * -Type png -Export
.EXAMPLE
#>
function Read-Ico {
[CmdletBinding()]
[Alias('rdi','ireader')]
param (
[Parameter(Position=0,Mandatory)]
[alias('Fullname','FilePath','LiteralPath')]
[string] $Path,
[alias('directory')][string] $Output, # file folder name
[string] $Rename, # alternative name of output file
# commands
[alias('extract')][switch] $Export,
# output options
[ValidateSet('ico','png','jpg','jpeg','gif')]
[string] $Type = 'ico',
[alias('include')][int[]] $Sizes, # pri size filter (inclusive)
[alias('split')][switch] $Series, # one ICO per size
[int[]] $Exclude = 8, # complementary size filter (exclusive)
[int[]] $Colors = 32, # color filter (inclusive)
[System.Drawing.Color] $Background = 'Transparent', # primarily jpeg option
[alias('nal')][switch] $Noaliasing, # experimental
[switch] $Quiet # experimental; mute diagnostic (and error?) messages
)
$error.Clear()
if ($IsLinux -or $IsMacOS) {
Write-Warning 'Operation is not supported on this platform. Windows only.'
return
}
try {Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class IconHelper {
//public static IntPtr GetHicon(byte[] presbits, int dwResSize, int cxDesired, int cyDesired) {return CreateIconFromResourceEx(presbits, dwResSize, $true, 0x30000, cxDesired, cyDesired, 0);}
[DllImport("user32")]
public static extern IntPtr CreateIconFromResourceEx(byte[] presbits, int dwResSize, bool fIcon, int dwVer, int cxDesired, int cyDesired, int flags);
}
"@} catch {return}
function _message ($text, $type, $ForegroundColor, [switch]$NoNewline) {
if ($Quiet) {return}
if ($type -eq 'warn') {
Write-Warning $text
} elseif ($type -eq 'error') {
Write-Host "ERROR: $text" @param
} else {
$param = if ($ForegroundColor) {@{ForegroundColor=$ForegroundColor}} else {@{}}
Write-Host $text @param -NoNewline:$NoNewline
}
} # END _message
function _ico2image ([string]$outFile, $igroup, [int[]]$sizes, [int[]]$exclude, [int[]]$colors, [string]$type) {
if ($Type -eq 'jpg') {$Type = 'jpeg'}
if ($Type -eq 'jpeg' -and (-not $Background -or $Background -eq 'Transparent')) {$Background = 'White'}
foreach ($s in $sizes) {
if ($exclude.count -and $s -in $exclude) {continue}
if ($s -in $igroup.width) {
$native = $true
$icon = ($igroup | Where-Object {$_.width -eq $s})[0]
$bitmap = if ($Noaliasing) {
[System.Drawing.Bitmap]::FromHicon($icon.Handle)
} else {
[System.Drawing.Icon]::FromHandle($icon.Handle).ToBitmap()
}
}
else {
$native = $false
$nearest = $igroup | Where-Object {$_.width -gt $s}
if ($null -eq $nearest) {$nearest = @($igroup[0])}
$bitmap = if ($Noaliasing) {
[System.Drawing.Bitmap]::FromHicon($nearest[-1].handle)
} else {
[System.Drawing.Icon]::FromHandle($nearest[-1].Handle).ToBitmap()
}
}
$cbit = [System.Drawing.Bitmap]::GetPixelFormatSize($bitmap.PixelFormat)
if ($colors.count -and $cbit -notin $colors) {$bitmap.Dispose(); continue}
$width = $height = $s
$newBitmap = if ($native -and $type -ne 'jpeg') {$bitmap}
else {_resize $bitmap $width $height $Background}
$ext = if ($type -eq 'jpeg') {'jpg'} else {$type}
$imgFile = '{0}-{1}.{2}' -f $outFile,$s,$ext
try {
$newBitmap.Save($imgFile,[System.Drawing.Imaging.ImageFormat]$type)
} catch { # quite a rare case
Write-Warning "Failed to create file '$(Split-Path $imgFile -Leaf)'."
Remove-Item $imgFile -Force -ErrorAction 0 # zero length file created
continue
}
finally {$newBitmap.Dispose()}
if (Test-Path $imgFile) {
Write-Host "File '$imgFile' successfully created" -ForegroundColor Green
} else {
Write-Warning "Failed to create file '$(Split-Path $imgFile -Leaf)'."
}
}
} # END _ico2image
function _resize ([System.Drawing.Bitmap]$image, [int]$width, [int]$height, [System.Drawing.Color]$background='Transparent') {
# HighQualityBilinear VS HighQualityBicubic
# smaller, use bicubic; larger, use bilinear
# bicubic-smoother for enlarging; bicubic-sharper for reduction
$destRect = [System.Drawing.Rectangle]::new(0, 0, $width, $height)
$destImage = [System.Drawing.Bitmap]::new($width, $height)
$destImage.SetResolution($image.HorizontalResolution, $image.VerticalResolution)
$graphics = [System.Drawing.Graphics]::FromImage($destImage)
#$graphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
$graphics.Clear($background)
$graphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceOver
$graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$wrapMode = [System.Drawing.Imaging.ImageAttributes]::new()
$wrapMode.SetWrapMode([System.Drawing.Drawing2D.WrapMode]::TileFlipXY)
$graphics.DrawImage($image, $destRect, 0, 0, $image.Width, $image.Height, [System.Drawing.GraphicsUnit]::Pixel, $wrapMode)
$wrapMode.Dispose()
$graphics.Dispose()
$destImage
} # END _resize
if (([uri]$Path).Scheme -match '^http') {
if ($Path -match '\.icon?$') {
$fn = $home,'downloads',[IO.Path]::GetFileName($Path) -join '\'
if (Test-Path $fn) {Remove-Item $fn -Force -ErrorAction 0}
$ProgressPreference = 'SilentlyContinue'
$null = Invoke-WebRequest -Uri $Path -UseBasicParsing -OutFile $fn -ErrorAction 0
$ProgressPreference = 'Continue'
$Path = $fn
if (-not $output) {$output = "$home\downloads"}
} else {
Write-Warning "The file requested doesn't look like an image file. It must have corresponding extension. Check URL and try again."
return
}
} # internet request
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
# Check for dialog
if ($Path -in '*','%' -or (Test-Path $Path -PathType Container)) {
if ($Quiet) {return} # One or more parameters issued cannot be used together
Add-Type -AssemblyName System.Windows.Forms
$dialog = [System.Windows.Forms.OpenFileDialog]::new()
$dialog.Title = "Select ICO file"
$dialog.Filter = "ICO files (*.ico)|*.ico|All files (*.*)|*.*"
$dialog.Multiselect = $false
$dialog.CheckFileExists = $true
##$dialog.ShowPreview = $true # 'ShowPreview' cannot be found
$dialog.InitialDirectory = if ($Path -in '*','%') {"$home\desktop"} else {$Path}
#$dialog.RestoreDirectory = $true
$r = $dialog.ShowDialog()
$Path = if ($r -eq 'OK') {$dialog.filename}
$dialog.dispose()
if (-not $Path) {
Write-Host 'ERROR: File not selected.' -ForegroundColor Red
return
}
} # file dialog
[byte[]]$bytes = try {[System.IO.File]::ReadAllBytes($Path)} catch {}
# Basic validation
if ($bytes.Length -lt 4 -or ([System.BitConverter]::ToInt32($bytes[0..3],0)) -ne 0x10000) {
Write-Warning "'$([System.IO.Path]::GetFileName($Path))' is not a valid ICO file."
return
}
# ICO extractor
# https://en.wikipedia.org/wiki/ICO_(file_format)
# ICONDIRENTRY structure - 16 bytes
# 0 - width
# 1 - height
# 2 - colors
# 6-7 - bits per pixel
# 8-11 - size of image data
# 12-15 - offset of image data
##$nid = 0 # experimental; synthetic analogue of NID in EXE/DLL resources
$dirlength = 16 * [System.BitConverter]::ToInt16(($bytes[4,5]),0) # entrylength * amount
$icons = for (($i=6),($k=0); $i -lt $dirlength; ($i+=16),($k++)) {
$direntry = $bytes[$i..($i+15)]
$w = $direntry[0] # 0 if = 256
$h = $direntry[1] # 0 if = 256
$l = [System.BitConverter]::ToUInt32($direntry[8..11],0)
$bpp = [System.BitConverter]::ToUInt16(($direntry[6,7]),0)
$dataoffset = [System.BitConverter]::ToUInt32($direntry[12..15],0)
##if (-not $nid) {$nid = [int]($l/$dataoffset)} else {$nid++}
[byte[]]$imgbytes = $bytes[$dataoffset..($dataoffset+$l-1)]
$hicon = [IconHelper]::CreateIconFromResourceEx($imgbytes, $l, $true, 0x30000, $w, $h, 0)
[pscustomobject]@{
Index = $k # index in directory
Handle = $hicon # primary ID for any icon
Width = if ($w -eq 0) {256} else {$w} # normalize
Height = if ($h -eq 0) {256} else {$h} # normalize
Colors = $direntry[2] # 0 if >= 8bpp
PixelFormat = $bpp
Length = $l # size of raw bytes
Bytes = $imgbytes # raw bytes
}
} # icons
if (-not $Export) {return $icons}
# Resolve extraction limits
if ($Exclude.count) {
$Exclude = @($Exclude | Where-Object {$_ -gt 15 -and $_ -lt 257} | Sort-Object -Unique)
}
$Sizes = if ($Sizes -eq -1) {256,192,128,96,72,64,48,40,32,24,20,16} # max range
elseif ($Sizes -eq -2) {256,64,48,40,32,24,20,16} # Windows standard
elseif ($Sizes -eq -3) {256,48,32,24,16} # Windows app minimum
elseif ($Sizes.Count) {
@($Sizes | Where-Object {
(-not $Exclude.count -or $_ -notin $Exclude) -and
($_ -gt 15 -and $_ -lt 257)
} | Sort-Object -Unique -Descending)
} else {$icons.Width | Sort-Object -Descending}
if (-not $Series) { # nothing to do if nothing new
if ($null -eq (Compare-Object $Sizes $icons.Width -ErrorAction 0)) {return}
}
# Resolve file names
if ($Output -eq '*') {$Output = [System.IO.Path]::GetDirectoryName($Path)}
elseif ($Output) {
$Output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Output.TrimEnd('\'))
}
if (-not $Output -or -not (Test-Path $Output -PathType Container -ErrorAction 0)) {
$Output = "$env:TEMP\icons"
Write-Warning "Output directory is not specified or not available. Reset to default: '$Output'"
if (-not (Test-Path $Output)) { # create worktable
$null = New-Item -Path $Output -ItemType Directory -Force -ErrorAction 0
} else { # cleanup worktable
Remove-Item $Output\*.* -Force -ErrorAction 0
}
}
$basename = if ($Rename) {
if ($Rename -match '^+') {
[System.IO.Path]::GetFileNameWithoutExtension($Path) + $Rename.Substring(1)
} else {$Rename}
} else {
$sfx = if (-not $Series -and $Type -eq 'ico') {'-extract'}
[System.IO.Path]::GetFileNameWithoutExtension($Path) + $sfx
}
#region Export actions
if ($type -ne 'ico') {
_ico2image "$output\$basename" -igroup $icons -sizes $sizes -exclude $exclude -colors $colors -type $type
if ($output -like "$env:temp*") {Invoke-Item $Output}
return
}
# Sort icons by size and create missing if any
$natives = $icons.Width
[array]$icoinfo = $Sizes | ForEach-Object {
$size = $_
if ($size -notin $natives) {
$nearest = $icons | Where-Object {$_.width -gt $size}
if (-not $nearest) {$nearest = @($icons[0])}
$bmp = if ($Noaliasing) {
[System.Drawing.Bitmap]::FromHicon($nearest[-1].handle)
} else {
[System.Drawing.Icon]::FromHandle($nearest[-1].handle).ToBitmap()
}
$newBitmap = _resize $bmp $size $size
$memoryStream = [System.IO.MemoryStream]::new()
$newBitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png)
$newIcon = [pscustomobject]@{} | Select-Object Index,Handle,Width,Height,Colors,PixelFormat,Length,Bytes
$newIcon.width = $newIcon.height = $size
$newIcon.Bytes = $memoryStream.ToArray()
$newIcon.Length = $newIcon.Bytes.Length
#$newIcon.Handle = [IconHelper]::CreateIconFromResourceEx($newIcon.Bytes, $newIcon.Length, $true, 0x30000, $size, $size, 0)
$newBitmap.Dispose()
$memoryStream.Close()
$memoryStream.Dispose()
$newIcon
} else {
$icons | Where-Object {$_.width -eq $size}
}
} | Sort-Object width -Descending
# Write ICO
$icoCount = if ($Series) {$icoinfo.Count} else {1}
for ($i=0; $i -lt $icoCount; $i++) {
$icoWork = if ($Series) {@($icoinfo[$i])} else {$icoinfo}
$icofile = if ($Series) {
'{0}\{1}-{2}.ico' -f $Output, $basename, $icoWork[0].width
} else {
'{0}\{1}.ico' -f $Output, $basename
}
try {[System.IO.Stream]$outputStream = [System.IO.FileStream]::new($icofile, [System.IO.FileMode]::OpenOrCreate)} catch {continue}
$iconWriter = if ($outputStream) {[System.IO.BinaryWriter]::new($outputStream)}
if ($null -eq $iconWriter) {$outputStream.Close()}
if ($null -eq $outputStream -or $null -eq $iconWriter) {continue}
# File header
# 0-1 reserved, 0
$iconWriter.Write([byte]0)
$iconWriter.Write([byte]0)
# 2-3 image type, 1 = icon, 2 = cursor
$iconWriter.Write([int16]1)
# 4-5 number of images
$iconWriter.Write([int16]@($icoWork).Length)
$offset = 6 + (16 * @($icoWork).Length)
foreach ($item in $icoWork) {
if ($item.width -eq 256) {$item.width = 0}
if ($item.height -eq 256) {$item.height = 0}
# ICONDIRENTRY
# 0 image width
$iconWriter.Write([byte]$item.width)
# 1 image height
$iconWriter.Write([byte]$item.height)
# 2 number of colors (0 if >= 8bpp)
$iconWriter.Write([byte]0)
# 3 reserved
$iconWriter.Write([byte]0)
# 4-5 color planes
$iconWriter.Write([int16]0)
# 6-7 bits per pixel
$iconWriter.Write([int16]32)
# 8-11 size of image data
$iconWriter.Write([int]$item.Length)
# 12-15 offset of image data
$iconWriter.Write([int]$offset)
$offset += $item.Length
}
# write image data
foreach ($item in $icoWork) {
$iconWriter.Write($item.Bytes)
}
# Cleanup
if ($iconWriter) {
$iconWriter.Flush()
$iconWriter.Close()
$iconWriter.Dispose()
}
if ($outputStream) {$outputStream.Close()}
if (Test-Path $icofile) {
Write-Host "File '$icofile' successfully created" -ForegroundColor Green
}
} # write icon
if ($Output -like "$env:temp*") {Invoke-Item $Output}
#endregion Export actions
} # END Read-Ico
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment