diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..5d72de40 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,64 @@ +# Piskel CLI + +Wraps the Piskel pixel editing application to enable similar export options via the command line. + +## Installation + +Option 1: Globally install Piskel +``` +npm install -g https://github.com/piskelapp/piskel/tarball/master +``` + +Option 2: Clone and install Piskel normally and then run npm link inside the installation root + +## Usage + +**Export provided .piskel file as a png sprite sheet using app defaults** +``` +piskel-cli snow-monster.piskel +``` + +**Export scaled sprite sheet** +``` +piskel-cli snow-monster.piskel --scale 5 +``` + +**Export scaled to specific (single frame) width value** +``` +piskel-cli snow-monster.piskel --scaledWidth 435 +``` + +**Export scaled to specific (single frame) height value** +``` +piskel-cli snow-monster.piskel --scaledHeight 435 +``` + +**Export sprite sheet as a single column** +``` +piskel-cli snow-monster.piskel --columns 1 +``` + +**Export sprite sheet as a single row** +``` +piskel-cli snow-monster.piskel --rows 1 +``` + +**Export a single frame (0 is first frame)** +``` +piskel-cli snow-monster.piskel --frame 3 +``` + +**Export a second file containing the data-uri for the exported png** +``` +piskel-cli snow-monster.piskel --dataUri +``` + +**Export cropped** +``` +piskel-cli snow-monster.piskel --crop +``` + +**Custom output path and/or filename** +``` +piskel-cli snow-monster.piskel --dest ./output-folder/snah-monstah.png +``` \ No newline at end of file diff --git a/cli/export-png.js b/cli/export-png.js new file mode 100644 index 00000000..9ea50acc --- /dev/null +++ b/cli/export-png.js @@ -0,0 +1,130 @@ +const fs = require('fs'); + +function onPageEvaluate(window, options, piskel) { + console.log("\nPiskel name: " + piskel.descriptor.name); + + // Setup piskelController + var piskelController = new pskl.controller.piskel.PiskelController(piskel); + + pskl.app.piskelController = piskelController; + + piskelController.init(); + + // Apply crop if enabled + if (options.crop) { + // Mock selection manager to avoid errors during crop + pskl.app.selectionManager = {}; + + // Setup crop tool + var crop = new pskl.tools.transform.Crop(); + + // Perform crop + crop.applyTransformation(); + + // Get cropped piskel + piskel = piskelController.getPiskel(); + } + + // Mock exportController to provide zoom value based on cli args + // and to avoid errors and/or unnecessary bootstrapping + var exportController = { + getExportZoom: function () { + var zoom = options.zoom; + + if (options.scaledWidth) { + zoom = options.scaledWidth / piskel.getWidth(); + } else if (options.scaledHeight) { + zoom = options.scaledHeight / piskel.getHeight(); + } + + return zoom; + } + }; + + // Setup pngExportController + var pngExportController = new pskl.controller.settings.exportimage.PngExportController(piskelController, exportController); + + // Mock getColumns and getRows to use values from cli arguments + pngExportController.getColumns_ = function () { + if (options.columns) return options.columns; + + if (options.rows) { + return Math.ceil(piskelController.getFrameCount() / pngExportController.getRows_()); + } else { + return pngExportController.getBestFit_(); + } + }; + + pngExportController.getRows_ = function () { + if (options.columns && !options.rows) { + return Math.ceil(piskelController.getFrameCount() / pngExportController.getColumns_()); + } + + return options.rows; + }; + + // Render to output canvas + var canvas; + + if (options.frame > -1) { + // Render a single frame + canvas = piskelController.renderFrameAt(options.frame, true); + + var zoom = exportController.getExportZoom(); + + if (zoom != 1) { + // Scale rendered frame + canvas = pskl.utils.ImageResizer.resize(canvas, canvas.width * zoom, canvas.height * zoom, false); + } + } else { + // Render the sprite sheet + canvas = pngExportController.createPngSpritesheet_(); + } + + // Add output canvas to DOM + window.document.body.appendChild(canvas); + + // Prepare return data + const returnData = { + width: canvas.width, + height: canvas.height + }; + + // Wait a tick for things to wrap up + setTimeout(function () { + // Exit and pass data to parent process + window.callPhantom(returnData); + }, 0); +} + +function onPageExit(page, options, data) { + // Set clip for output image + if (data.width && data.height) { + page.clipRect = { top: 0, left: 0, width: data.width, height: data.height }; + } + + console.log("\n" + 'Generated file(s):'); + + const dest = options.dest + '.png'; + + // Render page to the output image + page.render(dest); + + console.log(" " + dest); + + if (options.dataUri) { + const dataUriPath = options.dest + '.datauri'; + + const dataUri = `data:image/png;base64,${page.renderBase64('PNG')}`; + + // Write data-uri to file + fs.write(dataUriPath, dataUri, 'w'); + + console.log(" " + dataUriPath); + } +} + +module.exports = { + onPageEvaluate: onPageEvaluate, + onPageExit: onPageExit +}; \ No newline at end of file diff --git a/cli/index.js b/cli/index.js new file mode 100644 index 00000000..916a3285 --- /dev/null +++ b/cli/index.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const minimist = require('minimist'); +const childProcess = require('child_process'); +const phantomjs = require('phantomjs'); +const binPath = phantomjs.path; + +// Parse command args +let args = minimist(process.argv.slice(2), { + default: { + crop: false, + dataUri: false, + debug: false, + scale: 1 + }, +}); + +if (args.debug) console.log(args); + +// Ensure a path for the src file was passed +if (!args._ || (args._ && !args._.length)) { + console.error('Path to a .piskel file is required'); + + return; +} + +const src = args._[0]; + +// Ensure the src file exists +if (!fs.existsSync(src)) { + console.error('No such file: ' + src); + + return; +} + +// Read src piskel file +const piskelFile = fs.readFileSync(src, 'utf-8'); + +const dest = args.dest || path.basename(src, '.piskel'); + +console.log('Piskel CLI is exporting...'); + +// Get path to Piskel's app js bundle +let piskelAppJsDir = path.resolve(__dirname +'/../dest/prod/js/'); +let minJsFiles = fs.readdirSync(piskelAppJsDir).filter(filename => filename.indexOf('min') > -1); +let piskelAppJsFileName = minJsFiles[0]; +let piskelAppJsPath = (piskelAppJsFileName) ? path.join(piskelAppJsDir, piskelAppJsFileName) : ''; + +if (!fs.existsSync(piskelAppJsPath)) { + console.error(`Piskel's application JS file not found in: ${piskelAppJsDir}. Run prod build and try again.`); + + return; +} + +// Prepare args to pass to phantom script +const options = { + dest: dest, + zoom: args.scale, + crop: !!args.crop, + rows: args.rows, + columns: args.columns, + frame: args.frame, + dataUri: !!args.dataUri, + debug: args.debug, + piskelAppJsPath: piskelAppJsPath, + scaledWidth: args.scaledWidth, + scaledHeight: args.scaledHeight +}; + +const childArgs = [ + path.join(__dirname, 'piskel-export.js'), + piskelFile, + JSON.stringify(options) +]; + +if (args.debug) { + childArgs.unshift( + '--remote-debugger-port=9035', + '--remote-debugger-autorun=yes' + ); +} + +// Run phantom script +childProcess.execFile(binPath, childArgs, function (err, stdout, stderr) { + // Print any output the from child process + if (err) console.log(err); + if (stderr) console.log(stderr); + if (stdout) console.log(stdout); + + console.log('Export complete'); +}); \ No newline at end of file diff --git a/cli/piskel-export.js b/cli/piskel-export.js new file mode 100644 index 00000000..b22c8523 --- /dev/null +++ b/cli/piskel-export.js @@ -0,0 +1,45 @@ +// PhantomJS system +const system = require('system'); + +// Exporter +const exporter = require('./export-png'); + +// Get passed args +const args = system.args; + +// Parse input piskel file and options +const piskelFile = JSON.parse(args[1]); +const options = JSON.parse(args[2]); + +// Create page w/ canvas +const page = require('webpage').create(); + +page.content = ''; + +// Inject Piskel JS +page.injectJs(options.piskelAppJsPath); + +// Listen for page console logs +page.onConsoleMessage = function (msg) { + console.log(msg); +}; + +// Run page logic +page.evaluate(function (piskelFile, options, onPageEvaluate) { + // Zero out default body margin + document.body.style.margin = 0; + + // Deserialize piskel file and run exporter's page evaluate task + pskl.utils.serialization.Deserializer.deserialize(piskelFile, function (piskel) { + onPageEvaluate(window, options, piskel); + }); +}, piskelFile, options, exporter.onPageEvaluate); + +// Wait for page to trigger exit +page.onCallback = function (data) { + // Run exporter page exit task + exporter.onPageExit(page, options, data); + + // Exit + phantom.exit(0); +}; diff --git a/package.json b/package.json index 8405118e..b9eed24c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "misc/scripts/piskel-root" ], "bin": { - "piskel-root": "./misc/scripts/piskel-root" + "piskel-root": "./misc/scripts/piskel-root", + "piskel-cli": "./cli/index.js" }, "main": "./dest/prod/index.html", "scripts": { @@ -63,5 +64,8 @@ "toolbar": false, "width": 1000, "height": 700 + }, + "dependencies": { + "minimist": "^1.2.0" } }