Public/Convert-PX2PS.ps1

function Convert-PX2PS {
    [alias('px2ps')] 
    <#
    .SYNOPSIS
        Converts Pixquare .px files to terminal pixel graphics.
    
    .DESCRIPTION
        Reads .px 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 .px files with automatic
        layer compositing and transparency handling.
    
    .PARAMETER Path
        Path to a .px file or directory containing .px files.
        If a directory is provided, all .px files in that directory 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 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
        $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 = 'PassThru')]
        [switch]$PassThru
    )
    
    begin {
        Write-Verbose 'Starting .px 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 -Filter '*.px' -File
            
            if ($pxFiles.Count -eq 0) {
                Write-Warning "No .px files found in $Path"
                return
            }
            
            Write-Host "Found $($pxFiles.Count) .px file(s)" -ForegroundColor Green
            
            foreach ($file in $pxFiles) {
                $params = @{
                    Path = $file.FullName
                }
                
                if ($PassThru.IsPresent) {
                    $params['PassThru'] = $true
                }
                
                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 -ne '.px') {
            Write-Warning "File does not appear to be a .px file: $Path"
        }
        
        try {
            $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 "
                
                $scriptContent = @"
`$ESC = [char]27
`$LowerHalfBlock = [char]0x2584
`$width = $width
`$height = $height
`$pixels = @(
    $pixelsString
)

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

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) }
        
        `$botR = if (`$bottomPixel[3] -lt 32) { 0 } else { `$bottomPixel[0] }
        `$botG = if (`$bottomPixel[3] -lt 32) { 0 } else { `$bottomPixel[1] }
        `$botB = if (`$bottomPixel[3] -lt 32) { 0 } else { `$bottomPixel[2] }
        
        if (`$null -eq `$topPixel) {
            `$fg = "`$ESC[38;2;`${botR};`${botG};`${botB}m"
            `$line += "`${fg}`$LowerHalfBlock"
        } else {
            `$topR = if (`$topPixel[3] -lt 32) { 0 } else { `$topPixel[0] }
            `$topG = if (`$topPixel[3] -lt 32) { 0 } else { `$topPixel[1] }
            `$topB = if (`$topPixel[3] -lt 32) { 0 } else { `$topPixel[2] }
            
            `$bg = "`$ESC[48;2;`${topR};`${topG};`${topB}m"
            `$fg = "`$ESC[38;2;`${botR};`${botG};`${botB}m"
            `$line += "`${bg}`${fg}`$LowerHalfBlock"
        }
    }
    `$line += "`$ESC[0m`$ESC[K"
    Write-Host `$line
}

Write-Host ""
"@

                Write-Output ([scriptblock]::Create($scriptContent))
            } elseif ($OutputMode -eq 'Script') {
                $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') {
    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

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 }

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) }
        
        `$botR = if (`$bottomPixel[3] -lt 32) { 0 } else { `$bottomPixel[0] }
        `$botG = if (`$bottomPixel[3] -lt 32) { 0 } else { `$bottomPixel[1] }
        `$botB = if (`$bottomPixel[3] -lt 32) { 0 } else { `$bottomPixel[2] }
        
        if (`$null -eq `$topPixel) {
            `$fg = Get-TrueColorFg -R `$botR -G `$botG -B `$botB
            `$line += "`${fg}`$LowerHalfBlock"
        } else {
            `$topR = if (`$topPixel[3] -lt 32) { 0 } else { `$topPixel[0] }
            `$topG = if (`$topPixel[3] -lt 32) { 0 } else { `$topPixel[1] }
            `$topB = if (`$topPixel[3] -lt 32) { 0 } else { `$topPixel[2] }
            
            `$bg = Get-TrueColorBg -R `$topR -G `$topG -B `$topB
            `$fg = Get-TrueColorFg -R `$botR -G `$botG -B `$botB
            `$line += "`${bg}`${fg}`$LowerHalfBlock"
        }
    }
    `$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 {
                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 .px file processing'
    }
}