Here’s a WPF-based ASCII Art generator that I wrote. Merry Christmas!
The GUI just consists of a button for selecting a file and a label indicating the file that was selected.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="{Binding InputFile}"
HorizontalAlignment="Left" VerticalAlignment="Center"
Margin="10"/>
<Button Content="Select File" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center"
Padding="10,5" Margin="10"
Click="btnSelectFile_Click"/>
</Grid>
In the code-behind, we generate an output file name and load the input file into a Bitmap before calling the main ASCII Art generation method.
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
inputFile = "Select an image file to use for ASCII Art";
}
private string inputFile;
public string InputFile
{
get { return inputFile; }
set
{
if (value != inputFile)
{
inputFile = value;
RaisePropertyChanged("InputFile");
}
}
}
async private void btnSelectFile_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "Image Files (*.jpg, *.png)|*.jpg;*.png;";
ofd.Title = "Select an image file to convert";
try
{
bool? bResult = ofd.ShowDialog();
if (bResult.HasValue && (bool)bResult)
{
InputFile = ofd.FileName;
await Task.Run(() => DoGenerate(InputFile));
}
}
catch (Exception xx)
{
MessageBox.Show(string.Format("Fatal Error: {0}", xx.ToString()), "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void DoGenerate(string inputFile)
{
int outputWidth = 100;
FileInfo fi = new FileInfo(inputFile);
if (!fi.Exists)
throw new Exception(string.Format("File {0} not found", inputFile));
string outputFile = Path.Combine(fi.DirectoryName, Path.GetFileNameWithoutExtension(inputFile) + ".txt");
Bitmap bmInput = new Bitmap(inputFile);
if (outputWidth > bmInput.Width)
throw new Exception("Output width must be <= pixel width of image");
// Generate the ASCII art
AscArt.GenerateAsciiArt(bmInput, outputFile, outputWidth);
// This will fire up default app for .txt files
Process.Start(outputFile);
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Finally, here is the code for the class that generates the ASCII art.
public static class AscArt
{
// Typical width/height for ASCII characters
private const double FontAspectRatio = 0.6;
// Available character set, ordered by decreasing intensity (brightness)
private const string OutputCharSet = "@%#*+=-:. ";
// Alternate char set uses more chars, but looks less realistic
private const string OutputCharSetAlternate = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
public static void GenerateAsciiArt(Bitmap bmInput, string outputFile, int outputWidth)
{
// pixelChunkWidth/pixelChunkHeight - size of a chunk of pixels that will
// map to 1 character. These are doubles to avoid progressive rounding
// error.
double pixelChunkWidth = (double)bmInput.Width / (double)outputWidth;
double pixelChunkHeight = pixelChunkWidth / FontAspectRatio;
// Calculate output height to capture entire image
int outputHeight = (int)Math.Round((double)bmInput.Height / pixelChunkHeight);
// Generate output image, row by row
double pixelOffSetTop = 0.0;
StringBuilder sbOutput = new StringBuilder();
for (int row = 1; row <= outputHeight; row++)
{
double pixelOffSetLeft = 0.0;
for (int col = 1; col <= outputWidth; col++)
{
// Calculate brightness for this set of pixels by averaging
// brightness across all pixels in this pixel chunk
double brightSum = 0.0;
int pixelCount = 0;
for (int pixelLeftInd = 0; pixelLeftInd < (int)pixelChunkWidth; pixelLeftInd++)
for (int pixelTopInd = 0; pixelTopInd < (int)pixelChunkHeight; pixelTopInd++)
{
// Each call to GetBrightness returns value between 0.0 and 1.0
int x = (int)pixelOffSetLeft + pixelLeftInd;
int y = (int)pixelOffSetTop + pixelTopInd;
if ((x < bmInput.Width) && (y < bmInput.Height))
{
brightSum += bmInput.GetPixel(x, y).GetBrightness();
pixelCount++;
}
}
// Average brightness for this entire pixel chunk, between 0.0 and 1.0
double pixelChunkBrightness = brightSum / pixelCount;
// Target character is just relative position in ordered set of output characters
int outputIndex = (int)Math.Floor(pixelChunkBrightness * OutputCharSet.Length);
if (outputIndex == OutputCharSet.Length)
outputIndex--;
char targetChar = OutputCharSet[outputIndex];
sbOutput.Append(targetChar);
pixelOffSetLeft += pixelChunkWidth;
}
sbOutput.AppendLine();
pixelOffSetTop += pixelChunkHeight;
}
// Dump output string to file
File.WriteAllText(outputFile, sbOutput.ToString());
}
}

Full solution available here.