A lot of choosing your palette for ASCII art is an art, because the distribution of pixels in each character matters as much as the number of them. Even resolution can matter: a densely packed character such as an asterisk might look darker than a sparsely packed character such as a thick letter O if there are only a few characters per row, but instead have the same effect if there are thousands of characters per row.
But a script can get you started. I wrote densitySort to help me put together the palettes I used in 42 Astounding Scripts•.
The script is fairly simple. It creates an image of each character, calculates the darkness of the image, and then saves that darkness in an array.
Then, it sorts the array by darkness descending and prints out the sorted palette.
At its most basic, you can give it a palette and it will sort the palette using the Monaco font, because that’s the font I use in Terminal.
Say you want to use all of the letters that make up a musical scale in an ASCII image:
- $ ~/bin/densitySort ABCDEFG
- BDAGECF
You can now use that palette to create, say, an ASCII art image of a guitar (or of a viola, as in the top image on this post):
- asciiArt Guitar.jpg --palette "BDAGECF "
Because some characters have special meanings on the command line, you’ll often need to surround the palette with apostrophes:
- $ ~/bin/densitySort '$&%@!'
- @&%$!
You can change the font using the --font option.
- $ ~/bin/densitySort '$&%@!' --font Courier
- @$&%!
If you leave off the palette, it will provide the entire normal ASCII art character set for the font:
- $ ~/bin/densitySort --font Courier
- MQW#BNqpHERmKdgAGbX8@SDO$PUkwZyF69heT0a&xV%Cs4fY52Lonz3ucJjvItr}{li?1][7<>=)(+*|!/\;:-,"_~^.'`
Those are all characters from ASCII character 33 (the exclamation) through 126 (the tilde).
You can also ask it to choose x somewhat evenly-spaced characters from the default or custom palette.
- $ ~/bin/densitySort --font Courier --choose 10
- ME$sj1|-^`
Here’s the guitar again, using that palette:
[toggle code]
- $ ~/bin/asciiArt Guitar.jpg --font Courier --palette 'ME$sj1|-^` '
Not as symbolic, but more contrast.
There are a couple of common palettes hardcoded into the script. If you want to limit your palette to numbers, use “numbers” in place of the palette.
- $ ~/bin/densitySort --font Courier numbers
- 8960452317
Here are all of the built-in palettes:
numbers | 1234567890 |
letters | abcdefghijklmnopqrstuvwxyz |
LETTERS | ABCDEFGHIJKLMNOPQRSTUVWXYZ |
math | 1234567890+-/*= |
punctuation | !\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~ |
short | @%#*+=-:. |
long | $@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. |
The “short” and “long” palettes are from Paul Bourke’s ASCII art palettes.
If there is a collection of characters you often use in different fonts, you can create your own built-in palette. Modify the “palettes” array in the script.
If you want to see what the calculations are for each character in the palette, add --tabulate:
- $ ~/bin/densitySort --font Courier --choose 10 --tabulate
Coverage for font Courier | ||
---|---|---|
M | 100.0 | 77 |
E | 88.3 | 69 |
$ | 75.1 | 36 |
s | 66.5 | 115 |
j | 54.5 | 106 |
1 | 42.8 | 49 |
| | 32.3 | 124 |
- | 17.1 | 45 |
^ | 10.5 | 94 |
` | 0.0 | 96 |
The first column of numbers is the percentage of coverage. It will be normalized if you’re using --choose. The lowest coverage character will be zero, and the highest 100. The second column of numbers is the ASCII value for the character.
And if you want to see the character images that the script is using, add --save to the command line. The script will save each image, using the font name and the character, to the current directory. (So you may wish to mkdir a new directory and cd into it before running the command.)
The density calculation is performed in the function calculateDensity.
[toggle code]
-
func calculateDensity(bitmap:NSBitmapImageRep) -> Double {
- var density = 0.0
-
for row in 0 ... Int(bitmap.pixelsHigh-1) {
-
for column in 0 ... Int(bitmap.pixelsWide-1) {
-
guard let color = bitmap.colorAt(x: column, y: row) else {
- print("Trouble getting color at", column, row, "from", bitmap.size)
- exit(0)
- }
- //red, green, and blue are all the same, so only need to add one of them
- density += Double(1-color.redComponent)
-
guard let color = bitmap.colorAt(x: column, y: row) else {
- }
-
for column in 0 ... Int(bitmap.pixelsWide-1) {
- }
- return density
- }
It does nothing but add up the red value of each pixel in the character image—because the foreground color is set to black elsewhere, and the background to white, the red, green, and blue values are all the same. If you want to play around with different calculations, such as checking to see how dispersed the black pixels are, you can change that function to use your custom formula.
Here’s the complete code:
[toggle code]
- #!/usr/bin/swift
- // sort ASCII characters according to fake grayscale density
- // Jerry Stratton astoundingscripts.com
- import AppKit
- var arguments = CommandLine.arguments
- let commandName = arguments.removeFirst()
- let fontSize:CGFloat = 48.0
- var fontName = "Monaco"
- //some common palettes to sort
-
let palettes = [
- //Paul Bourke's palettes
- "short": "@%#*+=-:.",
- "long": "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'.",
- //other sets
- "numbers": "1234567890",
- "letters": "abcdefghijklmnopqrstuvwxyz",
- "LETTERS": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
- "math": "1234567890+-/*=",
- "punctuation": " !\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~",
- ]
-
func help(message: String = "") {
-
if message != "" {
- print(message)
- }
- print("Syntax:", commandName, "[--options] [palette]")
- print("\t--choose <x>: choose x characters from palette, evenly spaced")
- print("\t--help: print this help and exit")
- print("\t--font <name>: font name to calculate densities for")
- print("\t--tabulate: display table of coverage percentages for each character in palette")
- exit(0)
-
if message != "" {
- }
-
var characterAttributes = [
- NSAttributedString.Key.font:NSFont(name: fontName, size: fontSize)!,
- NSAttributedString.Key.backgroundColor: NSColor.white,
- NSAttributedString.Key.foregroundColor: NSColor.black,
- ]
-
func makeCharacter(asciiCharacter:Character) -> NSBitmapImageRep {
- //create image of character and return a bitmap version
- let character = NSAttributedString(string:String(asciiCharacter), attributes:characterAttributes)
-
let outputImage = NSImage(size:character.size(), flipped: false) { (outputRect) -> Bool in
- character.draw(in: outputRect)
- return true
- }
- //get bitmap
- let imageData = outputImage.tiffRepresentation
-
guard let bitmap = NSBitmapImageRep(data: imageData!) else {
-
- print("Unable to get bitmap version of letter.")
- exit(0)
-
- }
- return bitmap;
- }
- //get character density
-
func calculateDensity(bitmap:NSBitmapImageRep) -> Double {
- var density = 0.0
-
for row in 0 ... Int(bitmap.pixelsHigh-1) {
-
for column in 0 ... Int(bitmap.pixelsWide-1) {
-
guard let color = bitmap.colorAt(x: column, y: row) else {
- print("Trouble getting color at", column, row, "from", bitmap.size)
- exit(0)
- }
- //red, green, and blue are all the same, so only need to add one of them
- density += Double(1-color.redComponent)
-
guard let color = bitmap.colorAt(x: column, y: row) else {
- }
-
for column in 0 ... Int(bitmap.pixelsWide-1) {
- }
- return density
- }
- //handle command-line arguments
- var displayTable = false
- var palette = ""
- var chooseCount:UInt = 0
-
while arguments.count > 0 {
- let argument = arguments.removeFirst()
-
switch argument {
-
case "--choose", "-c":
- chooseCount = UInt(arguments.removeFirst()) ?? 0
-
case "--help", "-h":
- help()
-
case "--font", "-f":
- fontName = arguments.removeFirst()
-
guard let font = NSFont(name: fontName, size: fontSize) else {
- print("Unknown font " + fontName)
- exit(0)
- }
-
if !font.isFixedPitch {
- help(message:fontName + " is not a fixed pitch font.")
- }
- characterAttributes[NSAttributedString.Key.font] = font
-
case "--tabulate", "-t":
- displayTable = true
-
default:
- //if it matches a common palette, use as key
-
if palettes.keys.contains(argument) {
- palette += palettes[argument]!
-
} else {
- palette += argument
- }
-
case "--choose", "-c":
- }
- }
-
if palette == "" {
- //if no palette specified, use full ASCII set
-
for character in 33...126 {
- palette += String(Unicode.Scalar(character)!)
- }
- }
- //calculate densities
- var densities:[Character: Double] = [:]
- var charWidth = 0
- var charHeight = 0
- var totalCoverage = 0.0
-
for character in palette {
- let bitmapCharacter = makeCharacter(asciiCharacter:character)
- //make sure that the character is comparable
-
if charWidth == 0 && charHeight == 0 {
- charWidth = bitmapCharacter.pixelsWide
- charHeight = bitmapCharacter.pixelsHigh
- totalCoverage = Double(charWidth*charHeight)
-
} else {
-
if bitmapCharacter.pixelsHigh != charHeight {
- print("Height is", bitmapCharacter.pixelsHigh, "not", charHeight)
- exit(0)
- }
-
if bitmapCharacter.pixelsWide != charWidth {
- print("Width is", bitmapCharacter.pixelsWide, "not", charWidth)
- exit(0)
- }
-
if bitmapCharacter.pixelsHigh != charHeight {
- }
- let density = calculateDensity(bitmap:bitmapCharacter)
- densities[character] = density
- }
- //sort densities
- let sortedByDensity = densities.sorted(by: { (leftItem, rightItem) -> Bool in leftItem.value > rightItem.value })
- var sortedPalette = ""
-
if displayTable {
- print("Coverage for font", fontName)
- }
- //grade on a curve if choosing evenly spaced characters
- var subtractor = 0.0
- var multiplier = 1.0
-
if chooseCount > 0 {
- subtractor = sortedByDensity.last!.value
- multiplier = 100/(sortedByDensity[0].value - subtractor)
- }
- var previousCharacter = sortedByDensity[0]
- var desiredCharacter = chooseCount
- var desiredValue = 100.0
- var previousCoverage = 100.0
-
for character in sortedByDensity {
- var coverageValue = character.value
- var useCharacter = false
-
if chooseCount > 0 {
- coverageValue -= subtractor
- coverageValue *= multiplier
-
if coverageValue <= desiredValue {
- useCharacter = true
-
if desiredCharacter > 1 {
- desiredCharacter -= 1
- desiredValue = 100.0*Double(desiredCharacter-1)/Double(chooseCount-1)
- }
- }
-
} else {
- coverageValue *= 100/totalCoverage
- useCharacter = true
- }
-
if useCharacter {
-
if displayTable {
- print(String(character.key) + "\t" + String(format:"%5.1f\t%3i", coverageValue, character.key.asciiValue!))
- }
- sortedPalette += String(character.key)
-
if displayTable {
- }
- previousCharacter = character
- previousCoverage = coverageValue
- }
-
if !displayTable {
- print(sortedPalette)
- }
Use toggle code to turn it into copyable tabbed text, and then paste it into a text editor. You might find the edit script helpful in doing this.
- 42 Astoundingly Useful Scripts and Automations for the Macintosh•: Jerry Stratton at Amazon.com (paperback)
- If you have a Macintosh and you want to get your retro on, take a look at 42 Astoundingly Useful Scripts and Automations for the Macintosh. These modern scripts will help you work faster and more reliably, and inspire your own custom scripts for your own workflow.
- Character representation of grey scale images: Paul Bourke
- Paul Bourke provides a standard “character ramp” for photo to greyscale conversion, as well as a more convincing, shorter sequence.
- Edit (Zsh)
- One of the first scripts in the book is a script to edit scripts. But that elicits a bootstrapping problem. Without the edit script, you can’t use the edit script to edit the edit script!
- Leduc Guitar at Wikimedia Commons
- “Leduc Guitar, model: Clean, year: 1984, Maple body and neck-through, ebony fingerboard, SH2 S. Duncan pickups”
- Viola: Museum of Arts and Crafts, Hamburg at Wikimedia Commons
- Public domain image of on 1885 viola in boxwood and maple, against a white background.
More ascii art
- Random colors in your ASCII art
- One of the great things about writing your own scripts is that when you need new functionality, you can add it. I needed random colors in a single-character ASCII art image. It was easy to add to the asciiArt script. Here’s how.
- Hello World in Amber
- A hello world too retro even for me.
- Have a Merry Scripting Christmas with Persistence of Vision
- The ASCII Merry Christmas from Astounding Scripts was taken from a scene I created in Persistence of Vision. It’s a very simple scene that highlights many of the advantages of using POV to create images.
- A thousand points of color: give your photos a pointillist turn
- I had far too much fun with that kleenex mask in the book. Here’s a more serious look at creating pointellated images using the asciiArt script in 42 Astounding Scripts.
- Commemorate Patriot Day with Betsy Ross
- The Declaration of Independence overlaid on the Betsy Ross flag.
- Three more pages with the topic ascii art, and other related pages