Public/New-MSIXAssetFrom.ps1

function New-MSIXAssetFrom {
<#
.SYNOPSIS
    Generates the six standard MSIX VisualElements PNGs from an .exe / .ico / image.

.DESCRIPTION
    Auto-detects the source type by extension:
      .exe / .dll - extracts the icon group at -IconIndex (default 0) and
                         picks the largest embedded frame (PNG-aware).
      .ico - loads as Icon and uses the largest embedded frame.
      .png/.jpg/.bmp/.gif - loads as Bitmap directly.

    Output files are written to <MSIXFolder>\Assets:
      <AssetId>-Square44x44Logo.png (44x44)
      <AssetId>-Square71x71Logo.png (71x71)
      <AssetId>-Square150x150Logo.png (150x150)
      <AssetId>-Square310x310Logo.png (310x310)
      <AssetId>-Wide310x150Logo.png (310x150, letterboxed)
      <AssetId>-StoreLogo.png (50x50)

.PARAMETER MSIXFolder
    Path to the expanded MSIX package folder.

.PARAMETER SourcePath
    Source file: .exe / .dll / .ico / .png / .jpg / .bmp / .gif.

.PARAMETER AssetId
    Filename prefix for the generated PNGs. Defaults to a sanitised form of the
    source's base name.

.PARAMETER IconIndex
    Icon index inside the PE file (only meaningful for .exe / .dll). Default 0.

.EXAMPLE
    $a = New-MSIXAssetFrom -MSIXFolder $pkg -SourcePath "$env:windir\System32\WindowsPowerShell\v1.0\powershell.exe"
    Add-MSIXApplication -MSIXFolder $pkg -Executable 'NITTracer.ps1' -AssetId $a.AssetId

.NOTES
    https://www.nick-it.de
    Andreas Nick, 2026
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.IO.DirectoryInfo] $MSIXFolder,

        [Parameter(Mandatory = $true, Position = 1)]
        [System.IO.FileInfo] $SourcePath,

        [string] $AssetId,

        [int] $IconIndex = 0,

        # Also overwrites Assets\StoreLogo.png so the package-level Logo
        # (shown by AppInstaller) matches the generated icon.
        [switch] $SetAsPackageLogo
    )

    process {
        if (-not (Test-Path $SourcePath.FullName)) {
            Write-Error "Source file not found: $($SourcePath.FullName)"
            return $null
        }
        if (-not (Test-Path $MSIXFolder.FullName)) {
            Write-Error "MSIX folder not found: $($MSIXFolder.FullName)"
            return $null
        }

        $assetsDir = Join-Path $MSIXFolder.FullName 'Assets'
        if (-not (Test-Path $assetsDir)) {
            New-Item -ItemType Directory -Path $assetsDir -Force | Out-Null
            Write-Verbose "Created Assets folder: $assetsDir"
        }

        # Derive AssetId from filename if not given
        if ([string]::IsNullOrEmpty($AssetId)) {
            $base = [IO.Path]::GetFileNameWithoutExtension($SourcePath.Name)
            $sanitised = ($base -replace '[^A-Za-z0-9]', '')
            if ($sanitised.Length -eq 0) {
                Write-Error "Could not derive AssetId from '$($SourcePath.Name)'. Provide -AssetId explicitly."
                return $null
            }
            $AssetId = $sanitised
            Write-Verbose "Derived AssetId: $AssetId"
        }

        # Lazy-load the icon helper module
        $libPath = Join-Path $Script:ScriptPath 'Libs\SymbolModIconLib.psm1'
        if (-not (Test-Path $libPath)) {
            Write-Error "SymbolModIconLib.psm1 not found at: $libPath"
            return $null
        }
        Import-Module $libPath -Force -Verbose:$false

        # Resolve source to a Bitmap (high-quality)
        $ext = $SourcePath.Extension.ToLowerInvariant()
        Write-Verbose "Source type detected: $ext"

        $sourceBitmap = $null
        try {
            switch ($ext) {
                { $_ -in '.exe', '.dll' } {
                    # Reads RT_GROUP_ICON resources directly so PNG-encoded frames
                    # are preserved with full alpha (ported from SymbolModIconLib.dll).
                    $sourceBitmap = Get-SymbolModBitmapAtSize -FileName $SourcePath.FullName -GroupIndex $IconIndex
                    if ($null -eq $sourceBitmap) {
                        Write-Error "No icon at group index $IconIndex in '$($SourcePath.FullName)'."
                        return $null
                    }
                    Write-Verbose "Extracted icon as bitmap: $($sourceBitmap.Width)x$($sourceBitmap.Height)"
                    break
                }
                '.ico' {
                    $icon = New-Object System.Drawing.Icon($SourcePath.FullName)
                    $frames = Split-SymbolModIcon -Icon $icon
                    $largest = $frames | Sort-Object { [int]$_.Width } -Descending | Select-Object -First 1
                    Write-Verbose "Largest .ico frame: $($largest.Width)x$($largest.Height)"
                    $sourceBitmap = ConvertTo-SymbolModIconBitmap -Icon $largest
                    $icon.Dispose()
                    foreach ($f in $frames) { $f.Dispose() }
                    break
                }
                { $_ -in '.png', '.jpg', '.jpeg', '.bmp', '.gif' } {
                    $sourceBitmap = New-Object System.Drawing.Bitmap($SourcePath.FullName)
                    Write-Verbose "Loaded image: $($sourceBitmap.Width)x$($sourceBitmap.Height)"
                    break
                }
                default {
                    Write-Error "Unsupported source extension '$ext'. Use .exe/.dll/.ico/.png/.jpg/.bmp/.gif."
                    return $null
                }
            }
        }
        catch {
            Write-Error "Failed to load source '$($SourcePath.FullName)': $_"
            return $null
        }

        if ($null -eq $sourceBitmap) {
            Write-Error "Source bitmap could not be produced."
            return $null
        }

        # Target sizes: name -> [width, height]
        $targets = [ordered]@{
            "$AssetId-Square44x44Logo.png"   = @(44,  44)
            "$AssetId-Square71x71Logo.png"   = @(71,  71)
            "$AssetId-Square150x150Logo.png" = @(150, 150)
            "$AssetId-Square310x310Logo.png" = @(310, 310)
            "$AssetId-Wide310x150Logo.png"   = @(310, 150)
            "$AssetId-StoreLogo.png"         = @(50,  50)
        }

        $written = New-Object 'System.Collections.Generic.List[string]'
        try {
            foreach ($pair in $targets.GetEnumerator()) {
                $w = $pair.Value[0]
                $h = $pair.Value[1]
                $outPath = Join-Path $assetsDir $pair.Key

                # Letterbox: fit source aspect-preserved onto a transparent canvas of (w,h)
                $canvas = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
                $g = [System.Drawing.Graphics]::FromImage($canvas)
                try {
                    $g.Clear([System.Drawing.Color]::Transparent)
                    $g.InterpolationMode  = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
                    $g.SmoothingMode      = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
                    $g.PixelOffsetMode    = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
                    $g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality

                    $ratio = [Math]::Min([double]$w / $sourceBitmap.Width, [double]$h / $sourceBitmap.Height)
                    $rw = [int]([Math]::Round($sourceBitmap.Width  * $ratio))
                    $rh = [int]([Math]::Round($sourceBitmap.Height * $ratio))
                    $rx = [int](($w - $rw) / 2)
                    $ry = [int](($h - $rh) / 2)
                    $g.DrawImage($sourceBitmap, $rx, $ry, $rw, $rh)
                }
                finally {
                    $g.Dispose()
                }
                $canvas.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png)
                $canvas.Dispose()
                $written.Add($outPath) | Out-Null
                Write-Verbose "Wrote $outPath ($w x $h)"
            }
        }
        catch {
            Write-Error "Failed while writing assets: $_"
            return $null
        }
        finally {
            $sourceBitmap.Dispose()
        }

        if ($SetAsPackageLogo) {
            $assetStoreLogo   = Join-Path $assetsDir "$AssetId-StoreLogo.png"
            $packageStoreLogo = Join-Path $assetsDir 'StoreLogo.png'
            Copy-Item -LiteralPath $assetStoreLogo -Destination $packageStoreLogo -Force
            Write-Verbose "Replaced Assets\StoreLogo.png with the generated $AssetId-StoreLogo.png"
        }

        return [PSCustomObject]@{
            AssetId   = $AssetId
            Source    = $SourcePath.FullName
            Files     = $written.ToArray()
            StoreLogo = "Assets\$AssetId-StoreLogo.png"
        }
    }
}