{"id":819,"date":"2020-01-01T14:27:56","date_gmt":"2020-01-01T19:27:56","guid":{"rendered":"http:\/\/pmcgovern.ca\/wp\/?p=819"},"modified":"2020-05-10T13:40:33","modified_gmt":"2020-05-10T17:40:33","slug":"powershell-ansi-art","status":"publish","type":"post","link":"https:\/\/pmcgovern.ca\/wp\/?p=819","title":{"rendered":"PowerShell ANSI Art"},"content":{"rendered":"<p>Recent versions of the Windows console support RGB <a href=\"https:\/\/en.wikipedia.org\/wiki\/ANSI_escape_code\">ANSI<\/a> colors. Here&#8217;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.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"powershell\">param(\n  [string] $imgFile\n)\n\n# Needed for working with images\n[void][System.Reflection.Assembly]::LoadWithPartialName( \"System.Drawing\" )\n\n# Assume console default font size: 8 x 16 pixels.\nSet-Variable -Name FONT_W_PX -Value ([int]8)  -Option Constant\nSet-Variable -Name FONT_H_PX -Value ([int]16) -Option Constant\n\n# Get the average color of an area.\n# Return an ANSI RGB color spec string\n# to print a space with background color\nfunction getColorChar( [System.Drawing.Bitmap] $img,\n                                         [int] $xPos,\n                                         [int] $yPos,\n                                         [int] $width,\n                                         [int] $height ) {\n\n  [int]$xEnd = $xPos + $width  - 1\n  [int]$yEnd = $yPos + $height - 1\n\n  [long] $r = [long] $g = [long] $b = 0\n\n  ForEach( $x in $xPos .. $xEnd ) {\n    ForEach( $y in $yPos .. $yEnd ) {\n\n      $pixel = $img.GetPixel( $x, $y )\n      $r += $pixel.R\n      $g += $pixel.G\n      $b += $pixel.B\n    }\n  }\n\n  [int]$sampleCount = $width * $height\n\n  return \"{0}[48;2;{1};{2};{3}m \" -f [char]0x1B,\n                                     [math]::Floor( $r \/ $sampleCount ),\n                                     [math]::Floor( $g \/ $sampleCount ),\n                                     [math]::Floor( $b \/ $sampleCount )\n}\n\nif( -not (Test-Path $imgFile)) {\n  Throw \"File not found: $imgFile\"\n}\n\n[System.Drawing.Image]$img = [System.Drawing.Bitmap]::FromFile( $imgFile )\n\n$imgW = $img.Width\n$imgH = $img.Height\n\nWrite-Host \"Image: $imgW x $imgH pixels\"\n\n# Get console size, in pixels\n$consW = ((Get-Host).UI.RawUI.WindowSize.Width  - 1) * $FONT_W_PX\n$consH = ((Get-Host).UI.RawUI.WindowSize.Height - 4) * $FONT_H_PX\n\nWrite-Host \"Console: $consW x $consH pixels\"\n\n# Compare image against console size\n$ratioW = $consW \/ $imgW\n$ratioH = $consH \/ $imgH\n\n# Fit image to console size\n# Case: image is smaller than console\nif(($ratioW -gt 1) -and ($ratioH -gt 1)) {\n  $targW = $imgW\n  $targH = $imgH\n} else {\n\n  # Case: Image is larger than console\n  if( $ratioH -gt $ratioW ) {\n    $targW = $consW\n    $targH = [math]::Floor( $imgH * $ratioW )\n  } else {\n    $targH = $consH\n    $targW = [math]::Floor( $imgW * $ratioH )\n  }\n}\n\n# Adjust target width and height to multiples\n# of console character sizes\n$targW = $targW - ($targW % $FONT_W_PX )\n$targH = $targH - ($targH % $FONT_H_PX )\n\nWrite-Host \"Target Size: $targW x $targH pixels\"\n\n# Target size, in characters\n$targWch = $targW \/ $FONT_W_PX\n$targHch = $targH \/ $FONT_H_PX\n\nWrite-Host \"Target Size: $targWch x $targHch chars\"\n\n# Derive the bounds of the sample area.\n# Drive from long edge of the image.\n$sampleWidth  = [math]::Floor( $imgW \/ $targWch )\n$sampleHeight = [math]::Floor( $imgH \/ $targHch )\n\nWrite-Host \"Sample size: $sampleWidth x $sampleHeight pixels\"\n\n$numSamplesX = [math]::Floor( $imgW \/ $sampleWidth )\n$numSamplesY = [math]::Floor( $imgH \/ $sampleHeight )\n\nWrite-Host \"Horiz Samples: $numSamplesX\"\nWrite-Host \"Vert Samples:  $numSamplesY\"\n\n# Output image line by line\n$line = $Null\n\nForEach( $y in 0..($numSamplesY - 1)) {\n\n  $yPos = $y * $sampleHeight\n\n  ForEach( $x in 0..($numSamplesX - 1)) {\n\n    $xPos = $x * $sampleWidth\n    $line += getColorChar $img $xPos $yPos $sampleWidth $sampleHeight\n  }\n\n  Write-Output $line\n  $line = \"\"\n}\n\n# Reset terminal output to default\nWrite-Output (\"{0}[39;49m\" -f [char]0x1B)\n\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp\/wp-content\/uploads\/2020\/01\/mona_lisa_ps.png\" width=\"772\" height=\"488\"><\/p>\n<p>Looking good, Mona.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Recent versions of the Windows console support RGB ANSI colors. Here&#8217;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&#8230;<\/p>\n","protected":false},"author":1,"featured_media":826,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11,1],"tags":[13],"class_list":["post-819","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","category-various","tag-programming"],"_links":{"self":[{"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/posts\/819","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=819"}],"version-history":[{"count":9,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/posts\/819\/revisions"}],"predecessor-version":[{"id":857,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/posts\/819\/revisions\/857"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=\/wp\/v2\/media\/826"}],"wp:attachment":[{"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=819"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=819"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pmcgovern.ca\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=819"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}