From 2842f3f6ce406406d830ad3e2fc97ca04daf4f14 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 28 Jun 2015 22:46:04 -0700 Subject: [PATCH] add PNG/ANSIImage element. --- README.md | 75 ++- lib/widget.js | 3 +- lib/widgets/png.js | 142 +++++ test/widget-png.js | 69 +++ vendor/tng/.gitignore | 3 + vendor/tng/.npmignore | 6 + vendor/tng/LICENSE | 20 + vendor/tng/README.md | 39 ++ vendor/tng/index.js | 1 + vendor/tng/lib/gif.js | 399 +++++++++++++ vendor/tng/lib/tng.js | 1217 +++++++++++++++++++++++++++++++++++++++ vendor/tng/package.json | 17 + 12 files changed, 1985 insertions(+), 6 deletions(-) create mode 100644 lib/widgets/png.js create mode 100644 test/widget-png.js create mode 100644 vendor/tng/.gitignore create mode 100644 vendor/tng/.npmignore create mode 100644 vendor/tng/LICENSE create mode 100644 vendor/tng/README.md create mode 100644 vendor/tng/index.js create mode 100644 vendor/tng/lib/gif.js create mode 100644 vendor/tng/lib/tng.js create mode 100644 vendor/tng/package.json diff --git a/README.md b/README.md index b4b543d..eac7e37 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/widget.js b/lib/widget.js index b6f32bd..5ec1c05 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -36,7 +36,8 @@ widget.classes = [ 'ListTable', 'Terminal', 'Image', - 'Layout' + 'Layout', + 'PNG' ]; widget.classes.forEach(function(name) { diff --git a/lib/widgets/png.js b/lib/widgets/png.js new file mode 100644 index 0000000..804d088 --- /dev/null +++ b/lib/widgets/png.js @@ -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; diff --git a/test/widget-png.js b/test/widget-png.js new file mode 100644 index 0000000..5023f1c --- /dev/null +++ b/test/widget-png.js @@ -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); +}); diff --git a/vendor/tng/.gitignore b/vendor/tng/.gitignore new file mode 100644 index 0000000..63c8e61 --- /dev/null +++ b/vendor/tng/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +debug.log +png.json diff --git a/vendor/tng/.npmignore b/vendor/tng/.npmignore new file mode 100644 index 0000000..2948cb8 --- /dev/null +++ b/vendor/tng/.npmignore @@ -0,0 +1,6 @@ +.git* +test/ +img/ +node_modules/ +debug.log +png.json diff --git a/vendor/tng/LICENSE b/vendor/tng/LICENSE new file mode 100644 index 0000000..e7918fa --- /dev/null +++ b/vendor/tng/LICENSE @@ -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. diff --git a/vendor/tng/README.md b/vendor/tng/README.md new file mode 100644 index 0000000..197fd66 --- /dev/null +++ b/vendor/tng/README.md @@ -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. `` + + +## License + +Copyright (c) 2015, Christopher Jeffrey. (MIT License) + +See LICENSE for more info. + +[blessed]: https://github.com/chjj/blessed diff --git a/vendor/tng/index.js b/vendor/tng/index.js new file mode 100644 index 0000000..834fc9d --- /dev/null +++ b/vendor/tng/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/tng.js'); diff --git a/vendor/tng/lib/gif.js b/vendor/tng/lib/gif.js new file mode 100644 index 0000000..72bba34 --- /dev/null +++ b/vendor/tng/lib/gif.js @@ -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; diff --git a/vendor/tng/lib/tng.js b/vendor/tng/lib/tng.js new file mode 100644 index 0000000..da17163 --- /dev/null +++ b/vendor/tng/lib/tng.js @@ -0,0 +1,1217 @@ +/** + * tng.js - png reader + * Copyright (c) 2015, Christopher Jeffrey (MIT License). + * https://github.com/chjj/tng + */ + +var fs = require('fs') + , util = require('util') + , path = require('path') + , zlib = require('zlib') + , assert = require('assert') + , cp = require('child_process') + , exec = cp.execFileSync; + +var GIF = require('./gif'); + +/** + * PNG + */ + +function PNG(file, options) { + var buf + , chunks + , idat + , pixels; + + if (!(this instanceof PNG)) { + return new PNG(file, options); + } + + if (!file) throw new Error('no file'); + + this.options = options || {}; + this.colors = options.colors || require('blessed/lib/colors'); + this.options.optimization = this.options.optimization || 'mem'; + + if (Buffer.isBuffer(file)) { + buf = file; + this.file = null; + } else { + this.file = path.resolve(process.cwd(), file); + this.format = path.extname(this.file).slice(1).toLowerCase(); + if (this.format !== 'png') { + try { + return this.toPNG(); + } catch (e) { + console.error('could not convert ' + this.format + ' to png'); + throw e; + } + } + buf = fs.readFileSync(this.file); + } + + chunks = this.parseRaw(buf); + idat = this.parseChunks(chunks); + pixels = this.parseLines(idat); + + this.bmp = this.createBitmap(pixels); + this.cellmap = this.createCellmap(this.bmp); + this.frames = this.compileFrames(this.frames); +} + +PNG.prototype.defaultScale = 0.20; + +PNG.prototype.parseRaw = function(buf) { + var chunks = [] + , index = 0 + , i = 0 + , buf + , len + , type + , name + , data + , crc + , check + , critical + , public_ + , conforming + , copysafe + , pos; + + this._debug(this.file); + + if (buf.readUInt32BE(0) !== 0x89504e47 + || buf.readUInt32BE(4) !== 0x0d0a1a0a) { + throw new Error('bad header'); + } + + i += 8; + + while (i < buf.length) { + try { + pos = i; + len = buf.readUInt32BE(i); + i += 4; + type = buf.slice(i, i + 4); + name = type.toString('ascii'); + i += 4; + data = buf.slice(i, i + len); + i += len; + check = crc32(buf.slice(pos, i)); + crc = buf.readUInt32BE(i); + i += 4; + critical = !!(~type[0] & 32); + public_ = !!(~type[1] & 32); + conforming = !!(~type[2] & 32); + copysafe = !!(~type[3] & 32); + } catch (e) { + if (this.options.debug) throw e; + break; + } + chunks.push({ + index: index++, + id: name.toLowerCase(), + len: len, + pos: pos, + end: i, + type: type, + name: name, + data: data, + crc: crc, + check: check, + raw: buf.slice(pos, i), + flags: { + critical: critical, + public_: public_, + conforming: conforming, + copysafe: copysafe + } + }); + } + + return chunks; +}; + +PNG.prototype.parseChunks = function(chunks) { + var i + , chunk + , name + , data + , p + , idat + , info; + + for (i = 0; i < chunks.length; i++) { + chunk = chunks[i]; + name = chunk.id; + data = chunk.data; + info = {}; + switch (name) { + case 'ihdr': { + this.width = info.width = data.readUInt32BE(0); + this.height = info.height = data.readUInt32BE(4); + this.bitDepth = info.bitDepth = data.readUInt8(8); + this.colorType = info.colorType = data.readUInt8(9); + this.compression = info.compression = data.readUInt8(10); + this.filter = info.filter = data.readUInt8(11); + this.interlace = info.interlace = data.readUInt8(12); + switch (this.bitDepth) { + case 1: case 2: case 4: case 8: case 16: case 24: case 32: break; + default: throw new Error('bad bit depth: ' + this.bitDepth); + } + switch (this.colorType) { + case 0: case 2: case 3: case 4: case 6: break; + default: throw new Error('bad color: ' + this.colorType); + } + switch (this.compression) { + case 0: break; + default: throw new Error('bad compression: ' + this.compression); + } + switch (this.filter) { + case 0: case 1: case 2: case 3: case 4: break; + default: throw new Error('bad filter: ' + this.filter); + } + switch (this.interlace) { + case 0: case 1: break; + default: throw new Error('bad interlace: ' + this.interlace); + } + break; + } + case 'plte': { + this.palette = info.palette = []; + for (p = 0; p < data.length; p += 3) { + this.palette.push({ + r: data[p + 0], + g: data[p + 1], + b: data[p + 2], + a: 255 + }); + } + break; + } + case 'idat': { + this.size = this.size || 0; + this.size += data.length; + this.idat = this.idat || []; + this.idat.push(data); + info.size = data.length; + break; + } + case 'iend': { + this.end = true; + break; + } + case 'trns': { + this.alpha = info.alpha = Array.prototype.slice.call(data); + if (this.palette) { + for (p = 0; p < data.length; p++) { + if (!this.palette[p]) break; + this.palette[p].a = data[p]; + } + } + break; + } + // https://wiki.mozilla.org/APNG_Specification + case 'actl': { + this.actl = info = {}; + this.frames = []; + this.actl.numFrames = data.readUInt32BE(0); + this.actl.numPlays = data.readUInt32BE(4); + break; + } + case 'fctl': { + // IDAT is the first frame depending on the order: + // IDAT is a frame: acTL->fcTL->IDAT->[fcTL]->fdAT + // IDAT is not a frame: acTL->IDAT->[fcTL]->fdAT + if (!this.idat) { + this.idat = []; + this.frames.push({ + idat: true, + fctl: info, + fdat: this.idat + }); + } else { + this.frames.push({ + fctl: info, + fdat: [] + }); + } + info.sequenceNumber = data.readUInt32BE(0); + info.width = data.readUInt32BE(4); + info.height = data.readUInt32BE(8); + info.xOffset = data.readUInt32BE(12); + info.yOffset = data.readUInt32BE(16); + info.delayNum = data.readUInt16BE(20); + info.delayDen = data.readUInt16BE(22); + info.disposeOp = data.readUInt8(24); + info.blendOp = data.readUInt8(25); + break; + } + case 'fdat': { + info.sequenceNumber = data.readUInt32BE(0); + info.data = data.slice(4); + this.frames[this.frames.length - 1].fdat.push(info.data); + break; + } + } + chunk.info = info; + } + + this._debug(chunks); + + if (this.frames) { + this.frames = this.frames.map(function(frame, i) { + frame.fdat = decompress(frame.fdat); + if (!frame.fdat.length) throw new Error('no data'); + return frame; + }); + } + + idat = decompress(this.idat); + if (!idat.length) throw new Error('no data'); + + return idat; +}; + +PNG.prototype.parseLines = function(data) { + var pixels = [] + , x + , p + , prior + , line + , filter + , samples + , pendingSamples + , ch + , shiftStart + , i + , toShift + , sample; + + this.sampleDepth = + this.colorType === 0 ? 1 + : this.colorType === 2 ? 3 + : this.colorType === 3 ? 1 + : this.colorType === 4 ? 2 + : this.colorType === 6 ? 4 + : 1; + this.bitsPerPixel = this.bitDepth * this.sampleDepth; + this.bytesPerPixel = Math.ceil(this.bitsPerPixel / 8); + this.wastedBits = ((this.width * this.bitsPerPixel) / 8) - ((this.width * this.bitsPerPixel / 8) | 0); + this.byteWidth = Math.ceil(this.width * (this.bitsPerPixel / 8)); + + this.shiftStart = ((this.bitDepth + (8 / this.bitDepth - this.bitDepth)) - 1) | 0; + this.shiftMult = this.bitDepth >= 8 ? 0 : this.bitDepth; + this.mask = this.bitDepth === 32 ? 0xffffffff : (1 << this.bitDepth) - 1; + + if (this.interlace === 1) { + samples = this.sampleInterlacedLines(data); + for (i = 0; i < samples.length; i += this.sampleDepth) { + pixels.push(samples.slice(i, i + this.sampleDepth)); + } + return pixels; + } + + for (p = 0; p < data.length; p += this.byteWidth) { + prior = line || []; + filter = data[p++]; + line = data.slice(p, p + this.byteWidth); + line = this.unfilterLine(filter, line, prior); + samples = this.sampleLine(line); + for (i = 0; i < samples.length; i += this.sampleDepth) { + pixels.push(samples.slice(i, i + this.sampleDepth)); + } + } + + return pixels; +}; + +PNG.prototype.unfilterLine = function(filter, line, prior) { + for (var x = 0; x < line.length; x++) { + if (filter === 0) { + // line[x] = line[x]; + } else if (filter === 1) { + line[x] = this.filters.sub(x, line, prior, this.bytesPerPixel); + } else if (filter === 2) { + line[x] = this.filters.up(x, line, prior, this.bytesPerPixel); + } else if (filter === 3) { + line[x] = this.filters.average(x, line, prior, this.bytesPerPixel); + } else if (filter === 4) { + line[x] = this.filters.paeth(x, line, prior, this.bytesPerPixel); + } + } + return line; +}; + +PNG.prototype.sampleLine = function(line, width) { + var samples = [] + , x = 0 + , pendingSamples + , ch + , i + , sample + , shiftStart + , toShift; + + while (x < line.length) { + pendingSamples = this.sampleDepth; + while (pendingSamples--) { + ch = line[x]; + if (this.bitDepth === 16) { + ch = (ch << 8) | line[++x]; + } else if (this.bitDepth === 24) { + ch = (ch << 16) | (line[++x] << 8) | line[++x]; + } else if (this.bitDepth === 32) { + ch = (ch << 24) | (line[++x] << 16) | (line[++x] << 8) | line[++x]; + } else if (this.bitDepth > 32) { + throw new Error('bitDepth ' + this.bitDepth + ' unsupported.'); + } + shiftStart = this.shiftStart; + toShift = shiftStart - (x === line.length - 1 ? this.wastedBits : 0); + for (i = 0; i <= toShift; i++) { + sample = (ch >> (this.shiftMult * shiftStart)) & this.mask; + if (this.colorType !== 3) { + if (this.bitDepth < 8) { // <= 8 would work too, doesn't matter + // sample = sample * (0xff / this.mask) | 0; // would work too + sample *= 0xff / this.mask; + sample |= 0; + } else if (this.bitDepth > 8) { + sample = (sample / this.mask) * 255 | 0; + } + } + samples.push(sample); + shiftStart--; + } + x++; + } + } + + // Needed for deinterlacing? + if (width != null) { + samples = samples.slice(0, width * this.sampleDepth); + } + + return samples; +}; + +// http://www.w3.org/TR/PNG-Filters.html +PNG.prototype.filters = { + sub: function Sub(x, line, prior, bpp) { + if (x < bpp) return line[x]; + return (line[x] + line[x - bpp]) % 256; + }, + up: function Up(x, line, prior, bpp) { + return (line[x] + (prior[x] || 0)) % 256; + }, + average: function Average(x, line, prior, bpp) { + if (x < bpp) return Math.floor((prior[x] || 0) / 2); + // if (x < bpp) return (prior[x] || 0) >> 1; + return (line[x] + + Math.floor((line[x - bpp] + prior[x]) / 2) + // + ((line[x - bpp] + prior[x]) >> 1) + ) % 256; + }, + paeth: function Paeth(x, line, prior, bpp) { + if (x < bpp) return prior[x] || 0; + return (line[x] + this._predictor( + line[x - bpp], prior[x] || 0, prior[x - bpp] || 0 + )) % 256; + }, + _predictor: function PaethPredictor(a, b, c) { + // a = left, b = above, c = upper left + var p = a + b - c + , pa = Math.abs(p - a) + , pb = Math.abs(p - b) + , pc = Math.abs(p - c); + if (pa <= pb && pa <= pc) return a; + if (pb <= pc) return b; + return c; + } +}; + +/** + * Adam7 deinterlacing ported to javascript from PyPNG: + * pypng - Pure Python library for PNG image encoding/decoding + * Copyright (c) 2009-2015, David Jones (MIT License). + * https://github.com/drj11/pypng + * + * 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. + */ + +PNG.prototype.sampleInterlacedLines = function(raw) { + var psize + , vpr + , samples + , source_offset + , i + , pass + , xstart + , ystart + , xstep + , ystep + , recon + , ppr + , row_size + , y + , filter_type + , scanline + , flat + , offset + , k + , end_offset + , skip + , j + , k + , f; + + var adam7 = [ + [0, 0, 8, 8], + [4, 0, 8, 8], + [0, 4, 4, 8], + [2, 0, 4, 4], + [0, 2, 2, 4], + [1, 0, 2, 2], + [0, 1, 1, 2] + ]; + + // Fractional bytes per pixel + psize = (this.bitDepth / 8) * this.sampleDepth; + + // Values per row (of the target image) + vpr = this.width * this.sampleDepth; + + // Make a result array, and make it big enough. Interleaving + // writes to the output array randomly (well, not quite), so the + // entire output array must be in memory. + samples = new Buffer(vpr * this.height); + samples.fill(0); + + source_offset = 0; + + for (i = 0; i < adam7.length; i++) { + pass = adam7[i]; + xstart = pass[0]; + ystart = pass[1]; + xstep = pass[2]; + ystep = pass[3]; + if (xstart >= this.width) continue; + // The previous (reconstructed) scanline. Empty array at the + // beginning of a pass to indicate that there is no previous + // line. + recon = []; + // Pixels per row (reduced pass image) + ppr = Math.ceil((this.width - xstart) / xstep); + // Row size in bytes for this pass. + row_size = Math.ceil(psize * ppr); + for (y = ystart; y < this.height; y += ystep) { + filter_type = raw[source_offset]; + source_offset += 1; + scanline = raw.slice(source_offset, source_offset + row_size); + source_offset += row_size; + recon = this.unfilterLine(filter_type, scanline, recon); + // Convert so that there is one element per pixel value + flat = this.sampleLine(recon, ppr); + if (xstep === 1) { + assert.equal(xstart, 0); + offset = y * vpr; + for (k = offset, f = 0; k < offset + vpr; k++, f++) { + samples[k] = flat[f]; + } + } else { + offset = y * vpr + xstart * this.sampleDepth; + end_offset = (y + 1) * vpr; + skip = this.sampleDepth * xstep; + for (j = 0; j < this.sampleDepth; j++) { + for (k = offset + j, f = j; k < end_offset; k += skip, f += this.sampleDepth) { + samples[k] = flat[f]; + } + } + } + } + } + + return samples; +}; + +PNG.prototype.createBitmap = function(pixels) { + var bmp = []; + + if (this.colorType === 0) { + pixels = pixels.map(function(sample) { + return { r: sample[0], g: sample[0], b: sample[0], a: 255 }; + }); + } else if (this.colorType === 2) { + pixels = pixels.map(function(sample) { + return { r: sample[0], g: sample[1], b: sample[2], a: 255 }; + }); + } else if (this.colorType === 3) { + pixels = pixels.map(function(sample) { + if (!this.palette[sample[0]]) throw new Error('bad palette index'); + return this.palette[sample[0]]; + }, this); + } else if (this.colorType === 4) { + pixels = pixels.map(function(sample) { + return { r: sample[0], g: sample[0], b: sample[0], a: sample[1] }; + }); + } else if (this.colorType === 6) { + pixels = pixels.map(function(sample) { + return { r: sample[0], g: sample[1], b: sample[2], a: sample[3] }; + }); + } + + for (var i = 0; i < pixels.length; i += this.width) { + bmp.push(pixels.slice(i, i + this.width)); + } + + return bmp; +}; + +PNG.prototype.createCellmap = function(bmp, options) { + var bmp = bmp || this.bmp + , options = options || this.options + , cellmap = [] + , scale = options.cellmapScale || this.defaultScale + , height = bmp.length + , width = bmp[0].length + , cmwidth = options.cellmapWidth + , cmheight = options.cellmapHeight + , line + , x + , y + , scale + , xs + , ys; + + if (cmwidth) { + scale = cmwidth / width; + } else if (cmheight) { + scale = cmheight / height; + } + + ys = Math.ceil(height / (height * scale)); + xs = Math.ceil(width / (width * scale)); + + // ys++; + // xs++; + + // add a reducePixels() method here + for (y = 0; y < bmp.length; y += ys) { + line = []; + if (cmheight && cellmap.length === cmheight) break; + cellmap.push(line); + for (x = 0; x < bmp[y].length; x += xs) { + if (cmwidth && line.length === cmwidth) break; + line.push(bmp[y][x]); + } + } + + return cellmap; +}; + +PNG.prototype.renderANSI = function(bmp) { + var self = this + , out = ''; + + bmp.forEach(function(line, y) { + line.forEach(function(pixel, x) { + var outch = self.getOutch(x, y, line, pixel); + out += self.pixelToSGR(pixel, outch); + }); + out += '\n'; + }); + + return out; +}; + +PNG.prototype.renderContent = function(bmp, el) { + var self = this + , out = ''; + + bmp.forEach(function(line, y) { + line.forEach(function(pixel, x) { + var outch = self.getOutch(x, y, line, pixel); + out += self.pixelToTags(pixel, outch); + }); + out += '\n'; + }); + + el.setContent(out); + + return out; +}; + +PNG.prototype.renderScreen = function(bmp, screen, xi, xl, yi, yl) { + var self = this + , lines = screen.lines + , cellLines + , y + , yy + , x + , xx + , alpha + , attr + , ch; + + cellLines = bmp.reduce(function(cellLines, line, y) { + var cellLine = []; + line.forEach(function(pixel, x) { + var outch = self.getOutch(x, y, line, pixel); + var cell = self.pixelToCell(pixel, outch); + cellLine.push(cell); + }); + cellLines.push(cellLine); + return cellLines; + }, []); + + for (y = yi; y < yl; y++) { + yy = y - yi; + for (x = xi; x < xl; x++) { + xx = x - xi; + if (lines[y] && lines[y][x] && cellLines[yy] && cellLines[yy][xx]) { + alpha = cellLines[yy][xx].pop(); + // completely transparent + if (alpha === 0.0) { + continue; + } + // translucency / blending + if (alpha < 1.0) { + attr = cellLines[yy][xx][0]; + ch = cellLines[yy][xx][1]; + lines[y][x][0] = this.colors.blend(lines[y][x][0], attr, alpha); + if (ch !== ' ') lines[y][x][1] = ch; + lines[y].dirty = true; + continue; + } + // completely opaque + lines[y][x] = cellLines[yy][xx]; + lines[y].dirty = true; + } + } + } +}; + +PNG.prototype.renderElement = function(bmp, el) { + var xi = el.aleft + el.ileft, xl = el.aleft + el.width - el.iright + , yi = el.atop + el.itop, yl = el.atop + el.height - el.ibottom; + + return this.renderScreen(bmp, el.screen, xi, xl, yi, yl); +}; + +PNG.prototype.pixelToSGR = function(pixel, ch) { + var bga = 1.0 + , fga = 0.5 + , a = pixel.a / 255 + , bg + , fg; + + bg = this.colors.match( + pixel.r * a * bga | 0, + pixel.g * a * bga | 0, + pixel.b * a * bga | 0); + + if (ch && this.options.ascii) { + fg = this.colors.match( + pixel.r * a * fga | 0, + pixel.g * a * fga | 0, + pixel.b * a * fga | 0); + if (a === 0) { + return '\x1b[38;5;' + fg + 'm' + ch + '\x1b[m'; + } + return '\x1b[38;5;' + fg + 'm\x1b[48;5;' + bg + 'm' + ch + '\x1b[m'; + } + + if (a === 0) return ' '; + + return '\x1b[48;5;' + bg + 'm \x1b[m'; +}; + +PNG.prototype.pixelToTags = function(pixel, ch) { + var bga = 1.0 + , fga = 0.5 + , a = pixel.a / 255 + , bg + , fg; + + bg = this.colors.RGBtoHex( + pixel.r * a * bga | 0, + pixel.g * a * bga | 0, + pixel.b * a * bga | 0); + + if (ch && this.options.ascii) { + fg = this.colors.RGBtoHex( + pixel.r * a * fga | 0, + pixel.g * a * fga | 0, + pixel.b * a * fga | 0); + if (a === 0) { + return '{' + fg + '-fg}' + ch + '{/}'; + } + return '{' + fg + '-fg}{' + bg + '-bg}' + ch + '{/}'; + } + + if (a === 0) return ' '; + + return '{' + bg + '-bg} {/' + bg + '-bg}'; +}; + +PNG.prototype.pixelToCell = function(pixel, ch) { + var bga = 1.0 + , fga = 0.5 + , a = pixel.a / 255 + , bg + , fg; + + bg = this.colors.match( + pixel.r * bga | 0, + pixel.g * bga | 0, + pixel.b * bga | 0); + + if (ch && this.options.ascii) { + fg = this.colors.match( + pixel.r * fga | 0, + pixel.g * fga | 0, + pixel.b * fga | 0); + } else { + fg = 0x1ff; + ch = null; + } + + // if (a === 0) bg = 0x1ff; + + return [(0 << 18) | (fg << 9) | (bg << 0), ch || ' ', a]; +}; + +// Taken from libcaca: +PNG.prototype.getOutch = (function() { + var dchars = '????8@8@#8@8##8#MKXWwz$&%x><\\/xo;+=|^-:i\'.`, `. '; + + var luminance = function(pixel) { + var a = pixel.a / 255 + , r = pixel.r * a + , g = pixel.g * a + , b = pixel.b * a + , l = 0.2126 * r + 0.7152 * g + 0.0722 * b; + + return l / 255; + }; + + return function(x, y, line, pixel) { + var lumi = luminance(pixel) + , outch = dchars[lumi * (dchars.length - 1) | 0]; + + return outch; + }; +})(); + +PNG.prototype.compileFrames = function(frames) { + return this.options.optimization === 'mem' + ? this.compileFrames_lomem(frames) + : this.compileFrames_locpu(frames); +}; + +PNG.prototype.compileFrames_lomem = function(frames) { + if (!this.actl) return; + return frames.map(function(frame, i) { + this.width = frame.fctl.width; + this.height = frame.fctl.height; + + var pixels = frame._pixels || this.parseLines(frame.fdat) + , bmp = frame._bmp || this.createBitmap(pixels) + , fc = frame.fctl; + + return { + actl: this.actl, + fctl: frame.fctl, + delay: (fc.delayNum / (fc.delayDen || 100)) * 1000 | 0, + bmp: bmp + }; + }, this); +}; + +PNG.prototype.compileFrames_locpu = function(frames) { + if (!this.actl) return; + + this._curBmp = null; + this._lastBmp = null; + + return frames.map(function(frame, i) { + this.width = frame.fctl.width; + this.height = frame.fctl.height; + + var pixels = frame._pixels || this.parseLines(frame.fdat) + , bmp = frame._bmp || this.createBitmap(pixels) + , renderBmp = this.renderFrame(bmp, frame, i) + , cellmap = this.createCellmap(renderBmp) + , fc = frame.fctl; + + return { + actl: this.actl, + fctl: frame.fctl, + delay: (fc.delayNum / (fc.delayDen || 100)) * 1000 | 0, + bmp: renderBmp, + cellmap: cellmap + }; + }, this); +}; + +PNG.prototype.renderFrame = function(bmp, frame, i) { + var renderBmp = bmp + , first = this.frames[0] + , last = this.frames[i - 1] + , fc = frame.fctl + , xo = fc.xOffset + , yo = fc.yOffset + , lxo + , lyo + , ops + , x + , y + , p; + + ops = (xo + yo + fc.blendOp) + + (last ? last.fctl.disposeOp : 0) + + ~~(fc.width !== first.fctl.width) + + ~~(fc.height !== first.fctl.height); + + // XXX Disable dispose ops for now. + ops -= fc.disposeOp + (last ? last.fctl.disposeOp : 0); + + if (!this._curBmp) { + this._curBmp = []; + for (y = 0; y < first.fctl.height; y++) { + var line = []; + for (x = 0; x < first.fctl.width; x++) { + p = bmp[y][x]; + line.push({ r: p.r, g: p.g, b: p.b, a: p.a }); + } + this._curBmp.push(line); + } + } + + if (last && ops) { + // XXX Disable dispose ops for now. + if (last.fctl.disposeOp) { + lxo = last.fctl.xOffset; + lyo = last.fctl.yOffset; + for (y = 0; y < last.fctl.height; y++) { + for (x = 0; x < last.fctl.width; x++) { + if (last.fctl.disposeOp === 1) { + this._curBmp[lyo + y][lxo + x] = { r: 0, g: 0, b: 0, a: 0 }; + } else if (last.fctl.disposeOp === 2) { + p = this._lastBmp[y][x]; + this._curBmp[lyo + y][lxo + x] = { r: p.r, g: p.g, b: p.b, a: p.a }; + } + } + } + } + for (y = 0; y < frame.fctl.height; y++) { + for (x = 0; x < frame.fctl.width; x++) { + p = bmp[y][x]; + if (fc.blendOp === 0) { + this._curBmp[yo + y][xo + x] = { r: p.r, g: p.g, b: p.b, a: p.a }; + } else if (fc.blendOp === 1) { + if (bmp[y][x].a !== 0) { + this._curBmp[yo + y][xo + x] = { r: p.r, g: p.g, b: p.b, a: p.a }; + } + } + } + } + renderBmp = this._curBmp; + } + + this._lastBmp = bmp; + + return renderBmp; +}; + +PNG.prototype._animate = function(callback) { + if (!this.frames) { + return callback(this.bmp, this.cellmap); + } + + var self = this + , numPlays = this.actl.numPlays || Infinity + , running = 0 + , i = -1; + + this._curBmp = null; + this._lastBmp = null; + + var next_lomem = function() { + if (!running) return; + var frame = self.frames[++i]; + if (!frame) { + if (!--numPlays) return callback(); + i = -1; + // XXX may be able to optimize by only setting the self._curBmp once??? + self._curBmp = null; + self._lastBmp = null; + return setImmediate(next); + } + + var bmp = frame.bmp + , renderBmp = self.renderFrame(bmp, frame, i) + , cellmap = self.createCellmap(renderBmp); + + callback(renderBmp, cellmap); + return setTimeout(next, frame.delay); + }; + + var next_locpu = function() { + if (!running) return; + var frame = self.frames[++i]; + if (!frame) { + if (!--numPlays) return callback(); + i = -1; + return setImmediate(next); + } + callback(frame.bmp, frame.cellmap); + return setTimeout(next, frame.delay); + }; + + var next = this.options.optimization === 'mem' + ? next_lomem + : next_locpu; + + this._control = function(state) { + if (state === -1) { + i = -1; + self._curBmp = null; + self._lastBmp = null; + running = 0; + callback(self.frames[0].bmp, + self.frames[0].cellmap || self.createCellmap(self.frames[0].bmp)); + return; + } + if (state === running) return; + running = state; + return next(); + }; + + this._control(1); +}; + +PNG.prototype.play = function(callback) { + if (!this._control || callback) { + this.stop(); + return this._animate(callback); + } + this._control(1); +}; + +PNG.prototype.pause = function() { + if (!this._control) return; + this._control(0); +}; + +PNG.prototype.stop = function() { + if (!this._control) return; + this._control(-1); +}; + +PNG.prototype.toPNG = function() { + var options = this.options + , file = this.file + , format = this.format + , buf + , img + , gif; + + if (format !== 'gif') { + buf = exec('convert', + [format + ':' + file, 'png:-'], + { stdio: ['ignore', 'pipe', 'ignore']}); + img = PNG(buf, options); + img.file = file; + return img; + } + + gif = GIF(file, options); + + this.width = gif.screenWidth; + this.height = gif.screenHeight; + this.frames = []; + + for (var i = 0; i < gif.images.length; i++) { + var img = gif.images[i]; + // Convert from gif disposal to png disposal. See: + // http://www.w3.org/Graphics/GIF/spec-gif89a.txt + var disposeOp = Math.max(0, (gif.disposeMethod || 0) - 1); + if (disposeOp > 2) disposeOp = 0; + this.frames.push({ + fctl: { + sequenceNumber: i, + width: img.width, + height: img.height, + xOffset: img.left, + yOffset: img.top, + delayNum: gif.delay, + delayDen: 100, + disposeOp: disposeOp, + blendOp: 1 + }, + fdat: [], + _pixels: [], + _bmp: img.bmp + }); + } + + this.bmp = this.frames[0]._bmp; + this.cellmap = this.createCellmap(this.bmp); + + if (this.frames.length > 1) { + this.actl = { numFrames: gif.images.length, numPlays: gif.numPlays || 0 }; + this.frames = this.compileFrames(this.frames); + } else { + this.frames = undefined; + } + + return this; + +}; + +// Convert a gif to an apng using imagemagick. Unfortunately imagemagick +// doesn't support apngs, so we coalesce the gif frames into one image and then +// slice them into frames. +PNG.prototype.gifMagick = function() { + var options = this.options + , file = this.file + , format = this.format + , buf + , fmt + , img + , frames + , frame + , width + , height + , iwidth + , twidth + , i + , lines + , line + , x + , y; + + buf = exec('convert', + [format + ':' + file, '-coalesce', '+append', 'png:-']); + + fmt = '{"W":%W,"H":%H,"w":%w,"h":%h,"d":%T,"x":"%X","y":"%Y"},' + frames = exec('identify', ['-format', fmt, format + ':' + file], + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }); + frames = JSON.parse('[' + frames.trim().slice(0, -1) + ']'); + + img = PNG(buf, options); + img.file = file; + Object.keys(img).forEach(function(key) { + this[key] = img[key]; + }, this); + + width = frames[0].W; + height = frames[0].H; + iwidth = 0; + twidth = 0; + + this.width = width; + this.height = height; + + this.frames = []; + + for (i = 0; i < frames.length; i++) { + frame = frames[i]; + frame.x = +frame.x; + frame.y = +frame.y; + + iwidth = twidth; + twidth += width; + + lines = []; + for (y = frame.y; y < height; y++) { + line = []; + for (x = iwidth + frame.x; x < twidth; x++) { + line.push(img.bmp[y][x]); + } + lines.push(line); + } + + this.frames.push({ + fctl: { + sequenceNumber: i, + width: frame.w, + height: frame.h, + xOffset: frame.x, + yOffset: frame.y, + delayNum: frame.d, + delayDen: 100, + disposeOp: 0, + blendOp: 0 + }, + fdat: [], + _pixels: [], + _bmp: lines + }); + } + + this.bmp = this.frames[0]._bmp; + this.cellmap = this.createCellmap(this.bmp); + + if (this.frames.length > 1) { + this.actl = { numFrames: frames.length, numPlays: 0 }; + this.frames = this.compileFrames(this.frames); + } else { + this.frames = undefined; + } + + return this; +}; + +PNG.prototype._debug = function() { + if (!this.options.log) return; + return this.options.log.apply(null, arguments); +}; + +/** + * Helpers + */ + +function decompress(buffers) { + return zlib.inflateSync(new Buffer(buffers.reduce(function(out, data) { + return out.concat(Array.prototype.slice.call(data)); + }, []))); +} + +function crc32() { + return 0; +} + +function dump() { + Array.prototype.slice.call(arguments).forEach(function(v) { console.log(v) }); + return process.exit(0); +} + +/** + * Expose + */ + +exports = PNG; +exports.png = PNG; +exports.gif = GIF; + +module.exports = exports; diff --git a/vendor/tng/package.json b/vendor/tng/package.json new file mode 100644 index 0000000..94c0a42 --- /dev/null +++ b/vendor/tng/package.json @@ -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" + } +}