overlayimage.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /**
  2. * overlayimage.js - w3m image element for blessed
  3. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
  4. * https://github.com/chjj/blessed
  5. */
  6. /**
  7. * Modules
  8. */
  9. var fs = require('fs')
  10. , cp = require('child_process');
  11. var helpers = require('../helpers');
  12. var Node = require('./node');
  13. var Box = require('./box');
  14. /**
  15. * OverlayImage
  16. * Good example of w3mimgdisplay commands:
  17. * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py
  18. */
  19. function OverlayImage(options) {
  20. var self = this;
  21. if (!(this instanceof Node)) {
  22. return new OverlayImage(options);
  23. }
  24. options = options || {};
  25. Box.call(this, options);
  26. if (options.w3m) {
  27. OverlayImage.w3mdisplay = options.w3m;
  28. }
  29. if (OverlayImage.hasW3MDisplay == null) {
  30. if (fs.existsSync(OverlayImage.w3mdisplay)) {
  31. OverlayImage.hasW3MDisplay = true;
  32. } else if (options.search !== false) {
  33. var file = helpers.findFile('/usr', 'w3mimgdisplay')
  34. || helpers.findFile('/lib', 'w3mimgdisplay')
  35. || helpers.findFile('/bin', 'w3mimgdisplay');
  36. if (file) {
  37. OverlayImage.hasW3MDisplay = true;
  38. OverlayImage.w3mdisplay = file;
  39. } else {
  40. OverlayImage.hasW3MDisplay = false;
  41. }
  42. }
  43. }
  44. this.on('hide', function() {
  45. self._lastFile = self.file;
  46. self.clearImage();
  47. });
  48. this.on('show', function() {
  49. if (!self._lastFile) return;
  50. self.setImage(self._lastFile);
  51. });
  52. this.on('detach', function() {
  53. self._lastFile = self.file;
  54. self.clearImage();
  55. });
  56. this.on('attach', function() {
  57. if (!self._lastFile) return;
  58. self.setImage(self._lastFile);
  59. });
  60. this.onScreenEvent('resize', function() {
  61. self._needsRatio = true;
  62. });
  63. // Get images to overlap properly. Maybe not worth it:
  64. // this.onScreenEvent('render', function() {
  65. // self.screen.program.flush();
  66. // if (!self._noImage) return;
  67. // function display(el, next) {
  68. // if (el.type === 'w3mimage' && el.file) {
  69. // el.setImage(el.file, next);
  70. // } else {
  71. // next();
  72. // }
  73. // }
  74. // function done(el) {
  75. // el.children.forEach(recurse);
  76. // }
  77. // function recurse(el) {
  78. // display(el, function() {
  79. // var pending = el.children.length;
  80. // el.children.forEach(function(el) {
  81. // display(el, function() {
  82. // if (!--pending) done(el);
  83. // });
  84. // });
  85. // });
  86. // }
  87. // recurse(self.screen);
  88. // });
  89. this.onScreenEvent('render', function() {
  90. self.screen.program.flush();
  91. if (!self._noImage) {
  92. self.setImage(self.file);
  93. }
  94. });
  95. if (this.options.file || this.options.img) {
  96. this.setImage(this.options.file || this.options.img);
  97. }
  98. }
  99. OverlayImage.prototype.__proto__ = Box.prototype;
  100. OverlayImage.prototype.type = 'overlayimage';
  101. OverlayImage.w3mdisplay = '/usr/lib/w3m/w3mimgdisplay';
  102. OverlayImage.prototype.spawn = function(file, args, opt, callback) {
  103. var spawn = require('child_process').spawn
  104. , ps;
  105. opt = opt || {};
  106. ps = spawn(file, args, opt);
  107. ps.on('error', function(err) {
  108. if (!callback) return;
  109. return callback(err);
  110. });
  111. ps.on('exit', function(code) {
  112. if (!callback) return;
  113. if (code !== 0) return callback(new Error('Exit Code: ' + code));
  114. return callback(null, code === 0);
  115. });
  116. return ps;
  117. };
  118. OverlayImage.prototype.setImage = function(img, callback) {
  119. var self = this;
  120. if (this._settingImage) {
  121. this._queue = this._queue || [];
  122. this._queue.push([img, callback]);
  123. return;
  124. }
  125. this._settingImage = true;
  126. var reset = function() {
  127. self._settingImage = false;
  128. self._queue = self._queue || [];
  129. var item = self._queue.shift();
  130. if (item) {
  131. self.setImage(item[0], item[1]);
  132. }
  133. };
  134. if (OverlayImage.hasW3MDisplay === false) {
  135. reset();
  136. if (!callback) return;
  137. return callback(new Error('W3M Image Display not available.'));
  138. }
  139. if (!img) {
  140. reset();
  141. if (!callback) return;
  142. return callback(new Error('No image.'));
  143. }
  144. this.file = img;
  145. return this.getPixelRatio(function(err, ratio) {
  146. if (err) {
  147. reset();
  148. if (!callback) return;
  149. return callback(err);
  150. }
  151. return self.renderImage(img, ratio, function(err, success) {
  152. if (err) {
  153. reset();
  154. if (!callback) return;
  155. return callback(err);
  156. }
  157. if (self.shrink || self.options.autofit) {
  158. delete self.shrink;
  159. delete self.options.shrink;
  160. self.options.autofit = true;
  161. return self.imageSize(function(err, size) {
  162. if (err) {
  163. reset();
  164. if (!callback) return;
  165. return callback(err);
  166. }
  167. if (self._lastSize
  168. && ratio.tw === self._lastSize.tw
  169. && ratio.th === self._lastSize.th
  170. && size.width === self._lastSize.width
  171. && size.height === self._lastSize.height
  172. && self.aleft === self._lastSize.aleft
  173. && self.atop === self._lastSize.atop) {
  174. reset();
  175. if (!callback) return;
  176. return callback(null, success);
  177. }
  178. self._lastSize = {
  179. tw: ratio.tw,
  180. th: ratio.th,
  181. width: size.width,
  182. height: size.height,
  183. aleft: self.aleft,
  184. atop: self.atop
  185. };
  186. self.position.width = size.width / ratio.tw | 0;
  187. self.position.height = size.height / ratio.th | 0;
  188. self._noImage = true;
  189. self.screen.render();
  190. self._noImage = false;
  191. reset();
  192. return self.renderImage(img, ratio, callback);
  193. });
  194. }
  195. reset();
  196. if (!callback) return;
  197. return callback(null, success);
  198. });
  199. });
  200. };
  201. OverlayImage.prototype.renderImage = function(img, ratio, callback) {
  202. var self = this;
  203. if (cp.execSync) {
  204. callback = callback || function(err, result) { return result; };
  205. try {
  206. return callback(null, this.renderImageSync(img, ratio));
  207. } catch (e) {
  208. return callback(e);
  209. }
  210. }
  211. if (OverlayImage.hasW3MDisplay === false) {
  212. if (!callback) return;
  213. return callback(new Error('W3M Image Display not available.'));
  214. }
  215. if (!ratio) {
  216. if (!callback) return;
  217. return callback(new Error('No ratio.'));
  218. }
  219. // clearImage unsets these:
  220. var _file = self.file;
  221. var _lastSize = self._lastSize;
  222. return self.clearImage(function(err) {
  223. if (err) return callback(err);
  224. self.file = _file;
  225. self._lastSize = _lastSize;
  226. var opt = {
  227. stdio: 'pipe',
  228. env: process.env,
  229. cwd: process.env.HOME
  230. };
  231. var ps = self.spawn(OverlayImage.w3mdisplay, [], opt, function(err, success) {
  232. if (!callback) return;
  233. return err
  234. ? callback(err)
  235. : callback(null, success);
  236. });
  237. var width = self.width * ratio.tw | 0
  238. , height = self.height * ratio.th | 0
  239. , aleft = self.aleft * ratio.tw | 0
  240. , atop = self.atop * ratio.th | 0;
  241. var input = '0;1;'
  242. + aleft + ';'
  243. + atop + ';'
  244. + width + ';'
  245. + height + ';;;;;'
  246. + img
  247. + '\n4;\n3;\n';
  248. self._props = {
  249. aleft: aleft,
  250. atop: atop,
  251. width: width,
  252. height: height
  253. };
  254. ps.stdin.write(input);
  255. ps.stdin.end();
  256. });
  257. };
  258. OverlayImage.prototype.clearImage = function(callback) {
  259. if (cp.execSync) {
  260. callback = callback || function(err, result) { return result; };
  261. try {
  262. return callback(null, this.clearImageSync());
  263. } catch (e) {
  264. return callback(e);
  265. }
  266. }
  267. if (OverlayImage.hasW3MDisplay === false) {
  268. if (!callback) return;
  269. return callback(new Error('W3M Image Display not available.'));
  270. }
  271. if (!this._props) {
  272. if (!callback) return;
  273. return callback(null);
  274. }
  275. var opt = {
  276. stdio: 'pipe',
  277. env: process.env,
  278. cwd: process.env.HOME
  279. };
  280. var ps = this.spawn(OverlayImage.w3mdisplay, [], opt, function(err, success) {
  281. if (!callback) return;
  282. return err
  283. ? callback(err)
  284. : callback(null, success);
  285. });
  286. var width = this._props.width + 2
  287. , height = this._props.height + 2
  288. , aleft = this._props.aleft
  289. , atop = this._props.atop;
  290. if (this._drag) {
  291. aleft -= 10;
  292. atop -= 10;
  293. width += 10;
  294. height += 10;
  295. }
  296. var input = '6;'
  297. + aleft + ';'
  298. + atop + ';'
  299. + width + ';'
  300. + height
  301. + '\n4;\n3;\n';
  302. delete this.file;
  303. delete this._props;
  304. delete this._lastSize;
  305. ps.stdin.write(input);
  306. ps.stdin.end();
  307. };
  308. OverlayImage.prototype.imageSize = function(callback) {
  309. var img = this.file;
  310. if (cp.execSync) {
  311. callback = callback || function(err, result) { return result; };
  312. try {
  313. return callback(null, this.imageSizeSync());
  314. } catch (e) {
  315. return callback(e);
  316. }
  317. }
  318. if (OverlayImage.hasW3MDisplay === false) {
  319. if (!callback) return;
  320. return callback(new Error('W3M Image Display not available.'));
  321. }
  322. if (!img) {
  323. if (!callback) return;
  324. return callback(new Error('No image.'));
  325. }
  326. var opt = {
  327. stdio: 'pipe',
  328. env: process.env,
  329. cwd: process.env.HOME
  330. };
  331. var ps = this.spawn(OverlayImage.w3mdisplay, [], opt);
  332. var buf = '';
  333. ps.stdout.setEncoding('utf8');
  334. ps.stdout.on('data', function(data) {
  335. buf += data;
  336. });
  337. ps.on('error', function(err) {
  338. if (!callback) return;
  339. return callback(err);
  340. });
  341. ps.on('exit', function() {
  342. if (!callback) return;
  343. var size = buf.trim().split(/\s+/);
  344. return callback(null, {
  345. raw: buf.trim(),
  346. width: +size[0],
  347. height: +size[1]
  348. });
  349. });
  350. var input = '5;' + img + '\n';
  351. ps.stdin.write(input);
  352. ps.stdin.end();
  353. };
  354. OverlayImage.prototype.termSize = function(callback) {
  355. var self = this;
  356. if (cp.execSync) {
  357. callback = callback || function(err, result) { return result; };
  358. try {
  359. return callback(null, this.termSizeSync());
  360. } catch (e) {
  361. return callback(e);
  362. }
  363. }
  364. if (OverlayImage.hasW3MDisplay === false) {
  365. if (!callback) return;
  366. return callback(new Error('W3M Image Display not available.'));
  367. }
  368. var opt = {
  369. stdio: 'pipe',
  370. env: process.env,
  371. cwd: process.env.HOME
  372. };
  373. var ps = this.spawn(OverlayImage.w3mdisplay, ['-test'], opt);
  374. var buf = '';
  375. ps.stdout.setEncoding('utf8');
  376. ps.stdout.on('data', function(data) {
  377. buf += data;
  378. });
  379. ps.on('error', function(err) {
  380. if (!callback) return;
  381. return callback(err);
  382. });
  383. ps.on('exit', function() {
  384. if (!callback) return;
  385. if (!buf.trim()) {
  386. // Bug: w3mimgdisplay will sometimes
  387. // output nothing. Try again:
  388. return self.termSize(callback);
  389. }
  390. var size = buf.trim().split(/\s+/);
  391. return callback(null, {
  392. raw: buf.trim(),
  393. width: +size[0],
  394. height: +size[1]
  395. });
  396. });
  397. ps.stdin.end();
  398. };
  399. OverlayImage.prototype.getPixelRatio = function(callback) {
  400. var self = this;
  401. if (cp.execSync) {
  402. callback = callback || function(err, result) { return result; };
  403. try {
  404. return callback(null, this.getPixelRatioSync());
  405. } catch (e) {
  406. return callback(e);
  407. }
  408. }
  409. // XXX We could cache this, but sometimes it's better
  410. // to recalculate to be pixel perfect.
  411. if (this._ratio && !this._needsRatio) {
  412. return callback(null, this._ratio);
  413. }
  414. return this.termSize(function(err, dimensions) {
  415. if (err) return callback(err);
  416. self._ratio = {
  417. tw: dimensions.width / self.screen.width,
  418. th: dimensions.height / self.screen.height
  419. };
  420. self._needsRatio = false;
  421. return callback(null, self._ratio);
  422. });
  423. };
  424. OverlayImage.prototype.renderImageSync = function(img, ratio) {
  425. if (OverlayImage.hasW3MDisplay === false) {
  426. throw new Error('W3M Image Display not available.');
  427. }
  428. if (!ratio) {
  429. throw new Error('No ratio.');
  430. }
  431. // clearImage unsets these:
  432. var _file = this.file;
  433. var _lastSize = this._lastSize;
  434. this.clearImageSync();
  435. this.file = _file;
  436. this._lastSize = _lastSize;
  437. var width = this.width * ratio.tw | 0
  438. , height = this.height * ratio.th | 0
  439. , aleft = this.aleft * ratio.tw | 0
  440. , atop = this.atop * ratio.th | 0;
  441. var input = '0;1;'
  442. + aleft + ';'
  443. + atop + ';'
  444. + width + ';'
  445. + height + ';;;;;'
  446. + img
  447. + '\n4;\n3;\n';
  448. this._props = {
  449. aleft: aleft,
  450. atop: atop,
  451. width: width,
  452. height: height
  453. };
  454. try {
  455. cp.execFileSync(OverlayImage.w3mdisplay, [], {
  456. env: process.env,
  457. encoding: 'utf8',
  458. input: input,
  459. timeout: 1000
  460. });
  461. } catch (e) {
  462. ;
  463. }
  464. return true;
  465. };
  466. OverlayImage.prototype.clearImageSync = function() {
  467. if (OverlayImage.hasW3MDisplay === false) {
  468. throw new Error('W3M Image Display not available.');
  469. }
  470. if (!this._props) {
  471. return false;
  472. }
  473. var width = this._props.width + 2
  474. , height = this._props.height + 2
  475. , aleft = this._props.aleft
  476. , atop = this._props.atop;
  477. if (this._drag) {
  478. aleft -= 10;
  479. atop -= 10;
  480. width += 10;
  481. height += 10;
  482. }
  483. var input = '6;'
  484. + aleft + ';'
  485. + atop + ';'
  486. + width + ';'
  487. + height
  488. + '\n4;\n3;\n';
  489. delete this.file;
  490. delete this._props;
  491. delete this._lastSize;
  492. try {
  493. cp.execFileSync(OverlayImage.w3mdisplay, [], {
  494. env: process.env,
  495. encoding: 'utf8',
  496. input: input,
  497. timeout: 1000
  498. });
  499. } catch (e) {
  500. ;
  501. }
  502. return true;
  503. };
  504. OverlayImage.prototype.imageSizeSync = function() {
  505. var img = this.file;
  506. if (OverlayImage.hasW3MDisplay === false) {
  507. throw new Error('W3M Image Display not available.');
  508. }
  509. if (!img) {
  510. throw new Error('No image.');
  511. }
  512. var buf = '';
  513. var input = '5;' + img + '\n';
  514. try {
  515. buf = cp.execFileSync(OverlayImage.w3mdisplay, [], {
  516. env: process.env,
  517. encoding: 'utf8',
  518. input: input,
  519. timeout: 1000
  520. });
  521. } catch (e) {
  522. ;
  523. }
  524. var size = buf.trim().split(/\s+/);
  525. return {
  526. raw: buf.trim(),
  527. width: +size[0],
  528. height: +size[1]
  529. };
  530. };
  531. OverlayImage.prototype.termSizeSync = function(_, recurse) {
  532. if (OverlayImage.hasW3MDisplay === false) {
  533. throw new Error('W3M Image Display not available.');
  534. }
  535. var buf = '';
  536. try {
  537. buf = cp.execFileSync(OverlayImage.w3mdisplay, ['-test'], {
  538. env: process.env,
  539. encoding: 'utf8',
  540. timeout: 1000
  541. });
  542. } catch (e) {
  543. ;
  544. }
  545. if (!buf.trim()) {
  546. // Bug: w3mimgdisplay will sometimes
  547. // output nothing. Try again:
  548. recurse = recurse || 0;
  549. if (++recurse === 5) {
  550. throw new Error('Term size not determined.');
  551. }
  552. return this.termSizeSync(_, recurse);
  553. }
  554. var size = buf.trim().split(/\s+/);
  555. return {
  556. raw: buf.trim(),
  557. width: +size[0],
  558. height: +size[1]
  559. };
  560. };
  561. OverlayImage.prototype.getPixelRatioSync = function() {
  562. // XXX We could cache this, but sometimes it's better
  563. // to recalculate to be pixel perfect.
  564. if (this._ratio && !this._needsRatio) {
  565. return this._ratio;
  566. }
  567. this._needsRatio = false;
  568. var dimensions = this.termSizeSync();
  569. this._ratio = {
  570. tw: dimensions.width / this.screen.width,
  571. th: dimensions.height / this.screen.height
  572. };
  573. return this._ratio;
  574. };
  575. OverlayImage.prototype.displayImage = function(callback) {
  576. return this.screen.displayImage(this.file, callback);
  577. };
  578. /**
  579. * Expose
  580. */
  581. module.exports = OverlayImage;