From 4c41ccd7efc147d4ed7e902a79b928c01bb5009b Mon Sep 17 00:00:00 2001 From: "Nils P. Ellingsen" Date: Tue, 3 Dec 2013 21:22:46 +0100 Subject: [PATCH 1/2] Remove pngcrush -recuce option, keeps greyscale images from loosing alpha channel --- src/builder.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builder.coffee b/src/builder.coffee index c4ddaa5..5875ab6 100644 --- a/src/builder.coffee +++ b/src/builder.coffee @@ -31,7 +31,7 @@ class SpriteSheetBuilder crushed = "#{ image }.crushed" console.log "\n pngcrushing, this may take a few moments...\n" movecmd = if process.platform != "win32" then "mv" else "move" - exec "pngcrush -reduce #{ image } #{ crushed } && #{ movecmd } #{ crushed } #{ image }", ( error, stdout, stderr ) => + exec "pngcrush #{ image } #{ crushed } && #{ movecmd } #{ crushed } #{ image }", ( error, stdout, stderr ) => callback() else callback() From b000fa4ad615daaaf9e54d5bd032d7b599f9e504 Mon Sep 17 00:00:00 2001 From: "Nils P. Ellingsen" Date: Tue, 3 Dec 2013 21:23:33 +0100 Subject: [PATCH 2/2] =?UTF-8?q?[noop]=C2=A0generated=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/builder.js | 282 +++++++++++++++++++++++++++++++++++++++++++++ lib/imagemagick.js | 76 ++++++++++++ lib/layout.js | 168 +++++++++++++++++++++++++++ lib/style.js | 73 ++++++++++++ 4 files changed, 599 insertions(+) create mode 100644 lib/builder.js create mode 100644 lib/imagemagick.js create mode 100644 lib/layout.js create mode 100644 lib/style.js diff --git a/lib/builder.js b/lib/builder.js new file mode 100644 index 0000000..de2c63e --- /dev/null +++ b/lib/builder.js @@ -0,0 +1,282 @@ +var ImageMagick, Layout, SpriteSheetBuilder, SpriteSheetConfiguration, Style, async, ensureDirectory, exec, fs, path, qfs, separator, _, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +fs = require('fs'); + +path = require('path'); + +qfs = require('q-fs'); + +exec = require('child_process').exec; + +async = require('async'); + +_ = require("underscore"); + +ImageMagick = require('./imagemagick'); + +Layout = require('./layout'); + +Style = require('./style'); + +separator = path.sep || "/"; + +ensureDirectory = function(directory) { + return function(callback) { + return qfs.isDirectory(directory).then(function(isDir) { + if (isDir) { + return callback(); + } else { + return qfs.makeTree(directory).then(callback); + } + }); + }; +}; + +SpriteSheetBuilder = (function() { + SpriteSheetBuilder.supportsPngcrush = function(callback) { + var _this = this; + return exec("which pngcrush", function(error, stdout, stderr) { + return callback(stdout && !error && !stderr); + }); + }; + + SpriteSheetBuilder.pngcrush = function(image, callback) { + return SpriteSheetBuilder.supportsPngcrush(function(supported) { + var crushed, movecmd, + _this = this; + if (supported) { + crushed = "" + image + ".crushed"; + console.log("\n pngcrushing, this may take a few moments...\n"); + movecmd = process.platform !== "win32" ? "mv" : "move"; + return exec("pngcrush " + image + " " + crushed + " && " + movecmd + " " + crushed + " " + image, function(error, stdout, stderr) { + return callback(); + }); + } else { + return callback(); + } + }); + }; + + SpriteSheetBuilder.fromGruntTask = function(options) { + var builder, config, key, outputConfigurations; + builder = new SpriteSheetBuilder(options); + outputConfigurations = options.output; + delete options.output; + if (outputConfigurations && Object.keys(outputConfigurations).length > 0) { + for (key in outputConfigurations) { + config = outputConfigurations[key]; + builder.addConfiguration(key, config); + } + } + return builder; + }; + + function SpriteSheetBuilder(options) { + this.options = options; + this.writeStyleSheet = __bind(this.writeStyleSheet, this); + this.buildConfig = __bind(this.buildConfig, this); + this.files = options.images; + this.outputConfigurations = {}; + this.outputDirectory = path.normalize(options.outputDirectory); + if (options.outputCss) { + this.outputStyleFilePath = [this.outputDirectory, options.outputCss].join(separator); + this.outputStyleDirectoryPath = path.dirname(this.outputStyleFilePath); + } + } + + SpriteSheetBuilder.prototype.addConfiguration = function(name, options) { + var baseConfig, config, ssc; + config = _.extend(this.options, options, { + name: name, + outputStyleFilePath: this.outputStyleFilePath, + outputStyleDirectoryPath: this.outputStyleDirectoryPath + }); + ssc = new SpriteSheetConfiguration(options.images || this.files, config); + this.outputConfigurations[name] = ssc; + if (!baseConfig || config.pixelRatio > baseConfig.pixelRatio) { + baseConfig = config; + } + return ssc; + }; + + SpriteSheetBuilder.prototype.build = function(done) { + var baseConfig, config, key, + _this = this; + if (!this.outputStyleFilePath) { + throw "no output style file specified"; + } + if (Object.keys(this.outputConfigurations).length === 0) { + this.addConfiguration("default", { + pixelRatio: 1 + }); + } + this.configs = []; + baseConfig = null; + for (key in this.outputConfigurations) { + config = this.outputConfigurations[key]; + if (!baseConfig || config.pixelRatio > baseConfig.pixelRatio) { + baseConfig = config; + } + this.configs.push(config); + } + SpriteSheetConfiguration.baseConfiguration = baseConfig; + return async.series([ + function(callback) { + return async.forEachSeries(_this.configs, _this.buildConfig, callback); + }, ensureDirectory(this.outputStyleDirectoryPath), this.writeStyleSheet + ], done); + }; + + SpriteSheetBuilder.prototype.buildConfig = function(config, callback) { + return config.build(callback); + }; + + SpriteSheetBuilder.prototype.writeStyleSheet = function(callback) { + var css, + _this = this; + css = this.configs.map(function(config) { + return config.css; + }); + return fs.writeFile(this.outputStyleFilePath, css.join("\n\n"), function(err) { + if (err) { + throw err; + } else { + console.log("CSS file written to", _this.outputStyleFilePath, "\n"); + return callback(); + } + }); + }; + + return SpriteSheetBuilder; + +})(); + +SpriteSheetConfiguration = (function() { + function SpriteSheetConfiguration(files, options) { + this.createSprite = __bind(this.createSprite, this); + this.generateCSS = __bind(this.generateCSS, this); + this.identify = __bind(this.identify, this); + this.layoutImages = __bind(this.layoutImages, this); + this.build = __bind(this.build, this); + if (!options.selector) { + throw "no selector specified"; + } + this.images = []; + this.filter = options.filter; + this.outputDirectory = path.normalize(options.outputDirectory); + this.files = this.filter ? files.filter(this.filter) : files; + this.downsampling = options.downsampling; + this.pixelRatio = options.pixelRatio || 1; + this.name = options.name || "default"; + if (options.outputStyleDirectoryPath) { + this.outputStyleDirectoryPath = options.outputStyleDirectoryPath; + } + if (options.outputImage) { + this.outputImageFilePath = [this.outputDirectory, options.outputImage].join(separator); + this.outputImageDirectoryPath = path.dirname(this.outputImageFilePath); + this.httpImagePath = options.httpImagePath || path.relative(this.outputStyleDirectoryPath, this.outputImageFilePath); + } + if (options.outputStyleFilePath) { + this.outputStyleFilePath = options.outputStyleFilePath; + } + this.style = new Style(options); + } + + SpriteSheetConfiguration.prototype.build = function(callback) { + var _this = this; + if (!this.outputImageFilePath) { + throw "No output image file specified"; + } + console.log("--------------------------------------------------------------"); + console.log("Building '" + this.name + "' at pixel ratio " + this.pixelRatio); + console.log("--------------------------------------------------------------"); + this.derived = (!this.filter && SpriteSheetConfiguration.baseConfiguration.name !== this.name) || this.files.length === 0; + this.baseRatio = this.pixelRatio / SpriteSheetConfiguration.baseConfiguration.pixelRatio; + return this.layoutImages(function() { + if (_this.images.length === 0) { + throw "No image files specified"; + } + console.log(_this.summary()); + _this.generateCSS(); + return async.series([ensureDirectory(_this.outputImageDirectoryPath), _this.createSprite], callback); + }); + }; + + SpriteSheetConfiguration.prototype.layoutImages = function(callback) { + var _this = this; + return async.forEachSeries(this.files, this.identify, function() { + var layout; + layout = new Layout(); + _this.layout = layout.layout(_this.images, _this.options); + return callback(); + }); + }; + + SpriteSheetConfiguration.prototype.identify = function(filepath, callback) { + var _this = this; + return ImageMagick.identify(filepath, function(image) { + if (_this.derived) { + image.width = image.width * _this.baseRatio; + image.height = image.height * _this.baseRatio; + if (Math.round(image.width) !== image.width || Math.round(image.height) !== image.height) { + image.width = Math.ceil(image.width); + image.height = Math.ceil(image.height); + console.log(" WARN: Dimensions for " + image.filename + " don't use multiples of the pixel ratio, so they've been rounded."); + } + image.baseRatio = _this.baseRatio; + } + _this.images.push(image); + return callback(null, image); + }); + }; + + SpriteSheetConfiguration.prototype.generateCSS = function() { + return this.css = this.style.generate({ + relativeImagePath: this.httpImagePath, + images: this.images, + pixelRatio: this.pixelRatio, + width: this.layout.width, + height: this.layout.height + }); + }; + + SpriteSheetConfiguration.prototype.createSprite = function(callback) { + var _this = this; + return ImageMagick.composite({ + filepath: this.outputImageFilePath, + images: this.images, + width: this.layout.width, + height: this.layout.height, + downsampling: this.downsampling + }, function() { + return SpriteSheetBuilder.pngcrush(_this.outputImageFilePath, callback); + }); + }; + + SpriteSheetConfiguration.prototype.summary = function() { + var i, output, _i, _len, _ref; + output = "\n Creating a sprite from following images:\n"; + _ref = this.images; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + i = _ref[_i]; + output += " " + (this.reportPath(i.path)) + " (" + i.width + "x" + i.height; + if (this.derived) { + output += " - derived from " + SpriteSheetConfiguration.baseConfiguration.name; + } + output += ")\n"; + } + output += "\n Output files: " + (this.reportPath(this.outputImageFilePath)); + output += "\n Output size: " + this.layout.width + "x" + this.layout.height + " \n"; + return output; + }; + + SpriteSheetConfiguration.prototype.reportPath = function(path) { + return path; + }; + + return SpriteSheetConfiguration; + +})(); + +module.exports = SpriteSheetBuilder; diff --git a/lib/imagemagick.js b/lib/imagemagick.js new file mode 100644 index 0000000..8d4b6a7 --- /dev/null +++ b/lib/imagemagick.js @@ -0,0 +1,76 @@ +var ImageMagick, async, exec; + +exec = require('child_process').exec; + +async = require('async'); + +ImageMagick = (function() { + function ImageMagick() {} + + ImageMagick.prototype.identify = function(filepath, callback) { + return this.exec("identify " + filepath, function(error, stdout, stderr) { + var dims, filename, h, image, name, parts, w; + if (error || stderr) { + throw "Error in identify (" + filepath + "): " + (error || stderr); + } + parts = stdout.split(" "); + dims = parts[2].split("x"); + w = parseInt(dims[0]); + h = parseInt(dims[1]); + filename = filepath.split('/').pop(); + name = filename.split('.').shift(); + image = { + width: w, + height: h, + filename: filename, + name: name, + path: filepath + }; + return callback(image); + }); + }; + + ImageMagick.prototype.composite = function(options, callback) { + var command, downsampling, filepath, height, images, width, + _this = this; + filepath = options.filepath, images = options.images, width = options.width, height = options.height, downsampling = options.downsampling; + console.log(' Writing images to sprite sheet...'); + command = " convert -size " + width + "x" + height + " canvas:transparent -alpha transparent " + filepath + " "; + return this.exec(command, function(error, stdout, stderr) { + var compose; + if (error || stderr) { + throw "Error in creating canvas (" + filepath + "): " + (error || stderr); + } + compose = function(image, next) { + console.log(" Composing " + image.path); + return _this.composeImage(filepath, image, downsampling, next); + }; + return async.forEachSeries(images, compose, callback); + }); + }; + + ImageMagick.prototype.exec = function(command, callback) { + return exec(command, callback); + }; + + ImageMagick.prototype.composeImage = function(filepath, image, downsampling, callback) { + var command, movecmd; + command = " composite -geometry " + image.width + "x" + image.height + "+" + image.cssx + "+" + image.cssy + " "; + if (downsampling) { + command += "-filter " + downsampling; + } + movecmd = process.platform !== "win32" ? "mv" : "move"; + command += " " + image.path + " " + filepath + " " + filepath + ".tmp && " + movecmd + " " + filepath + ".tmp " + filepath + " "; + return exec(command, function(error, stdout, stderr) { + if (error || stderr) { + throw "Error in composite (" + filepath + "): " + (error || stderr); + } + return callback(); + }); + }; + + return ImageMagick; + +})(); + +module.exports = new ImageMagick(); diff --git a/lib/layout.js b/lib/layout.js new file mode 100644 index 0000000..4023da7 --- /dev/null +++ b/lib/layout.js @@ -0,0 +1,168 @@ +var Layout; + +Layout = (function() { + function Layout() {} + + Layout.prototype.layout = function(images, options) { + var hmargin, hpadding, i, lp, root, vmargin, vpadding, _i, _j, _len, _len1, + _this = this; + if (options == null) { + options = {}; + } + if (!images || !images.length) { + return { + width: 0, + height: 0 + }; + } + hpadding = options.hpadding || 0; + vpadding = options.vpadding || 0; + hmargin = options.hmargin || 0; + vmargin = options.vmargin || 0; + for (_i = 0, _len = images.length; _i < _len; _i++) { + i = images[_i]; + i.w = i.width + (2 * hpadding) + (2 * hmargin); + i.h = i.height + (2 * vpadding) + (2 * vmargin); + } + images.sort(function(a, b) { + var diff; + diff = _this.compare(Math.max(b.w, b.h), Math.max(a.w, a.h)); + if (diff === 0) { + diff = _this.compare(Math.min(b.w, b.h), Math.min(a.w, a.h)); + } + if (diff === 0) { + diff = _this.compare(b.h, a.h); + } + if (diff === 0) { + diff = _this.compare(b.w, a.w); + } + return diff; + }); + root = { + x: 0, + y: 0, + w: images[0].w, + h: images[0].h + }; + lp = function(i) { + var node; + node = _this.findNode(root, i.w, i.h); + if (node) { + _this.placeImage(i, node, hpadding, vpadding, hmargin, vmargin); + return _this.splitNode(node, i.w, i.h); + } else { + root = _this.grow(root, i.w, i.h); + return lp(i); + } + }; + for (_j = 0, _len1 = images.length; _j < _len1; _j++) { + i = images[_j]; + lp(i); + } + return { + width: root.w, + height: root.h + }; + }; + + Layout.prototype.compare = function(a, b) { + if (a > b) { + return 1; + } + if (b > a) { + return -1; + } + return 0; + }; + + Layout.prototype.placeImage = function(image, node, hpadding, vpadding, hmargin, vmargin) { + image.cssx = node.x + hmargin; + image.cssy = node.y + vmargin; + image.cssw = image.width + (2 * hpadding); + image.cssh = image.height + (2 * vpadding); + image.x = image.cssx + hpadding; + return image.y = image.cssy + vpadding; + }; + + Layout.prototype.findNode = function(root, w, h) { + if (root.used) { + return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); + } else if ((w <= root.w) && (h <= root.h)) { + return root; + } + }; + + Layout.prototype.splitNode = function(node, w, h) { + node.used = true; + node.down = { + x: node.x, + y: node.y + h, + w: node.w, + h: node.h - h + }; + return node.right = { + x: node.x + w, + y: node.y, + w: node.w - w, + h: h + }; + }; + + Layout.prototype.grow = function(root, w, h) { + var canGrowDown, canGrowRight, shouldGrowDown, shouldGrowRight; + canGrowDown = w <= root.w; + canGrowRight = h <= root.h; + shouldGrowRight = canGrowRight && (root.h >= (root.w + w)); + shouldGrowDown = canGrowDown && (root.w >= (root.h + h)); + if (shouldGrowRight) { + return this.growRight(root, w, h); + } else if (shouldGrowDown) { + return this.growDown(root, w, h); + } else if (canGrowRight) { + return this.growRight(root, w, h); + } else if (canGrowDown) { + return this.growDown(root, w, h); + } else { + throw "Can't fit " + w + "x" + h + " block into root " + root.w + "x" + root.h + " - this should not happen if images are pre-sorted correctly"; + } + }; + + Layout.prototype.growRight = function(root, w, h) { + return { + used: true, + x: 0, + y: 0, + w: root.w + w, + h: root.h, + down: root, + right: { + x: root.w, + y: 0, + w: w, + h: root.h + } + }; + }; + + Layout.prototype.growDown = function(root, w, h) { + return { + used: true, + x: 0, + y: 0, + w: root.w, + h: root.h + h, + down: { + x: 0, + y: root.h, + w: root.w, + h: h + }, + right: root + }; + }; + + return Layout; + +})(); + +module.exports = Layout; diff --git a/lib/style.js b/lib/style.js new file mode 100644 index 0000000..eabdc79 --- /dev/null +++ b/lib/style.js @@ -0,0 +1,73 @@ +var Style, path; + +path = require("path"); + +Style = (function() { + function Style(options) { + this.selector = options.selector; + this.pixelRatio = options.pixelRatio || 1; + if (options.resolveImageSelector) { + this.resolveImageSelector = options.resolveImageSelector; + } + } + + Style.prototype.css = function(selector, attributes) { + return "" + selector + " {\n" + (this.cssStyle(attributes)) + ";\n}\n"; + }; + + Style.prototype.cssStyle = function(attributes) { + return attributes.join(";\n"); + }; + + Style.prototype.cssComment = function(comment) { + return "/*\n" + comment + "\n*/"; + }; + + Style.prototype.resolveImageSelector = function(name) { + return name; + }; + + Style.prototype.generate = function(options) { + var attr, css, height, image, imagePath, images, pixelRatio, positionX, positionY, relativeImagePath, styles, width, _i, _len; + imagePath = options.imagePath, relativeImagePath = options.relativeImagePath, images = options.images, pixelRatio = options.pixelRatio, width = options.width, height = options.height; + relativeImagePath = relativeImagePath.replace(/(\\+)/g, "/"); + this.pixelRatio = pixelRatio || 1; + styles = [this.css(this.selector, [" background: url( '" + relativeImagePath + "' ) no-repeat", " background-size: " + (width / pixelRatio) + "px " + (height / pixelRatio) + "px"])]; + if (pixelRatio === 1) { + for (_i = 0, _len = images.length; _i < _len; _i++) { + image = images[_i]; + positionX = -image.cssx / pixelRatio; + if (positionX !== 0) { + positionX = positionX + 'px'; + } + positionY = -image.cssy / pixelRatio; + if (positionY !== 0) { + positionY = positionY + 'px'; + } + attr = [" width: " + (image.cssw / pixelRatio) + "px", " height: " + (image.cssh / pixelRatio) + "px", " background-position: " + positionX + " " + positionY]; + image.style = this.cssStyle(attr); + image.selector = this.resolveImageSelector(image.name, image.path); + styles.push(this.css([this.selector, image.selector].join('.'), attr)); + } + } + styles.push(""); + css = styles.join("\n"); + if (pixelRatio > 1) { + css = this.wrapMediaQuery(css); + } + return css; + }; + + Style.prototype.comment = function(comment) { + return this.cssComment(comment); + }; + + Style.prototype.wrapMediaQuery = function(css) { + return "@media (min--moz-device-pixel-ratio: " + this.pixelRatio + "),\n(-o-min-device-pixel-ratio: " + this.pixelRatio + "/1),\n(-webkit-min-device-pixel-ratio: " + this.pixelRatio + "),\n(min-device-pixel-ratio: " + this.pixelRatio + ") {\n" + css + "}\n"; + }; + + return Style; + +})(); + +module.exports = Style;