Private/Export-AppIcon.ps1

function Export-AppIcon {
    <#
    .SYNOPSIS
        Extracts the largest icon of a file (.exe, .dll, .ico or an image) and saves it as an image.
    .DESCRIPTION
        For .exe and .dll files the icon group resources are read directly: -Index selects the n-th
        group (0 based); a negative -Index is a resource ID, as in the Windows DisplayIcon syntax
        'file.exe,-101'. The largest entry of the group is used (up to 256 px, higher bit depth wins
        a tie). A PNG-compressed entry is written as-is when the format is png and it is not larger
        than -MaxSize. A file without icon groups falls back to the shell's associated icon (32 px).
        .ico files use their largest frame; .png, .jpg and .bmp files are loaded as images. Images
        larger than -MaxSize are scaled down with high-quality bicubic resampling, never up.
        Returns the written file with the NoteProperties IconWidth and IconHeight, or writes a
        warning and returns nothing when no icon can be read.
    .NOTES
        Adapted from "Extracting Icons with PowerShell" by Jeff Hicks:
        https://jdhitsolutions.com/blog/powershell/7931/extracting-icons-with-powershell/
        Inspired by https://community.spiceworks.com/topic/592770-extract-icon-from-exe-powershell
 
        Twin file: psadt-package-to-intune\PSADTIntune\Private\Export-AppIcon.ps1. Keep both identical.
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -LiteralPath $_ })]
        [string]$Path,

        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$Destination = '.',

        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [ValidateSet('ico', 'bmp', 'png', 'jpg', 'gif')]
        [string]$Format = 'png',

        # Icon group number (0 based), or with a negative value the resource ID of the group
        [int]$Index = 0,

        # Larger icons are scaled down to this size (pixels)
        [ValidateRange(1, 4096)]
        [int]$MaxSize = 256
    )

    Add-Type -AssemblyName System.Drawing -ErrorAction Stop

    if (-not ('ExportAppIconNative' -as [type])) {
        Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
 
public static class ExportAppIconNative
{
    delegate bool EnumResNameProc(IntPtr hModule, IntPtr lpszType, IntPtr lpszName, IntPtr lParam);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool FreeLibrary(IntPtr hModule);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool EnumResourceNames(IntPtr hModule, IntPtr lpszType, EnumResNameProc lpEnumFunc, IntPtr lParam);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo);
 
    [DllImport("kernel32.dll")]
    static extern IntPtr LockResource(IntPtr hResData);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern uint SizeofResource(IntPtr hModule, IntPtr hResInfo);
 
    static byte[] ReadResource(IntPtr module, object name, int type)
    {
        IntPtr namePtr = IntPtr.Zero;
        bool allocated = false;
        if (name is int) { namePtr = (IntPtr)(int)name; }
        else { namePtr = Marshal.StringToHGlobalUni((string)name); allocated = true; }
        try
        {
            IntPtr info = FindResource(module, namePtr, (IntPtr)type);
            if (info == IntPtr.Zero) { return null; }
            uint size = SizeofResource(module, info);
            IntPtr loaded = LoadResource(module, info);
            if (loaded == IntPtr.Zero || size == 0) { return null; }
            IntPtr locked = LockResource(loaded);
            if (locked == IntPtr.Zero) { return null; }
            byte[] data = new byte[size];
            Marshal.Copy(locked, data, 0, (int)size);
            return data;
        }
        finally
        {
            if (allocated) { Marshal.FreeHGlobal(namePtr); }
        }
    }
 
    // Returns the data of the largest RT_ICON image of an icon group, or null when the file has no
    // such group. index >= 0 selects the n-th group; index < 0 selects the group with ID -index.
    public static byte[] GetBestIcon(string path, int index)
    {
        // LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE
        IntPtr module = LoadLibraryEx(path, IntPtr.Zero, 0x22);
        if (module == IntPtr.Zero) { return null; }
        try
        {
            List<object> groups = new List<object>();
            EnumResourceNames(module, (IntPtr)14, delegate (IntPtr m, IntPtr t, IntPtr n, IntPtr l)
            {
                if ((n.ToInt64() >> 16) == 0) { groups.Add((int)n.ToInt64()); }
                else { groups.Add(Marshal.PtrToStringUni(n)); }
                return true;
            }, IntPtr.Zero);
            if (groups.Count == 0) { return null; }
 
            object group;
            if (index >= 0)
            {
                if (index >= groups.Count) { return null; }
                group = groups[index];
            }
            else
            {
                if (!groups.Contains(-index)) { return null; }
                group = -index;
            }
 
            byte[] dir = ReadResource(module, group, 14);
            if (dir == null || dir.Length < 6) { return null; }
            int count = BitConverter.ToUInt16(dir, 4);
            int bestId = -1, bestWidth = -1, bestBits = -1;
            for (int i = 0; i < count; i++)
            {
                int o = 6 + 14 * i;
                if (o + 14 > dir.Length) { break; }
                int width = dir[o] == 0 ? 256 : dir[o];
                int bits = BitConverter.ToUInt16(dir, o + 6);
                if (width > bestWidth || (width == bestWidth && bits > bestBits))
                {
                    bestWidth = width; bestBits = bits; bestId = BitConverter.ToUInt16(dir, o + 12);
                }
            }
            if (bestId < 0) { return null; }
            return ReadResource(module, bestId, 3);
        }
        finally
        {
            FreeLibrary(module);
        }
    }
}
'@

    }

    # True when the bytes start with the PNG signature
    function Test-PngData {
        param([byte[]]$Data)
        $Data.Length -ge 24 -and $Data[0] -eq 0x89 -and $Data[1] -eq 0x50 -and $Data[2] -eq 0x4E -and $Data[3] -eq 0x47
    }

    # Image data of the largest frame of an .ico file; $null when the file is not a valid icon
    function Get-LargestIcoFrame {
        param([byte[]]$Bytes)
        if ($Bytes.Length -lt 22 -or [BitConverter]::ToUInt16($Bytes, 2) -ne 1) { return $null }
        $count = [BitConverter]::ToUInt16($Bytes, 4)
        $best = $null
        for ($i = 0; $i -lt $count; $i++) {
            $o = 6 + 16 * $i
            if ($o + 16 -gt $Bytes.Length) { break }
            $width = if ($Bytes[$o] -eq 0) { 256 } else { [int]$Bytes[$o] }
            $bits = [BitConverter]::ToUInt16($Bytes, $o + 6)
            if (-not $best -or $width -gt $best.Width -or ($width -eq $best.Width -and $bits -gt $best.Bits)) {
                $best = @{ Width = $width; Bits = $bits; Size = [BitConverter]::ToUInt32($Bytes, $o + 8); Offset = [BitConverter]::ToUInt32($Bytes, $o + 12) }
            }
        }
        if (-not $best -or $best.Offset + $best.Size -gt $Bytes.Length) { return $null }
        $frame = [byte[]]::new($best.Size)
        [Array]::Copy($Bytes, $best.Offset, $frame, 0, $best.Size)
        , $frame
    }

    # Decodes icon image data (PNG, or a BMP DIB without file header) to a bitmap
    function ConvertFrom-IconData {
        param([byte[]]$Data)
        if (Test-PngData $Data) {
            $stream = [System.IO.MemoryStream]::new($Data)
            try {
                $image = [System.Drawing.Image]::FromStream($stream)
                try { return [System.Drawing.Bitmap]::new($image) } finally { $image.Dispose() }
            }
            finally { $stream.Dispose() }
        }
        # Wrap the DIB in a one-entry .ico so System.Drawing can decode it; the DIB height covers the
        # XOR and AND masks, so the icon height is half of it
        $width = [BitConverter]::ToInt32($Data, 4)
        $height = [int]([Math]::Abs([BitConverter]::ToInt32($Data, 8)) / 2)
        $bits = [BitConverter]::ToUInt16($Data, 14)
        $stream = [System.IO.MemoryStream]::new()
        $writer = [System.IO.BinaryWriter]::new($stream)
        try {
            $writer.Write([uint16]0); $writer.Write([uint16]1); $writer.Write([uint16]1)
            $writer.Write([byte]($width % 256)); $writer.Write([byte]($height % 256)); $writer.Write([byte]0); $writer.Write([byte]0)
            $writer.Write([uint16]1); $writer.Write([uint16]$bits); $writer.Write([uint32]$Data.Length); $writer.Write([uint32]22)
            $writer.Write($Data)
            $writer.Flush()
            [void]$stream.Seek(0, [System.IO.SeekOrigin]::Begin)
            $icon = [System.Drawing.Icon]::new($stream, $width, $height)
            try { return $icon.ToBitmap() } finally { $icon.Dispose() }
        }
        finally { $writer.Dispose(); $stream.Dispose() }
    }

    # High-quality downscale to fit MaxSize; returns the same bitmap when it already fits
    function Resize-IconBitmap {
        param([System.Drawing.Bitmap]$Bitmap, [int]$Limit)
        if ($Bitmap.Width -le $Limit -and $Bitmap.Height -le $Limit) { return $Bitmap }
        $scale = $Limit / [double][Math]::Max($Bitmap.Width, $Bitmap.Height)
        $width = [Math]::Max(1, [int][Math]::Round($Bitmap.Width * $scale))
        $height = [Math]::Max(1, [int][Math]::Round($Bitmap.Height * $scale))
        $resized = [System.Drawing.Bitmap]::new($width, $height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
        $graphics = [System.Drawing.Graphics]::FromImage($resized)
        $attributes = [System.Drawing.Imaging.ImageAttributes]::new()
        try {
            $graphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
            $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
            $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
            $graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
            $attributes.SetWrapMode([System.Drawing.Drawing2D.WrapMode]::TileFlipXY)
            $graphics.DrawImage($Bitmap, [System.Drawing.Rectangle]::new(0, 0, $width, $height), 0, 0, $Bitmap.Width, $Bitmap.Height, [System.Drawing.GraphicsUnit]::Pixel, $attributes)
        }
        finally { $attributes.Dispose(); $graphics.Dispose(); $Bitmap.Dispose() }
        $resized
    }

    $imageFormat = switch ($Format) {
        'ico' { [System.Drawing.Imaging.ImageFormat]::Icon }
        'bmp' { [System.Drawing.Imaging.ImageFormat]::Bmp }
        'png' { [System.Drawing.Imaging.ImageFormat]::Png }
        'jpg' { [System.Drawing.Imaging.ImageFormat]::Jpeg }
        'gif' { [System.Drawing.Imaging.ImageFormat]::Gif }
    }

    $file = Get-Item -LiteralPath $Path
    $base = if ($Name) { $Name } else { $file.BaseName }
    $out = Join-Path -Path (Convert-Path -LiteralPath $Destination) -ChildPath "$base.$Format"

    Write-Verbose "Extracting icon from '$($file.FullName)' to '$out'"
    $bitmap = $null
    try {
        $data = $null
        switch ($file.Extension.ToLowerInvariant()) {
            { $_ -in '.exe', '.dll' } { $data = [ExportAppIconNative]::GetBestIcon($file.FullName, $Index) }
            '.ico' { $data = Get-LargestIcoFrame -Bytes ([System.IO.File]::ReadAllBytes($file.FullName)) }
            { $_ -in '.png', '.jpg', '.jpeg', '.bmp', '.gif' } {
                $stream = [System.IO.MemoryStream]::new([System.IO.File]::ReadAllBytes($file.FullName))
                try {
                    $image = [System.Drawing.Image]::FromStream($stream)
                    try { $bitmap = [System.Drawing.Bitmap]::new($image) } finally { $image.Dispose() }
                }
                finally { $stream.Dispose() }
            }
        }

        if ($data) {
            if ((Test-PngData $data) -and $Format -eq 'png') {
                # Width and height are big-endian at offsets 16 and 20 of the PNG header
                $pngWidth = ([int]$data[16] -shl 24) + ([int]$data[17] -shl 16) + ([int]$data[18] -shl 8) + $data[19]
                $pngHeight = ([int]$data[20] -shl 24) + ([int]$data[21] -shl 16) + ([int]$data[22] -shl 8) + $data[23]
                if ($pngWidth -le $MaxSize -and $pngHeight -le $MaxSize) {
                    [System.IO.File]::WriteAllBytes($out, $data)
                    return Get-Item -LiteralPath $out | Add-Member -NotePropertyName IconWidth -NotePropertyValue $pngWidth -PassThru |
                        Add-Member -NotePropertyName IconHeight -NotePropertyValue $pngHeight -PassThru
                }
            }
            $bitmap = ConvertFrom-IconData -Data $data
        }
    }
    catch {
        Write-Verbose "Could not read the icon resources of '$($file.FullName)': $($_.Exception.Message)"
        if ($bitmap) { $bitmap.Dispose() }
        $bitmap = $null
    }

    if (-not $bitmap) {
        # No icon resources: the shell's associated icon (32 px)
        $icon = $null
        try { $icon = [System.Drawing.Icon]::ExtractAssociatedIcon($file.FullName) }
        catch { Write-Verbose "ExtractAssociatedIcon failed: $($_.Exception.Message)" }
        if (-not $icon) {
            Write-Warning "No associated icon image found in $($file.FullName)"
            return
        }
        try { $bitmap = $icon.ToBitmap() } finally { $icon.Dispose() }
    }

    try {
        $bitmap = Resize-IconBitmap -Bitmap $bitmap -Limit $MaxSize
        $width = $bitmap.Width
        $height = $bitmap.Height
        $bitmap.Save($out, $imageFormat)
    }
    finally {
        $bitmap.Dispose()
    }

    Get-Item -LiteralPath $out | Add-Member -NotePropertyName IconWidth -NotePropertyValue $width -PassThru |
        Add-Member -NotePropertyName IconHeight -NotePropertyValue $height -PassThru
}