├── .gitignore ├── .npmignore ├── parrot.bmp ├── test.bmp ├── test2.bmp ├── testrb.bmp ├── palette.bmp ├── rgb-test.js ├── package.json ├── test.js ├── readme.md ├── img-test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.bmp 2 | node_modules 3 | .git 4 | *est.js 5 | -------------------------------------------------------------------------------- /parrot.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenwick67/term-px/HEAD/parrot.bmp -------------------------------------------------------------------------------- /test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenwick67/term-px/HEAD/test.bmp -------------------------------------------------------------------------------- /test2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenwick67/term-px/HEAD/test2.bmp -------------------------------------------------------------------------------- /testrb.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenwick67/term-px/HEAD/testrb.bmp -------------------------------------------------------------------------------- /palette.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenwick67/term-px/HEAD/palette.bmp -------------------------------------------------------------------------------- /rgb-test.js: -------------------------------------------------------------------------------- 1 | //decode image and print it to term 2 | 3 | var bmp = require('bmp-js'); 4 | var fs = require('fs'); 5 | var rgbPairToChar = require('./index.js'); 6 | var cache = {}; 7 | 8 | 9 | /* console.log(dat.length); 10 | console.log(dat); */ 11 | 12 | function printImage(filename){ 13 | var bmpData; 14 | if (cache[filename]){ 15 | bmpData = cache[filename] 16 | }else{ 17 | var bmpBuffer = fs.readFileSync(filename); 18 | var bmpData = bmp.decode(bmpBuffer); 19 | cache[filename] = bmpData; 20 | } 21 | 22 | var img = rgbPairToChar.convertImage(bmpData,{format:'rgb',reset:'line'}); 23 | process.stdout.write(img); 24 | 25 | } 26 | 27 | printImage('testrb.bmp'); 28 | printImage('parrot.bmp'); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "term-px", 3 | "version": "1.0.8", 4 | "description": "Write pixels to the terminal in vertical pairs", 5 | "main": "index.js", 6 | "author": "Drew Harwell", 7 | "license": "MIT", 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "ansi-escapes": "^1.4.0", 11 | "bmp-js": "0.0.1", 12 | "lodash": "^4.13.1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/fenwick67/term-px.git" 17 | }, 18 | "keywords": [ 19 | "terminal", 20 | "pixels", 21 | "image", 22 | "px", 23 | "term", 24 | "ansi", 25 | "colors", 26 | "gif", 27 | "image", 28 | "display" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/fenwick67/term-px/issues" 32 | }, 33 | "homepage": "https://github.com/fenwick67/term-px#readme" 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | //test rgb to codes 2 | 3 | var rgbPairToChar = require('./index.js'); 4 | var _ = require('lodash'); 5 | 6 | var red = [150,0,0] 7 | var blu = [0,0,150] 8 | var ltred = [255,0,0] 9 | var ltblu = [85,91,253] 10 | 11 | //process.stdout.write(rgbPairToChar(top,bottom)); 12 | 13 | var print = [ 14 | [red,red], 15 | [blu,blu], 16 | [blu,red], 17 | [red,blu], 18 | 19 | [ltred,ltred], 20 | [ltblu,ltblu], 21 | [ltred,ltblu], 22 | [ltblu,ltred], 23 | 24 | [red,ltred], 25 | [blu,blu], 26 | [blu,ltred], 27 | [ltred,blu], 28 | 29 | [ltred,red], 30 | [ltblu,ltblu], 31 | [red,ltblu], 32 | [ltblu,red] 33 | ] 34 | 35 | 36 | process.stdout.write('\r\n Check out these pairs. Compare to test.js to ensure they match up:\r\n'); 37 | print.forEach(function(c){ 38 | process.stdout.write( rgbPairToChar(c[0],c[1]) ); 39 | }) 40 | 41 | function randColor(){ 42 | return [Math.random()*255,Math.random()*255,Math.random()*255] 43 | } 44 | 45 | process.stdout.write('\r\n Now testing solids:'); 46 | 47 | for (var i = 0; i < 30; i ++){ 48 | var col = randColor(); 49 | process.stdout.write('\r\n' + _.repeat(rgbPairToChar(col,col),10) ); 50 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # term-px 2 | 3 | Write pixels to the terminal using ANSI colors and half-characters. [Try it here](http://fenwick67.github.io/term-px). 4 | 5 | Verified to work in Windows CMD, Git Bash, xterm, and more. 6 | 7 | ![Demo](http://i.imgur.com/iwlN1Ru.gif) 8 | 9 | You can see it yourself by running `img-test.js`. 10 | 11 | ## WHAT? 12 | 13 | I'm kinda cheating. Using special characters (namely ▀ █ and ▄) we're able to write an approximation of actual pixels to the terminal. 14 | 15 | 16 | ## Examples: 17 | 18 | ```javascript 19 | var termPx = require('term-px'); 20 | 21 | /* 22 | 23 | default usage 24 | 25 | */ 26 | 27 | var pixels = termPx([0,0,0],[255,255,255]) 28 | // => '\u001b[97;40m▄\u001b[0m' 29 | // => 30 | 31 | // provide an options object with "format" set to "rgb" for use in full color terminals 32 | var rgbPixels = termPx([0x9A,0x00,0xC4],[0xBB,0x00,0xC4],{format:"rgb"}); 33 | // => "\u001b[38;2;154;0;196;48;2;187;0;196m▀\u001b[0m" 34 | // 35 | 36 | // set "reset" to false to not reset the color at the end. 37 | var unClosedRgbPixels = termPx([0x9A,0x00,0xC4],[0xBB,0x00,0xC4],{format:"rgb",reset:false}); 38 | // => "\u001b[38;2;154;0;196;48;2;187;0;196m▀" 39 | 40 | /* 41 | 42 | Preparing images: 43 | 44 | */ 45 | 46 | // Image data must be in RGBA 0-255 format, and have data and width keys. 47 | var bmp = require("bmp-js"); 48 | var bmpBuffer = fs.readFileSync('aa.bmp'); 49 | var bmpData = bmp.decode(bmpBuffer); // {data:[255,0,255,255,...],width:10,height:20} 50 | 51 | var logo = termPx.convertImage(bmpData,{format:"rgb"}); 52 | // => (a massive String) 53 | 54 | process.stdout.write(logo) 55 | // logo is shown 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /img-test.js: -------------------------------------------------------------------------------- 1 | //decode image and print it to term 2 | 3 | var bmp = require('bmp-js'); 4 | var fs = require('fs'); 5 | var rgbPairToChar = require('./index.js'); 6 | var ansiEscapes = require('ansi-escapes'); 7 | var cache = {}; 8 | 9 | for (var i = 0; i < 25; i ++){ 10 | process.stdout.write('\r\n'); 11 | } 12 | 13 | setInterval(alternate,100) 14 | 15 | var i = 1; 16 | function alternate(){ 17 | 18 | i++; 19 | if (i%2==0){ 20 | printImage('test.bmp'); 21 | return; 22 | } 23 | printImage('test2.bmp'); 24 | return; 25 | } 26 | 27 | 28 | function printImage(filename){ 29 | var bmpData; 30 | if (cache[filename]){ 31 | bmpData = cache[filename] 32 | }else{ 33 | var bmpBuffer = fs.readFileSync(filename); 34 | var bmpData = bmp.decode(bmpBuffer); 35 | cache[filename] = bmpData; 36 | } 37 | 38 | process.stdout.write(ansiEscapes.cursorUp( bmpData.height / 2)); 39 | 40 | var img = rgbPairToChar.convertImage(bmpData,{reset:'line'}); 41 | process.stdout.write(img); 42 | 43 | } 44 | 45 | //printing starts here 46 | 47 | var colorMatx = 48 | [// dark bright 49 | [[ 0, 0, 0],[ 58, 58, 58]],// black 50 | [[178, 0, 0],[247, 48, 58]],// red 51 | [[ 50,184, 26],[ 89,255, 68]],// green 52 | [[185,183, 26],[255,253, 67]],// yellow 53 | [[ 0, 21,182],[ 85, 91,253]],// blue 54 | [[177, 0,182],[246,055,253]],// magenta 55 | [[ 47,186,184],[ 86,255,255]],// cyan 56 | [[184,184,184],[255,255,255]] // white 57 | ] 58 | 59 | 60 | colorMatx.forEach(function(row){ 61 | process.stdout.write(rgbPairToChar(row[0],row[1])); 62 | process.stdout.write(rgbPairToChar.reset); 63 | }); 64 | 65 | process.stdout.write('\r\n'); 66 | 67 | colorMatx.forEach(function(row){ 68 | process.stdout.write(rgbPairToChar(row[1],row[0])); 69 | }); 70 | 71 | process.stdout.write(ansiEscapes.cursorUp(2)); 72 | 73 | alternate(); 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // decode an image into an approximate terminal output 3 | 4 | var topChar = String.fromCharCode(9600); 5 | var bottomChar = String.fromCharCode(9604); 6 | var fullChar = String.fromCharCode(9608); 7 | 8 | var esc = String.fromCharCode(27); 9 | var reset = esc + '[0m'; 10 | 11 | // convert to uint8 12 | function normalizeRgb(rgb){ 13 | return rgb.map(function(n){ 14 | return Math.round( Math.max(Math.min(n,255),0) ) 15 | }); 16 | } 17 | 18 | // take rgb top and bottom pixels and convert to an terminal character 19 | function rgbToCodes(top,bottom,options){ 20 | var options = options || {}; 21 | var doReset = (typeof options.reset == 'boolean')?options.reset:true; 22 | if (options.format=="rgb"){ 23 | return rgbToRgbCodes(top,bottom,options); 24 | } 25 | 26 | var topColor = findColor(top); 27 | var bottomBg = findBg(bottom); 28 | 29 | //same color top and bottom... save some space 30 | if (topColor == findColor(bottom)){ 31 | if (topColor == 30 ){//if it's black 32 | return esc + '[40m '+(doReset?reset:'');// black BG, then a space, then reset 33 | } 34 | return esc + '[' + topColor + 'm'+fullChar + (doReset?reset:''); 35 | } 36 | else{ 37 | //use topChar usually 38 | 39 | //if top = black, or bottom = white, use bottomChar and invert 40 | if (topColor == 30 || bottomBg == 107){ 41 | var bottomColor = findColor(bottom); 42 | var topBg = findBg(top); 43 | 44 | return esc + '[' + bottomColor + ';' + topBg + 'm' + bottomChar + (doReset?reset:'') 45 | } 46 | 47 | return esc + '[' + topColor + ';' + bottomBg + 'm' + topChar + (doReset?reset:''); 48 | } 49 | } 50 | 51 | module.exports = rgbToCodes; 52 | 53 | //colors and bgs all have keys that are strings like '30;1' and '30' 54 | // to get a esc seq from them, join with a ; and add '[' to the beginning and 'm' to the end 55 | 56 | //for reference only 57 | var styles = ['black','red','green','yellow','blue','magenta','cyan','white']; 58 | 59 | // TODO: some terminals have different color grays for background and text. 60 | var RGBs = 61 | [// dark bright 62 | [[ 0, 0, 0],[ 58, 58, 58]],// black 63 | [[178, 0, 0],[247, 48, 58]],// red 64 | [[ 50,184, 26],[ 89,255, 68]],// green 65 | [[185,183, 26],[255,253, 67]],// yellow 66 | [[ 0, 21,182],[ 85, 91,253]],// blue 67 | [[177, 0,182],[246,055,253]],// magenta 68 | [[ 47,186,184],[ 86,255,255]],// cyan 69 | [[184,184,184],[255,255,255]] // white 70 | ] 71 | 72 | /* 73 | notes on ANSI video escape seqs: 74 | 75 | they are always Esc[Value;...;Valuem 76 | 77 | 78 | */ 79 | var colors = {}; 80 | var bgs = {}; 81 | 82 | var colorIndexes = []; 83 | 84 | RGBs.forEach(function(rgbPair,index){ 85 | var color = index + 30; 86 | var bg = index + 40; 87 | 88 | colors[color] = rgbPair[0]; 89 | bgs[bg] = rgbPair[0]; 90 | 91 | var colorBright = index+ 90; 92 | var bgBright = index + 100; 93 | 94 | colors[colorBright] = rgbPair[1]; 95 | bgs[bgBright] = rgbPair[1]; 96 | 97 | }); 98 | 99 | function rgbDistance(rgb,rgb2){ 100 | return Math.sqrt( Math.pow(rgb[0]-rgb2[0],2) + Math.pow(rgb[1]-rgb2[1],2) + Math.pow(rgb[2]-rgb2[2],2) ); 101 | } 102 | 103 | //find closest bg color 104 | function findBg(rgb){ 105 | var closest = ''; 106 | var closestDist = 100000; 107 | 108 | objEach(bgs,function(color,ansi){ 109 | var dist = rgbDistance(color,rgb); 110 | if (dist < 5){// executive decision: within a distance of 5 units is basically spot-on 111 | closest = ansi; 112 | return false;// break early 113 | } 114 | if (dist < closestDist){ 115 | closestDist = dist; 116 | closest = ansi 117 | } 118 | }); 119 | 120 | return closest; 121 | } 122 | 123 | //find closest fg color 124 | function findColor(rgb){ 125 | var closest = ''; 126 | var closestDist = 100000; 127 | 128 | objEach(colors,function(color,ansi){ 129 | var dist = rgbDistance(color,rgb); 130 | if (dist < closestDist){ 131 | closestDist = dist; 132 | closest = ansi 133 | } 134 | }); 135 | 136 | return closest; 137 | } 138 | 139 | function findColorRgb(rgb){ 140 | return `38;2;${rgb[0]};${rgb[1]};${rgb[2]}`; 141 | } 142 | function findBgRgb(rgb){ 143 | return `48;2;${rgb[0]};${rgb[1]};${rgb[2]}` 144 | } 145 | 146 | // RGB escapes! 147 | // see https://superuser.com/questions/270214/how-can-i-change-the-colors-of-my-xterm-using-ansi-escape-sequences 148 | function rgbToRgbCodes(top,bottom,options){ 149 | 150 | var topColor = findColorRgb(top); 151 | var bottomBg = findBgRgb(bottom); 152 | var doReset = (typeof options.reset == 'boolean')?options.reset:true; 153 | 154 | //same color top and bottom... save some space 155 | if (top[0] == bottom[0] && top[1] == bottom[1] && top[2] == bottom[2]){ 156 | if (topColor == 30 ){//if it's black 157 | return esc + '[40m '+(doReset?reset:'');// black BG, then a space, then reset 158 | } 159 | return esc + '[' + topColor + 'm'+fullChar + (doReset?reset:''); 160 | } 161 | else{ 162 | //use topChar usually 163 | 164 | 165 | //if top = black, or bottom = white, use bottomChar and invert 166 | if (topColor == 30 || bottomBg == 107){ 167 | var bottomColor = findColorRgb(bottom); 168 | var topBg = findBgRgb(top); 169 | 170 | return esc + '[' + bottomColor + ';' + topBg + 'm' + bottomChar + (doReset?reset:''); 171 | } 172 | 173 | 174 | return esc + '[' + topColor + ';' + bottomBg + 'm' + topChar + (doReset?reset:''); 175 | } 176 | } 177 | 178 | function objEach(o,func){ 179 | var result; 180 | for (var property in o) { 181 | if (o.hasOwnProperty(property)) { 182 | // do stuff 183 | result = func(o[property],property); 184 | if (result == false){ 185 | break; 186 | } 187 | } 188 | } 189 | } 190 | 191 | function getPrintingCharacters(str){ 192 | return str.replace(/[\033m;\d\\\[]/g,''); 193 | } 194 | 195 | // other exports 196 | module.exports.reset = reset; 197 | 198 | /* 199 | image is an object with a 'width' key for the pixel width and a 'data' key with rgba data 200 | options are { 201 | format:"rgb" or 'ansi', 202 | reset:true('all'),false,'line', or 'end' 203 | } 204 | returns the string to print 205 | */ 206 | module.exports.convertImage = function convertImage(image,options){ 207 | var ret = '' 208 | var options = options || {}; 209 | var format = options.format || ''; 210 | var doReset = (typeof options.reset == 'undefined')?true:options.reset; 211 | 212 | var dat = image.data; 213 | 214 | var i = 0; 215 | var lastChar = false; 216 | while (i < dat.length){ 217 | 218 | // each row 219 | 220 | //it's RGBA 221 | var top = [dat[i],dat[i+1],dat[i+2]]; 222 | var bottom; 223 | 224 | if (i + image.width*4+2 < dat.length - 1){ 225 | bottom = [dat[i + image.width*4 ],dat[i + image.width*4+1],dat[i + image.width*4+2]]; 226 | }else{ 227 | bottom = [0,0,0]; 228 | } 229 | 230 | var thisChar = rgbToCodes(top,bottom,{ 231 | reset:(doReset === true || doReset == 'all'), 232 | format:format 233 | }); 234 | 235 | // check for previous == this, which means don't reuse reset char, just use the printing character 236 | if ( doReset !== true && doReset != 'all' && lastChar == thisChar){ 237 | ret += getPrintingCharacters(thisChar); 238 | }else{ 239 | ret += thisChar; 240 | } 241 | 242 | lastChar = thisChar; 243 | 244 | i = i + 4; 245 | if ( (i/4) % image.width == 0 ){ 246 | if (doReset == 'line'){ 247 | ret += reset; 248 | } 249 | ret += '\n'; 250 | i = i + image.width*4;//skip line 251 | lastChar = false;// reset last char 252 | } 253 | } 254 | 255 | if (doReset == 'end'){ 256 | ret += reset; 257 | } 258 | 259 | return ret; 260 | } 261 | --------------------------------------------------------------------------------