add PNG/ANSIImage element.

This commit is contained in:
Christopher Jeffrey 2015-06-28 22:46:04 -07:00
parent 305aa063d4
commit 2842f3f6ce
12 changed files with 1985 additions and 6 deletions

View File

@ -172,6 +172,7 @@ screen.render();
- [Terminal](#terminal-from-box)
- [Image](#image-from-box)
- [Layout](#layout-from-element)
- [PNG](#png-from-box)
### Other
@ -1384,11 +1385,13 @@ terminals.
##### Methods:
- inherits all from Box.
- __setImage(img, callback)__ - set the image in the box to a new path.
- __clearImage(callback)__ - clear the current image.
- __imageSize(img, callback)__ - get the size of an image file in pixels.
- __termSize(callback)__ - get the size of the terminal in pixels.
- __getPixelRatio(callback)__ - get the pixel to cell ratio for the terminal.
- __setImage(img, [callback])__ - set the image in the box to a new path.
- __clearImage([callback])__ - clear the current image.
- __imageSize(img, [callback])__ - get the size of an image file in pixels.
- __termSize([callback])__ - get the size of the terminal in pixels.
- __getPixelRatio([callback])__ - get the pixel to cell ratio for the terminal.
- _Note:_ All methods above can be synchronous as long as the host version of
node supports `spawnSync`.
#### Layout (from Element)
@ -1565,6 +1568,68 @@ for (var i = 0; i < 10; i++) {
```
#### PNG (from Box)
Convert any `.png` file (or `.gif`, see below) to an ANSI image and display it
as an element. This differs from the `Image` element in that it uses blessed's
internal PNG parser and does not require external dependencies.
Blessed uses an internal from-scratch PNG reader because no other javascript
PNG reader supports Adam7 interlaced images (much less pass the png test
suite).
The blessed PNG reader supports adam7 deinterlacing, animation (APNG), all
color types, bit depths 1-32, alpha, alpha palettes, and outputs scaled bitmaps
(cellmaps) in blessed for efficient rendering to the screen buffer. It also
uses some code from libcaca/libcucul to add density ASCII characters in order
to give the image more detail in the terminal.
If a corrupt PNG or a non-PNG is passed in, blessed will display error text in
the element.
`.gif` files are also supported via a javascript implementation (they are
internally converted to bitmaps and fed to the PNG renderer). Any other image
format is support only if the user has imagemagick (`convert` and `identify`)
installed.
##### Options:
- inherits all from Box.
- __file__ - URL or path to PNG file. can also be a buffer.
- __scale__ - scale cellmap down (`0-1.0`) from its original pixel width/height
(default: `1.0`).
- __width/height__ - this differs from other element's `width` or `height` in
that only one of them is needed: blessed will maintain the aspect ratio of
the image as it scales down to the proper number of cells. __NOTE__: PNG's
are always automatically shrunken to size (based on scale) if a `width` or
`height` is not given.
- __ascii__ - add various "density" ASCII characters over the rendering to give
the image more detail, similar to libcaca/libcucul (the library mplayer uses
to display videos in the terminal).
- __animate__ - whether to animate if the image is an APNG. if false, only
display the first frame or IDAT (default: `true`).
##### Properties:
- inherits all from Box.
- __img__ - image object from the png reader.
- __img.width__ - pixel width.
- __img.height__ - pixel height.
- __img.bmp__ - image bitmap.
- __img.cellmap__ - image cellmap (bitmap scaled down to cell size).
##### Events:
- inherits all from Box.
##### Methods:
- inherits all from Box.
- __setImage(file)__ - set the image in the box to a new path. file can be a
path, url, or buffer.
- __clearImage()__ - clear the image.
### Other

View File

@ -36,7 +36,8 @@ widget.classes = [
'ListTable',
'Terminal',
'Image',
'Layout'
'Layout',
'PNG'
];
widget.classes.forEach(function(name) {

142
lib/widgets/png.js Normal file
View File

@ -0,0 +1,142 @@
/**
* png.js - render PNGs as ANSI
* Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
* https://github.com/chjj/blessed
*/
/**
* Modules
*/
var cp = require('child_process')
, path = require('path')
, fs = require('fs');
var helpers = require('../helpers');
var colors = require('../colors');
var Node = require('./node');
var Box = require('./box');
var tng = require('../../vendor/tng');
/**
* PNG
*/
function PNG(options) {
var self = this;
if (!(this instanceof Node)) {
return new PNG(options);
}
options = options || {};
options.shrink = true;
Box.call(this, options);
this.scale = this.options.scale || 1.0;
this.options.animate = this.options.animate !== false;
this._noFill = true;
if (this.options.file) {
this.setImage(this.options.file);
}
this.screen.on('prerender', function() {
var lpos = self.lpos;
if (!lpos) return;
// prevent image from blending with itself if there are alpha channels
self.screen.clearRegion(lpos.xi, lpos.xl, lpos.yi, lpos.yl);
});
}
PNG.prototype.__proto__ = Box.prototype;
PNG.prototype.type = 'png';
PNG.curl = function(url) {
try {
return cp.execFileSync('curl',
['-s', '-A', '', url],
{ stdio: ['ignore', 'pipe', 'ignore'] });
} catch (e) {
;
}
try {
return cp.execFileSync('wget',
['-U', '', '-O', '-', url],
{ stdio: ['ignore', 'pipe', 'ignore'] });
} catch (e) {
;
}
throw new Error('curl or wget failed.');
};
PNG.prototype.setImage = function(file) {
var self = this;
if (/^https?:/.test(file)) {
file = PNG.curl(file);
}
this.file = file;
var width = this.position.width;
var height = this.position.height;
if (width != null) {
width = this.width;
}
if (height != null) {
height = this.height;
}
try {
this.setContent('');
this.img = tng(this.file, {
colors: colors,
cellmapWidth: width,
cellmapHeight: height,
cellmapScale: this.scale,
ascii: this.options.ascii
});
if (width == null || height == null) {
this.width = this.img.cellmap[0].length;
this.height = this.img.cellmap.length;
}
if (this.img.frames && this.options.animate) {
this.img.play(function(bmp, cellmap) {
self.cellmap = cellmap;
self.screen.render();
});
} else {
self.cellmap = self.img.cellmap;
}
} catch (e) {
this.setContent('PNG Error: ' + e.message);
this.img = null;
this.cellmap = null;
}
};
PNG.prototype.clearImage = function() {
this.setContent('');
this.img = null;
this.cellmap = null;
};
PNG.prototype.render = function() {
var self = this;
var coords = this._render();
if (!coords) return;
if (this.img) {
this.img.renderElement(this.cellmap, this);
}
return coords;
};
/**
* Expose
*/
module.exports = PNG;

69
test/widget-png.js Normal file
View File

@ -0,0 +1,69 @@
var blessed = require('../');
var fs = require('fs');
var screen = blessed.screen({
tput: true,
smartCSR: true,
dump: __dirname + '/logs/png.log'
});
var box = blessed.box({
parent: screen,
left: 4,
top: 3,
width: 10,
height: 6,
border: 'line',
style: {
bg: 'green'
},
content: 'Lorem ipsum doler',
align: 'center'
});
var file = process.argv[2];
var testImage = __dirname + '/test-image.png';
var spinfox = __dirname + '/spinfox.png';
// XXX I'm not sure of the license of this file,
// so I'm not going to redistribute it in the repo.
var url = 'https://people.mozilla.org/~dolske/apng/spinfox.png';
if (!file) {
try {
if (!fs.existsSync(spinfox)) {
var buf = blessed.png.curl(url);
fs.writeFileSync(spinfox, buf);
}
file = spinfox;
} catch (e) {
file = testImage;
}
}
var png = blessed.png({
parent: screen,
// border: 'line',
width: 20,
height: 19,
top: 2,
left: 0,
file: file,
ascii: false,
draggable: true
});
screen.render();
screen.key('q', function() {
process.exit(0);
});
var timeout = setInterval(function() {
png.left++;
screen.render();
}, 100);
png.on('mousedown', function() {
clearInterval(timeout);
});

3
vendor/tng/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
debug.log
png.json

6
vendor/tng/.npmignore vendored Normal file
View File

@ -0,0 +1,6 @@
.git*
test/
img/
node_modules/
debug.log
png.json

20
vendor/tng/LICENSE vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2015, Christopher Jeffrey
https://github.com/chjj/tng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

39
vendor/tng/README.md vendored Normal file
View File

@ -0,0 +1,39 @@
# tng
A full-featured PNG renderer for the terminal, built for [blessed][blessed].
![tng](https://raw.githubusercontent.com/chjj/blessed/master/img/demo.png)
Convert any `.png` file (or `.gif`, see below) to an ANSI image and display it
as an element or ANSI text.
Blessed uses an internal from-scratch PNG reader because no other javascript
PNG reader supports Adam7 interlaced images (much less pass the png test
suite).
The blessed PNG reader supports adam7 deinterlacing, animation (APNG), all
color types, bit depths 1-32, alpha, alpha palettes, and outputs scaled bitmaps
(cellmaps) in blessed for efficient rendering to the screen buffer. It also
uses some code from libcaca/libcucul to add density ASCII characters in order
to give the image more detail in the terminal.
`.gif` files are also supported via a javascript implementation (they are
internally converted to bitmaps and fed to the PNG renderer). Any other image
format is support only if the user has imagemagick (`convert` and `identify`)
installed.
## Contribution and License Agreement
If you contribute code to this project, you are implicitly allowing your code
to be distributed under the MIT license. You are also implicitly verifying that
all code is your original work. `</legalese>`
## License
Copyright (c) 2015, Christopher Jeffrey. (MIT License)
See LICENSE for more info.
[blessed]: https://github.com/chjj/blessed

1
vendor/tng/index.js vendored Normal file
View File

@ -0,0 +1 @@
module.exports = require('./lib/tng.js');

399
vendor/tng/lib/gif.js vendored Normal file
View File

@ -0,0 +1,399 @@
/**
* gif.js - gif reader for tng
* Copyright (c) 2015, Christopher Jeffrey (MIT License).
* https://github.com/chjj/tng
*/
var fs = require('fs')
, cp = require('child_process')
, path = require('path')
, assert = require('assert');
/**
* GIF
*/
function GIF(file, options) {
var info = {}
, p = 0
, buf
, i
, total
, sig
, desc
, img
, ext
, label
, size;
if (!file) throw new Error('no file');
options = options || {};
if (Buffer.isBuffer(file)) {
buf = file;
file = null;
} else {
file = path.resolve(process.cwd(), file);
buf = fs.readFileSync(file);
}
sig = buf.slice(0, 6).toString('ascii');
if (sig !== 'GIF87a' && sig !== 'GIF89a') {
throw new Error('bad header: ' + sig);
}
info.screenWidth = buf.readUInt16LE(6);
info.screenHeight = buf.readUInt16LE(8);
info.flags = buf.readUInt8(10);
info.gct = !!(info.flags & 0x80);
info.gctsize = (info.flags & 0x07) + 1;
info.bgIndex = buf.readUInt8(11);
info.aspect = buf.readUInt8(12);
p += 13;
if (info.gct) {
info.colors = [];
total = 1 << info.gctsize;
for (i = 0; i < total; i++, p += 3) {
info.colors.push([buf[p], buf[p + 1], buf[p + 2], 255]);
}
}
info.images = [];
info.extensions = [];
try {
while (p < buf.length) {
desc = buf.readUInt8(p);
p += 1;
if (desc === 0x2c) {
img = {};
img.left = buf.readUInt16LE(p);
p += 2;
img.top = buf.readUInt16LE(p);
p += 2;
img.width = buf.readUInt16LE(p);
p += 2;
img.height = buf.readUInt16LE(p);
p += 2;
img.flags = buf.readUInt8(p);
p += 1;
img.lct = !!(img.flags & 0x80);
img.ilace = !!(img.flags & 0x40);
img.lctsize = (img.flags & 0x07) + 1;
if (img.lct) {
img.lcolors = [];
total = 1 << img.lctsize;
for (i = 0; i < total; i++, p += 3) {
img.lcolors.push([buf[p], buf[p + 1], buf[p + 2], 255]);
}
}
img.codeSize = buf.readUInt8(p);
p += 1;
img.size = buf.readUInt8(p);
p += 1;
img.lzw = [buf.slice(p, p + img.size)];
p += img.size;
while (buf[p] !== 0x00) {
// Some gifs screw up their size.
// XXX Same for all subblocks?
if (buf[p] === 0x3b) {
p--;
break;
}
size = buf.readUInt8(p);
p += 1;
img.lzw.push(buf.slice(p, p + size));
p += size;
}
assert.equal(buf.readUInt8(p), 0x00);
p += 1;
info.images.push(img);
} else if (desc === 0x21) {
// Extensions:
// http://www.w3.org/Graphics/GIF/spec-gif89a.txt
ext = {};
label = buf.readUInt8(p);
p += 1;
ext.label = label;
if (label === 0xf9) {
size = buf.readUInt8(p);
assert.equal(size, 0x04);
p += 1;
ext.fields = buf.readUInt8(p);
ext.disposeMethod = (ext.fields >> 2) & 0x07;
ext.useTransparent = !!(ext.fields & 0x01);
p += 1;
ext.delay = buf.readUInt16LE(p);
p += 2;
ext.transparentColor = buf.readUInt8(p);
p += 1;
while (buf[p] !== 0x00) {
size = buf.readUInt8(p);
p += 1;
p += size;
}
assert.equal(buf.readUInt8(p), 0x00);
p += 1;
info.delay = ext.delay;
info.transparentColor = ext.transparentColor;
info.disposeMethod = ext.disposeMethod;
info.useTransparent = ext.useTransparent;
} else if (label === 0xff) {
size = buf.readUInt8(p);
p += 1;
ext.id = buf.slice(p, p + 8).toString('ascii');
p += 8;
ext.auth = buf.slice(p, p + 3).toString('ascii');
p += 3;
ext.data = [];
while (buf[p] !== 0x00) {
size = buf.readUInt8(p);
p += 1;
ext.data.push(buf.slice(p, p + size));
p += size;
}
// http://graphcomp.com/info/specs/ani_gif.html
if (ext.id === 'NETSCAPE' && ext.auth === '2.0') {
assert.equal(ext.data[0].readUInt8(0), 0x01);
ext.numPlays = ext.data[0].readUInt16LE(1);
info.numPlays = ext.numPlays;
}
assert.equal(buf.readUInt8(p), 0x00);
p += 1;
} else {
ext.data = [];
while (buf[p] !== 0x00) {
size = buf.readUInt8(p);
p += 1;
ext.data.push(buf.slice(p, p + size));
p += size;
}
assert.equal(buf.readUInt8(p), 0x00);
p += 1;
}
info.extensions.push(ext);
} else if (desc === 0x3b) {
break;
} else if (p === buf.length - 1) {
// } else if (desc === 0x00 && p === buf.length - 1) {
break;
} else {
throw new Error('unknown block');
}
}
} catch (e) {
if (options.debug) {
throw e;
}
}
info.images = info.images.map(function(img) {
img.lzw = new Buffer(img.lzw.reduce(function(out, data) {
return out.concat(Array.prototype.slice.call(data));
}, []));
try {
img.data = decompress(img.lzw, img.codeSize);
} catch (e) {
if (options.debug) throw e;
return;
}
var interlacing = [
[ 0, 8 ],
[ 4, 8 ],
[ 2, 4 ],
[ 1, 2 ],
[ 0, 0 ]
];
var table = img.lcolors || info.colors
, row = 0
, col = 0
, ilp = 0
, p = 0
, b
, idx
, i
, y
, x
, line
, pixel;
img.samples = [];
// Rewritten version of:
// https://github.com/lbv/ka-cs-programs/blob/master/lib/gif-reader.js
for (;;) {
b = img.data[p++];
if (b == null) break;
idx = (row * img.width + col) * 4;
if (!table[b]) {
if (options.debug) throw new Error('bad samples');
table[b] = [0, 0, 0, 0];
}
img.samples[idx] = table[b][0];
img.samples[idx + 1] = table[b][1];
img.samples[idx + 2] = table[b][2];
img.samples[idx + 3] = table[b][3];
if (info.useTransparent && b === info.transparentColor) {
img.samples[idx + 3] = 0;
}
if (++col >= img.width) {
col = 0;
if (img.ilace) {
row += interlacing[ilp][1];
if (row >= img.height) {
row = interlacing[++ilp][0];
}
} else {
row++;
}
}
}
img.pixels = [];
for (i = 0; i < img.samples.length; i += 4) {
img.pixels.push(img.samples.slice(i, i + 4));
}
img.bmp = [];
for (y = 0, p = 0; y < img.height; y++) {
line = [];
for (x = 0; x < img.width; x++) {
pixel = img.pixels[p++];
if (!pixel) {
if (options.debug) throw new Error('no pixel');
line.push({ r: 0, g: 0, b: 0, a: 0 });
continue;
}
line.push({ r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] });
}
img.bmp.push(line);
}
return img;
}, this).filter(Boolean);
if (!info.images.length) {
throw new Error('no image data or bad decompress');
}
return info;
}
// Rewritten version of:
// https://github.com/lbv/ka-cs-programs/blob/master/lib/gif-reader.js
function decompress(input, codeSize) {
var bitDepth = codeSize + 1
, CC = 1 << codeSize
, EOI = CC + 1
, stack = []
, table = []
, ntable = 0
, oldCode = null
, buffer = 0
, nbuffer = 0
, p = 0
, buf = []
, bits
, read
, ans
, n
, code
, i
, K
, b
, maxElem;
for (;;) {
if (stack.length === 0) {
bits = bitDepth;
read = 0;
ans = 0;
while (read < bits) {
if (nbuffer === 0) {
if (p >= input.length) return buf;
buffer = input[p++];
nbuffer = 8;
}
n = Math.min(bits - read, nbuffer);
ans |= (buffer & ((1 << n) - 1)) << read;
read += n;
nbuffer -= n;
buffer >>= n;
}
code = ans;
if (code === EOI) {
break;
}
if (code === CC) {
table = [];
for (i = 0; i < CC; ++i) {
table[i] = [i, -1, i];
}
bitDepth = codeSize + 1;
maxElem = 1 << bitDepth;
ntable = CC + 2;
oldCode = null;
continue;
}
if (oldCode === null) {
oldCode = code;
buf.push(table[code][0]);
continue;
}
if (code < ntable) {
for (i = code; i >= 0; i = table[i][1]) {
stack.push(table[i][0]);
}
table[ntable++] = [
table[code][2],
oldCode,
table[oldCode][2]
];
} else {
K = table[oldCode][2];
table[ntable++] = [K, oldCode, K];
for (i = code; i >= 0; i = table[i][1]) {
stack.push(table[i][0]);
}
}
oldCode = code;
if (ntable === maxElem) {
maxElem = 1 << (++bitDepth);
if (bitDepth > 12) bitDepth = 12;
}
}
b = stack.pop();
if (b == null) break;
buf.push(b);
}
return buf;
}
/**
* Expose
*/
module.exports = GIF;

1217
vendor/tng/lib/tng.js vendored Normal file

File diff suppressed because it is too large Load Diff

17
vendor/tng/package.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"name": "tng",
"description": "A full-featured PNG renderer for the terminal.",
"author": "Christopher Jeffrey",
"version": "0.0.1",
"main": "./lib/tng.js",
"bin": "./test/index.js",
"preferGlobal": false,
"repository": "git://github.com/chjj/tng.git",
"homepage": "https://github.com/chjj/tng",
"bugs": { "url": "http://github.com/chjj/tng/issues" },
"keywords": ["png", "gif", "image", "terminal", "term", "tty", "tui"],
"tags": ["png", "gif", "image", "terminal", "term", "tty", "tui"],
"peerDependencies": {
"blessed": ">=0.1.61"
}
}