coa.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /* jshint quotmark: false */
  2. 'use strict';
  3. require('colors');
  4. var FS = require('fs'),
  5. PATH = require('path'),
  6. SVGO = require('../svgo.js'),
  7. YAML = require('js-yaml'),
  8. PKG = require('../../package.json'),
  9. mkdirp = require('mkdirp'),
  10. encodeSVGDatauri = require('./tools.js').encodeSVGDatauri,
  11. decodeSVGDatauri = require('./tools.js').decodeSVGDatauri,
  12. regSVGFile = /\.svg$/;
  13. /**
  14. * Command-Option-Argument.
  15. *
  16. * @see https://github.com/veged/coa
  17. */
  18. module.exports = require('coa').Cmd()
  19. .helpful()
  20. .name(PKG.name)
  21. .title(PKG.description)
  22. .opt()
  23. .name('version').title('Version')
  24. .short('v').long('version')
  25. .only()
  26. .flag()
  27. .act(function() {
  28. // output the version to stdout instead of stderr if returned
  29. process.stdout.write(PKG.version + '\n');
  30. // coa will run `.toString` on the returned value and send it to stderr
  31. return '';
  32. })
  33. .end()
  34. .opt()
  35. .name('input').title('Input file, "-" for STDIN')
  36. .short('i').long('input')
  37. .val(function(val) {
  38. return val || this.reject("Option '--input' must have a value.");
  39. })
  40. .end()
  41. .opt()
  42. .name('string').title('Input SVG data string')
  43. .short('s').long('string')
  44. .end()
  45. .opt()
  46. .name('folder').title('Input folder, optimize and rewrite all *.svg files')
  47. .short('f').long('folder')
  48. .val(function(val) {
  49. return val || this.reject("Option '--folder' must have a value.");
  50. })
  51. .end()
  52. .opt()
  53. .name('output').title('Output file or folder (by default the same as the input), "-" for STDOUT')
  54. .short('o').long('output')
  55. .val(function(val) {
  56. return val || this.reject("Option '--output' must have a value.");
  57. })
  58. .end()
  59. .opt()
  60. .name('precision').title('Set number of digits in the fractional part, overrides plugins params')
  61. .short('p').long('precision')
  62. .val(function(val) {
  63. return !isNaN(val) ? val : this.reject("Option '--precision' must be an integer number");
  64. })
  65. .end()
  66. .opt()
  67. .name('config').title('Config file or JSON string to extend or replace default')
  68. .long('config')
  69. .val(function(val) {
  70. return val || this.reject("Option '--config' must have a value.");
  71. })
  72. .end()
  73. .opt()
  74. .name('disable').title('Disable plugin by name')
  75. .long('disable')
  76. .arr()
  77. .val(function(val) {
  78. return val || this.reject("Option '--disable' must have a value.");
  79. })
  80. .end()
  81. .opt()
  82. .name('enable').title('Enable plugin by name')
  83. .long('enable')
  84. .arr()
  85. .val(function(val) {
  86. return val || this.reject("Option '--enable' must have a value.");
  87. })
  88. .end()
  89. .opt()
  90. .name('datauri').title('Output as Data URI string (base64, URI encoded or unencoded)')
  91. .long('datauri')
  92. .val(function(val) {
  93. return val || this.reject("Option '--datauri' must have one of the following values: 'base64', 'enc' or 'unenc'");
  94. })
  95. .end()
  96. .opt()
  97. .name('multipass').title('Enable multipass')
  98. .long('multipass')
  99. .flag()
  100. .end()
  101. .opt()
  102. .name('pretty').title('Make SVG pretty printed')
  103. .long('pretty')
  104. .flag()
  105. .end()
  106. .opt()
  107. .name('indent').title('Indent number when pretty printing SVGs')
  108. .long('indent')
  109. .val(function(val) {
  110. return !isNaN(val) ? val : this.reject("Option '--indent' must be an integer number");
  111. })
  112. .end()
  113. .opt()
  114. .name('quiet').title('Only output error messages, not regular status messages')
  115. .short('q').long('quiet')
  116. .flag()
  117. .end()
  118. .opt()
  119. .name('show-plugins').title('Show available plugins and exit')
  120. .long('show-plugins')
  121. .flag()
  122. .end()
  123. .arg()
  124. .name('input').title('Alias to --input')
  125. .end()
  126. .arg()
  127. .name('output').title('Alias to --output')
  128. .end()
  129. .act(function(opts, args) {
  130. var input = args && args.input ? args.input : opts.input,
  131. output = args && args.output ? args.output : opts.output,
  132. config = {};
  133. // --show-plugins
  134. if (opts['show-plugins']) {
  135. showAvailablePlugins();
  136. process.exit(0);
  137. }
  138. // w/o anything
  139. if (
  140. (!input || input === '-') &&
  141. !opts.string &&
  142. !opts.stdin &&
  143. !opts.folder &&
  144. process.stdin.isTTY
  145. ) return this.usage();
  146. // --config
  147. if (opts.config) {
  148. // string
  149. if (opts.config.charAt(0) === '{') {
  150. try {
  151. config = JSON.parse(opts.config);
  152. } catch (e) {
  153. console.error("Error: Couldn't parse config JSON.");
  154. console.error(String(e));
  155. return;
  156. }
  157. // external file
  158. } else {
  159. var configPath = PATH.resolve(opts.config);
  160. try {
  161. // require() adds some weird output on YML files
  162. config = JSON.parse(FS.readFileSync(configPath, 'utf8'));
  163. } catch (err) {
  164. if (err.code === 'ENOENT') {
  165. console.error('Error: couldn\'t find config file \'' + opts.config + '\'.');
  166. return;
  167. } else if (err.code === 'EISDIR') {
  168. console.error('Error: directory \'' + opts.config + '\' is not a config file.');
  169. return;
  170. }
  171. config = YAML.safeLoad(FS.readFileSync(configPath, 'utf8'));
  172. if (!config || Array.isArray(config)) {
  173. console.error('Error: invalid config file \'' + opts.config + '\'.');
  174. return;
  175. }
  176. }
  177. }
  178. }
  179. // --quiet
  180. if (opts.quiet) {
  181. config.quiet = opts.quiet;
  182. }
  183. // --precision
  184. if (opts.precision) {
  185. config.floatPrecision = Math.max(0, parseInt(opts.precision));
  186. }
  187. // --disable
  188. if (opts.disable) {
  189. changePluginsState(opts.disable, false, config);
  190. }
  191. // --enable
  192. if (opts.enable) {
  193. changePluginsState(opts.enable, true, config);
  194. }
  195. // --multipass
  196. if (opts.multipass) {
  197. config.multipass = true;
  198. }
  199. // --pretty
  200. if (opts.pretty) {
  201. config.js2svg = config.js2svg || {};
  202. config.js2svg.pretty = true;
  203. if (opts.indent) {
  204. config.js2svg.indent = parseInt(opts.indent, 10);
  205. }
  206. }
  207. // --output
  208. if (opts.output) {
  209. config.output = opts.output;
  210. }
  211. // --folder
  212. if (opts.folder) {
  213. optimizeFolder(opts.folder, config, output);
  214. return;
  215. }
  216. // --input
  217. if (input) {
  218. // STDIN
  219. if (input === '-') {
  220. var data = '';
  221. process.stdin.pause();
  222. process.stdin
  223. .on('data', function(chunk) {
  224. data += chunk;
  225. })
  226. .once('end', function() {
  227. optimizeFromString(data, config, opts.datauri, input, output);
  228. })
  229. .resume();
  230. // file
  231. } else {
  232. FS.readFile(input, 'utf8', function(err, data) {
  233. if (err) {
  234. if (err.code === 'EISDIR')
  235. optimizeFolder(input, config, output);
  236. else if (err.code === 'ENOENT')
  237. console.error('Error: no such file or directory \'' + input + '\'.');
  238. else
  239. console.error(err);
  240. return;
  241. }
  242. optimizeFromString(data, config, opts.datauri, input, output);
  243. });
  244. }
  245. // --string
  246. } else if (opts.string) {
  247. opts.string = decodeSVGDatauri(opts.string);
  248. optimizeFromString(opts.string, config, opts.datauri, input, output);
  249. }
  250. });
  251. function optimizeFromString(svgstr, config, datauri, input, output) {
  252. var startTime = Date.now(config),
  253. time,
  254. inBytes = Buffer.byteLength(svgstr, 'utf8'),
  255. outBytes,
  256. svgo = new SVGO(config);
  257. svgo.optimize(svgstr, function(result) {
  258. if (result.error) {
  259. console.error(result.error);
  260. return;
  261. }
  262. if (datauri) {
  263. result.data = encodeSVGDatauri(result.data, datauri);
  264. }
  265. outBytes = Buffer.byteLength(result.data, 'utf8');
  266. time = Date.now() - startTime;
  267. // stdout
  268. if (output === '-' || (!input || input === '-') && !output) {
  269. process.stdout.write(result.data + '\n');
  270. // file
  271. } else {
  272. // overwrite input file if there is no output
  273. if (!output && input) {
  274. output = input;
  275. }
  276. if (!config.quiet) {
  277. console.log('\r');
  278. }
  279. saveFileAndPrintInfo(config, result.data, output, inBytes, outBytes, time);
  280. }
  281. });
  282. }
  283. function saveFileAndPrintInfo(config, data, path, inBytes, outBytes, time) {
  284. FS.writeFile(path, data, 'utf8', function() {
  285. if (config.quiet) {
  286. return;
  287. }
  288. // print time info
  289. printTimeInfo(time);
  290. // print optimization profit info
  291. printProfitInfo(inBytes, outBytes);
  292. });
  293. }
  294. function printTimeInfo(time) {
  295. console.log('Done in ' + time + ' ms!');
  296. }
  297. function printProfitInfo(inBytes, outBytes) {
  298. var profitPercents = 100 - outBytes * 100 / inBytes;
  299. console.log(
  300. (Math.round((inBytes / 1024) * 1000) / 1000) + ' KiB' +
  301. (profitPercents < 0 ? ' + ' : ' - ') +
  302. String(Math.abs((Math.round(profitPercents * 10) / 10)) + '%').green + ' = ' +
  303. (Math.round((outBytes / 1024) * 1000) / 1000) + ' KiB\n'
  304. );
  305. }
  306. /**
  307. * Change plugins state by names array.
  308. *
  309. * @param {Array} names plugins names
  310. * @param {Boolean} state active state
  311. * @param {Object} config original config
  312. * @return {Object} changed config
  313. */
  314. function changePluginsState(names, state, config) {
  315. // extend config
  316. if (config.plugins) {
  317. names.forEach(function(name) {
  318. var matched,
  319. key;
  320. config.plugins.forEach(function(plugin) {
  321. // get plugin name
  322. if (typeof plugin === 'object') {
  323. key = Object.keys(plugin)[0];
  324. } else {
  325. key = plugin;
  326. }
  327. // if there is such a plugin name
  328. if (key === name) {
  329. // don't replace plugin's params with true
  330. if (typeof plugin[key] !== 'object' || !state) {
  331. plugin[key] = state;
  332. }
  333. // mark it as matched
  334. matched = true;
  335. }
  336. });
  337. // if not matched and current config is not full
  338. if (!matched && !config.full) {
  339. var obj = {};
  340. obj[name] = state;
  341. // push new plugin Object
  342. config.plugins.push(obj);
  343. matched = true;
  344. }
  345. });
  346. // just push
  347. } else {
  348. config.plugins = [];
  349. names.forEach(function(name) {
  350. var obj = {};
  351. obj[name] = state;
  352. config.plugins.push(obj);
  353. });
  354. }
  355. return config;
  356. }
  357. function optimizeFolder(dir, config, output) {
  358. var svgo = new SVGO(config);
  359. if (!config.quiet) {
  360. console.log('Processing directory \'' + dir + '\':\n');
  361. }
  362. // absoluted folder path
  363. var path = PATH.resolve(dir);
  364. // list folder content
  365. FS.readdir(path, function(err, files) {
  366. if (err) {
  367. console.error(err);
  368. return;
  369. }
  370. if (!files.length) {
  371. console.log('Directory \'' + dir + '\' is empty.');
  372. return;
  373. }
  374. var i = 0,
  375. found = false;
  376. function optimizeFile(file) {
  377. // absoluted file path
  378. var filepath = PATH.resolve(path, file);
  379. var outfilepath = output ? PATH.resolve(output, file) : filepath;
  380. // check if file name matches *.svg
  381. if (regSVGFile.test(filepath)) {
  382. found = true;
  383. FS.readFile(filepath, 'utf8', function(err, data) {
  384. if (err) {
  385. console.error(err);
  386. return;
  387. }
  388. var startTime = Date.now(),
  389. time,
  390. inBytes = Buffer.byteLength(data, 'utf8'),
  391. outBytes;
  392. svgo.optimize(data, function(result) {
  393. if (result.error) {
  394. console.error(result.error);
  395. return;
  396. }
  397. outBytes = Buffer.byteLength(result.data, 'utf8');
  398. time = Date.now() - startTime;
  399. writeOutput();
  400. function writeOutput() {
  401. FS.writeFile(outfilepath, result.data, 'utf8', report);
  402. }
  403. function report(err) {
  404. if (err) {
  405. if (err.code === 'ENOENT') {
  406. mkdirp(output, writeOutput);
  407. return;
  408. } else if (err.code === 'ENOTDIR') {
  409. console.error('Error: output \'' + output + '\' is not a directory.');
  410. return;
  411. }
  412. console.error(err);
  413. return;
  414. }
  415. if (!config.quiet) {
  416. console.log(file + ':');
  417. // print time info
  418. printTimeInfo(time);
  419. // print optimization profit info
  420. printProfitInfo(inBytes, outBytes);
  421. }
  422. //move on to the next file
  423. if (++i < files.length) {
  424. optimizeFile(files[i]);
  425. }
  426. }
  427. });
  428. });
  429. }
  430. //move on to the next file
  431. else if (++i < files.length) {
  432. optimizeFile(files[i]);
  433. } else if (!found) {
  434. console.log('No SVG files have been found.');
  435. }
  436. }
  437. optimizeFile(files[i]);
  438. });
  439. }
  440. var showAvailablePlugins = function () {
  441. var svgo = new SVGO(),
  442. // Flatten an array of plugins grouped per type and sort alphabetically
  443. list = Array.prototype.concat.apply([], svgo.config.plugins).sort(function(a, b) {
  444. return a.name > b.name ? 1 : -1;
  445. });
  446. console.log('Currently available plugins:');
  447. list.forEach(function (plugin) {
  448. console.log(' [ ' + plugin.name.green + ' ] ' + plugin.description);
  449. });
  450. //console.log(JSON.stringify(svgo, null, 4));
  451. };