-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
160 lines (126 loc) · 5.9 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
* Takes an image source and converts it into ASCII art.
* @param {string} imageSource The absolute path to the image
* @param {Object} [config] Object to specify configuration for the output.
* @param {number} [config.maxWidth=300] Character limit in one row of the generated ASCII image.
* @param {number} [config.maxHeight=500] Character limit in one column of the generated ASCII image .
* @param {Array.<string>} [config.avoidedCharacters=null] Array of ASCII characters that are to be avoided from the output.
* @return {Promise} Promise object representing the generated ASCII string.
*/
const getAsciiImage = (imageSource, config) => {
let maxWidth, maxHeight, avoidedCharacters;
if (!imageSource) {
return new Promise((resolve, reject) => {
reject(new Error('Invalid image source'));
})
}
if (config) {
maxHeight = config.maxHeight || 500;
maxWidth = config.maxWidth || 300;
avoidedCharacters = config.avoidedCharacters || null;
}
//Creating canvas and context for image manipulation
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
return new Promise((resolve, reject) => {
const image = new Image();
//To solve cross origin image access
image.crossOrigin = "Anonymous";
//Function to be run after image is loaded
image.onload = () => {
//Reducing width and height based on maxWidth and maxHeight preserving Aspect-ratio
const [width, height] = _reduceDimension(image.width, image.height, maxWidth, maxHeight);
canvas.width = width;
canvas.height = height;
//Since width and height are reduced this will generate low res image
context.drawImage(image, 0, 0, width, height);
const grayScaleArray = _canvasToGrayScale(context, width, height);
resolve(_getAsciiFromGrayScaleArray(grayScaleArray, width, avoidedCharacters));
}
image.onerror = () => reject(new Error("Unable to load image"));
image.src = imageSource;
});
}
/**
* Decrease the width and height to fit in the limit, preserving the aspect-ratio.
* @param {int} width The width to be decreased.
* @param {int} height The height to be decreased.
* @param {int} maxWidth The maximum height possible.
* @param {int} maxHeight The maximum height possible.
* @return {Array.<int>} The array containing reduced width and height respectively.
*/
const _reduceDimension = (width, height, maxWidth, maxHeight) => {
if (width > maxWidth) {
//Calculating new height using maximum width as the width
height = Math.floor((height * maxWidth) / width);
width = maxWidth;
}
if (height > maxHeight) {
//Calculating new width using maximum height and height
width = Math.floor((width * maxHeight) / height);
height = maxHeight;
}
return [width, height];
}
/**
*
* Generates an array with grayscale values of each pixel from the context.
* @param {Object} context The context of the canvas, from which the image is to be taken.
* @param {int} width The width of the image.
* @param {int} height The height of the image.
* @return {Array.<int>} Array containing the grayscale values corresponding to each pixels.
*/
const _canvasToGrayScale = (context, width, height) => {
//Getting underlying pixel data from canvas
const imgDataObject = context.getImageData(0, 0, width, height);
const imgData = imgDataObject.data;
const grayScaleArray = [];
//Traversing through pixel data, getting rgba and converting it to grayscale
for (let i = 0; i < imgData.length; i += 4) {
const r = imgData[i];
const g = imgData[i + 1];
const b = imgData[i + 2];
let grayScale = _rgbToGrayScale(r, g, b);
//Adding grayscale data to array
grayScaleArray.push(grayScale);
}
return grayScaleArray;
}
/**
*
* Converts rgb to corresponding grayscale value.
* @param {int} r Value of red (0 to 255).
* @param {int} g Value of green (0 to 255).
* @param {int} b Value of blue (0 to 255).
* @return {int} Grayscale scale value calculated from rgb (0 to 255).
*/
const _rgbToGrayScale = (r, g, b) => (0.3 * r) + (0.59 * g) + (0.11 * b);
/**
*
* Generates ASCII string from the grayscale array.
* @param {Array.<int>} grayScaleArray Array containing the grayscale values corresponding to each pixels.
* @param {int} width Width of the image.
* @param {Array.<string>} avoidedCharacters Array of ASCII characters that are to be avoided from the output.
* @return {string} String containing the generated ASCII image.
*/
const _getAsciiFromGrayScaleArray = (grayScaleArray, width, avoidedCharacters) => {
//70 ASCII shades of grey in descending order of intensity;
let asciiIntensityArray = ["$", "@", "B", "%", "8", "&", "W", "M", "#", "*", "o", "a", "h", "k", "b", "d", "p", "q", "w", "m", "Z", "O", "0", "Q", "L", "C", "J", "U", "Y", "X", "z", "c", "v", "u", "n", "x", "r", "j", "f", "t", "/", "|", "(", ")", "1", "{", "}", "[", "]", "?", "-", "_", "+", "~", "<", ">", "i", "!", "l", "I", ";", ":", ",", '"', "^", "`", "'", ".", " "];
//Removing unwanted characters from the array
if (avoidedCharacters) {
asciiIntensityArray = asciiIntensityArray.filter(asciiChar => !avoidedCharacters.includes(asciiChar));
}
let noOfShades = asciiIntensityArray.length;
let asciiImage = "";
grayScaleArray.forEach((grayScale, index) => {
//Converting Grayscale value to corresponding ascii character
let asciiCorrespondent = asciiIntensityArray[Math.ceil((grayScale / 255) * (noOfShades - 1))];
//Adding new line at the end of each row traversal
if ((index + 1) % width === 0) {
asciiCorrespondent += "\n";
}
asciiImage += asciiCorrespondent + " ";
});
return asciiImage;
}
module.exports = getAsciiImage;