C# Ascii Art Tutorial
If you got here, you probably want to know how Ascii Art works and how to use C# to transform images into text. We’ll do this by making good use of LockBits() and UnlockBits(), and also, a pointer - going unsafe !
I know those make everything more complicated, but they’re more efficient.
How does an Ascii Art generator work?
- first, it opens the Image and resizes it to a custom size (about 100x100)
- using 2 loops and a pointer, it gets the color of each pixel in the image (the image, stored in memory, looks like a two-dimensional array of pixels)
- for each pixel, it adds a character into a text file, depending on the alpha (transparency)
Now if you got a basic idea about how this works, you can build your own program - no need to worry about the source code, you’ll find everything here, including the necessary explanations.
Start by creating a Forms Project, make sure you have checked Allow unsafe code from Project->Properties->Build.
In form1_load add the following line to load the image from the executable’s directory:
1
Image img = Image.FromFile("image.png");
Then, we transform this image into a Bitmap, and resize it to 100x100 pixels - don’t use HD images there, because it will take some time to check every pixel:
1
2
3
Bitmap bmp = new Bitmap(img, 100, 100);
// you can increase the Ascii Art's quality by increasing the bitmap's dimensions
// this also increases the time taken by the conversion process...
Now we need a StringBuilder in which we store the characters corresponding to the image’s pixels.
Update: it is more efficient to use a StringBuilder instead of a string (see here why).
1.From Pixel to Char
As I said, we’ll use those 2 functions :
LockBits() - locks the image in the system’s memory so we can directly get pixel’s attributes by using a pointer
UnlockBits() - releases the memory used
As you know, an image is created by a group of pixels and each pixel takes 4 bytes of memory, that means it has 4 properties: Red, Green, Blue and Alpha/transparency. From the memory we can read each pixel’s property.
Each pixel must be transformed into a character with the same color and all the characters must be the same width and height (monospaced) so we maintain the aspect ratio.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private unsafe StringBuilder convert_image(Bitmap bmp)
{
StringBuilder asciiResult = new StringBuilder(); //here we store the ascii-art string
//setting the font's size & type (Courier new is monospace)
asciiResult.Append("");
//storing the image's height & width
int bmpHeight = bmp.Height;
int bmpWidth = bmp.Width;
//here we lock the image in the memory by using LockBits
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmpWidth, bmpHeight), ImageLockMode.ReadOnly, bmp.PixelFormat);
// bmpStride tells us how many pixels are on a line
// because images have multiple lines of pixels (like 2D arrays)
int bmpStride = bmpData.Stride;
// this gets the memory address of the first pixel in the image
// currentPixel is the pointer we'll use
byte* currentPixel = (byte*)bmpData.Scan0;
for (int y = 0; y < bmpHeight; y++)
{
for (int x = 0; x < bmpWidth; x++)
{
// as I said a pixel takes 4 bytes of memory so it has 4 attributes
int r = currentPixel[x*4];
int g = currentPixel[x*4 + 1];
int b = currentPixel[x*4 + 2];
int alpha = currentPixel[x * 4 + 3];
// appending the character to the ascii-art stringbuilder
// note there's a custom function 'getAsciiChar()' - I'll explain it soon
asciiResult.Append(String.Format("<span style="color:rgb({0},{1},{2});">{3}</span>", r, g, b, getAsciiChar(alpha)));
}
// reached end of this line, by adding bmpStride (number of pixels on each line)
// to the memory address, it gives us the address of the first pixel on the next line
currentPixel += bmpStride;
asciiResult.Append("
");
}
asciiResult.Append(""); // closing the body tag we opened at the beginning
bmp.UnlockBits(bmpData); //removing the image from the memory
return asciiResult; // returning the ascii-art stringbuilder
}
2.Choosing the right character
There’s a function in the code above that I’ll explain here: getAsciiChar(). What it does? It returns a character depending on the transparency of the current pixel (so it looks like true ascii art).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private char getAsciiChar(int alpha)
{
if (alpha >= 240)
return '@';
if (alpha >= 200)
return '#';
if (alpha >= 160)
return '/pre>;
if (alpha >= 120)
return '%';
if (alpha >= 80)
return '8';
if (alpha >= 40)
return '|';
return '.';
}
3.Displaying the Ascii-Art
Now we just have to display our image, which is easily done using this:
1
2
3
4
5
6
private void show_image(StringBuilder asciiResult)
{
StreamWriter sw = new StreamWriter("image.html");
sw.Write(asciiResult.ToString());
sw.Close();
}
Finally, we get this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Image img = Image.FromFile("image.png");
Bitmap bmp = new Bitmap(img, 100, 100);
show_image(convert_image(bmp));
}
private unsafe StringBuilder convert_image(Bitmap bmp)
{
StringBuilder asciiResult = new StringBuilder();
asciiResult.Append("");
int bmpHeight = bmp.Height;
int bmpWidth = bmp.Width;
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmpWidth, bmpHeight), ImageLockMode.ReadOnly, bmp.PixelFormat);
int bmpStride = bmpData.Stride;
byte* currentPixel = (byte*)bmpData.Scan0;
for (int y = 0; y < bmpHeight; y++)
{
for (int x = 0; x < bmpWidth; x++)
{
int r = currentPixel[x * 4];
int g = currentPixel[x * 4 + 1];
int b = currentPixel[x * 4 + 2];
int alpha = currentPixel[x * 4 + 3];
asciiResult.Append(String.Format("<span style="color:rgb({0},{1},{2});">{3}</span>", r, g, b, getAsciiChar(alpha)));
}
currentPixel += bmpStride;
asciiResult.Append("
");
}
asciiResult.Append("");
bmp.UnlockBits(bmpData);
return asciiResult;
}
private char getAsciiChar(int alpha)
{
if (alpha >= 240)
return '@';
if (alpha >= 200)
return '#';
if (alpha >= 160)
return '/pre>;
if (alpha >= 120)
return '%';
if (alpha >= 80)
return '8';
if (alpha >= 40)
return '|';
return '.';
}
private void show_image(StringBuilder asciiResult)
{
StreamWriter sw = new StreamWriter("image.html");
sw.Write(asciiResult.ToString());
sw.Close();
}
}
}
When you run the application, wait until the Form shows up - that’s when the image conversion is done, then simply open “image.html”.
Here’s a small screenshot of an Ascii Art made with this program - this is how the result will look like:
Well, that’s all, if you have problems, you can always leave a comment :).