Hi 👋
In this article you'll see how to make a ASCII art generator from an image
The 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
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
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");
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 ");
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);
}
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);
}
};
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
};
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;
};
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
};
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();
};
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);
Then we finally resize the image and return it like this:
const resized = await blackAndWhite.resize(newWidth, newHeight, {
fit: "outside",
});
return resized;
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;
};
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
};
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();
};
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)];
});
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(
""
);
- interval is the ascii that should be assign to the color (intensity)
charLength = ASCII_CHARS.length;
interval = charLength / 256;
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;
};
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
};
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()))
);
};
then I'll get the length of my characters and create an empty variable named ASCII like this:
const pixels = newImgData.length;
let ASCII = "";
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;
}
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);
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();
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)
I wonder what the output would look like.
This is now on the bottom of the article, thank you for letting me know that I forgot to add the images!
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
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!
That's great because you show the source image also. the output looks great. thanks