DEV Community

Cover image for I coded a ASCII art generator in Node JS
Moulun Kevin
Moulun Kevin

Posted on • Updated on

I coded a ASCII art generator in Node JS

Hi 👋
In this article you'll see how to make a ASCII art generator from an image

The result:

The img used

result

but first

what is this ASCII art ?

ASCII art is a graphic design technique that uses computers for presentation and consists of pictures pieced together from the 95 printable characters defined by the ASCII Standard from 1963 and ASCII compliant character sets with proprietary extended characters

Prerequisites

I'll use those packages:
For this project I wanted to use my JS knowledge, so I'll use:

npm i sharp readline-sync
Enter fullscreen mode Exit fullscreen mode

Steps for the program:

When I was thinking of ASCII art, I imagined that it was made with some sort of edge detection algorythm, Oh boy I was wrong, for making an ASCII art you from a picture, you'll need to:

  • turn the image into a black and white image
  • resize the image
  • replace all the black and white pixel by character defines for brightness and darkness / shadow

Alright let's get into it, I'll first create a package.json file by doing a:

npm init
Enter fullscreen mode Exit fullscreen mode

Once I have my package, I'll create a index.js file, this is where my code will be.

Alright once that done, I'll import all the dependencies necessary for this project like this:

const sharp = require("sharp");
const readlineSync = require("readline-sync");
const fs = require("fs");
Enter fullscreen mode Exit fullscreen mode

then let's first ask the user the image that it want to convert

Get the user input

For this I'll create function called loadFileFromPath and into it I will get the user input like this:

var filePath = readlineSync.question("What's the file path ");
Enter fullscreen mode Exit fullscreen mode

Why do we need readlineSync?
You probably wondering what is tha readlineSync package. This allow us to enter a input in the console Synchronously since node JS is Asynchronous, the code continues it's execution, so we use this to wait for the user input

then I'll test if the path is correct or not with a try/catch like this:

try {
    const file = await sharp(filePath);
    return file;
  } catch (error) {
    console.error(error);
  }
Enter fullscreen mode Exit fullscreen mode

and the all function looks like this:

const loadFileFromPath = async () => {
  var filePath = readlineSync.question("What's the file path ");
  try {
    const file = await sharp(filePath);
    return file;
  } catch (error) {
    console.error(error);
  }
};
Enter fullscreen mode Exit fullscreen mode

Convert to black and white

For this I'll first create a function named convertToGrayscale with a path parameter like this:

const convertToGrayscale = async (path) => {
  // code
};
Enter fullscreen mode Exit fullscreen mode

in this function I'll load the img and change it's color values to B&W and finally I return the b&w result

const convertToGrayscale = async (path) => {
 const img = await path; 
 const bw = await img.gamma().greyscale();
 return bw;
};
Enter fullscreen mode Exit fullscreen mode

Resizing the image

For this I'll first create a function named resizeImg with bw and newWidth = 100 parameters like this:

const resizeImg = async (bw, newWidth = 100) => {
  //code
};
Enter fullscreen mode Exit fullscreen mode

t
I'll then await for the bw image and await the blackAndWhite wariable result then get it's metadatas for getting access to the sizes properties

const resizeImg = async (bw, newWidth = 100) => {
  const blackAndWhite = await bw;
  const size = await blackAndWhite.metadata();
};
Enter fullscreen mode Exit fullscreen mode

we then calculate the ratio of the image, for that we just divide the width by the height and we get the ratio. Then we calculate our new height with:

const ratio = size.width / size.height;
newHeight = parseInt(newWidth * ratio);
Enter fullscreen mode Exit fullscreen mode

Then we finally resize the image and return it like this:

const resized = await blackAndWhite.resize(newWidth, newHeight, {
    fit: "outside",
  });
return resized;
Enter fullscreen mode Exit fullscreen mode

The whole function should look like this:

const resizeImg = async (bw, newWidth = 100) => {
  const blackAndWhite = await bw;
  const size = await blackAndWhite.metadata();
  const ratio = size.width / size.height;
  newHeight = parseInt(newWidth * ratio);
  const resized = await blackAndWhite.resize(newWidth, newHeight, {
    fit: "outside",
  });

  return resized;
};
Enter fullscreen mode Exit fullscreen mode

Convert pixels to ASCII characters

For this I'll first create a function named pixelToAscii with a img parameter like this:

const pixelToAscii = async (img) => {
 //code
};
Enter fullscreen mode Exit fullscreen mode

then I'll create variable to hold the img with an await keyword. I'll then get the pixels Array of the image and store it in a variable named pixels.

var newImg = await img;
const pixels = await newImg.raw().toBuffer();
};
Enter fullscreen mode Exit fullscreen mode

Then I'll create a variable named characters which gonna contains an empty String. I then go through each pixel from the pixels array and the ASCII character to the string I created earlier:

characters = "";
pixels.forEach((pixel) => {
    characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];
  });
Enter fullscreen mode Exit fullscreen mode

You might notice two global variable that I did not mentioned yet:

  • interval
  • ASCII_CHARS

I'll explain you what both those variables are:

  • ASCII_CHARS is the variable that hold all the ASCII characters:
ASCII_CHARS = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".split(
  ""
);
Enter fullscreen mode Exit fullscreen mode
  • interval is the ascii that should be assign to the color (intensity)
charLength = ASCII_CHARS.length;
interval = charLength / 256;
Enter fullscreen mode Exit fullscreen mode

Okay now we know what are those variable let's get back to the function, it should now look like this:

const pixelToAscii = async (img) => {
  var newImg = await img;
  const pixels = await newImg.raw().toBuffer();
  characters = "";
  pixels.forEach((pixel) => {
    characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];
  });
  return characters;
};
Enter fullscreen mode Exit fullscreen mode

Now we have all our steps, let's create the core of the app:

The main function

For this I'll first create a function named main with newWidth = 100 parameters like this:

const main = async (newWidth = 100) => {
  //code
};
Enter fullscreen mode Exit fullscreen mode

in this function I'll create a function named: *newImgData which gonna be equal to all those function we created earlier nested like so:

const main = async (newWidth = 100) => {
  const newImgData = await pixelToAscii(
    resizeImg(convertToGrayscale(loadFileFromPath()))
  );
};
Enter fullscreen mode Exit fullscreen mode

then I'll get the length of my characters and create an empty variable named ASCII like this:

const pixels = newImgData.length;
let ASCII = "";
Enter fullscreen mode Exit fullscreen mode

then I'll loop through the pixels list like so:

for (i = 0; i < pixels; i += newWidth) {
    let line = newImgData.split("").slice(i, i + newWidth);
    ASCII = ASCII + "\n" + line;
  }
Enter fullscreen mode Exit fullscreen mode

so basicaly I'm setting the line splitting. I'm getting the size of newWidth and then slice the array as a line of this newWidth
and then add the "\n" character to go to the next line.

Export to a text file

And finally in the same function I had this to save the text in a text file

 setTimeout(() => {
    fs.writeFile("output.txt", ASCII, () => {
      console.log("done");
    });
  }, 5000);
Enter fullscreen mode Exit fullscreen mode

and VOILA we got a ASCII art generator from image, oh and of course don't forget the main() to first call the function

the complete code should look like this:

const sharp = require("sharp");
const readlineSync = require("readline-sync");
const fs = require("fs");

ASCII_CHARS = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".split(
  ""
);
charLength = ASCII_CHARS.length;
interval = charLength / 256;
var newHeight = null;
const main = async (newWidth = 100) => {
  const newImgData = await pixelToAscii(
    resizeImg(convertToGrayscale(loadFileFromPath()))
  );
  const pixels = newImgData.length;
  let ASCII = "";
  for (i = 0; i < pixels; i += newWidth) {
    let line = newImgData.split("").slice(i, i + newWidth);
    ASCII = ASCII + "\n" + line;
  }

  setTimeout(() => {
    fs.writeFile("output.txt", ASCII, () => {
      console.log("done");
    });
  }, 5000);
};

const convertToGrayscale = async (path) => {
  const img = await path;
  const bw = await img.gamma().greyscale();
  return bw;
};

const resizeImg = async (bw, newWidth = 100) => {
  const blackAndWhite = await bw;
  const size = await blackAndWhite.metadata();
  const ratio = size.width / size.height;
  newHeight = parseInt(newWidth * ratio);
  const resized = await blackAndWhite.resize(newWidth, newHeight, {
    fit: "outside",
  });

  return resized;
};

const pixelToAscii = async (img) => {
  var newImg = await img;
  const pixels = await newImg.raw().toBuffer();
  characters = "";
  pixels.forEach((pixel) => {
    characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];
  });
  return characters;
};

const loadFileFromPath = async () => {
  var filePath = readlineSync.question("What's the file path ");
  try {
    const file = await sharp(filePath);
    return file;
  } catch (error) {
    console.error(error);
  }
};
main();
Enter fullscreen mode Exit fullscreen mode

What I learned throughout this project ?

This project was really interesting to make I first discovered that you can nest functions, I also discovered, how does ASCII art was working, I learned about node js Asynchronous problem for user input and how to solve this problem, and finally how to do some basic image manipulation.

Conclusion

Thank you for reading this, I hope this helped you in any way
You can follow me on:
instagram
youtube

Hope you'll have an awesome day / Having an awesome day
and don't forget keep learning

Top comments (5)

Collapse
 
raddevus profile image
raddevus

I wonder what the output would look like.

Collapse
 
atndesign profile image
Moulun Kevin

This is now on the bottom of the article, thank you for letting me know that I forgot to add the images!

Collapse
 
igor_bykov profile image
Igor Bykov

It might be a nice idea to put a sample output example in the begginning of the article since otherwise it's hard to understand what is being built before reading the entire article

Thread Thread
 
atndesign profile image
Moulun Kevin

Yeah, I did not think about that, thank you for this tip, I have changed the output to the top now, and as a reader it's indeed more appealing, thank you!

Collapse
 
raddevus profile image
raddevus

That's great because you show the source image also. the output looks great. thanks