Public/Convert-PX2PS.ps1

function Convert-PX2PS {
    [alias('px2ps')] 
    <#
    .SYNOPSIS
        Converts Pixquare .px and Piskel .piskel files to terminal pixel graphics.
    
    .DESCRIPTION
        Reads .px and .piskel pixel art files and renders them in the PowerShell
        terminal using the lower half block character (▄) with ANSI True Color.
        Each line of output represents two rows of pixels: top pixel uses
        background color, bottom pixel uses foreground color.
        
        Supports both single-layer and multi-layer files with automatic
        layer compositing and transparency handling.
        
        For .piskel files, only the first frame of each layer is used
        (animation is not supported).
    
    .PARAMETER Path
        Path to a .px or .piskel file, or a directory containing such files.
        If a directory is provided, all .px and .piskel files are processed.
    
    .PARAMETER OutputMode
        Controls the output format:
        - Display: Renders directly to terminal (default)
        - ScriptBlock: Returns a scriptblock that can be invoked to render
        - Script: Generates a standalone .ps1 file (requires -OutputPath)
    
    .PARAMETER OutputPath
        File path for generated script when using -OutputMode Script.
        Must end with .ps1 extension.
    
    .PARAMETER RenderMode
        Controls the rendering engine used for terminal output:
        - Auto: Detect host and use ConsoleColor for ISE, VT for everything else (default)
        - VT: Force ANSI True Color rendering regardless of host
        - ConsoleColor: Force 16-color Write-Host rendering regardless of host

    .PARAMETER PassThru
        If specified, returns PSCustomObject with file information instead of
        rendering directly to terminal. Maintained for backward compatibility.
    
    .EXAMPLE
        Convert-PX2PS -Path "Stepper 4.px"
        
        Renders the specified .px file to the terminal.
    
    .EXAMPLE
        Convert-PX2PS -Path "C:\PixelArt"
        
        Renders all .px files found in the specified directory.
    
    .EXAMPLE
        Get-ChildItem -Path "." -Filter "*.px" | Convert-PX2PS
        
        Processes .px files from pipeline input.
    
    .EXAMPLE
        $sb = Convert-PX2PS -Path "logo.px" -OutputMode ScriptBlock
        & $sb
        
        Gets a scriptblock for deferred rendering.
    
    .EXAMPLE
        Convert-PX2PS -Path "banner.px" -OutputMode Script -OutputPath "banner.ps1"
        
        Generates a standalone script file that can render the image.
    
    .EXAMPLE
        Convert-PX2PS -Path "logo.px" -RenderMode ConsoleColor

        Forces 16-color ConsoleColor rendering for testing without ISE.

    .EXAMPLE
        $data = Convert-PX2PS -Path "image.px" -PassThru
        
        Gets pixel data without rendering.
    
    .OUTPUTS
        None by default.
        With -OutputMode ScriptBlock, outputs [scriptblock].
        With -PassThru, outputs PSCustomObject with:
        - FilePath: Full path to the .px file
        - Width: Image width in pixels
        - Height: Image height in pixels
        - Pixels: Array of RGBA pixel data
    
    .NOTES
        Requires PowerShell 5.1 or later.
        On Windows PowerShell 5.1, automatically enables Virtual Terminal Processing.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Display')]
    [OutputType([void], [PSCustomObject], [scriptblock])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Display')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ScriptBlock')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Script')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'PassThru')]
        [Alias('FullName')]
        [ValidateNotNullOrEmpty()]
        [string]$Path,
        
        [Parameter(ParameterSetName = 'ScriptBlock')]
        [Parameter(ParameterSetName = 'Script')]
        [ValidateSet('ScriptBlock', 'Script')]
        [string]$OutputMode,
        
        [Parameter(Mandatory, ParameterSetName = 'Script')]
        [ValidatePattern('\.ps1$')]
        [ValidateNotNullOrEmpty()]
        [string]$OutputPath,
        
        [Parameter(ParameterSetName = 'Display')]
        [Parameter(ParameterSetName = 'ScriptBlock')]
        [Parameter(ParameterSetName = 'Script')]
        [ValidateSet('Auto', 'VT', 'ConsoleColor')]
        [string]$RenderMode = 'Auto',

        [Parameter(ParameterSetName = 'PassThru')]
        [switch]$PassThru
    )
    
    begin {
        Write-Verbose 'Starting pixel art file processing'
    }
    
    process {
        if (-not (Test-Path -Path $Path)) {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.IO.FileNotFoundException]::new("Path not found: $Path"),
                'PathNotFound',
                [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                $Path
            )
            $PSCmdlet.WriteError($errorRecord)
            return
        }
        
        $pathItem = Get-Item -Path $Path
        
        if ($pathItem.PSIsContainer) {
            $pxFiles = Get-ChildItem -Path $Path -File | Where-Object { $_.Extension -in '.px', '.piskel', '.ase', '.aseprite' }
            
            if ($pxFiles.Count -eq 0) {
                Write-Warning "No .px, .piskel, .ase, or .aseprite files found in $Path"
                return
            }
            
            Write-Host "Found $($pxFiles.Count) pixel art file(s)" -ForegroundColor Green
            
            foreach ($file in $pxFiles) {
                $params = @{
                    Path = $file.FullName
                }
                
                if ($PassThru.IsPresent) {
                    $params['PassThru'] = $true
                }
                
                if ($PSBoundParameters.ContainsKey('RenderMode')) {
                    $params['RenderMode'] = $RenderMode
                }

                if ($PSBoundParameters.ContainsKey('OutputMode')) {
                    $params['OutputMode'] = $OutputMode
                    
                    if ($PSBoundParameters.ContainsKey('OutputPath')) {
                        $baseName = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
                        $directory = [System.IO.Path]::GetDirectoryName($OutputPath)
                        $extension = [System.IO.Path]::GetExtension($OutputPath)
                        $params['OutputPath'] = [System.IO.Path]::Combine($directory, "$baseName$extension")
                    }
                }
                
                Convert-PX2PS @params
            }
            return
        }
        
        if ($pathItem.Extension -notin '.px', '.piskel', '.ase', '.aseprite') {
            Write-Warning "File does not appear to be a supported pixel art file: $Path"
        }
        
        try {
            if ($pathItem.Extension -eq '.piskel') {
                $json = Get-Content -Path $pathItem.FullName -Raw | ConvertFrom-Json
                $width = [int]$json.piskel.width
                $height = [int]$json.piskel.height

                if ($width -le 0 -or $height -le 0) {
                    Write-Warning "Invalid dimensions in $($pathItem.Name)"
                    return
                }

                Write-Verbose "Processing $($pathItem.Name): ${width}x${height}"

                $layers = Read-PiskelLayerData -PiskelData $json -Width $width -Height $height
            } elseif ($pathItem.Extension -in '.ase', '.aseprite') {
                $data = [System.IO.File]::ReadAllBytes($pathItem.FullName)
                $dimensions = Get-AseDimension -Data $data
                $width = $dimensions.Width
                $height = $dimensions.Height

                if ($width -le 0 -or $height -le 0) {
                    Write-Warning "Invalid dimensions in $($pathItem.Name)"
                    return
                }

                Write-Verbose "Processing $($pathItem.Name): ${width}x${height}"

                $layers = Read-AseLayerData -Data $data -Width $width -Height $height
            } else {
                $data = [System.IO.File]::ReadAllBytes($pathItem.FullName)
                $dimensions = Get-PxDimension -Data $data
                $width = $dimensions.Width
                $height = $dimensions.Height

                if ($width -le 0 -or $height -le 0) {
                    Write-Warning "Invalid dimensions in $($pathItem.Name)"
                    return
                }

                Write-Verbose "Processing $($pathItem.Name): ${width}x${height}"

                $layers = Read-PxLayerData -Data $data -Width $width -Height $height
            }
            
            if ($layers.Count -eq 0) {
                Write-Warning "Could not extract layer data from $($pathItem.Name)"
                return
            }
            
            # Ensure $layers is treated as an array of byte arrays
            [byte[][]]$layersArray = $layers
            $pixels = Merge-PxLayer -Layers $layersArray -Width $width -Height $height
            
            if ($PassThru.IsPresent) {
                Write-Output ([PSCustomObject]@{
                    FilePath = $pathItem.FullName
                    Width = $width
                    Height = $height
                    Pixels = $pixels
                })
            } elseif ($OutputMode -eq 'ScriptBlock') {
                $pixelsString = ($pixels | ForEach-Object { "@($($_ -join ','))" }) -join ",`n "

                $useConsoleColorExpr = switch ($RenderMode) {
                    'ConsoleColor' { '$true' }
                    'VT' { '$false' }
                    default { '$Host.Name -eq ''Windows PowerShell ISE Host''' }
                }
                
                $scriptContent = @"
`$ESC = [char]27
`$LowerHalfBlock = [char]0x2584
`$UpperHalfBlock = [char]0x2580
`$width = $width
`$height = $height
`$pixels = @(
    $pixelsString
)

`$oddHeight = (`$height % 2) -eq 1
`$startY = if (`$oddHeight) { -1 } else { 0 }
`$endY = if (`$oddHeight) { `$height - 1 } else { `$height }

if ($useConsoleColorExpr) {
    function ConvertTo-ConsoleColor {
        param([int]`$R, [int]`$G, [int]`$B)
        `$colorMap = @(
            @{ Color = [System.ConsoleColor]::Black; R = 0; G = 0; B = 0 }
            @{ Color = [System.ConsoleColor]::DarkBlue; R = 0; G = 0; B = 128 }
            @{ Color = [System.ConsoleColor]::DarkGreen; R = 0; G = 128; B = 0 }
            @{ Color = [System.ConsoleColor]::DarkCyan; R = 0; G = 128; B = 128 }
            @{ Color = [System.ConsoleColor]::DarkRed; R = 128; G = 0; B = 0 }
            @{ Color = [System.ConsoleColor]::DarkMagenta; R = 128; G = 0; B = 128 }
            @{ Color = [System.ConsoleColor]::DarkYellow; R = 128; G = 128; B = 0 }
            @{ Color = [System.ConsoleColor]::Gray; R = 192; G = 192; B = 192 }
            @{ Color = [System.ConsoleColor]::DarkGray; R = 128; G = 128; B = 128 }
            @{ Color = [System.ConsoleColor]::Blue; R = 0; G = 0; B = 255 }
            @{ Color = [System.ConsoleColor]::Green; R = 0; G = 255; B = 0 }
            @{ Color = [System.ConsoleColor]::Cyan; R = 0; G = 255; B = 255 }
            @{ Color = [System.ConsoleColor]::Red; R = 255; G = 0; B = 0 }
            @{ Color = [System.ConsoleColor]::Magenta; R = 255; G = 0; B = 255 }
            @{ Color = [System.ConsoleColor]::Yellow; R = 255; G = 255; B = 0 }
            @{ Color = [System.ConsoleColor]::White; R = 255; G = 255; B = 255 }
        )
        `$nearestColor = [System.ConsoleColor]::Black
        `$minDistance = [int]::MaxValue
        foreach (`$entry in `$colorMap) {
            `$dr = `$R - `$entry.R
            `$dg = `$G - `$entry.G
            `$db = `$B - `$entry.B
            `$distance = (`$dr * `$dr) + (`$dg * `$dg) + (`$db * `$db)
            if (`$distance -lt `$minDistance) {
                `$minDistance = `$distance
                `$nearestColor = `$entry.Color
            }
        }
        return `$nearestColor
    }

    for (`$y = `$startY; `$y -lt `$endY; `$y += 2) {
        for (`$x = 0; `$x -lt `$width; `$x++) {
            `$topY = `$y
            `$bottomY = `$y + 1

            if (`$topY -lt 0) {
                `$topPixel = `$null
            } else {
                `$topIdx = (`$topY * `$width) + `$x
                `$topPixel = if (`$topIdx -lt `$pixels.Count) { `$pixels[`$topIdx] } else { @(0, 0, 0, 0) }
            }

            `$bottomIdx = (`$bottomY * `$width) + `$x
            `$bottomPixel = if (`$bottomIdx -lt `$pixels.Count) { `$pixels[`$bottomIdx] } else { @(0, 0, 0, 0) }

            `$topTransparent = (`$null -eq `$topPixel) -or (`$topPixel[3] -lt 32)
            `$bottomTransparent = `$bottomPixel[3] -lt 32

            if (-not `$topTransparent -and -not `$bottomTransparent) {
                `$fgColor = ConvertTo-ConsoleColor -R `$bottomPixel[0] -G `$bottomPixel[1] -B `$bottomPixel[2]
                `$bgColor = ConvertTo-ConsoleColor -R `$topPixel[0] -G `$topPixel[1] -B `$topPixel[2]
                Write-Host `$LowerHalfBlock -ForegroundColor `$fgColor -BackgroundColor `$bgColor -NoNewline
            } elseif (-not `$topTransparent -and `$bottomTransparent) {
                `$fgColor = ConvertTo-ConsoleColor -R `$topPixel[0] -G `$topPixel[1] -B `$topPixel[2]
                Write-Host `$UpperHalfBlock -ForegroundColor `$fgColor -NoNewline
            } elseif (`$topTransparent -and -not `$bottomTransparent) {
                `$fgColor = ConvertTo-ConsoleColor -R `$bottomPixel[0] -G `$bottomPixel[1] -B `$bottomPixel[2]
                Write-Host `$LowerHalfBlock -ForegroundColor `$fgColor -NoNewline
            } else {
                Write-Host ' ' -NoNewline
            }
        }
        Write-Host ''
    }

    Write-Host ''
} else {
    for (`$y = `$startY; `$y -lt `$endY; `$y += 2) {
        `$line = ""
        for (`$x = 0; `$x -lt `$width; `$x++) {
            `$topY = `$y
            `$bottomY = `$y + 1

            if (`$topY -lt 0) {
                `$topPixel = `$null
            } else {
                `$topIdx = (`$topY * `$width) + `$x
                `$topPixel = if (`$topIdx -lt `$pixels.Count) { `$pixels[`$topIdx] } else { @(0, 0, 0, 0) }
            }

            `$bottomIdx = (`$bottomY * `$width) + `$x
            `$bottomPixel = if (`$bottomIdx -lt `$pixels.Count) { `$pixels[`$bottomIdx] } else { @(0, 0, 0, 0) }

            `$topTransparent = (`$null -eq `$topPixel) -or (`$topPixel[3] -lt 32)
            `$bottomTransparent = `$bottomPixel[3] -lt 32

            if (-not `$topTransparent -and -not `$bottomTransparent) {
                `$bg = "`$ESC[48;2;`$(`$topPixel[0]);`$(`$topPixel[1]);`$(`$topPixel[2])m"
                `$fg = "`$ESC[38;2;`$(`$bottomPixel[0]);`$(`$bottomPixel[1]);`$(`$bottomPixel[2])m"
                `$line += "`${bg}`${fg}`$LowerHalfBlock"
            } elseif (-not `$topTransparent -and `$bottomTransparent) {
                `$fg = "`$ESC[38;2;`$(`$topPixel[0]);`$(`$topPixel[1]);`$(`$topPixel[2])m"
                `$line += "`$ESC[0m`${fg}`$UpperHalfBlock"
            } elseif (`$topTransparent -and -not `$bottomTransparent) {
                `$fg = "`$ESC[38;2;`$(`$bottomPixel[0]);`$(`$bottomPixel[1]);`$(`$bottomPixel[2])m"
                `$line += "`$ESC[0m`${fg}`$LowerHalfBlock"
            } else {
                `$line += "`$ESC[0m "
            }
        }
        `$line += "`$ESC[0m`$ESC[K"
        Write-Host `$line
    }

    Write-Host ""
}
"@

                Write-Output ([scriptblock]::Create($scriptContent))
            } elseif ($OutputMode -eq 'Script') {
                $useConsoleColorExprScript = switch ($RenderMode) {
                    'ConsoleColor' { '$true' }
                    'VT' { '$false' }
                    default { '$Host.Name -eq ''Windows PowerShell ISE Host''' }
                }

                $scriptContent = @"
#!/usr/bin/env pwsh
# Auto-generated by PX2PS 2025.12.29
# Source: $($pathItem.Name) (${width}x${height})
# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')

# Enable Virtual Terminal Processing for ANSI colors (Windows PowerShell 5.1 compatibility)
if (`$PSVersionTable.PSVersion.Major -le 5 -and `$env:OS -eq 'Windows_NT' -and `$Host.Name -ne 'Windows PowerShell ISE Host') {
    try {
        Add-Type -TypeDefinition @`"
using System;
using System.Runtime.InteropServices;
public class VTConsole {
    [DllImport(`"kernel32.dll`", SetLastError = true)]
    public static extern IntPtr GetStdHandle(int nStdHandle);
    [DllImport(`"kernel32.dll`", SetLastError = true)]
    public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
    [DllImport(`"kernel32.dll`", SetLastError = true)]
    public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
    public static void EnableVT() {
        IntPtr handle = GetStdHandle(-11);
        uint mode;
        GetConsoleMode(handle, out mode);
        SetConsoleMode(handle, mode | 0x4);
    }
}
`"@ -ErrorAction SilentlyContinue
        [VTConsole]::EnableVT()
    } catch {
        # VT processing may already be enabled or not available
    }
}

`$ESC = [char]27
`$LowerHalfBlock = [char]0x2584
`$UpperHalfBlock = [char]0x2580

function ConvertTo-ConsoleColor {
    param([int]`$R, [int]`$G, [int]`$B)
    `$colorMap = @(
        @{ Color = [System.ConsoleColor]::Black; R = 0; G = 0; B = 0 }
        @{ Color = [System.ConsoleColor]::DarkBlue; R = 0; G = 0; B = 128 }
        @{ Color = [System.ConsoleColor]::DarkGreen; R = 0; G = 128; B = 0 }
        @{ Color = [System.ConsoleColor]::DarkCyan; R = 0; G = 128; B = 128 }
        @{ Color = [System.ConsoleColor]::DarkRed; R = 128; G = 0; B = 0 }
        @{ Color = [System.ConsoleColor]::DarkMagenta; R = 128; G = 0; B = 128 }
        @{ Color = [System.ConsoleColor]::DarkYellow; R = 128; G = 128; B = 0 }
        @{ Color = [System.ConsoleColor]::Gray; R = 192; G = 192; B = 192 }
        @{ Color = [System.ConsoleColor]::DarkGray; R = 128; G = 128; B = 128 }
        @{ Color = [System.ConsoleColor]::Blue; R = 0; G = 0; B = 255 }
        @{ Color = [System.ConsoleColor]::Green; R = 0; G = 255; B = 0 }
        @{ Color = [System.ConsoleColor]::Cyan; R = 0; G = 255; B = 255 }
        @{ Color = [System.ConsoleColor]::Red; R = 255; G = 0; B = 0 }
        @{ Color = [System.ConsoleColor]::Magenta; R = 255; G = 0; B = 255 }
        @{ Color = [System.ConsoleColor]::Yellow; R = 255; G = 255; B = 0 }
        @{ Color = [System.ConsoleColor]::White; R = 255; G = 255; B = 255 }
    )
    `$nearestColor = [System.ConsoleColor]::Black
    `$minDistance = [int]::MaxValue
    foreach (`$entry in `$colorMap) {
        `$dr = `$R - `$entry.R
        `$dg = `$G - `$entry.G
        `$db = `$B - `$entry.B
        `$distance = (`$dr * `$dr) + (`$dg * `$dg) + (`$db * `$db)
        if (`$distance -lt `$minDistance) {
            `$minDistance = `$distance
            `$nearestColor = `$entry.Color
        }
    }
    return `$nearestColor
}

function Get-TrueColorFg {
    param([int]`$R, [int]`$G, [int]`$B)
    return "`$ESC[38;2;`${R};`${G};`${B}m"
}

function Get-TrueColorBg {
    param([int]`$R, [int]`$G, [int]`$B)
    return "`$ESC[48;2;`${R};`${G};`${B}m"
}

`$width = $width
`$height = $height
`$pixels = @(
$(($pixels | ForEach-Object { " @($($_ -join ','))" }) -join ",`n")
)

`$oddHeight = (`$height % 2) -eq 1
`$startY = if (`$oddHeight) { -1 } else { 0 }
`$endY = if (`$oddHeight) { `$height - 1 } else { `$height }

if ($useConsoleColorExprScript) {
    for (`$y = `$startY; `$y -lt `$endY; `$y += 2) {
        for (`$x = 0; `$x -lt `$width; `$x++) {
            `$topY = `$y
            `$bottomY = `$y + 1

            if (`$topY -lt 0) {
                `$topPixel = `$null
            } else {
                `$topIdx = (`$topY * `$width) + `$x
                `$topPixel = if (`$topIdx -lt `$pixels.Count) { `$pixels[`$topIdx] } else { @(0, 0, 0, 0) }
            }

            `$bottomIdx = (`$bottomY * `$width) + `$x
            `$bottomPixel = if (`$bottomIdx -lt `$pixels.Count) { `$pixels[`$bottomIdx] } else { @(0, 0, 0, 0) }

            `$topTransparent = (`$null -eq `$topPixel) -or (`$topPixel[3] -lt 32)
            `$bottomTransparent = `$bottomPixel[3] -lt 32

            if (-not `$topTransparent -and -not `$bottomTransparent) {
                `$fgColor = ConvertTo-ConsoleColor -R `$bottomPixel[0] -G `$bottomPixel[1] -B `$bottomPixel[2]
                `$bgColor = ConvertTo-ConsoleColor -R `$topPixel[0] -G `$topPixel[1] -B `$topPixel[2]
                Write-Host `$LowerHalfBlock -ForegroundColor `$fgColor -BackgroundColor `$bgColor -NoNewline
            } elseif (-not `$topTransparent -and `$bottomTransparent) {
                `$fgColor = ConvertTo-ConsoleColor -R `$topPixel[0] -G `$topPixel[1] -B `$topPixel[2]
                Write-Host `$UpperHalfBlock -ForegroundColor `$fgColor -NoNewline
            } elseif (`$topTransparent -and -not `$bottomTransparent) {
                `$fgColor = ConvertTo-ConsoleColor -R `$bottomPixel[0] -G `$bottomPixel[1] -B `$bottomPixel[2]
                Write-Host `$LowerHalfBlock -ForegroundColor `$fgColor -NoNewline
            } else {
                Write-Host ' ' -NoNewline
            }
        }
        Write-Host ''
    }

    Write-Host ''
} else {
    for (`$y = `$startY; `$y -lt `$endY; `$y += 2) {
        `$line = ""
        for (`$x = 0; `$x -lt `$width; `$x++) {
            `$topY = `$y
            `$bottomY = `$y + 1

            if (`$topY -lt 0) {
                `$topPixel = `$null
            } else {
                `$topIdx = (`$topY * `$width) + `$x
                `$topPixel = if (`$topIdx -lt `$pixels.Count) { `$pixels[`$topIdx] } else { @(0, 0, 0, 0) }
            }

            `$bottomIdx = (`$bottomY * `$width) + `$x
            `$bottomPixel = if (`$bottomIdx -lt `$pixels.Count) { `$pixels[`$bottomIdx] } else { @(0, 0, 0, 0) }

            `$topTransparent = (`$null -eq `$topPixel) -or (`$topPixel[3] -lt 32)
            `$bottomTransparent = `$bottomPixel[3] -lt 32

            if (-not `$topTransparent -and -not `$bottomTransparent) {
                `$bg = Get-TrueColorBg -R `$topPixel[0] -G `$topPixel[1] -B `$topPixel[2]
                `$fg = Get-TrueColorFg -R `$bottomPixel[0] -G `$bottomPixel[1] -B `$bottomPixel[2]
                `$line += "`${bg}`${fg}`$LowerHalfBlock"
            } elseif (-not `$topTransparent -and `$bottomTransparent) {
                `$fg = Get-TrueColorFg -R `$topPixel[0] -G `$topPixel[1] -B `$topPixel[2]
                `$line += "`$ESC[0m`${fg}`$UpperHalfBlock"
            } elseif (`$topTransparent -and -not `$bottomTransparent) {
                `$fg = Get-TrueColorFg -R `$bottomPixel[0] -G `$bottomPixel[1] -B `$bottomPixel[2]
                `$line += "`$ESC[0m`${fg}`$LowerHalfBlock"
            } else {
                `$line += "`$ESC[0m "
            }
        }
        `$line += "`$ESC[0m`$ESC[K"
        Write-Host `$line
    }

    Write-Host ""
}
"@

                Set-Content -Path $OutputPath -Value $scriptContent -Encoding UTF8 -NoNewline
                Write-Verbose "Script file created: $OutputPath"
            } else {
                $useConsoleColor = switch ($RenderMode) {
                    'ConsoleColor' { $true }
                    'VT' { $false }
                    default { $Host.Name -eq 'Windows PowerShell ISE Host' }
                }

                if ($useConsoleColor) {
                    Write-PxTerminalISE -Width $width -Height $height -Pixels $pixels
                } else {
                    Write-PxTerminal -Width $width -Height $height -Pixels $pixels
                }
            }
        } catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $_.Exception,
                'PxFileProcessingFailed',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $Path
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }
    
    end {
        Write-Verbose 'Completed pixel art file processing'
    }
}