PowerShell ANSI Art

PowerShell ANSI Art

Recent versions of the Windows console support RGB ANSI colors. Here’s a script that outputs an image to the console, useful for when you feel the need to beautify the user experience of some backup script.

It repeatedly samples parts of an image, averages the colour of the sampled area, and outputs a space with the background colour set to the derived colour. It assumes the console is using the default font, which is 8 x 16 pixels. The script ignores alpha, and I make no claim of it working with all image types.

It runs slowly. The obvious solution would be to not evaluate every pixel, but a random sample of pixels within the sample area. The trade-off would be speed at the expense of accuracy.

param(
  [string] $imgFile
)

# Needed for working with images
[void][System.Reflection.Assembly]::LoadWithPartialName( "System.Drawing" )

# Assume console default font size: 8 x 16 pixels.
Set-Variable -Name FONT_W_PX -Value ([int]8)  -Option Constant
Set-Variable -Name FONT_H_PX -Value ([int]16) -Option Constant

# Get the average color of an area.
# Return an ANSI RGB color spec string
# to print a space with background color
function getColorChar( [System.Drawing.Bitmap] $img,
                                         [int] $xPos,
                                         [int] $yPos,
                                         [int] $width,
                                         [int] $height ) {

  [int]$xEnd = $xPos + $width  - 1
  [int]$yEnd = $yPos + $height - 1

  [long] $r = [long] $g = [long] $b = 0

  ForEach( $x in $xPos .. $xEnd ) {
    ForEach( $y in $yPos .. $yEnd ) {

      $pixel = $img.GetPixel( $x, $y )
      $r += $pixel.R
      $g += $pixel.G
      $b += $pixel.B
    }
  }

  [int]$sampleCount = $width * $height

  return "{0}[48;2;{1};{2};{3}m " -f [char]0x1B,
                                     [math]::Floor( $r / $sampleCount ),
                                     [math]::Floor( $g / $sampleCount ),
                                     [math]::Floor( $b / $sampleCount )
}

if( -not (Test-Path $imgFile)) {
  Throw "File not found: $imgFile"
}

[System.Drawing.Image]$img = [System.Drawing.Bitmap]::FromFile( $imgFile )

$imgW = $img.Width
$imgH = $img.Height

Write-Host "Image: $imgW x $imgH pixels"

# Get console size, in pixels
$consW = ((Get-Host).UI.RawUI.WindowSize.Width  - 1) * $FONT_W_PX
$consH = ((Get-Host).UI.RawUI.WindowSize.Height - 4) * $FONT_H_PX

Write-Host "Console: $consW x $consH pixels"

# Compare image against console size
$ratioW = $consW / $imgW
$ratioH = $consH / $imgH

# Fit image to console size
# Case: image is smaller than console
if(($ratioW -gt 1) -and ($ratioH -gt 1)) {
  $targW = $imgW
  $targH = $imgH
} else {

  # Case: Image is larger than console
  if( $ratioH -gt $ratioW ) {
    $targW = $consW
    $targH = [math]::Floor( $imgH * $ratioW )
  } else {
    $targH = $consH
    $targW = [math]::Floor( $imgW * $ratioH )
  }
}

# Adjust target width and height to multiples
# of console character sizes
$targW = $targW - ($targW % $FONT_W_PX )
$targH = $targH - ($targH % $FONT_H_PX )

Write-Host "Target Size: $targW x $targH pixels"

# Target size, in characters
$targWch = $targW / $FONT_W_PX
$targHch = $targH / $FONT_H_PX

Write-Host "Target Size: $targWch x $targHch chars"

# Derive the bounds of the sample area.
# Drive from long edge of the image.
$sampleWidth  = [math]::Floor( $imgW / $targWch )
$sampleHeight = [math]::Floor( $imgH / $targHch )

Write-Host "Sample size: $sampleWidth x $sampleHeight pixels"

$numSamplesX = [math]::Floor( $imgW / $sampleWidth )
$numSamplesY = [math]::Floor( $imgH / $sampleHeight )

Write-Host "Horiz Samples: $numSamplesX"
Write-Host "Vert Samples:  $numSamplesY"

# Output image line by line
$line = $Null

ForEach( $y in 0..($numSamplesY - 1)) {

  $yPos = $y * $sampleHeight

  ForEach( $x in 0..($numSamplesX - 1)) {

    $xPos = $x * $sampleWidth
    $line += getColorChar $img $xPos $yPos $sampleWidth $sampleHeight
  }

  Write-Output $line
  $line = ""
}

# Reset terminal output to default
Write-Output ("{0}[39;49m" -f [char]0x1B)

Looking good, Mona.