Click here to Skip to main content
Click here to Skip to main content
Go to top

ASCII Art Generator

, 28 May 2007
Rate this:
Please Sign up or sign in to vote.
ASCII Art generator in ASP.NET.

** The Windows application port is provided by David Luu, and is not maintained by me.

Original image

ASCII Art image (colored and monochrome) [click for enlarged view]

ASCII Art image (pure color-HTML and unformatted text-only) [click for enlarged view]

Introduction

Have you ever seen a C# application that converts a given image to a text-based ASCII Art image like the ones shown above?

Well, a few months ago, I stumbled across an article4 on Code Project by Daniel Fisher which talks about creating an application that does just this. According to Daniel, he was not able to find any C# application on the web that does image-to-ASCII conversions, so he decided to write his own, and hence his article. This is yet another article on the same topic, but with a slightly more enhanced ASCII Art generation. What I did was I searched through the web, found some web sites with image-to-ASCII conversion applications in PHP, combined all their ideas (including Daniel's), and implemented another (more enhanced) version of the ASCII Art generator in .NET.

For the list of web sites from where I got all my ideas/information, or if you just want to know more about ASCII Art in general, please check out the References section given below.

Using the code (Installation)

In the source files included above, you will find the following two Visual Studio projects:

  • ASCII - An ASP.NET web page that demonstrates the ASCII Art generator functionality.
  • Library - A library (DLL) for generating ASCII Art.

To install the project:

  1. First, unzip the files to an empty directory. Let us call this directory ASCIIArt.
  2. Next, make a virtual directory in IIS that links to the ASCII sub-folder mentioned above.
  3. After that, give read permissions to the following (local) user accounts for the ASCII sub-folder:
    • IUSR_<MachineName>
    • ASPNET (See Note below)
  4. Also, give read/write permissions to the following (local) user accounts for the Images sub-folder (residing under ASCII sub-folder):
    • ASPNET (See Note below)

    Note: On some machines (especially servers), IIS doesn't actually use the ASPNET account to run ASP.NET pages. To find out exactly what account your IIS is using, just copy and paste the following code into Notepad and save it as who.aspx, then put the file into a web-folder under IIS and view it in Internet Explorer. The page will display the correct user account that ASP.NET is running under.

    <%@ Page Language="C#" %>
    <%@ import Namespace="System.Security.Principal" %>
    <html>
      <body>
        ASP.NET User Account: <b><%= WindowsIdentity.GetCurrent().Name %></b>
      </body>
    </html>

The library logic

The logic of my Image-To-ASCII conversion library is actually not that complicated. (No, really!) Derived from IMG2ASCII1, Boosty's ASCII Artist2, Daniel Fisher's ASCII Art with C# 4, ASCGEN 6, and Playing with ColorMatrix8, the basic idea is as follows:

  1. From the set of allowed ASCII characters given by the user, find the brightness/luminance of each character from a pre-defined XML data file. (Data mainly based on the ASCII.sql from IMG2ASCII 1)
  2. Construct an array of the above characters sorted by their brightness/luminance.
  3. Resize the given image to the proper scale/dimension. There are two ways of doing this. The simple way is to use System.Drawing.Bitmap's built-in image resize feature:
    using System;
    using System.Drawing;
    
    public GetResizedImage (Image originalImage, 
                         int newWidth, int newHeight)
    {
        return (new Bitmap (originalImage, newWidth, 
                                          newHeight));
    }

    The above method is fast, but may not produce a high-quality resized image, and you don't have the option to select only a portion/section of the image to work with. The other alternative is to use System.Drawing.Graphics, as follows:

    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    
    // NOTE: section = portion of the image you want to use.
    public GetResizedImage (Image originalImage, 
        Rectangle section, int newWidth, int newHeight)
    {
        // Create an empty bitmap with the new size.
        Bitmap resizedImage = new Bitmap (newWidth, 
                                           newHeight);
        Rectangle imageArea = new Rectangle (0, 0, 
                                    newWidth, newHeight);
    
        // Use Graphics object to draw the 
        // resized image onto the bitmap.
        Graphics g = Graphics.FromImage (resizedImage);
        g.InterpolationMode = 
                  InterpolationMode.HighQualityBicubic;
        g.DrawImage (originalImage, imageArea, section.X, 
                   section.Y, section.Width, section.Height, 
                   GraphicsUnit.Pixel);
    
        return (resizedImage);
    }
  4. Now, if you need to modify the brightness, contrast, saturation, and gamma of the image, do the following:
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    
    // NOTE1: brightness = Amount of 'sunlight' in picture.
    //                   = -1.0 to 1.0, -1 = pitch-black, 
    //                          0 = original, 1 = total white
    //        contrast = Amount of difference between red, 
    //                                 green, and blue colors.
    //                 = 0.0 or above, 0 = complete gray, 
    //                   1 = original, higher = glaring white
    //        saturation = The amount of 'grayscale-ness' 
    //                                             in picture.
    //                   = 0.0 or above, 0 = grayscale, 
    //                     1 = original (colors), 
    //                     higher = very 'colorful'
    //        gamma = extra brightness correction to picture.
    //              = 0.0 or above, 0 = total white, 
    //                1 = original, higher = darker
    //
    // NOTE2: hue is not implemented in this version.
    // NOTE3: The implementation of the CreateColorMatrix() 
    // method will be discussed later.
    
    public GetTransformedImage (Image resizedImage, 
                  float brightness, float contrast,
                  float saturation, float gamma)
    {
        // Create yet another new image for 
        // the color transformation.
        Bitmap transformedImage = 
          new Bitmap (resizedImage.Width, resizedImage.Height);
        Rectangle imageArea = 
            new Rectangle (0, 0, resizedImage.Width, 
                                    resizedImage.Height);
    
        // Set up the image transformation parameters.
        ImageAttributes transformData = new ImageAttributes();
        transformData.SetColorMatrix (
          CreateColorMatrix (brightness, contrast, saturation));
        transformData.SetGamma (gamma);
    
        // Transform the image.
        Graphics g = Graphics.FromImage (transformedImage);
        g.DrawImage (resizeImage, imageArea, imageArea.X, 
                     imageArea.Y, imageArea.Width,
                     imageArea.Height, GraphicsUnit.Pixel, 
                     transformData);
    
        return (transformedImage);
    }

    Now, at this stage, you may be wondering whether we can merge Step 3 and 4 together (assuming you use the second resizing method above), the short answer is: yes. The merged version will look like the following:

    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Drawing.Imaging;
    
    public GetResizedAndTransformedImage (
              Image originalImage, Rectangle section,
              int newWidth, int newHeight, 
              float brightness, float contrast, 
              float saturation, float gamma)
    {
        // Create an empty bitmap with the new size.
        Bitmap newImage = new Bitmap (newWidth, 
                                         newHeight);
        Rectangle imageArea = new Rectangle (0, 0, 
                               newWidth, newHeight);
    
        // Set up the image transformation parameters.
        ImageAttributes transformData = 
                             new ImageAttributes();
        transformData.SetColorMatrix (CreateColorMatrix(
                          brightness, contrast, saturation));
        transformData.SetGamma (gamma);
    
        // Use Graphics object to draw the resized and 
        // transformed image onto the bitmap.
        Graphics g = Graphics.FromImage (newImage);
        g.InterpolationMode = 
              InterpolationMode.HighQualityBicubic;
        g.DrawImage (originalImage, imageArea, 
              section.X, section.Y, section.Width,
              section.Height, GraphicsUnit.Pixel, 
              transformData);
    
        return (newImage);
    }

    However, there is a catch in using the above merged method. To illustrate this, consider the case where you want to resize a HUGE image down to a small size and then convert it to ASCII Art. If you use the merged method, you will need to transform the entire image before the resizing takes place. This takes a lot of time. Therefore, it is better to resize it first before transforming it, hence the two separate methods given above.

    Of course, you may argue that if somebody wants to resize a small image to a larger one and then transform it, the above situation will be reversed. But the fact is that this doesn't happen often in real life, so using the two separate methods will still be a better option.

    But then, you could always implement both ways and choose the correct way depending on the original vs. resized size ratio. I leave this up to you.

  5. In Step 4 above, we use a custom method called CreateColorMatrix(). This method simply calculates a proper 5x5 color matrix for the specified brightness, contrast, and saturation. This color matrix is used for transforming the image by means of matrix multiplication. In fact, the construction of this matrix is also based on matrix multiplication. In this article, I will not talk about how matrix multiplication works. But if you want to know more about it, you can check out the MSDN Site10 for an excellent explanation on this topic in relation to the usage of the ColorMatrix object.

    Warning: Implementing the CreateColorMatrix() method involves a lot of mathematical details. If you find this boring, simply skip the details given below and go straight here for the source code of this method.

    The following shows the actual matrix values of the brightness, contrast, and saturation matrices:

    Brightness Matrix    Contrast Matrix      Saturation Matrix
    
         R G B A W          R G B A W         R   G   B   A   W
    
     R  [1 0 0 0 0]      R  [c 0 0 0 0]      R  [sr+s sr  sr  0   0]
     G  [0 1 0 0 0]      G  [0 c 0 0 0]      G  [ sg sg+s sg  0   0]
     B  [0 0 1 0 0]      B  [0 0 c 0 0]      B  [ sb  sb sb+s 0   0]
     A  [0 0 0 1 0]      A  [0 0 0 1 0]      A  [ 0   0   0   1   0]
     W  [b b b 0 1]      W  [t t t 0 1]      W  [ 0   0   0   0   1]
    
     b = brightness      c = contrast         s  = saturation
                         t = (1.0 - c) / 2.0  sr = (1 - s) * lumR
     Legend                                   sg = (1 - s) * lumG
     R = red                                  sb = (1 - s) * lumB
     G = green
     B = blue                                 lumR = 0.3086  or  0.2125
     A = alpha (transparency)                 lumG = 0.6094  or  0.7154
     W = white (always = 1)                   lumB = 0.0820  or  0.0721
    
    - The brightness matrix is a simple translation 
      matrix on the RGB elements.
    
    - The contrast matrix is a scaling matrix on the RGB elements. 
      The extra translation parameters in the contrast matrix is used 
      for shifting the base color (when c = 0)from black to gray.
    
    - The saturation matrix re-adjust the RGB color distribution so 
      that at s = 0, R = G = B = luminance (brightness in grayscale).

    Notice that the saturation matrix has three special constants: lumR, lumG, and lumB. These represent the proportion of each RGB value that contributes to the luminance (brightness) value. In short, luminance for a pixel is calculated as follows:

    // Formula for calculating luminance 
    // based on NTSC standard
    // (as described in ITU-R Recommendation BT.709)
    double luminance = 
        0.2125 * red + 0.7154 * green + 0.0721 * blue;
    
    // Alternate formula for calculating 
    // luminance for linear RGB space.
    // (Widely used in color hue and saturation)
    double luminance = 
       0.3086 * red + 0.6094 * green + 0.0820 * blue;
    
    // DON'T use the following NTSC 
    // formula for YIQ Luminance.
    // (It's not used for linear RGB space.)
    // double luminance = 
      0.299 * red + 0.587 * green + 0.114 * blue;

    From the above information, we can calculate the proper color matrix to transform a given image. To use all three matrices, we need to multiply them together into one single transformation matrix (using matrix multiplication). The result of multiplication is as follows:

         R G B A W            R G B A W             R   G   B   A   W
    
     R  [1 0 0 0 0]       R  [c 0 0 0 0]       R  [sr+s sr  sr  0   0]
     G  [0 1 0 0 0]       G  [0 c 0 0 0]       G  [ sg sg+s sg  0   0]
     B  [0 0 1 0 0]    X  B  [0 0 c 0 0]    X  B  [ sb  sb sb+s 0   0]
     A  [0 0 0 1 0]       A  [0 0 0 1 0]       A  [ 0   0   0   1   0]
     W  [b b b 0 1]       W  [t t t 0 1]       W  [ 0   0   0   0   1]
    
    Brightness Matrix     Contrast Matrix          Saturation Matrix
    
    
                            R      G      B      A      W
    
                     R  [c(sr+s) c(sr)  c(sr)    0      0   ]
                     G  [ c(sg) c(sg+s) c(sg)    0      0   ]
             ===>    B  [ c(sb)  c(sb) c(sb+s)   0      0   ]
                     A  [   0      0      0      1      0   ]
                     W  [  t+b    t+b    t+b     0      1   ]
    
                               Transformation Matrix

    So, based on the above derived transformation matrix, we can proceed to implement the CreateColorMatrix() method:

    using System;
    using System.Drawing.Imaging;
    
    private const float LumR = 0.3086f;  // or  0.2125f
    private const float LumG = 0.6094f;  // or  0.7154f
    private const float LumB = 0.0820f;  // or  0.0721f
    
    private ColorMatrix CreateColorMatrix (float brightness, 
                             float contrast, float saturation)
    {
        if (brightness < -1f) brightness = -1f;
        if (brightness > 1f) brightness = 1f;
        if (contrast < 0f) contrast = 0f;
        if (saturation < 0f) saturation = 0f;
    
        float Wf = (1f - contrast) / 2f + brightness;
        float Rf = (1f - saturation) * LumR * contrast;
        float Gf = (1f - saturation) * LumG * contrast;
        float Bf = (1f - saturation) * LumB * contrast;
        float Rf2 = Rf + saturation * contrast;
        float Gf2 = Gf + saturation * contrast;
        float Bf2 = Bf + saturation * contrast;
    
        return (new ColorMatrix (new float[][]
        {
            new float[] {Rf2, Rf,  Rf,  0f,  0f},
            new float[] {Gf,  Gf2, Gf,  0f,  0f},
            new float[] {Bf,  Bf,  Bf2, 0f,  0f},
            new float[] {0f,  0f,  0f,  1f,  0f},
            new float[] {Wf,  Wf,  Wf,  0f,  1f}
        }));
    }
  6. First, you need to create a transformed image that is purely grayscale (saturation = 0). The brightness and contrast can be user-given. Then for each pixel in the grayscale image, get the luminance/brightness of that pixel, find the matching ASCII character with a similar proportion of brightness/luminance from the constructed character array in Step 2 above, and then output that character.
    using System;
    using System.Drawing;
    using System.Text;
    
    public string GetAsciiArt (Image originalImage, 
          Rectangle section, int outputWidth, 
          int outputHeight, float brightness,
          float contrast, float saturation, float gamma)
    {
        StringBuilder asciiArt = new StringBuilder();
    
        char[] AsciiCharSet = ... ;  // From Step 2 above.
    
        // Resize and transform image to grayscale.
        Bitmap resizedImage = GetResizedImage (originalImage, 
                          section, outputWidth, outputHeight)
        Bitmap grayImage = GetTransformedImage (resizedImage, 
                             brightness, contrast, 0, gamma);
        /* dostuff 1 ... */
    
          // Notice that we are forcing the 
          // saturation to be 0 (grayscale) above.
          // At this stage the user-given saturation 
          // is not used.
    
        // Loop through every pixel in grayscale image.
        for (int y = 0; y < outputHeight; y++)
        {
            for (int x = 0; x < outputWidth; x++)
            {
                // In grayscale, R = G = B.
                byte lum = grayImage.GetPixel (x, y).R;  
                int index = 
                    ((int) lum) * AsciiCharSet.Length / 256;
    
                /* dostuff 2 ... */
                asciiArt.Append (AsciiCharSet[index]);
                /* dostuff 3 ... */
            }
    
            asciiArt.Append ("\n");
        }
    
        return (asciiArt.ToString());
    }
  7. Sometimes, you need to display the transformed image in colors, or display the ASCII Art output in colors. In this case, you need to create one more transformed image based on the user-given saturation. Then for each generated ASCII character, color it based on the corresponding pixel color in the transformed colored image. For example, if you are displaying the ASCII Art in a web page, you can use the HTML <font> tags to change the character color.
    // Replace /* dostuff 1 */ in Step 6 above 
    // with the following code.
    Bitmap colorImage = GetTransformedImage (resizedImage, 
                   brightness, contrast, saturation, gamma);
    
    // Notice that we are now using the 
    // user-given saturation above.
    
    
    // Replace /* dostuff 2 */ in Step 6 above 
    // with the following code.
    
    // Get RGB values, no Alpha.
    int color = 
       (colorImage.GetPixel (x, y).ToArgb() & 0xFFFFFF);  
    string hexColor = "00000" + color.ToString ("X");
    
    asciiArt.Append ("<font color='#");
    asciiArt.Append (hexColor.Substring (hexColor.Length - 6));
    asciiArt.Append ("'>");
    
    
    // Replace /* dostuff 3 */ in Step 6 
    // above with the following code.
    asciiArt.Append ("</font>");
  8. The above code is actually not optimal as it always encloses each character with a separate <font> tag, even if adjacent characters have the same color. Therefore, you can optimize the code given above so that adjacent characters with the same font color will share the same <font> tags.

    For example, the HTML ASCII Art output:

    <font color="#FFCC00">W</font><font color="#FFCC00">N</font>

    should be changed to:

    <font color="#FFCC00">WN</font>
  9. Finally, if you are displaying the ASCII Art onto a web page, you should wrap the entire ASCII image in a properly-formatted block so that spaces between characters can be reduced. You can use style-sheet to do this:
    FontSize = (user-given font-size in px)
    LineHeight = Math.Round(FontSize * 3.0 / 5.0);
    
    <pre style=
       "font-size: FontSizepx; line-height: LineHeightpx">
    ...
    </pre>

Besides the above logic, the library also does a few other bits and pieces to provide more functionality. However, those are not so important as far as the 'technology' of ASCII Art is concerned.

The web page

The web page uses the following information to generate the ASCII image.

  • Image URL or upload a new image.
  • Make use of all alphabets in ASCII image.
  • Make use of all numbers in ASCII image.
  • Make use of all basic symbols in ASCII image (Non-Unicode symbols, Font-independent brightness).
  • Make use of all extended symbols in ASCII image (Non-Unicode symbols, Font-dependent brightness).
  • Make use of all block symbols in ASCII image (Unicode symbols: Blocks, pipes, etc.).
  • Only use a custom user-defined set of characters.
  • User-defined font-size.
  • User-defined background color.
  • Use only a single (user-defined) font color.
  • Use multiple font colors (in full color).
  • Use multiple font colors (in gray-scale).
  • Output image down-scaling (reduce size of image), from 1x to 16x.
  • Custom output image size (width & height), either in pixels or percentages.
  • Text-only output (no style-sheet formatting, useful for text images displayed in Notepad or other pure text editors).
  • Download a generated ASCII image file rather than viewing online.

Known issues and suggested solutions

  1. The character weighting (brightness/luminance) information is mainly from IMG2ASCII 1, which unfortunately is not very accurate.
  2. Some block symbols do not have the same fixed-width as other characters. Therefore, distortion can occur if you use block symbols.
  3. I suggest using custom character sets to compensate the above two problems. The default chosen custom character set is " .:,;+ijtfLGDKW#" (without the quotes). (This character set is derived from the ASCII data used in IMG2TXT 3.)
  4. If you just want color HTML, then just use the letter 'W' for the custom character set.
  5. If you include custom characters that are not found in XML data file, then those characters will automatically be removed from the custom character set. This behavior is due to the design.

That's all!

If you want to see the full implementation of the above logic, just download the source files given above and have a look at the source codes. Note that the C# code presented in this article is a simplified version of the actual code; I have left out all that is irrelevant to the purpose of this article, such as error-handling and freeing resources.

Also, in my source files, I have put open curly braces at the end of a line, not at the beginning of a new line. Now, I'm not going to argue with you guys about which style is the best one that we should use. Personally, I use the end-of-line curly braces because I find it easier to read long lines of code. (Each start-of-line curly brace use up one line of code, so long lines of code will get even longer.) And no, I don't have a problem locating the start and end of a block since indentation is used.

Other than that, have fun! Smile | :)

Please don't hesitate to write me suggestions/comments if you have any.

References

The following web sites were referred to while building this web application:

  1. IMG2ASCII - An open source PHP image-to-ASCII converter.
  2. Boosty's ASCII Artist - A online image-to-ASCII converter in PHP (with source).
  3. IMG2TXT - An online image-to-ASCII converter.
  4. Daniel Fisher's ASCII Art with C#.
  5. Glass-giant's ASCII Artist - An online image-to-ASCII converter.
  6. ASCGEN - A powerful .NET-based ASCII Art Generator (with source) - Thanks to Sire404 for this link!.
  7. Multiple Color Matrices - Transforming images in .NET.
  8. Playing with ColorMatrix - Transforming images brightness, contrast, saturation, hue, and gamma.
  9. Image transformation source code for Linux KDE Print Library.
  10. MSDN - Recoloring images.

The following web site contains lots of information on ASCII Art in general:

To-do list

  • Modify the web page code to allow user to change the brightness, contrast, and saturation of the image.
  • Modify the web page code to allow user to select only a portion of the image for ASCII Art output.
  • Modify the web page architecture so that uploading a file to a permanent physical location of the server is not necessary to generate its ASCII Art.
  • Implement hue adjustment of the image in ASCII Art library.

History

  • 12th July, 2005
    • Article: Added a few more sample ASCII Art images.
    • Article: Added a References section.
    • Article: Added a History section.
    • Article: Minor cosmetic changes in wording.
  • 13th July, 2005
    • Article: Added two more sample ASCII Art images.
  • 16th July, 2005
    • CODE: Improved the graphics manipulation logic in ASCII Art library.
    • Article: Added a To-do list section.
    • Article: Modified lot of stuff in The Library Logic section in response to the new changes in code.
  • 23 May 2007
    • Added a Windows desktop GUI port submitted by David Luu.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Sau Fan Lee
Web Developer
New Zealand New Zealand
Mainly a C# & ASP.NET developer, specializing in research & (back-end) development.

Comments and Discussions

 
QuestionHow to Manipulating Matrix to Adjust Contrast in VB.NET 2010 PinmemberMember 1062912826-Feb-14 20:26 
QuestionPort to Windows 8 Metro application and Windows Phone? Pinmemberdaluu14-Jul-13 14:47 
GeneralAwesome...Mindblowing Pinmembermohanjune198728-Oct-12 18:08 
QuestionAbout ASCII Art Generator Pinmembergigigou5-Sep-10 22:00 
GeneralMy vote of 5 Pinmembergigigou5-Sep-10 21:59 
GeneralPorted to Ajax PinmemberDoodie Woddy Doodlechips16-Mar-08 3:54 
GeneralRe: Ported to Ajax Pinmemberdaluu15-May-08 10:45 
GeneralSigh...people just have to reinvent the wheel...for ASCII art in C# that is... Pinmemberdaluu30-Dec-07 14:26 
GeneralRe: Sigh...people just have to reinvent the wheel...for ASCII art in C# that is... PinmemberSau Fan Lee1-Jan-08 16:33 
Actually, this is probably a good thing. When there are multiple articles for the same topic, people get more information, references, and inputs from different sources. Different articles have different level of usefulness to different people depending on what they are looking for.
 

> whoami
The system is unable to identify you when you don't even know your name!

GeneralRe: Sigh...people just have to reinvent the wheel...for ASCII art in C# that is... Pinmemberdaluu2-Jan-08 7:30 
Questionoiii u r still alive Pinmembertze fui18-Jun-07 16:22 
AnswerRe: oiii u r still alive PinmemberSau Fan Lee28-Jun-07 19:09 
QuestionHello Pinmemberamartttyyyyaa8-Jun-07 3:34 
AnswerRe: Hello PinmemberSau Fan Lee15-Jun-07 19:15 
Generaland.. PinmemberAlaa Jebran2-Jun-07 23:06 
GeneralExcellent ! PinmemberAlaa Jebran2-Jun-07 22:52 
GeneralAwesome Pinmembermerlin98128-May-07 10:55 
GeneralPorted as a Windows desktop GUI application Pinmemberdaluu8-Apr-07 14:59 
GeneralRe: Ported as a Windows desktop GUI application PinmemberSau Fan Lee7-May-07 22:19 
GeneralRe: Ported as a Windows desktop GUI application Pinmemberdaluu8-May-07 9:28 
GeneralRe: Ported as a Windows desktop GUI application PinmemberSau Fan Lee28-May-07 15:08 
GeneralRe: Ported as a Windows desktop GUI application Pinmemberdaluu28-May-07 18:27 
GeneralJust Great PinmemberM A A Mehedi Hasan14-Aug-06 1:22 
GeneralBBCode Art Generator PinmemberGonzalo Brusella25-Jul-06 9:17 
GeneralRe: BBCode Art Generator PinmemberSau Fan Lee25-Jul-06 14:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140926.1 | Last Updated 28 May 2007
Article Copyright 2005 by Sau Fan Lee
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid