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:
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:
Bitmap bmp = new Bitmap(img, 100, 100);
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 (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.
private unsafe StringBuilder convert_image(Bitmap bmp)
{
StringBuilder asciiResult = new StringBuilder();
asciiResult.Append("<body style=\"font-family:
'Courier New', Courier, monospace;font-size: 7px;\">");
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("<br>");
}
asciiResult.Append("</body>");
bmp.UnlockBits(bmpData);
return asciiResult;
}
2. Choosing the Right Character
There's a function in the code above that I'll explain here: getAsciiChar()
. What does it do? It returns a character depending on the transparency of the current pixel (so it looks like true ASCII art).
private char getAsciiChar(int alpha)
{
if (alpha >= 240)
return '@';
if (alpha >= 200)
return '#';
if (alpha >= 160)
return '$';
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:
private void show_image(StringBuilder asciiResult)
{
StreamWriter sw = new StreamWriter("image.html");
sw.Write(asciiResult.ToString());
sw.Close();
}
Finally, we get this:
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("<body style=\"font-family:
'Courier New', Courier, monospace;font-size: 7px;\">");
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("<br>");
}
asciiResult.Append("</body>");
bmp.UnlockBits(bmpData);
return asciiResult;
}
private char getAsciiChar(int alpha)
{
if (alpha >= 240)
return '@';
if (alpha >= 200)
return '#';
if (alpha >= 160)
return '$';
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 :).