Introduction
This article is a "just for fun" how-to for some of those out there old enough to remember the heyday of BBS'es and MUDs. I was always fascinated by the talent of the folks good enough to make ASCII art that looked like real images. That in mind, years later (many), I decided to create an algorithm that would convert a real image into its ASCII representation. Even if you don't use this for that purpose, there should be some little tidbits of code that you might enjoy, even if you're not a fan of the early internet.
Background
There are a few kinds of ASCII art. I've tried to represent at least three of those styles here in this article. First, we'll do a single character ASCII drawing, and just set the colors of each character to the same colors as each pixel in the image. Next, we'll reduce the image to a grayscale drawing, then output the HTML in varying shades of gray, still using a single ASCII character. Finally, we'll reduce the image to grayscale, then, based on the shade of gray for each pixel, output different ASCII characters that have a different "shade". By shade, I'm simply talking about how dark a character will appear on a white background. So, for example, "#" appears to be much darker than ":" on a white background.
You can experiment with changing the character constants in the code to output different effects. I got particularly interesting results when I reversed the order of the output characters to get a "negative" effect.
Using the code
I've documented the code in the solution, but I will include my favorite method here for review. This method is the third type of ASCII art (as I mentioned above). It will take a posted file (via HTTP), read the pixels in, get the grayscale value of each pixel, then find the appropriate ASCII character to output.
public static string GrayscaleImageToASCII(System.Drawing.Image img)
{
StringBuilder html = new StringBuilder();
Bitmap bmp = null;
try
{
bmp = new Bitmap(img);
html.Append("<br/&rt;");
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
Color col = bmp.GetPixel(x, y);
col = Color.FromArgb((col.R + col.G + col.B) / 3,
(col.R + col.G + col.B) / 3,
(col.R + col.G + col.B) / 3);
int rValue = int.Parse(col.R.ToString());
html.Append(getGrayShade(rValue));
if (x == bmp.Width - 1)
html.Append("<br/&rt");
}
}
html.Append("</p&rt;");
return html.ToString();
}
catch (Exception exc)
{
return exc.ToString();
}
finally
{
bmp.Dispose();
}
}
This static method is called from the Default.aspx web form. The Default.aspx page holds our File Upload input, as well as three buttons that call the associated methods. In a nutshell, we're doing the following:
- Convert the
Image
object into a Bitmap
object - Enclose the output in HTML paragraph tags
- Loop through each pixel in the bitmap, and obtain the color
- Strip the color information from the pixel (see below)
- Find the character to use based on the new shade (see below)
- Aggregate all of the characters, then return the HTML
Converting to grayscale
The simplest way to convert a pixel to grayscale is by taking each pixel's Red, Green, and Blue components, dividing the summed value for each by three, and building a new color like so:
Color col = bmp.GetPixel(x, y);
col = Color.FromArgb((col.R + col.G + col.B) / 3,
(col.R + col.G + col.B) / 3,
(col.R + col.G + col.B) / 3);
Converting colors to characters
To do this required some experimenting. The values I've included with the demo code seem to work fairly well, but feel free to experiment with different character sets. In order to convert the gray shade to a character, we really only need one value. I chose to use the new Red value.
private static string getGrayShade(int redValue)
{
string asciival = " ";
if (redValue >= 230)
{
asciival = WHITE;
}
else if (redValue >= 200)
{
asciival = LIGHTGRAY;
}
else if (redValue >= 180)
{
asciival = SLATEGRAY;
}
else if (redValue >= 160)
{
asciival = GRAY;
}
else if (redValue >= 130)
{
asciival = MEDIUM;
}
else if (redValue >= 100)
{
asciival = MEDIUMGRAY;
}
else if (redValue >= 70)
{
asciival = DARKGRAY;
}
else if (redValue >= 50)
{
asciival = CHARCOAL;
}
else
{
asciival = BLACK;
}
return asciival;
}
You can use as few or as many constants as you like. 9 seemed to provide good results. The constant values that I chose are are as follows:
private const string BLACK = "@";
private const string CHARCOAL = "#";
private const string DARKGRAY = "8";
private const string MEDIUMGRAY = "&";
private const string MEDIUM = "o";
private const string GRAY = ":";
private const string SLATEGRAY = "*";
private const string LIGHTGRAY = ".";
private const string WHITE = " ";
So now, as we get the gray shade for each pixel, we just output the corresponding ASCII character. The logo of my favorite team (Denver Broncos) now appears as follows:
Points of interest
Many of you will notice that I did not use an HTML Text Writer to build the HTML. Simply put, it was more overhead than I needed. StringBuilder
seemed to work very well, and though it doesn't afford me the luxury of ensuring I have the right formatting, it is by far the fastest solution.
Styling is handled by a CSS file (included). For this reason, the "class='ascii_art'" bit was added to the opening paragraph tag. You may notice that the VS2005 IDE flags the "line-spacing" attribute in the CSS designer. Don't worry... both IE and Firefox know how to handle it. Line spacing keeps the characters close so there is not a lot of whitespace between the lines. Also, it is important to use a MONOTYPE font (Lucida Console, Courier New, Terminal, etc.) for the font, otherwise your image will be extremely skewed.
Currently, the solution just performs a Response.Write()
of the HTML to the Default.aspx page on postback. This could obviously be modified to post to a separate page, but I chose not to for purposes of simplicity and illustration.
Happy ASCII'ing!