RSS Feed

Ascii Art!

February 25th, 2011 by totem in C#, Grafica



Oggi ho letto un simpatico articolo su The Code Project e mi sono preso la libertà di renderlo un po' più formale ed efficiente. Ho racchiuso la funzionalità di creazione in una classe factory e quella di salvataggio in un'altra classe che ha un costruttore dichiarato come internal, cosicché possa essere usato solo da classi dello stesso assembly (nella fattispecie, una volta compilata la libreria, solo dalla classe factory). Ecco il codice:

namespace AsciiArt
{
    public class AsciiArtCreator
    {
        // Contiene i caratteri che rappresentano varie tonalità di grigio, a seconda di
        // quanto è "densa" la rappresentazione del carattere
        private static Char[] asciiGreyScale = { '@', '#', '8', '&', 'o', ':', '*', '-', ' ' };

        // Queste due proprietà indicano a quanti pixel corrisponde un carattere,
        // in larghezza e in lunghezza. Di default un carattere è 6x8 pixel
        public Int32 AsciiUnitX { get; set; }
        public Int32 AsciiUnitY { get; set; }

        public AsciiArtCreator()
        {
            this.AsciiUnitX = 6;
            this.AsciiUnitY = 8;
        }

        public AsciiArtDocument CreateFromImage(String imagePath)
        {
            if (!File.Exists(imagePath))
                throw new FileNotFoundException(imagePath);
            return CreateFromImage(Image.FromFile(imagePath) as Bitmap);
        }

        public AsciiArtDocument CreateFromImage(Bitmap image)
        {
            AsciiArtDocument document = new AsciiArtDocument();
            BitmapData bmData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

            Int32 stride = bmData.Stride;
            IntPtr Scan0 = bmData.Scan0;
            // Usare un'iterazione unsafe tramite puntatori è molto più veloce
            // della stessa operazione con il metodo GetPixel(x, y)
            document.AsciiFormat = new Size(AsciiUnitX, AsciiUnitY);
            unsafe
            {
                byte* baseAddress = (byte*)(void*)Scan0;
                byte red, green, blue, grey;

                for (int i = 0; i < image.Height / AsciiUnitY; i++)
                {
                    for (int j = 0; j < image.Width / AsciiUnitX; j++)
                    {
                        int greySum = 0;
                        float greyAverage;
                        for (int x = 0; x < AsciiUnitX; x++)
                            for (int y = 0; y < AsciiUnitY; y++)
                            {
                                // Calcola la posizione in memoria del colore di questo pixel
                                byte* position = (byte*)(baseAddress + stride * (i * AsciiUnitY + y) + (j * AsciiUnitX + x) * 3);
                                blue = position[0];
                                green = position[1];
                                red = position[2];
                                // Ne fa la media per convertirlo in scala di grigi
                                grey = (byte)((float)(red + green + blue) / 3);
                                greySum += grey;
                            }
                        // Quindi ricava la media di tutte le tonalità di grigio presenti nell'area
                        // AsciiUnitX x AsciiUnitY che rappresenta un caratere
                        greyAverage = (float)greySum / (AsciiUnitY * AsciiUnitX);
                        // E stampa sul documento il carattere ascii corrispondente alla tonalità
                        // di grigio prevalente nel rettangolino considerato
                        document.Append(asciiGreyScale[(Int32)(greyAverage / (256.0f / asciiGreyScale.Length))]);
                    }
                    document.AppendLine();
                }
            }
            image.UnlockBits(bmData);

            return document;
        }
    }

    public class AsciiArtDocument
    {
        private StringBuilder buffer;

        public Size AsciiFormat { get; internal set; }

        internal AsciiArtDocument()
        {
            this.buffer = new StringBuilder();
        }

        internal void Append(Char c)
        {
            this.buffer.Append(c);
        }

        internal void AppendLine()
        {
            this.buffer.AppendLine();
        }

        public void SaveAsPlainText(String fileName)
        {
            File.WriteAllText(fileName, buffer.ToString());
        }

        public void SaveAsHtml(String fileName)
        {
            StringReader reader = new StringReader(buffer.ToString());
            StreamWriter writer = new StreamWriter(fileName);

            writer.WriteLine("");
            writer.WriteLine("     ");
            writer.WriteLine("   
", this.AsciiFormat.Height * 2, this.AsciiFormat.Height * 2 + 2);
            while (reader.Peek() > -1)
            {
                writer.Write(reader.ReadLine());
                writer.WriteLine("");
            }
            writer.WriteLine("   

");
writer.WriteLine("");

writer.Close();
reader.Close();
}
}
}

Esempi:

versione ascii (tagliata perché era troppo grande):