From e638d21dc3d88211e71d145a826eafd7e7876f53 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sat, 11 Jul 2015 14:59:50 -0700 Subject: [PATCH] separating image into w3mimage and png elements. --- README.md | 82 ++++- lib/widget.js | 1 + lib/widgets/image.js | 702 ++------------------------------------ lib/widgets/w3mimage.js | 725 ++++++++++++++++++++++++++++++++++++++++ test/widget-image.js | 1 + test/widget-png.js | 3 +- 6 files changed, 822 insertions(+), 692 deletions(-) create mode 100644 lib/widgets/w3mimage.js diff --git a/README.md b/README.md index 949f277..30c4890 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ screen.render(); - [Image](#image-from-box) - [Layout](#layout-from-element) - [PNG](#png-from-box) + - [Video](#video-from-box) ### Other @@ -287,7 +288,7 @@ The screen on which every other node renders. implementation, it's uncertain how much terminal performance this adds at the cost of overhead within node. - __resizeTimeout__ - Amount of time (in ms) to redraw the screen after the - terminal is resized (default: 300). + terminal is resized (Default: 300). - __tabSize__ - The width of tabs within an element's content. - __autoPadding__ - Automatically position child elements with border and padding in mind (__NOTE__: this is a recommended option. It may become @@ -774,9 +775,9 @@ A scrollable list which can display selectable items. called with the search string. The search string is then used to jump to an item that is found in `items`. - __interactive__ - Whether the list is interactive and can have items selected - (default: true). + (Default: true). - __invertSelected__ - Whether to automatically override tags and invert fg of - item when selected (default: `true`). + item when selected (Default: `true`). ##### Properties: @@ -1336,7 +1337,8 @@ manager. Requires term.js and pty.js to be installed. See - __shell__ - Name of shell. `$SHELL` by default. - __args__ - Args for shell. - __cursor__ - Can be `line`, `underline`, and `block`. -- __term__ - terminal name (default: `xterm`). +- __term__ - Terminal name (Default: `xterm`). +- __env__ - Object for process env. - Other options similar to term.js'. ##### Properties: @@ -1363,6 +1365,39 @@ manager. Requires term.js and pty.js to be installed. See #### Image (from Box) +Display an image in the terminal (jpeg, png, gif) using either blessed's +internal png/gif-to-terminal renderer (using a [PNG element](#png-from-box)) or +using `w3mimgdisplay` (using a [W3MImage element](#w3mimage-from-box)). + +##### Options: + +- Inherits all from Box. +- __file__ - Path to image. +- __itype__ - `ansi` or `w3m`. Whether to render the file as ANSI art or using + `w3m` to overlay Internally uses the PNG element. See the [PNG + element](#png-from-box) for more information/options. (__default__: `ansi`). + +##### Properties: + +- Inherits all from Box. +- See [PNG element](#png-from-box) +- See [W3MImage element](#w3mimage-from-box) + +##### Events: + +- Inherits all from Box. +- See [PNG element](#png-from-box) +- See [W3MImage element](#w3mimage-from-box) + +##### Methods: + +- Inherits all from Box. +- See [PNG element](#png-from-box) +- See [W3MImage element](#w3mimage-from-box) + + +#### W3MImage (from Box) + Display an image in the terminal (jpeg, png, gif) using w3mimgdisplay. Requires w3m to be installed. X11 required: works in xterm, urxvt, and possibly other terminals. @@ -1371,10 +1406,13 @@ terminals. - Inherits all from Box. - __file__ - Path to image. +- __ansi__ - Render the file as ANSI art instead of using `w3m` to overlay + Internally uses the PNG element. See the [PNG element](#png-from-box) for + more information/options. (Default: `true`). - __w3m__ - Path to w3mimgdisplay. If a proper `w3mimgdisplay` path is not given, blessed will search the entire disk for the binary. - __search__ - Whether to search `/usr`, `/bin`, and `/lib` for - `w3mimgdisplay` (default: `true`). + `w3mimgdisplay` (Default: `true`). ##### Properties: @@ -1599,7 +1637,7 @@ installed. - 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`). + (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 @@ -1609,14 +1647,14 @@ installed. 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/animating GIF. If - false, only display the first frame or IDAT (default: `true`). + false, only display the first frame or IDAT (Default: `true`). - __speed__ - Set the speed of animation. Slower: `0.0-1.0`. Faster: `1-1000`. It cannot go faster than 1 frame per millisecond, so 1000 is the fastest. - (default: 1.0) + (Default: 1.0) - __optimization__ - `mem` or `cpu`. If optimizing for memory, animation frames will be rendered to bitmaps _as the animation plays_, using less memory. Optimizing for cpu will precompile all bitmaps beforehand, which may be - faster, but might also OOM the process on large images. (default: `mem`). + faster, but might also OOM the process on large images. (Default: `mem`). ##### Properties: @@ -1639,6 +1677,32 @@ installed. - __clearImage()__ - Clear the image. +#### Video (from Box) + +A box which spins up a pseudo terminal in order to render a video via `mplayer +-vo caca` or `mpv --vo caca`. Requires `mplayer` or `mpv` to be installed with +libcaca support. + +##### Options: + +- Inherits all from Box. +- __file__ - Video to play. +- __start__ - Start time in seconds. + +##### Properties: + +- Inherits all from Box. +- __tty__ - The terminal element running `mplayer` or `mpv`. + +##### Events: + +- Inherits all from Box. + +##### Methods: + +- Inherits all from Box. + + ### Other diff --git a/lib/widget.js b/lib/widget.js index bb3b3d2..6dcfd41 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -36,6 +36,7 @@ widget.classes = [ 'ListTable', 'Terminal', 'Image', + 'W3MImage', 'Layout', 'PNG', 'Video' diff --git a/lib/widgets/image.js b/lib/widgets/image.js index f831bc5..8e13052 100644 --- a/lib/widgets/image.js +++ b/lib/widgets/image.js @@ -1,5 +1,5 @@ /** - * image.js - w3m image element for blessed + * image.js - image element for blessed * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ @@ -8,9 +8,6 @@ * Modules */ -var fs = require('fs') - , cp = require('child_process'); - var helpers = require('../helpers'); var Node = require('./node'); @@ -18,8 +15,6 @@ var Box = require('./box'); /** * Image - * Good example of w3mimgdisplay commands: - * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py */ function Image(options) { @@ -30,89 +25,30 @@ function Image(options) { } options = options || {}; + options.itype = options.itype || 'ansi'; Box.call(this, options); - if (options.w3m) { - Image.w3mdisplay = options.w3m; + if (options.itype === 'ansi' && this.type !== 'png') { + var PNG = require('./png'); + Object.getOwnPropertyNames(PNG.prototype).forEach(function(key) { + if (key === 'type') return; + Object.defineProperty(this, key, + Object.getOwnPropertyDescriptor(PNG.prototype, key)); + }, this); + PNG.call(this, options); + return this; } - if (Image.hasW3MDisplay == null) { - if (fs.existsSync(Image.w3mdisplay)) { - Image.hasW3MDisplay = true; - } else if (options.search !== false) { - var file = helpers.findFile('/usr', 'w3mimgdisplay') - || helpers.findFile('/lib', 'w3mimgdisplay') - || helpers.findFile('/bin', 'w3mimgdisplay'); - if (file) { - Image.hasW3MDisplay = true; - Image.w3mdisplay = file; - } else { - Image.hasW3MDisplay = false; - } - } - } - - this.on('hide', function() { - self._lastFile = self.file; - self.clearImage(); - }); - - this.on('show', function() { - if (!self._lastFile) return; - self.setImage(self._lastFile); - }); - - this.on('detach', function() { - self._lastFile = self.file; - self.clearImage(); - }); - - this.on('attach', function() { - if (!self._lastFile) return; - self.setImage(self._lastFile); - }); - - this.onScreenEvent('resize', function() { - self._needsRatio = true; - }); - - // Get images to overlap properly. Maybe not worth it: - // this.onScreenEvent('render', function() { - // self.screen.program.flush(); - // if (!self._noImage) return; - // function display(el, next) { - // if (el.type === 'image' && el.file) { - // el.setImage(el.file, next); - // } else { - // next(); - // } - // } - // function done(el) { - // el.children.forEach(recurse); - // } - // function recurse(el) { - // display(el, function() { - // var pending = el.children.length; - // el.children.forEach(function(el) { - // display(el, function() { - // if (!--pending) done(el); - // }); - // }); - // }); - // } - // recurse(self.screen); - // }); - - this.onScreenEvent('render', function() { - self.screen.program.flush(); - if (!self._noImage) { - self.setImage(self.file); - } - }); - - if (this.options.file || this.options.img) { - this.setImage(this.options.file || this.options.img); + if (options.itype === 'w3m' && this.type !== 'w3mimage') { + var W3MImage = require('./w3mimage'); + Object.getOwnPropertyNames(W3MImage.prototype).forEach(function(key) { + if (key === 'type') return; + Object.defineProperty(this, key, + Object.getOwnPropertyDescriptor(W3MImage.prototype, key)); + }, this); + W3MImage.call(this, options); + return this; } } @@ -120,604 +56,6 @@ Image.prototype.__proto__ = Box.prototype; Image.prototype.type = 'image'; -Image.w3mdisplay = '/usr/lib/w3m/w3mimgdisplay'; - -Image.prototype.spawn = function(file, args, opt, callback) { - var screen = this.screen - , opt = opt || {} - , spawn = require('child_process').spawn - , ps; - - ps = spawn(file, args, opt); - - ps.on('error', function(err) { - if (!callback) return; - return callback(err); - }); - - ps.on('exit', function(code) { - if (!callback) return; - if (code !== 0) return callback(new Error('Exit Code: ' + code)); - return callback(null, code === 0); - }); - - return ps; -}; - -Image.prototype.setImage = function(img, callback) { - var self = this; - - if (this._settingImage) { - this._queue = this._queue || []; - this._queue.push([img, callback]); - return; - } - this._settingImage = true; - - var reset = function(err, success) { - self._settingImage = false; - self._queue = self._queue || []; - var item = self._queue.shift(); - if (item) { - self.setImage(item[0], item[1]); - } - }; - - if (Image.hasW3MDisplay === false) { - reset(); - if (!callback) return; - return callback(new Error('W3M Image Display not available.')); - } - - if (!img) { - reset(); - if (!callback) return; - return callback(new Error('No image.')); - } - - this.file = img; - - return this.getPixelRatio(function(err, ratio) { - if (err) { - reset(); - if (!callback) return; - return callback(err); - } - - return self.renderImage(img, ratio, function(err, success) { - if (err) { - reset(); - if (!callback) return; - return callback(err); - } - - if (self.shrink || self.options.autofit) { - delete self.shrink; - delete self.options.shrink; - self.options.autofit = true; - return self.imageSize(function(err, size) { - if (err) { - reset(); - if (!callback) return; - return callback(err); - } - - if (self._lastSize - && ratio.tw === self._lastSize.tw - && ratio.th === self._lastSize.th - && size.width === self._lastSize.width - && size.height === self._lastSize.height - && self.aleft === self._lastSize.aleft - && self.atop === self._lastSize.atop) { - reset(); - if (!callback) return; - return callback(null, success); - } - - self._lastSize = { - tw: ratio.tw, - th: ratio.th, - width: size.width, - height: size.height, - aleft: self.aleft, - atop: self.atop - }; - - self.position.width = size.width / ratio.tw | 0; - self.position.height = size.height / ratio.th | 0; - - self._noImage = true; - self.screen.render(); - self._noImage = false; - - reset(); - return self.renderImage(img, ratio, callback); - }); - } - - reset(); - if (!callback) return; - return callback(null, success); - }); - }); -}; - -Image.prototype.renderImage = function(img, ratio, callback) { - var self = this; - - if (cp.execSync) { - callback = callback || function(err, result) { return result; }; - try { - return callback(null, this.renderImageSync(img, ratio)); - } catch (e) { - return callback(e); - } - } - - if (Image.hasW3MDisplay === false) { - if (!callback) return; - return callback(new Error('W3M Image Display not available.')); - } - - if (!ratio) { - if (!callback) return; - return callback(new Error('No ratio.')); - } - - // clearImage unsets these: - var _file = self.file; - var _lastSize = self._lastSize; - return self.clearImage(function(err) { - if (err) return callback(err); - - self.file = _file; - self._lastSize = _lastSize; - - var opt = { - stdio: 'pipe', - env: process.env, - cwd: process.env.HOME - }; - - var ps = self.spawn(Image.w3mdisplay, [], opt, function(err, success) { - if (!callback) return; - return err - ? callback(err) - : callback(null, success); - }); - - var width = self.width * ratio.tw | 0 - , height = self.height * ratio.th | 0 - , aleft = self.aleft * ratio.tw | 0 - , atop = self.atop * ratio.th | 0; - - var input = '0;1;' - + aleft + ';' - + atop + ';' - + width + ';' - + height + ';;;;;' - + img - + '\n4;\n3;\n'; - - self._props = { - aleft: aleft, - atop: atop, - width: width, - height: height - }; - - ps.stdin.write(input); - ps.stdin.end(); - }); -}; - -Image.prototype.clearImage = function(callback) { - var self = this; - - if (cp.execSync) { - callback = callback || function(err, result) { return result; }; - try { - return callback(null, this.clearImageSync()); - } catch (e) { - return callback(e); - } - } - - if (Image.hasW3MDisplay === false) { - if (!callback) return; - return callback(new Error('W3M Image Display not available.')); - } - - if (!this._props) { - if (!callback) return; - return callback(null); - } - - var opt = { - stdio: 'pipe', - env: process.env, - cwd: process.env.HOME - }; - - var ps = this.spawn(Image.w3mdisplay, [], opt, function(err, success) { - if (!callback) return; - return err - ? callback(err) - : callback(null, success); - }); - - var width = this._props.width + 2 - , height = this._props.height + 2 - , aleft = this._props.aleft - , atop = this._props.atop; - - if (this._drag) { - aleft -= 10; - atop -= 10; - width += 10; - height += 10; - } - - var input = '6;' - + aleft + ';' - + atop + ';' - + width + ';' - + height - + '\n4;\n3;\n'; - - delete this.file; - delete this._props; - delete this._lastSize; - - ps.stdin.write(input); - ps.stdin.end(); -}; - -Image.prototype.imageSize = function(callback) { - var self = this; - var img = this.file; - - if (cp.execSync) { - callback = callback || function(err, result) { return result; }; - try { - return callback(null, this.imageSizeSync()); - } catch (e) { - return callback(e); - } - } - - if (Image.hasW3MDisplay === false) { - if (!callback) return; - return callback(new Error('W3M Image Display not available.')); - } - - if (!img) { - if (!callback) return; - return callback(new Error('No image.')); - } - - var opt = { - stdio: 'pipe', - env: process.env, - cwd: process.env.HOME - }; - - var ps = this.spawn(Image.w3mdisplay, [], opt); - - var buf = ''; - - ps.stdout.setEncoding('utf8'); - - ps.stdout.on('data', function(data) { - buf += data; - }); - - ps.on('error', function(err) { - if (!callback) return; - return callback(err); - }); - - ps.on('exit', function() { - if (!callback) return; - var size = buf.trim().split(/\s+/); - return callback(null, { - raw: buf.trim(), - width: +size[0], - height: +size[1] - }); - }); - - var input = '5;' + img + '\n'; - - ps.stdin.write(input); - ps.stdin.end(); -}; - -Image.prototype.termSize = function(callback) { - var self = this; - - if (cp.execSync) { - callback = callback || function(err, result) { return result; }; - try { - return callback(null, this.termSizeSync()); - } catch (e) { - return callback(e); - } - } - - if (Image.hasW3MDisplay === false) { - if (!callback) return; - return callback(new Error('W3M Image Display not available.')); - } - - var opt = { - stdio: 'pipe', - env: process.env, - cwd: process.env.HOME - }; - - var ps = this.spawn(Image.w3mdisplay, ['-test'], opt); - - var buf = ''; - - ps.stdout.setEncoding('utf8'); - - ps.stdout.on('data', function(data) { - buf += data; - }); - - ps.on('error', function(err) { - if (!callback) return; - return callback(err); - }); - - ps.on('exit', function() { - if (!callback) return; - - if (!buf.trim()) { - // Bug: w3mimgdisplay will sometimes - // output nothing. Try again: - return self.termSize(callback); - } - - var size = buf.trim().split(/\s+/); - - return callback(null, { - raw: buf.trim(), - width: +size[0], - height: +size[1] - }); - }); - - ps.stdin.end(); -}; - -Image.prototype.getPixelRatio = function(callback) { - var self = this; - - if (cp.execSync) { - callback = callback || function(err, result) { return result; }; - try { - return callback(null, this.getPixelRatioSync()); - } catch (e) { - return callback(e); - } - } - - // XXX We could cache this, but sometimes it's better - // to recalculate to be pixel perfect. - if (this._ratio && !this._needsRatio) { - return callback(null, this._ratio); - } - - return this.termSize(function(err, dimensions) { - if (err) return callback(err); - - self._ratio = { - tw: dimensions.width / self.screen.width, - th: dimensions.height / self.screen.height - }; - - self._needsRatio = false; - - return callback(null, self._ratio); - }); -}; - -Image.prototype.renderImageSync = function(img, ratio) { - var self = this; - - if (Image.hasW3MDisplay === false) { - throw new Error('W3M Image Display not available.'); - } - - if (!ratio) { - throw new Error('No ratio.'); - } - - // clearImage unsets these: - var _file = this.file; - var _lastSize = this._lastSize; - - this.clearImageSync(); - - this.file = _file; - this._lastSize = _lastSize; - - var width = this.width * ratio.tw | 0 - , height = this.height * ratio.th | 0 - , aleft = this.aleft * ratio.tw | 0 - , atop = this.atop * ratio.th | 0; - - var input = '0;1;' - + aleft + ';' - + atop + ';' - + width + ';' - + height + ';;;;;' - + img - + '\n4;\n3;\n'; - - this._props = { - aleft: aleft, - atop: atop, - width: width, - height: height - }; - - try { - cp.execFileSync(Image.w3mdisplay, [], { - env: process.env, - encoding: 'utf8', - input: input, - timeout: 1000 - }); - } catch (e) { - ; - } - - return true; -}; - -Image.prototype.clearImageSync = function() { - if (Image.hasW3MDisplay === false) { - throw new Error('W3M Image Display not available.'); - } - - if (!this._props) { - return false; - } - - var width = this._props.width + 2 - , height = this._props.height + 2 - , aleft = this._props.aleft - , atop = this._props.atop; - - if (this._drag) { - aleft -= 10; - atop -= 10; - width += 10; - height += 10; - } - - var input = '6;' - + aleft + ';' - + atop + ';' - + width + ';' - + height - + '\n4;\n3;\n'; - - delete this.file; - delete this._props; - delete this._lastSize; - - try { - cp.execFileSync(Image.w3mdisplay, [], { - env: process.env, - encoding: 'utf8', - input: input, - timeout: 1000 - }); - } catch (e) { - ; - } - - return true; -}; - -Image.prototype.imageSizeSync = function() { - var img = this.file; - - if (Image.hasW3MDisplay === false) { - throw new Error('W3M Image Display not available.'); - } - - if (!img) { - throw new Error('No image.'); - } - - var buf = ''; - var input = '5;' + img + '\n'; - - try { - buf = cp.execFileSync(Image.w3mdisplay, [], { - env: process.env, - encoding: 'utf8', - input: input, - timeout: 1000 - }); - } catch (e) { - ; - } - - var size = buf.trim().split(/\s+/); - - return { - raw: buf.trim(), - width: +size[0], - height: +size[1] - }; -}; - -Image.prototype.termSizeSync = function(_, recurse) { - if (Image.hasW3MDisplay === false) { - throw new Error('W3M Image Display not available.'); - } - - var buf = ''; - - try { - buf = cp.execFileSync(Image.w3mdisplay, ['-test'], { - env: process.env, - encoding: 'utf8', - timeout: 1000 - }); - } catch (e) { - ; - } - - if (!buf.trim()) { - // Bug: w3mimgdisplay will sometimes - // output nothing. Try again: - recurse = recurse || 0; - if (++recurse === 5) { - throw new Error('Term size not determined.'); - } - return this.termSizeSync(_, recurse); - } - - var size = buf.trim().split(/\s+/); - - return { - raw: buf.trim(), - width: +size[0], - height: +size[1] - }; -}; - -Image.prototype.getPixelRatioSync = function() { - var self = this; - - // XXX We could cache this, but sometimes it's better - // to recalculate to be pixel perfect. - if (this._ratio && !this._needsRatio) { - return this._ratio; - } - this._needsRatio = false; - - var dimensions = this.termSizeSync(); - - this._ratio = { - tw: dimensions.width / this.screen.width, - th: dimensions.height / this.screen.height - }; - - return this._ratio; -}; - -Image.prototype.displayImage = function(callback) { - return this.screen.displayImage(this.file, callback); -}; - /** * Expose */ diff --git a/lib/widgets/w3mimage.js b/lib/widgets/w3mimage.js new file mode 100644 index 0000000..4dc969a --- /dev/null +++ b/lib/widgets/w3mimage.js @@ -0,0 +1,725 @@ +/** + * image.js - w3m image element for blessed + * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). + * https://github.com/chjj/blessed + */ + +/** + * Modules + */ + +var fs = require('fs') + , cp = require('child_process'); + +var helpers = require('../helpers'); + +var Node = require('./node'); +var Box = require('./box'); + +/** + * Image + * Good example of w3mimgdisplay commands: + * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py + */ + +function Image(options) { + var self = this; + + if (!(this instanceof Node)) { + return new Image(options); + } + + options = options || {}; + + Box.call(this, options); + + if (options.w3m) { + Image.w3mdisplay = options.w3m; + } + + if (Image.hasW3MDisplay == null) { + if (fs.existsSync(Image.w3mdisplay)) { + Image.hasW3MDisplay = true; + } else if (options.search !== false) { + var file = helpers.findFile('/usr', 'w3mimgdisplay') + || helpers.findFile('/lib', 'w3mimgdisplay') + || helpers.findFile('/bin', 'w3mimgdisplay'); + if (file) { + Image.hasW3MDisplay = true; + Image.w3mdisplay = file; + } else { + Image.hasW3MDisplay = false; + } + } + } + + this.on('hide', function() { + self._lastFile = self.file; + self.clearImage(); + }); + + this.on('show', function() { + if (!self._lastFile) return; + self.setImage(self._lastFile); + }); + + this.on('detach', function() { + self._lastFile = self.file; + self.clearImage(); + }); + + this.on('attach', function() { + if (!self._lastFile) return; + self.setImage(self._lastFile); + }); + + this.onScreenEvent('resize', function() { + self._needsRatio = true; + }); + + // Get images to overlap properly. Maybe not worth it: + // this.onScreenEvent('render', function() { + // self.screen.program.flush(); + // if (!self._noImage) return; + // function display(el, next) { + // if (el.type === 'image' && el.file) { + // el.setImage(el.file, next); + // } else { + // next(); + // } + // } + // function done(el) { + // el.children.forEach(recurse); + // } + // function recurse(el) { + // display(el, function() { + // var pending = el.children.length; + // el.children.forEach(function(el) { + // display(el, function() { + // if (!--pending) done(el); + // }); + // }); + // }); + // } + // recurse(self.screen); + // }); + + this.onScreenEvent('render', function() { + self.screen.program.flush(); + if (!self._noImage) { + self.setImage(self.file); + } + }); + + if (this.options.file || this.options.img) { + this.setImage(this.options.file || this.options.img); + } +} + +Image.prototype.__proto__ = Box.prototype; + +Image.prototype.type = 'w3mimage'; + +Image.w3mdisplay = '/usr/lib/w3m/w3mimgdisplay'; + +Image.prototype.spawn = function(file, args, opt, callback) { + var screen = this.screen + , opt = opt || {} + , spawn = require('child_process').spawn + , ps; + + ps = spawn(file, args, opt); + + ps.on('error', function(err) { + if (!callback) return; + return callback(err); + }); + + ps.on('exit', function(code) { + if (!callback) return; + if (code !== 0) return callback(new Error('Exit Code: ' + code)); + return callback(null, code === 0); + }); + + return ps; +}; + +Image.prototype.setImage = function(img, callback) { + var self = this; + + if (this._settingImage) { + this._queue = this._queue || []; + this._queue.push([img, callback]); + return; + } + this._settingImage = true; + + var reset = function(err, success) { + self._settingImage = false; + self._queue = self._queue || []; + var item = self._queue.shift(); + if (item) { + self.setImage(item[0], item[1]); + } + }; + + if (Image.hasW3MDisplay === false) { + reset(); + if (!callback) return; + return callback(new Error('W3M Image Display not available.')); + } + + if (!img) { + reset(); + if (!callback) return; + return callback(new Error('No image.')); + } + + this.file = img; + + return this.getPixelRatio(function(err, ratio) { + if (err) { + reset(); + if (!callback) return; + return callback(err); + } + + return self.renderImage(img, ratio, function(err, success) { + if (err) { + reset(); + if (!callback) return; + return callback(err); + } + + if (self.shrink || self.options.autofit) { + delete self.shrink; + delete self.options.shrink; + self.options.autofit = true; + return self.imageSize(function(err, size) { + if (err) { + reset(); + if (!callback) return; + return callback(err); + } + + if (self._lastSize + && ratio.tw === self._lastSize.tw + && ratio.th === self._lastSize.th + && size.width === self._lastSize.width + && size.height === self._lastSize.height + && self.aleft === self._lastSize.aleft + && self.atop === self._lastSize.atop) { + reset(); + if (!callback) return; + return callback(null, success); + } + + self._lastSize = { + tw: ratio.tw, + th: ratio.th, + width: size.width, + height: size.height, + aleft: self.aleft, + atop: self.atop + }; + + self.position.width = size.width / ratio.tw | 0; + self.position.height = size.height / ratio.th | 0; + + self._noImage = true; + self.screen.render(); + self._noImage = false; + + reset(); + return self.renderImage(img, ratio, callback); + }); + } + + reset(); + if (!callback) return; + return callback(null, success); + }); + }); +}; + +Image.prototype.renderImage = function(img, ratio, callback) { + var self = this; + + if (cp.execSync) { + callback = callback || function(err, result) { return result; }; + try { + return callback(null, this.renderImageSync(img, ratio)); + } catch (e) { + return callback(e); + } + } + + if (Image.hasW3MDisplay === false) { + if (!callback) return; + return callback(new Error('W3M Image Display not available.')); + } + + if (!ratio) { + if (!callback) return; + return callback(new Error('No ratio.')); + } + + // clearImage unsets these: + var _file = self.file; + var _lastSize = self._lastSize; + return self.clearImage(function(err) { + if (err) return callback(err); + + self.file = _file; + self._lastSize = _lastSize; + + var opt = { + stdio: 'pipe', + env: process.env, + cwd: process.env.HOME + }; + + var ps = self.spawn(Image.w3mdisplay, [], opt, function(err, success) { + if (!callback) return; + return err + ? callback(err) + : callback(null, success); + }); + + var width = self.width * ratio.tw | 0 + , height = self.height * ratio.th | 0 + , aleft = self.aleft * ratio.tw | 0 + , atop = self.atop * ratio.th | 0; + + var input = '0;1;' + + aleft + ';' + + atop + ';' + + width + ';' + + height + ';;;;;' + + img + + '\n4;\n3;\n'; + + self._props = { + aleft: aleft, + atop: atop, + width: width, + height: height + }; + + ps.stdin.write(input); + ps.stdin.end(); + }); +}; + +Image.prototype.clearImage = function(callback) { + var self = this; + + if (cp.execSync) { + callback = callback || function(err, result) { return result; }; + try { + return callback(null, this.clearImageSync()); + } catch (e) { + return callback(e); + } + } + + if (Image.hasW3MDisplay === false) { + if (!callback) return; + return callback(new Error('W3M Image Display not available.')); + } + + if (!this._props) { + if (!callback) return; + return callback(null); + } + + var opt = { + stdio: 'pipe', + env: process.env, + cwd: process.env.HOME + }; + + var ps = this.spawn(Image.w3mdisplay, [], opt, function(err, success) { + if (!callback) return; + return err + ? callback(err) + : callback(null, success); + }); + + var width = this._props.width + 2 + , height = this._props.height + 2 + , aleft = this._props.aleft + , atop = this._props.atop; + + if (this._drag) { + aleft -= 10; + atop -= 10; + width += 10; + height += 10; + } + + var input = '6;' + + aleft + ';' + + atop + ';' + + width + ';' + + height + + '\n4;\n3;\n'; + + delete this.file; + delete this._props; + delete this._lastSize; + + ps.stdin.write(input); + ps.stdin.end(); +}; + +Image.prototype.imageSize = function(callback) { + var self = this; + var img = this.file; + + if (cp.execSync) { + callback = callback || function(err, result) { return result; }; + try { + return callback(null, this.imageSizeSync()); + } catch (e) { + return callback(e); + } + } + + if (Image.hasW3MDisplay === false) { + if (!callback) return; + return callback(new Error('W3M Image Display not available.')); + } + + if (!img) { + if (!callback) return; + return callback(new Error('No image.')); + } + + var opt = { + stdio: 'pipe', + env: process.env, + cwd: process.env.HOME + }; + + var ps = this.spawn(Image.w3mdisplay, [], opt); + + var buf = ''; + + ps.stdout.setEncoding('utf8'); + + ps.stdout.on('data', function(data) { + buf += data; + }); + + ps.on('error', function(err) { + if (!callback) return; + return callback(err); + }); + + ps.on('exit', function() { + if (!callback) return; + var size = buf.trim().split(/\s+/); + return callback(null, { + raw: buf.trim(), + width: +size[0], + height: +size[1] + }); + }); + + var input = '5;' + img + '\n'; + + ps.stdin.write(input); + ps.stdin.end(); +}; + +Image.prototype.termSize = function(callback) { + var self = this; + + if (cp.execSync) { + callback = callback || function(err, result) { return result; }; + try { + return callback(null, this.termSizeSync()); + } catch (e) { + return callback(e); + } + } + + if (Image.hasW3MDisplay === false) { + if (!callback) return; + return callback(new Error('W3M Image Display not available.')); + } + + var opt = { + stdio: 'pipe', + env: process.env, + cwd: process.env.HOME + }; + + var ps = this.spawn(Image.w3mdisplay, ['-test'], opt); + + var buf = ''; + + ps.stdout.setEncoding('utf8'); + + ps.stdout.on('data', function(data) { + buf += data; + }); + + ps.on('error', function(err) { + if (!callback) return; + return callback(err); + }); + + ps.on('exit', function() { + if (!callback) return; + + if (!buf.trim()) { + // Bug: w3mimgdisplay will sometimes + // output nothing. Try again: + return self.termSize(callback); + } + + var size = buf.trim().split(/\s+/); + + return callback(null, { + raw: buf.trim(), + width: +size[0], + height: +size[1] + }); + }); + + ps.stdin.end(); +}; + +Image.prototype.getPixelRatio = function(callback) { + var self = this; + + if (cp.execSync) { + callback = callback || function(err, result) { return result; }; + try { + return callback(null, this.getPixelRatioSync()); + } catch (e) { + return callback(e); + } + } + + // XXX We could cache this, but sometimes it's better + // to recalculate to be pixel perfect. + if (this._ratio && !this._needsRatio) { + return callback(null, this._ratio); + } + + return this.termSize(function(err, dimensions) { + if (err) return callback(err); + + self._ratio = { + tw: dimensions.width / self.screen.width, + th: dimensions.height / self.screen.height + }; + + self._needsRatio = false; + + return callback(null, self._ratio); + }); +}; + +Image.prototype.renderImageSync = function(img, ratio) { + var self = this; + + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + if (!ratio) { + throw new Error('No ratio.'); + } + + // clearImage unsets these: + var _file = this.file; + var _lastSize = this._lastSize; + + this.clearImageSync(); + + this.file = _file; + this._lastSize = _lastSize; + + var width = this.width * ratio.tw | 0 + , height = this.height * ratio.th | 0 + , aleft = this.aleft * ratio.tw | 0 + , atop = this.atop * ratio.th | 0; + + var input = '0;1;' + + aleft + ';' + + atop + ';' + + width + ';' + + height + ';;;;;' + + img + + '\n4;\n3;\n'; + + this._props = { + aleft: aleft, + atop: atop, + width: width, + height: height + }; + + try { + cp.execFileSync(Image.w3mdisplay, [], { + env: process.env, + encoding: 'utf8', + input: input, + timeout: 1000 + }); + } catch (e) { + ; + } + + return true; +}; + +Image.prototype.clearImageSync = function() { + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + if (!this._props) { + return false; + } + + var width = this._props.width + 2 + , height = this._props.height + 2 + , aleft = this._props.aleft + , atop = this._props.atop; + + if (this._drag) { + aleft -= 10; + atop -= 10; + width += 10; + height += 10; + } + + var input = '6;' + + aleft + ';' + + atop + ';' + + width + ';' + + height + + '\n4;\n3;\n'; + + delete this.file; + delete this._props; + delete this._lastSize; + + try { + cp.execFileSync(Image.w3mdisplay, [], { + env: process.env, + encoding: 'utf8', + input: input, + timeout: 1000 + }); + } catch (e) { + ; + } + + return true; +}; + +Image.prototype.imageSizeSync = function() { + var img = this.file; + + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + if (!img) { + throw new Error('No image.'); + } + + var buf = ''; + var input = '5;' + img + '\n'; + + try { + buf = cp.execFileSync(Image.w3mdisplay, [], { + env: process.env, + encoding: 'utf8', + input: input, + timeout: 1000 + }); + } catch (e) { + ; + } + + var size = buf.trim().split(/\s+/); + + return { + raw: buf.trim(), + width: +size[0], + height: +size[1] + }; +}; + +Image.prototype.termSizeSync = function(_, recurse) { + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + var buf = ''; + + try { + buf = cp.execFileSync(Image.w3mdisplay, ['-test'], { + env: process.env, + encoding: 'utf8', + timeout: 1000 + }); + } catch (e) { + ; + } + + if (!buf.trim()) { + // Bug: w3mimgdisplay will sometimes + // output nothing. Try again: + recurse = recurse || 0; + if (++recurse === 5) { + throw new Error('Term size not determined.'); + } + return this.termSizeSync(_, recurse); + } + + var size = buf.trim().split(/\s+/); + + return { + raw: buf.trim(), + width: +size[0], + height: +size[1] + }; +}; + +Image.prototype.getPixelRatioSync = function() { + var self = this; + + // XXX We could cache this, but sometimes it's better + // to recalculate to be pixel perfect. + if (this._ratio && !this._needsRatio) { + return this._ratio; + } + this._needsRatio = false; + + var dimensions = this.termSizeSync(); + + this._ratio = { + tw: dimensions.width / this.screen.width, + th: dimensions.height / this.screen.height + }; + + return this._ratio; +}; + +Image.prototype.displayImage = function(callback) { + return this.screen.displayImage(this.file, callback); +}; + +/** + * Expose + */ + +module.exports = Image; diff --git a/test/widget-image.js b/test/widget-image.js index 1a42297..ff2f7ce 100644 --- a/test/widget-image.js +++ b/test/widget-image.js @@ -16,6 +16,7 @@ var file = process.argv[2] || __dirname + '/test-image.png'; var image = blessed.image({ parent: screen, + itype: 'w3m', left: 'center', top: 'center', width: 'shrink', diff --git a/test/widget-png.js b/test/widget-png.js index e02459f..3baef2b 100644 --- a/test/widget-png.js +++ b/test/widget-png.js @@ -74,7 +74,7 @@ if (!argv.width && !argv.height && !argv.scale) { argv.width = 20; } -var png = blessed.png({ +var png = blessed.image({ parent: screen, // border: 'line', width: argv.width, @@ -83,6 +83,7 @@ var png = blessed.png({ left: 0, file: file, draggable: true, + itype: 'ansi', scale: argv.scale, ascii: argv.ascii, optimization: argv.optimization,