Last active
August 8, 2026 12:53
-
-
Save scriptingstudio/2a610d68b36b193cb90757922a8e8402 to your computer and use it in GitHub Desktop.
ICO Reader with export capability demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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