Ascii art characters powershell script

From Svendsen Tech Powershell Wiki

Jump to: navigation, search

This PowerShell ASCII art script module uses an XML file that contains the supported characters, and produces ASCII art letter output to STDOUT or the pipeline. I used the Linux utility figlet to create the ASCII art letters and wrapped them in XML along with some metadata. This is sort of a poor man's figlet for PowerShell.

It has been tested with PowerShell version 2 and 3 in its current form.

Write-Ascii no longer prepends an apostrophe by default, so in all the examples/screenshots where you might see "-nop" or "-NoPrependChar", you can ignore that and just leave it out. If you do want to add it, use the parameter "-PrependChar".

Originally I wrote it to be used in conjunction with a modified version of PowerBot, a simple IRC bot framework based on the SmartIrc4Net library. This is why it can prepend an apostrophe, because somewhere along the way to IRC, the leading spaces seem to disappear.

Currently it supports only a pretty basic subset of characters (I did add a few "international"/accented ones randomly). If you want other characters, you will need to add them to the XML. The list of accepted characters is dynamically created from the read XML file. This XML file is expected to have the name "letters.xml", be encoded in UTF-8 and to be in the module's working directory.

I wrote this for my own use, but I figured I'd share it in case someone was looking for something like this. It was harder than I thought to write the algorithm for compression, and quite a bit of work to create the XML.

Of course, this isn't the most useful script in the world; it has to be placed in the "fun" category.

Contents






PowerBot IRC ASCII Art Screenshot

Image:Ascii-art-irc2-powerbot-powershell.png


PowerShell Console and ISE Screenshots

Image:Ascii-art-console-powershell.png

Screenshot from ISE v3:

Image:Ascii-art-ise-powershell.png

Documentation

There's some built-in documentation and comments in the code itself; all of them might not be up-to-date, but I tried to fix most of them.

Currently, only lower-case letters are supported, mostly because PowerShell/Windows is case-insensitive by default when reading the XML - and workarounds seem cumbersome.

Here's an example of the XML that's used for the letter "a":

  <char>
    <name>a</name>
    <fixation>default</fixation>
    <lines>4</lines>
    <width>7</width>
    <data>  __ _
 / _` |
| (_| |
 \__,_|</data>
  </char>

All the characters in the XML have from 4-6 lines, where some of them may consist of only whitespace. If the maximum number of lines in the letter with the most lines is 4, only 4 lines will be output and letters will be bottom-aligned (which doesn't matter in the case of max 4 lines). It's filtered just before output with a simple "Where-Object { $_ -match '\S' }", that prevents lines with only padding/whitespace to be output. Actually, an "extra" side-effect of the whitespace filtering, is that punctuation characters with only 2-3 lines will only produce those 2-3 lines if printed alone. Such as a period (two lines) and a comma (three lines).

After some consideration, I added an XML tag called "fixation", which ensures the correct bottom-alignment of the 5-line letters g, y, p and q - in addition to the special character "¤" - which I added for reasons unknown - beyond it being on the Norwegian default keyboard layout (shift-4).

However, if you use the parameter -Compression, and the letter with the most lines has less than 6 lines, these aforementioned 5-line letters will not be correctly bottom-aligned. It was designed like this to save line space on IRC. I made compression optional and made correct alignment the default behaviour.

This demonstrates how compression works for these special letters:

Image:Ascii-art-compress-demonstration.png


More Screenshots And Fun

The script module also sports -ForegroundColor and -BackgroundColor parameters that accept all the colors that the corresponding parameters to Write-Host accepts. I've implemented colors on IRC as well, but those are some custom changes to the script I use in the customized PowerBot, and not included on this page.

Image:Ascii-art-console-powershell2.png

Don't tell me this isn't useful!

Image:Ascii-art-console-color2-powershell.png

Here's a one-liner for a simple ASCII clock (press any key to stop it):

while (!$Host.UI.RawUI.KeyAvailable) { cls; Write-Ascii (get-date); sleep 1 }

And I simply couldn't resist adding a "color" called rainbow. Behold:

Image:Ascii-art-console-rainbow.png

Download

  • Write-Ascii.zip - contains the PowerShell script module and the XML with the currently supported characters. To use this module in PowerShell, unzip the compressed folder into one of your $env:PSModulePath folders, and then import it with "Import-Module Write-Ascii".

Type "Get-Help Write-Ascii -Detailed" to get the help text - after it's been imported.

  • 2013-03-02: v1.0.0.5. Fixed a bug that made it say spaces weren't allowed.
  • 2013-03-01: v1.0.0.4. Added support for pipeline input, removed maximum character limitation, made it not prepend an apostrophe by default. ++



Source Code

<#
.SYNOPSIS
Svendsen Tech's PowerShell ASCII art module creates ASCII art characters
from a subset of common letters, numbers and punctuation characters.
You can add new characters by editing the XML (for developers).

Author: Joakim Svendsen, Svendsen Tech.
Copyright (c) 2012, Svendsen Tech.
All rights reserved.

.DESCRIPTION
This script reads characters from an XML file that's expected to have the name
"letters.xml", be encoded in UTF-8 and to be in the module's working directory.

It was written to be used in conjunction with a modified version of
PowerBot (http://poshcode.org/2510), a simple IRC bot framework written
using SmartIrc4Net; that's why it can prepend an apostrophe - because somewhere
along the way the leading spaces get lost before it hits the IRC channel.

Currently the XML only contains lowercase letters, mostly because PowerShell/
Windows is case-insensitive by default, which isn't an advantage here.

Example:
PS C:\> Import-Module Write-Ascii
PS C:\> Write-Ascii "ASCII!"
                   _  _  _
  __ _  ___   ___ (_)(_)| |
 / _` |/ __| / __|| || || |
| (_| |\__ \| (__ | || ||_|
 \__,_||___/ \___||_||_|(_)
PS C:\>

.PARAMETER InputText
String(s) to convert to ASCII.
.PARAMETER PrependChar
Optional. Makes the script prepend an apostrophe.
.PARAMETER Compression
Optional. Compress to five lines when possible, even when it causes incorrect
alignment of the letters g, y, p and q (and "¤").
.PARAMETER ForegroundColor
Optional. Console only. Changes text foreground color.
.PARAMETER BackgroundColor
Optional. Console only. Changes text background color.
#>

function Write-Ascii {
# Wrapping the script in a function to make it a module

[CmdLetBinding()]
param(
    [Parameter(ValueFromPipeline=$true, Mandatory=$true)][string[]] $InputText,
    [switch] $PrependChar,
    [switch] $Compression,
    [string] $ForegroundColor = 'Default',
    [string] $BackgroundColor = 'Default'
    #[int] $MaxChars = '25'
    )

begin {
    
    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'
    
    # Algorithm from hell... This was painful. I hope there's a better way.
    function Get-Ascii {
    
        param([string] $Text)
    
        $LetterArray = [char[]] $Text.ToLower()
    
        #Write-Host -fore green $LetterArray
    
        # Find the letter with the most lines.
        $MaxLines = 0
        $LetterArray | ForEach-Object { if ($Letters.([string] $_).Lines -gt $MaxLines ) { $MaxLines = $Letters.([string] $_).Lines } }
    
        # Now this sure was a simple way of making sure all letter align tidily without changing a lot of code!
        if (-not $Compression) { $MaxLines = 6 }
    
        $LetterWidthArray = $LetterArray | ForEach-Object { $Letter = [string] $_; $Letters.$Letter.Width }
        $LetterLinesArray = $LetterArray | ForEach-Object { $Letter = [string] $_; $Letters.$Letter.Lines }
    
        #$LetterLinesArray
    
        $Lines = @{
            '1' = ''
            '2' = ''
            '3' = ''
            '4' = ''
            '5' = ''
            '6' = ''
        }
    
        #$LineLengths = @(0, 0, 0, 0, 0, 0)
    
        # Debug
        #Write-Host "MaxLines: $Maxlines"

        $LetterPos = 0
        foreach ($Letter in $LetterArray) {
        
            # We need to work with strings for indexing the hash by letter
            $Letter = [string] $Letter
        
            # Each ASCII letter can be from 4 to 6 lines.
        
            # If the letter has the maximum of 6 lines, populate hash with all lines.
            if ($LetterLinesArray[$LetterPos] -eq 6) {
            
                #Write-Host "Six letter letter"

                foreach ($Num in 1..6) {
                
                    $StringNum = [string] $Num
                
                    $LineFragment = [string](($Letters.$Letter.ASCII).Split("`n"))[$Num-1]
                
                    if ($LineFragment.Length -lt $Letters.$Letter.Width) {
                        $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
                    }
                
                    $Lines.$StringNum += $LineFragment
                
                }
            
            }
        
            # Add padding for line 6 for letters with 5 lines and populate lines 2-6.
            ## Changed to top-adjust 5-line letters if there are 6 total.
            ## Added XML properties for letter alignment. Most are "default", which is top-aligned.
            ## Also added script logic to handle it (2012-12-29): <fixation>bottom</fixation>
            elseif ($LetterLinesArray[$LetterPos] -eq 5) {
            
                #Write-Host "Five-letter letter"
            
                if ($MaxLines -lt 6 -or $Letters.$Letter.fixation -eq 'bottom') {
                
                    $Padding = ' ' * $LetterWidthArray[$LetterPos]
                    $Lines.'1' += $Padding
                
                    foreach ($Num in 2..6) {
                    
                        $StringNum = [string] $Num
                    
                        $LineFragment = [string](($Letters.$Letter.ASCII).Split("`n"))[$Num-2]
                    
                        if ($LineFragment.Length -lt $Letters.$Letter.Width) {
                            $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
                        }
                    
                        $Lines.$StringNum += $LineFragment
                    
                    }
                
                }
            
                else {
                
                    $Padding = ' ' * $LetterWidthArray[$LetterPos]
                    $Lines.'6' += $Padding
                
                    foreach ($Num in 1..5) {
                    
                        $StringNum = [string] $Num
                    
                        $LineFragment = [string](($Letters.$Letter.ASCII).Split("`n"))[$Num-1]
                    
                        if ($LineFragment.Length -lt $Letters.$Letter.Width) {
                            $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
                        }
                    
                        $Lines.$StringNum += $LineFragment
                    
                    }
                
                }
            
            }
        
            # Here we deal with letters with four lines.
            # Dynamic algorithm that places four-line letters on the bottom line if there are
            # 4 or 5 lines only in the letter with the most lines.
            else {
            
                #Write-Host "Four letter letter"

                # Default to putting the 4-liners at line 3-6
                $StartRange, $EndRange, $IndexSubtract = 3, 6, 3
                $Padding = ' ' * $LetterWidthArray[$LetterPos]
            
                # If there are 4 or 5 lines...
                if ($MaxLines -lt 6) {
                
                    $Lines.'2' += $Padding
                
                }
           
                # There are 6 lines maximum, put 4-line letters in the middle.
                else {
                
                    $Lines.'1' += $Padding
                    $Lines.'6' += $Padding
                    $StartRange, $EndRange, $IndexSubtract = 2, 5, 2
                
                }
            
                # There will always be at least four lines. Populate lines 2-5 or 3-6 in the hash.
                foreach ($Num in $StartRange..$EndRange) {
                
                    $StringNum = [string] $Num
                
                    $LineFragment = [string](($Letters.$Letter.ASCII).Split("`n"))[$Num-$IndexSubtract]
                
                    if ($LineFragment.Length -lt $Letters.$Letter.Width) {
                        $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
                    }
                
                    $Lines.$StringNum += $LineFragment
                
                }
            
            }
        
            $LetterPos++
        
        } # end of LetterArray foreach
    
        # Return stuff
        $Lines.GetEnumerator() | Sort Name | Select -ExpandProperty Value | ?{ $_ -match '\S' } | %{ if ($PrependChar) { "'" + $_ } else { $_ } }
    
    }

    # Populate the $Letters hashtable with character data from the XML.
    Function Get-LetterXML {
    
        $LetterFile = Join-Path $PSScriptRoot 'letters.xml'
        $Xml = [xml] (Get-Content $LetterFile)
    
        $Xml.Chars.Char | ForEach-Object {
        
            $Letters.($_.Name) = New-Object PSObject -Property @{
            
                'Fixation' = $_.fixation
                'Lines'    = $_.lines
                'ASCII'    = $_.data
                'Width'    = $_.width
            
            }
        
        }
    
    }

    function Write-RainbowString {
    
        param([string] $Line,
              [string] $ForegroundColor = '',
              [string] $BackgroundColor = '')

        $Colors = @('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow',
            'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')


        # $Colors[(Get-Random -Min 0 -Max 16)]

        [char[]] $Line | %{
        
            if ($ForegroundColor -and $ForegroundColor -ieq 'rainbow') {
            
                if ($BackgroundColor -and $BackgroundColor -ieq 'rainbow') {
                    Write-Host -ForegroundColor $Colors[(Get-Random -Min 0 -Max 16)] `
                        -BackgroundColor $Colors[(Get-Random -Min 0 -Max 16)] -NoNewline $_
                }
                elseif ($BackgroundColor) {
                    Write-Host -ForegroundColor $Colors[(Get-Random -Min 0 -Max 16)] `
                        -BackgroundColor $BackgroundColor -NoNewline $_
                }
                else {
                    Write-Host -ForegroundColor $Colors[(Get-Random -Min 0 -Max 16)] -NoNewline $_
                }

            }
            # One of them has to be a rainbow, so we know the background is a rainbow here...
            else {
            
                if ($ForegroundColor) {
                    Write-Host -ForegroundColor $ForegroundColor -BackgroundColor $Colors[(Get-Random -Min 0 -Max 16)] -NoNewline $_
                }
                else {
                    Write-Host -BackgroundColor $Colors[(Get-Random -Min 0 -Max 16)] -NoNewline $_
                }
            }

        }
    
        Write-Host ''
    
    }

    # Get ASCII art letters/characters and data from XML. Make it persistent for the module.
    if (-not (Get-Variable -EA SilentlyContinue -Scope Script -Name Letters)) {
        $script:Letters = @{}
        Get-LetterXML
    }

    # Turn the [string[]] into a [string] the only way I could figure out how... wtf
    #$Text = ''
    #$InputText | ForEach-Object { $Text += "$_ " }

    # Limit to 30 characters
    #$MaxChars = 30
    #if ($Text.Length -gt $MaxChars) { "Too long text. There's a maximum of $MaxChars characters."; return }

    # Replace spaces with underscores (that's what's used for spaces in the XML).
    #$Text = $Text -replace ' ', '_'

    # Define accepted characters (which are found in XML).
    #$AcceptedChars = '[^a-z0-9 _,!?./;:<>()¤{}\[\]\|\^=\$\-''+`\\"æøåâàáéèêóòôü]' # Some chars only works when sent as UTF-8 on IRC
    $LetterArray = [string[]]($Letters.GetEnumerator() | Sort Name | Select -ExpandProperty Name)
    $AcceptedChars = [regex] ( '(?i)[^' + ([regex]::Escape(($LetterArray -join '')) -replace '-', '\-' -replace '\]', '\]') + ' ]' )
    # Debug
    #Write-Host -fore cyan $AcceptedChars.ToString()
}

process {
    if ($InputText -match $AcceptedChars) { "Unsupported character, using these accepted characters: " + ($LetterArray -join ', ') + "."; return }

    # Filthy workaround (now worked around in the foreach creating the string).
    #if ($Text.Length -eq 1) { $Text += '_' }

    $Lines = @()

    foreach ($Text in $InputText) {
        
        $ASCII = Get-Ascii ($Text -replace ' ', '_')

        if ($ForegroundColor -ne 'Default' -and $BackgroundColor -ne 'Default') {
            if ($ForegroundColor -ieq 'rainbow' -or $BackGroundColor -ieq 'rainbow') {
                $ASCII | ForEach-Object { Write-RainbowString -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor -Line $_ }
            }
            else {
                Write-Host -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor ($ASCII -join "`n")
            }
        }
        elseif ($ForegroundColor -ne 'Default') {
            if ($ForegroundColor -ieq 'rainbow') {
                $ASCII | ForEach-Object { Write-RainbowString -ForegroundColor $ForegroundColor -Line $_ }
            }
            else {    
                Write-Host -ForegroundColor $ForegroundColor ($ASCII -join "`n")
            }
        }
        elseif ($BackgroundColor -ne 'Default') {
            if ($BackgroundColor -ieq 'rainbow') {
                $ASCII | ForEach-Object { Write-RainbowString -BackgroundColor $BackgroundColor -Line $_ }
            }    
            else {
                Write-Host -BackgroundColor $BackgroundColor ($ASCII -join "`n")
            }
        }
        else { $ASCII -replace '\s+$' }

    } # end of foreach

} # end of process block
    
}

Export-ModuleMember -Function Write-Ascii
Personal tools