Click here to Skip to main content
Click here to Skip to main content

C# ASCII Art Tutorial

, 18 Oct 2013
Rate this:
Please Sign up or sign in to vote.
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:

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);
// 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 (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();   //here we store the ascii-art string
            
            //setting the font's size & type (Courier new is monospace)
            asciiResult.Append("<body style=\"font-family: 
            'Courier New', Courier, monospace;font-size: 7px;\">"); 

            //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("<br>");
                
            }
            asciiResult.Append("</body>"); // 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 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:

Ascii Art

Well, that's all, if you have problems, you can always leave a comment Smile | :) .

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Apex95

Romania Romania
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 18 Oct 2013
Article Copyright 2013 by Apex95
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid