Last active
July 21, 2026 19:01
-
-
Save scriptingstudio/75ada25f1b943da9339b54d6f7662fac to your computer and use it in GitHub Desktop.
Simple image extractor from exe/dll/osx/cpl files
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 | |
| An AIO function to export icons stored within .DLL and .EXE files. | |
| .DESCRIPTION | |
| Export-Icon can export icons of all sizes, and all color formats to a number of formats, including bmp, png, jpg, gif, emf, exif, icon, tiff, heif, and webp. In addition, it can also export to a different size. All sizes are native, no scaling, except custom sizes. | |
| This function quickly exports *all* icons stored within the resource file. | |
| Export-Icon is independent on .NET version. It uses a custom icon extractor engine because Windows PowerShell doesn't have [System.Drawing.Icon]::ExtractIcon method (https://learn.microsoft.com/en-us/dotnet/api/system.drawing.icon?view=netframework-4.8.1). In addition, PowerShell 7.6+ [System.Drawing.Icon]::ExtractIcon method has a limited capability. Besides, neither ExtractIcon* functions from user32.dll have a parameter associated with size. | |
| .PARAMETER Path | |
| Mandatory. Position is 0. Specifies a filepath to the resource. | |
| .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. The value "*" selects the source's directory. | |
| .PARAMETER Rename | |
| Specifies an alternative name for the output files. | |
| .PARAMETER Type | |
| Specifies the type of file you would like to export to. The default is "ico". | |
| Valid values are ico, icon, bmp, png, jpg, jpeg, gif, emf, exif, tiff, wmf, heif, and webp. | |
| Note, that heif and webp are only available in PowerShell 7+. | |
| .PARAMETER Range | |
| Specifies index sets of icons to be exported. Empty value means *all* icons. | |
| .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, 24, 32, 48, 64, 72, 96, 128, and 256. The default is default size from the source. To select the standard size range specify -1. To disable this filter specify 0. | |
| .PARAMETER Exclude | |
| Secondary size filter. Specifies a list of sizes to exclude. The defaults are 8,20,40. 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 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 be set. | |
| You can specify the value in RGB(A) notation, i.e. "#123456EE" | |
| Predefined colors see here https://learn.microsoft.com/en-us/dotnet/api/system.drawing.color | |
| .PARAMETER Base64 | |
| Experimental. Indicates to create a complementary file for each exported icon with content in Base64 format. | |
| .PARAMETER List | |
| Indicates to get basic information about images stored in the source file. | |
| .NOTES | |
| Requires : PowerShell 5+ | |
| Version : 3.2 | |
| LastUpdate : 2026-July-21 | |
| .LINK | |
| https://gist.github.com/scriptingstudio/75ada25f1b943da9339b54d6f7662fac | |
| .EXAMPLE | |
| Export-Icon C:\windows\system32\imageres.dll | |
| Exports all icons stored within C:\windows\system32\imageres.dll to $env:temp\icons. Creates directory if required and automatically opens output directory. | |
| .EXAMPLE | |
| Export-Icon -Path "C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient.exe" -Sizes 128,64 -Type png -Output C:\myicons | |
| Exports the high-quality icon within VpxClient.exe to a transparent png in C:\myicons\. Resizes the exported image to 64x64 and 128x128. | |
| .EXAMPLE | |
| Export-Icon C:\windows\system32\imageres.dll -Range (10..24+0..5) | |
| Exports icons specified by index to $env:temp\icons. Creates directory if required and automatically opens output directory. | |
| .EXAMPLE | |
| Export-Icon C:\windows\system32\imageres.dll -Rename myicon -Type jpg -Background Red | |
| Exports all icons replacing default name "imageres" with "myicon" and setting background color to Red. | |
| #> | |
| function Export-Icon { | |
| [CmdletBinding()] | |
| [Alias('epi','e2i','cvi','ConvertTo-Icon')] | |
| param ( | |
| [Parameter(Position=0,Mandatory)] | |
| [alias('Fullname','FilePath','LiteralPath')] | |
| [string] $Path, | |
| [alias('directory')][string] $Output, # file folder name | |
| [alias('alter')][string] $Rename, # the alternative name for output files | |
| # commands | |
| [alias('info')][switch] $List, | |
| # output options | |
| [ValidateSet('ico','bmp','png','jpg','gif','jpeg','emf','exif','icon','tiff','tif','wmf','heif','webp')] | |
| [string] $Type = 'ico', | |
| [alias('b64')][switch] $Base64, # experimental; create a complementary file; for what types? | |
| [System.Drawing.Color] $Background = 'Transparent', | |
| # filters | |
| [alias('select','slice')][int[]] $Range, # icon indices | |
| [alias('include')][int[]] $Sizes, # pri size filter | |
| [int[]] $Exclude = (8,20), # complementary size filter | |
| [int[]] $Colors = 32 # color filter | |
| ) | |
| if ($IsLinux -or $IsMacOS) { | |
| Write-Warning 'Operation is not supported on this platform. Windows only.' | |
| return | |
| } | |
| # Extractor engine module | |
| $iconHelper = @" | |
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.Globalization; | |
| public sealed class IconInfo : IDisposable | |
| { | |
| private IconInfo(int groupIndex, string groupId, int index, string id, IntPtr handle) | |
| { | |
| GroupIndex = groupIndex; | |
| GroupId = groupId; | |
| Index = index; | |
| Id = id; | |
| Handle = handle; | |
| Icon = null; // initializes later because of .NET not having System.Drawing namespace | |
| } | |
| public IntPtr Handle { get; private set; } | |
| public int Index { get; private set; } | |
| public int GroupIndex { get; private set; } | |
| public string Id { get; private set; } | |
| public string GroupId { get; private set; } | |
| public object Icon { get; set; } | |
| public void Dispose() {DestroyIcon(Handle);} | |
| public static int GetTotal(string iconFilePath) | |
| { | |
| if (string.IsNullOrEmpty(iconFilePath)) return 0; // or ArgumentNullException? | |
| IntPtr large; IntPtr small; | |
| return ExtractIconEx(iconFilePath, -1, out large, out small, 1); | |
| } | |
| public static List<IconInfo> ImportIcons(string iconFilePath, int? byIndexOrResourceId = null) | |
| { | |
| if (string.IsNullOrEmpty(iconFilePath)) return null; // or ArgumentNullException? | |
| var list = new List<IconInfo>(); | |
| var handle = LoadLibraryEx(iconFilePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE); | |
| if (handle != IntPtr.Zero) | |
| { | |
| try | |
| { | |
| list = ImportIcons(handle, byIndexOrResourceId); | |
| } | |
| finally | |
| { | |
| FreeLibrary(handle); | |
| } | |
| } | |
| return list; | |
| } | |
| private static List<IconInfo> ImportIcons(IntPtr handle, int? byIndexOrResourceId) | |
| { | |
| var list = new List<IconInfo>(); | |
| var entries = new Dictionary<ushort, GRPICONDIRENTRY>(); | |
| var groupIndices = new Dictionary<ushort, int>(); | |
| var groupIds = new Dictionary<ushort, string>(); | |
| var groupIndex = 0; | |
| if (EnumResourceNames(handle, new IntPtr(RT_GROUP_ICON), (m, t, n, lp) => | |
| { | |
| if (byIndexOrResourceId.HasValue && byIndexOrResourceId.Value >= 0 && byIndexOrResourceId.Value != groupIndex) | |
| { | |
| groupIndex++; | |
| return true; | |
| } | |
| string name; | |
| if (n.ToInt64() > ushort.MaxValue) | |
| { | |
| name = Marshal.PtrToStringAuto(n); | |
| } | |
| else | |
| { | |
| name = n.ToInt32().ToString(CultureInfo.InvariantCulture); | |
| } | |
| if (byIndexOrResourceId.HasValue && byIndexOrResourceId.Value < 0 && !string.Equals((-byIndexOrResourceId.Value).ToString(CultureInfo.InvariantCulture), name, StringComparison.Ordinal)) | |
| { | |
| groupIndex++; | |
| return true; | |
| } | |
| try | |
| { | |
| ExtractIconGroupEntries(handle, n, t, groupIndex, entries, groupIndices, groupIds); | |
| groupIndex++; | |
| } | |
| catch | |
| { | |
| // do nothing | |
| } | |
| return true; | |
| }, IntPtr.Zero)) | |
| { | |
| EnumResourceNames(handle, new IntPtr(RT_ICON), (m, t, n, lp) => | |
| { | |
| var iconHandle = ExtractIcon(handle, n, t, entries); | |
| if (iconHandle != IntPtr.Zero) | |
| { | |
| var info = new IconInfo(groupIndices[(ushort)n.ToInt32()], groupIds[(ushort)n.ToInt32()], n.ToInt32() - 1, n.ToString(), iconHandle); // | |
| list.Add(info); | |
| } | |
| return true; | |
| }, IntPtr.Zero); | |
| } | |
| return list; | |
| } | |
| private static void ExtractIconGroupEntries(IntPtr module, IntPtr name, IntPtr type, int index, Dictionary<ushort, GRPICONDIRENTRY> entries, Dictionary<ushort, int> groupIndices, Dictionary<ushort, string> groupIds) | |
| { | |
| var handle = FindResource(module, name, type); | |
| if (handle == IntPtr.Zero) return; | |
| var size = SizeofResource(module, handle); | |
| if (size == 0) return; | |
| var resource = LoadResource(module, handle); | |
| if (resource == IntPtr.Zero) return; | |
| var ptr = LockResource(resource); | |
| if (ptr == IntPtr.Zero) return; | |
| // GRPICONDIR | |
| ptr += 2; // idReserved; | |
| var idtype = Marshal.ReadInt16(ptr); | |
| if (idtype != 1) return; // idType, 1 for ICO | |
| var elementSize = Marshal.SizeOf<GRPICONDIRENTRY>(); | |
| ptr += 2; | |
| var count = Marshal.ReadInt16(ptr); | |
| ptr += 2; | |
| for (var i = 0; i < count; i++) | |
| { | |
| var entry = Marshal.PtrToStructure<GRPICONDIRENTRY>(ptr); | |
| ptr += elementSize; | |
| entries[entry.nId] = entry; | |
| // is it a string or an id? | |
| groupIndices[entry.nId] = index; | |
| if (name.ToInt64() > ushort.MaxValue) | |
| { | |
| var id = Marshal.PtrToStringAuto(name); | |
| groupIds[entry.nId] = id; | |
| } | |
| else | |
| { | |
| groupIds[entry.nId] = "#" + name.ToInt32(); | |
| } | |
| } | |
| } | |
| private static IntPtr ExtractIcon(IntPtr module, IntPtr name, IntPtr type, Dictionary<ushort, GRPICONDIRENTRY> entries) | |
| { | |
| GRPICONDIRENTRY x; | |
| if (!entries.TryGetValue((ushort)name.ToInt32(), out x)) return IntPtr.Zero; | |
| var hres = FindResource(module, name, type); | |
| if (hres == IntPtr.Zero) return IntPtr.Zero; | |
| var size = SizeofResource(module, hres); | |
| if (size == 0) return IntPtr.Zero; | |
| var res = LoadResource(module, hres); | |
| if (res == IntPtr.Zero) return IntPtr.Zero; | |
| var ptr = LockResource(res); | |
| if (ptr == IntPtr.Zero) return IntPtr.Zero; | |
| return CreateIconFromResourceEx(ptr, size, true, 0x30000, 0, 0, 0); | |
| } | |
| private delegate bool EnumResNameProc(IntPtr hModule, IntPtr lpszType, IntPtr lpszName, IntPtr lParam); | |
| [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)] | |
| private static extern bool EnumResourceNames(IntPtr hModule, IntPtr lpszType, EnumResNameProc lpEnumFunc, IntPtr lParam); | |
| [DllImport("kernel32", CharSet = CharSet.Unicode)] | |
| private static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType); | |
| [DllImport("kernel32")] | |
| private static extern int SizeofResource(IntPtr hModule, IntPtr hResInfo); | |
| [DllImport("kernel32")] | |
| private static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo); | |
| [DllImport("user32")] | |
| private static extern IntPtr CreateIconFromResourceEx(IntPtr presbits, int dwResSize, bool fIcon, int dwVer, int cxDesired, int cyDesired, int flags); | |
| [DllImport("user32")] | |
| private static extern bool DestroyIcon(IntPtr handle); | |
| [DllImport("kernel32", CharSet = CharSet.Unicode)] | |
| private static extern IntPtr LockResource(IntPtr hResData); | |
| [DllImport("kernel32", CharSet = CharSet.Unicode)] | |
| private static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, int dwFlags); | |
| [DllImport("kernel32")] | |
| private static extern bool FreeLibrary(IntPtr hModule); | |
| [DllImport("Shell32", EntryPoint = "ExtractIconExW", CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] | |
| private static extern int ExtractIconEx(string sFile, int iIndex, out IntPtr piLargeVersion, out IntPtr piSmallVersion, int amountIcons); | |
| private const int LOAD_LIBRARY_AS_DATAFILE = 0x2; | |
| private const int LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x20; | |
| private const int RT_ICON = 3; | |
| private const int RT_GROUP_ICON = RT_ICON + 11; | |
| [StructLayout(LayoutKind.Sequential, Pack = 1)] | |
| private struct GRPICONDIRENTRY | |
| { | |
| public byte bWidth; | |
| public byte bHeight; | |
| public byte bColorCount; | |
| public byte bReserved; | |
| public short wPlanes; | |
| public short wBitCount; | |
| public int dwBytesInRes; | |
| public ushort nId; | |
| }; | |
| } | |
| "@ | |
| # TODO: improve try/catch | |
| try {$null = [IconInfo]} | |
| catch {Add-Type -TypeDefinition $iconHelper -IgnoreWarnings -ErrorAction 0} | |
| try {$null = [System.Drawing.Icon]} | |
| catch {Add-Type -AssemblyName System.Drawing -IgnoreWarnings -ErrorAction 0} | |
| #region Icon helpers module | |
| function _importIcon ([string]$file, [int]$index=0) { | |
| [IconInfo]::ImportIcons($file, $index) | ForEach-Object { | |
| if ($_) { | |
| $_.Icon = [System.Drawing.Icon]::FromHandle($_.Handle) | |
| $_ | |
| } | |
| } | |
| } # END _importIcon | |
| function _convert2ico ([string]$icofile, $igroup, [int[]]$sizes, [int[]]$exclude, [int[]]$colors) { | |
| $png = "$env:TEMP\_tempexporticon_.png" | |
| try {[System.IO.Stream]$outputStream = [System.IO.FileStream]::new($icofile, [System.IO.FileMode]::OpenOrCreate)} catch {return $false} | |
| $sizes = @($sizes | Where-Object {$_ -gt 7 -and $_ -lt 257} | Sort-Object -Unique -Descending) | |
| if ($sizes.count -eq 0) {$sizes = $igroup.icon.width} | |
| $imageSizes = [System.Collections.Generic.List[int]]::new() | |
| $imageStreams = foreach ($s in $sizes) { | |
| if ($exclude.count -and $s -in $exclude) {continue} | |
| if ($s -in $igroup.icon.width) { | |
| $bitmap = ($igroup.icon | Where-Object {$_.width -eq $s})[0].ToBitmap() | |
| $native = $true | |
| } | |
| else { # custom size | |
| # downscale (smaller) from the nearest | |
| # smaller, use bicubic; larger, use bilinear | |
| $nearest = $igroup.icon | Where-Object {$_.width -gt $s} | |
| $bitmap = $nearest[-1].icon.ToBitmap() | |
| $native = $false | |
| } | |
| $cbit = [System.Drawing.Bitmap]::GetPixelFormatSize($bitmap.PixelFormat) | |
| $bitmap.Save($png) | |
| $bitmap.Dispose() | |
| if ($colors.count -and $cbit -notin $colors) {continue} | |
| [System.IO.Stream]$inputStream = [System.IO.FileStream]::new($png, [System.IO.FileMode]::Open) | |
| if ($null -eq $inputStream) {continue} | |
| $inputBit = [System.Drawing.Bitmap]::FromStream($inputStream) | |
| $inputStream.Close() | |
| Remove-Item -LiteralPath $png -Force -ErrorAction 0 | |
| if ($null -eq $inputBit) {continue} | |
| $height = $inputBit.Height | |
| $width = $inputBit.Width | |
| #$width = $s # TODO: explore | |
| #$height = $inputBit.Height / $inputBit.Width * $s | |
| $imageSizes.Add($width) | |
| $newBitmap = if ($native) { | |
| [System.Drawing.Bitmap]::new($inputBit, [System.Drawing.Size]::new($width, $height)) | |
| } else { | |
| _resize $inputBit $width $height | |
| } | |
| ##if ($null -eq $newBitmap) {continue} # $sizes.count != $imageStreams.count | |
| $memoryStream = [System.IO.MemoryStream]::new() | |
| $newBitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png) | |
| $memoryStream | |
| } # sizes | |
| # possible issue: $imageSizes.count != $imageStreams.count | |
| if ($null -eq $imageStreams -or -not @($imageStreams).count) { | |
| $outputStream.Close() | |
| return $false | |
| } | |
| $iconWriter = if ($outputStream) {[System.IO.BinaryWriter]::new($outputStream)} | |
| if ($null -eq $iconWriter) {$outputStream.Close()} | |
| if ($null -eq $outputStream -or $null -eq $iconWriter) {return $false} | |
| # Normalize ico sizes for writing | |
| $sizes = @($imageSizes) | |
| if ($sizes.count -eq 1) { # 256 alone doesn't work, so we apply a pinpoint hack, 255 | |
| if ($sizes[0] -eq 256) {$sizes[0] = 255} | |
| } else { # 256 will fail, so we apply a pinpoint hack, 0 | |
| $sizes = @(foreach ($item in $sizes) { | |
| if ($item -eq 256) {0} else {$item} | |
| }) | |
| } | |
| # 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]$sizes.Length) | |
| $offset = 6 + (16 * $sizes.Length) | |
| for ($i = 0; $i -lt $sizes.Length; $i++) { | |
| # image entry 1 | |
| # 0 image width | |
| $iconWriter.Write([byte]$sizes[$i]) | |
| # 1 image height | |
| $iconWriter.Write([byte]$sizes[$i]) | |
| # 2 number of colors | |
| $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]$imageStreams[$i].Length) | |
| # 12-15 offset of image data | |
| $iconWriter.Write($offset) | |
| $offset += [int]$imageStreams[$i].Length; | |
| } | |
| # write image data | |
| # png data must contain the whole png data file | |
| for ($i = 0; $i -lt $sizes.Length; $i++) { | |
| $iconWriter.Write($imageStreams[$i].ToArray()) | |
| $imageStreams[$i].Close() | |
| $imageStreams[$i].Dispose() | |
| } | |
| # Cleanup | |
| $iconWriter.Flush() | |
| $outputStream.Close() | |
| Remove-Item -LiteralPath $png -Force -ErrorAction 0 | |
| $true | |
| } # END _convert2ico | |
| function _validateIco ([string]$icoPath) { | |
| if (Test-Path $icoPath) { | |
| $param = @{TotalCount = 4} | |
| if ($PSVersionTable.PSVersion.Major -lt 6) {$param['Encoding'] = 'Byte'} | |
| else {$param['AsByteStream'] = $true} | |
| $signature = Get-Content -LiteralPath $icoPath @param -ErrorAction 0 | |
| if ($signature -and ([System.BitConverter]::ToInt32($signature, 0)) -eq 0x10000) { | |
| #Write-Host "ICO file successfully created in '$Output'" -ForegroundColor Green | |
| } else { | |
| Write-Host "ERROR: '$([System.IO.Path]::GetFileName($icoPath))' is not a valid ICO file." -ForegroundColor Red | |
| Remove-Item -LiteralPath $icoPath -Force -ErrorAction 0 | |
| } | |
| } else { | |
| Write-Host "ERROR: ICO file is not created." -ForegroundColor Red | |
| } | |
| } # END _validateIco | |
| function _convert2image ([string]$outFile, $igroup, [int[]]$sizes, [int[]]$exclude, [int[]]$colors, [string]$type) { | |
| # using: $noalpha, $background | |
| $sizes = @($sizes | Where-Object {$_ -gt 7 -and $_ -lt 257} | Sort-Object -Unique -Descending) | |
| if ($sizes.count -eq 0) {$sizes = $igroup.icon.width} | |
| $opstatus = 0 | |
| foreach ($s in $sizes) { | |
| if ($exclude.count -and $s -in $exclude) {continue} | |
| if ($s -in $igroup.icon.width) { | |
| $native = $true | |
| $bitmap = ($igroup.icon | Where-Object {$_.width -eq $s})[0].ToBitmap() | |
| } | |
| else { | |
| $native = $false | |
| $nearest = $igroup.icon | Where-Object {$_.width -gt $s} | |
| $bitmap = $nearest[-1].icon.ToBitmap() | |
| } | |
| $cbit = [System.Drawing.Bitmap]::GetPixelFormatSize($bitmap.PixelFormat) | |
| if ($colors.count -and $cbit -notin $colors) {$bitmap.Dispose(); continue} | |
| $height = $bitmap.Height | |
| $width = $bitmap.Width | |
| #$width = $s # TODO: explore | |
| #$height = $bitmap.Height / $bitmap.Width * $s | |
| $newBitmap = if ($native -and $type -notmatch $noalpha) { | |
| #[System.Drawing.Bitmap]::new($bitmap, [System.Drawing.Size]::new($width, $height)) | |
| $bitmap | |
| } else {_resize $bitmap $width $height $Background} | |
| $imgFile = $outFile.replace('%size%',$s) | |
| try { | |
| $newBitmap.Save($imgFile,[System.Drawing.Imaging.ImageFormat]$type) | |
| if ($base64) {_toBase64 $newBitmap $imgFile} | |
| } catch {} | |
| $newBitmap.Dispose() | |
| if (-not (Test-Path $imgFile)) { | |
| Write-Warning "Failed to create file '$(Split-Path $imgFile -Leaf)'." | |
| } else {$opstatus++} | |
| } # sizes | |
| $opstatus | |
| } # END _convert2image | |
| function _resize ([System.Drawing.Image]$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.Clear($background) | |
| #$graphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy | |
| $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 | |
| function _toBase64 ($bitmap, $imgFile) { | |
| $icon = [System.Drawing.Icon]::FromHandle($bitmap.GetHicon()) | |
| $memoryStream = [System.IO.MemoryStream]::new() | |
| $icon.Save($memoryStream) | |
| $bytes = $memoryStream.ToArray() | |
| $memoryStream.Flush() | |
| $memoryStream.Dispose() | |
| $icon.Dispose() | |
| $b64file = [System.IO.Path]::ChangeExtension($imgFile,'txt') | |
| [Convert]::ToBase64String($bytes) | Out-File -FilePath $b64file -Encoding utf8 -Force -ErrorAction 0 | |
| } # END _toBase64 | |
| function _getinfo ($iconGroup) { | |
| $iconGroup | ForEach-Object { | |
| $bmp = $_.Icon.ToBitmap() | |
| [pscustomobject]@{ # anything else? | |
| Size = '{0}x{1}' -f $bmp.Width,$bmp.Height | |
| Dimension = '{0}x{1}' -f $bmp.PhysicalDimension.Width,$bmp.PhysicalDimension.Height | |
| Resolution = '{0}x{1}' -f $bmp.HorizontalResolution,$bmp.VerticalResolution | |
| Colors = [System.Drawing.Bitmap]::GetPixelFormatSize($bmp.PixelFormat) | |
| Flags = $bmp.Flags #[ImageFlags]$bmp.Flags | |
| Group = $_.GroupIndex # primary key | |
| Id = $_.GroupId | |
| #$bmp.PropertyItems | |
| } | |
| $bmp.Dispose() | |
| } | |
| } # END _getinfo | |
| #endregion Icon helpers | |
| # Resolve target image type | |
| if (-not $type) {$type = 'ico'} | |
| if (-not $list -and $Type -match 'Heif|Webp' -and $PSVersionTable.PSEdition -ne 'Core') { | |
| Write-Warning "The type specified ($Type) is not supported in Windows PowerShell. Please run this script in PowerShell 7+." | |
| return | |
| } | |
| switch ($type) { | |
| 'jpg' {$type = 'jpeg'} | |
| 'icon' {$type = 'ico'} | |
| 'tif' {$type = 'tiff'} | |
| } | |
| # image formats that don't have alpha channel | |
| $noalpha = 'jpeg' # regex | |
| if ($type -match $noalpha -and (-not $Background -or $Background -eq 'Transparent')) {$Background = 'White'} | |
| # Resolve file paths | |
| $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) | |
| if (-not (Test-Path $Path -PathType Leaf -ErrorAction 0)) { | |
| Write-Host 'ERROR: Input file is not found. Check the file path and try again.' -ForegroundColor Red | |
| return | |
| } | |
| if ($Path -notmatch '\.(exe|dll|ocx|cpl)$') { | |
| Write-Warning "It doesn't look like that the input file can contain icons." | |
| } | |
| if ($Output -eq '*') {$Output = [System.IO.Path]::GetDirectoryName($Path)} | |
| elseif ($Output) { | |
| $Output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Output.TrimEnd('\')) | |
| } | |
| if (-not $list -and -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) {$Rename} else {[System.IO.Path]::GetFileNameWithoutExtension($Path)} | |
| # Resolve extraction limits | |
| if ($Exclude.count) { | |
| $Exclude = @($Exclude | Where-Object {$_ -gt 15 -and $_ -lt 257} | Sort-Object -Unique) | |
| } | |
| if ($Colors.count) { | |
| $Colors = @($Colors | Where-Object {$_ -gt 0} | Sort-Object -Unique) | |
| } | |
| if ($Sizes -eq -1) {$Sizes = 16,20,24,32,40,48,64,72,96,128,256} # 192 | |
| elseif ($Sizes.Count) { | |
| $Sizes = $Sizes | Where-Object { | |
| (-not $Exclude.count -or $_ -notin $Exclude) -and | |
| ($_ -gt 15 -and $_ -lt 257) | |
| } | Sort-Object -Unique -Descending | |
| } else {$Sizes = $null} | |
| $startIndex = $lastIndex = 0 | |
| if ($list) {$Range = $null} | |
| if ($Range.Count) { | |
| $Range = @($Range | Where-Object {$_ -gt -1} | Sort-Object -Unique) | |
| if ($Range.Count) {$startIndex,$lastIndex = $Range[0],$Range[-1]} | |
| } | |
| $index = $startIndex # icon counter & index | |
| $total = [IconInfo]::GetTotal($Path) | |
| if (-not $total -or $index -gt $total) { | |
| Write-Warning "No icons found or start index was invalid." | |
| return | |
| } | |
| Write-Host "Icons found: $total" | |
| if ($lastIndex -lt 1 -or $lastIndex -gt $total) {$lastIndex = $total} | |
| $info = [System.Collections.Generic.List[object]]::new() | |
| # Extract icons | |
| # TODO: improve operation status ($opstatus) | |
| $opstatus = 0 | |
| if (-not $list) {Write-Host "Exporting to $($type.ToUpper())..."} | |
| do { # till extraction error or range limit | |
| if ($Range.Count -and $index -notin $Range) {$index++; continue} | |
| Write-Host "Extracting image #$index of $lastIndex" -NoNewline | |
| [array]$iconGroup = _importIcon $Path $index # handler group | |
| if (-not $iconGroup.count) {$iconGroup = $null} | |
| if ($null -eq $iconGroup) { | |
| Write-Host ' - failed' -ForegroundColor Red | |
| $index++ | |
| continue | |
| } | |
| Write-Host (' - {0} size(s)' -f $iconGroup.count) -ForegroundColor Green | |
| if ($list) { | |
| $info.AddRange(@(_getinfo $iconGroup)) | |
| } | |
| elseif ($type -eq 'ico') { | |
| $outFile = '{0}\{1}-{2}.{3}' -f $output,$basename,$index,$type | |
| $opstatus += _convert2ico $outFile $iconGroup $sizes $exclude $colors | |
| _validateIco $outFile | |
| } | |
| else { | |
| $outFile = '{0}\{1}-{2}-%size%.{3}' -f $output,$basename,$index,$type | |
| $opstatus += _convert2image $outFile $iconGroup $sizes $exclude $colors $type | |
| } | |
| # destroy current icon | |
| $iconGroup | ForEach-Object { | |
| $_.Icon.Dispose() | |
| $_.Dispose() | |
| } | |
| $iconGroup.Clear() | |
| $index++ | |
| } while ($null -ne $iconGroup -and $index -lt ($startIndex + $lastIndex)) | |
| Write-Host | |
| # Finishing | |
| if ($index -eq $startIndex) { | |
| Write-Warning "No icons to extract." | |
| if ($Output -like "$env:temp*") {Remove-Item -Path $Output -Force -Recurse -ErrorAction 0} | |
| } | |
| elseif ($list -and $info.count) { | |
| #Write-Host "File '$Path' content" | |
| $info | Group-Object -property Group | ForEach-Object { | |
| [pscustomobject]@{ | |
| Icon = $_.name | |
| Images = $_.count | |
| Size = ($_.group.size | Sort-Object {[int]$_.split('x')[0]} -Unique) -join ', ' | |
| Resolution = ($_.group.Resolution | Sort-Object -Unique) -join ', ' | |
| Colors = ($_.group.Colors | Sort-Object -Unique) -join ', ' | |
| Id = ($_.group.Id | Sort-Object -Unique) -join ', ' | |
| #Dimension = ($_.group.Dimension | Sort-Object -Unique) -join ', ' | |
| #Flags = ($_.group.Flags | Sort-Object -Unique) -join ', ' | |
| File = $Path | |
| } | |
| } | |
| } | |
| elseif ($opstatus) { # TODO: improve statistics | |
| Write-Host "Icons successfully created in '$Output' [$opstatus/$($lastIndex - $startIndex + 1) - file/image]" | |
| if ($Output -like "$env:temp*") {Invoke-Item $Output} | |
| } | |
| else { | |
| Write-Host "Failed to extract icons. Resolve all issues and try again." -ForegroundColor Red | |
| } | |
| } # END Export-Icon |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment