UNPKG

grunt-spritesmith

Version:

Grunt task for converting a set of images into a spritesheet and corresponding CSS variables.

378 lines (334 loc) 13 kB
// Load in dependencies var fs = require('fs'); var path = require('path'); var _ = require('underscore'); var async = require('async'); var templater = require('spritesheet-templates'); var Spritesmith = require('spritesmith'); var url = require('url2'); // Define class to contain different extension handlers function ExtFormat() { this.formatObj = {}; } ExtFormat.prototype = { add: function (name, val) { this.formatObj[name] = val; }, get: function (filepath) { // Grab the extension from the filepath var ext = path.extname(filepath); var lowerExt = ext.toLowerCase(); // Look up the file extenion from our format object var formatObj = this.formatObj; var format = formatObj[lowerExt]; return format; } }; // Create img and css formats var imgFormats = new ExtFormat(); var cssFormats = new ExtFormat(); // Add our img formats imgFormats.add('.png', 'png'); imgFormats.add('.jpg', 'jpeg'); imgFormats.add('.jpeg', 'jpeg'); // Add our css formats cssFormats.add('.styl', 'stylus'); cssFormats.add('.stylus', 'stylus'); cssFormats.add('.sass', 'sass'); cssFormats.add('.scss', 'scss'); cssFormats.add('.less', 'less'); cssFormats.add('.json', 'json'); cssFormats.add('.css', 'css'); function getCoordinateName(filepath) { // Extract the image name (exlcuding extension) var fullname = path.basename(filepath); var nameParts = fullname.split('.'); // If there is are more than 2 parts, pop the last one if (nameParts.length >= 2) { nameParts.pop(); } // Return our modified filename return nameParts.join('.'); } module.exports = function gruntSpritesmith(grunt) { // Create a gruntSpritesmithFn function function gruntSpritesmithFn() { // Grab the raw configuration var data = this.data; // If we were invoked via `grunt-newer`, re-localize the info if (data.src === undefined && data.files) { data = data.files[0] || {}; } // Determine the origin and destinations var src = data.src; var destImg = data.dest; var destCss = data.destCss; var cssTemplate = data.cssTemplate; var that = this; // Verify all properties are here if (!src || !destImg || !destCss) { return grunt.fatal('grunt.sprite requires a src, dest (img), and destCss property'); } // Expand all filepaths (e.g. `*.png` -> `home.png`) var srcFiles = grunt.file.expand(src); // If there are settings for retina var retinaSrcFiles; var retinaSrcFilter = data.retinaSrcFilter; var retinaDestImg = data.retinaDest; if (retinaSrcFilter || retinaDestImg) { grunt.log.debug('Retina settings detected'); // Verify our required set is present if (!retinaSrcFilter || !retinaDestImg) { return grunt.fatal('Retina settings detected. We must have both `retinaSrcFilter` and `retinaDest` ' + 'provided for retina to work'); } // Filter out our retina files retinaSrcFiles = []; srcFiles = srcFiles.filter(function filterSrcFile(filepath) { // If we have a retina file, filter it out if (grunt.file.match(retinaSrcFilter, filepath).length) { retinaSrcFiles.push(filepath); return false; // Otherwise, keep it in the src files } else { return true; } }); grunt.verbose.writeln('Retina images found: ' + retinaSrcFiles.join(', ')); // If we have a different amount of normal and retina images, complain and leave if (srcFiles.length !== retinaSrcFiles.length) { return grunt.fatal('Retina settings detected but ' + retinaSrcFiles.length + ' retina images were found. ' + 'We have ' + srcFiles.length + ' normal images and expect these numbers to line up. ' + 'Please double check `retinaSrcFilter`.'); } } // Create an async callback var callback = this.async(); // Determine the format of the image var imgOpts = data.imgOpts || {}; var imgFormat = imgOpts.format || imgFormats.get(destImg) || 'png'; // Set up the defautls for imgOpts _.defaults(imgOpts, {format: imgFormat}); // Prepare spritesmith parameters var spritesmithParams = { engine: data.engine, algorithm: data.algorithm, padding: data.padding || 0, algorithmOpts: data.algorithmOpts || {}, engineOpts: data.engineOpts || {}, exportOpts: imgOpts }; // Construct our spritesmiths var spritesmith = new Spritesmith(spritesmithParams); var retinaSpritesmithParams; // eslint-disable-line var retinaSpritesmith; // eslint-disable-line if (retinaSrcFiles) { retinaSpritesmithParams = _.defaults({ padding: spritesmithParams.padding * 2 }, spritesmithParams); retinaSpritesmith = new Spritesmith(retinaSpritesmithParams); } // In parallel async.parallel([ // Load in our normal images function generateNormalImages(callback) { spritesmith.createImages(srcFiles, callback); }, // If we have retina images, load them in as well function generateRetinaImages(callback) { if (retinaSrcFiles) { return retinaSpritesmith.createImages(retinaSrcFiles, callback); } else { return process.nextTick(callback); } } ], function handleImages(err, resultArr) { // If an error occurred, callback with it if (err) { grunt.fatal(err); return callback(err); } // Otherwise, validate our images line up var normalSprites = resultArr[0]; var retinaSprites = resultArr[1]; // TODO: Validate error looks good if (retinaSprites) { normalSprites.forEach(function validateSprites(normalSprite, i) { var retinaSprite = retinaSprites[i]; if (retinaSprite.width !== normalSprite.width * 2 || retinaSprite.height !== normalSprite.height * 2) { grunt.log.warn('Normal sprite has inconsistent size with retina sprite. ' + '"' + srcFiles[i] + '" is ' + normalSprite.width + 'x' + normalSprite.height + ' while ' + '"' + retinaSrcFiles[i] + '" is ' + retinaSprite.width + 'x' + retinaSprite.height + '.'); } }); } // Process our sprites into spritesheets var result = spritesmith.processImages(normalSprites, spritesmithParams); var retinaResult; if (retinaSprites) { retinaResult = retinaSpritesmith.processImages(retinaSprites, retinaSpritesmithParams); } // Generate a listing of CSS variables var coordinates = result.coordinates; var properties = result.properties; var spritePath = data.imgPath || url.relative(destCss, destImg); var spritesheetInfo = { width: properties.width, height: properties.height, image: spritePath }; var cssVarMap = data.cssVarMap || function noop() {}; var cleanCoords = []; // Clean up the file name of the file Object.getOwnPropertyNames(coordinates).sort().forEach(function prepareTemplateData(file) { // Extract out our name var name = getCoordinateName(file); var coords = coordinates[file]; // Specify the image for the sprite coords.name = name; coords.source_image = file; // DEV: `image`, `total_width`, `total_height` are deprecated as they are overwritten in `spritesheet-templates` coords.image = spritePath; coords.total_width = properties.width; coords.total_height = properties.height; // Map the coordinates through cssVarMap coords = cssVarMap(coords) || coords; // Save the cleaned name and coordinates cleanCoords.push(coords); }); // If we have retina sprites var retinaCleanCoords; // eslint-disable-line var retinaGroups; // eslint-disable-line var retinaSpritesheetInfo; // eslint-disable-line if (retinaResult) { // Generate a listing of CSS variables var retinaCoordinates = retinaResult.coordinates; var retinaProperties = retinaResult.properties; var retinaSpritePath = data.retinaImgPath || url.relative(destCss, retinaDestImg); retinaSpritesheetInfo = { width: retinaProperties.width, height: retinaProperties.height, image: retinaSpritePath }; // DEV: We reuse cssVarMap retinaCleanCoords = []; // Clean up the file name of the file Object.getOwnPropertyNames(retinaCoordinates).sort().forEach(function prepareRetinaTemplateData(file) { var name = getCoordinateName(file); var coords = retinaCoordinates[file]; coords.name = name; coords.source_image = file; coords.image = retinaSpritePath; coords.total_width = retinaProperties.width; coords.total_height = retinaProperties.height; coords = cssVarMap(coords) || coords; retinaCleanCoords.push(coords); }); // Generate groups for our coordinates retinaGroups = cleanCoords.map(function getRetinaGroups(normalSprite, i) { // DEV: Name is inherited from `cssVarMap` on normal sprite return { name: normalSprite.name, index: i }; }); } // If we have handlebars helpers, register them var handlebarsHelpers = data.cssHandlebarsHelpers; if (handlebarsHelpers) { Object.keys(handlebarsHelpers).forEach(function registerHelper(helperKey) { templater.registerHandlebarsHelper(helperKey, handlebarsHelpers[helperKey]); }); } // If there is a custom template, use it var cssFormat = 'spritesmith-custom'; var cssOptions = data.cssOpts || {}; if (cssTemplate) { if (typeof cssTemplate === 'function') { templater.addTemplate(cssFormat, cssTemplate); } else { templater.addHandlebarsTemplate(cssFormat, fs.readFileSync(cssTemplate, 'utf8')); } // Otherwise, override the cssFormat and fallback to 'json' } else { cssFormat = data.cssFormat; if (!cssFormat) { cssFormat = cssFormats.get(destCss) || 'json'; // If we are dealing with retina items, move to retina flavor (e.g. `scss` -> `scss_retina`) if (retinaGroups) { cssFormat += '_retina'; } } } // Render the variables via `spritesheet-templates` var cssStr = templater({ sprites: cleanCoords, spritesheet: spritesheetInfo, spritesheet_info: { name: data.cssSpritesheetName }, retina_groups: retinaGroups, retina_sprites: retinaCleanCoords, retina_spritesheet: retinaSpritesheetInfo, retina_spritesheet_info: { name: data.cssRetinaSpritesheetName }, retina_groups_info: { name: data.cssRetinaGroupsName } }, { format: cssFormat, formatOpts: cssOptions }); // Write out the content async.parallel([ function outputNormalImage(cb) { // Create our directory var destImgDir = path.dirname(destImg); grunt.file.mkdir(destImgDir); // Generate our write stream and pipe the image to it var writeStream = fs.createWriteStream(destImg); writeStream.on('error', cb); writeStream.on('finish', cb); result.image.pipe(writeStream); }, function outputRetinaImage(cb) { if (retinaResult) { var retinaDestImgDir = path.dirname(retinaDestImg); grunt.file.mkdir(retinaDestImgDir); var retinaWriteStream = fs.createWriteStream(retinaDestImg); retinaWriteStream.on('error', cb); retinaWriteStream.on('finish', cb); retinaResult.image.pipe(retinaWriteStream); return; } else { return process.nextTick(cb); } }, function outputCss(cb) { var destCssDir = path.dirname(destCss); grunt.file.mkdir(destCssDir); fs.writeFile(destCss, cssStr, 'utf8', cb); } ], function handleError(err) { // If there was an error, fail with it if (err) { grunt.fatal(err); return callback(err); } // Fail task if errors were logged if (that.errorCount) { return callback(false); } // Otherwise, print a success message if (retinaDestImg) { grunt.log.writeln('Files "' + destCss + '", "' + destImg + '", "' + retinaDestImg + '" created.'); } else { grunt.log.writeln('Files "' + destCss + '", "' + destImg + '" created.'); } // Callback callback(true); }); }); } // Export the gruntSpritesmithFn function grunt.registerMultiTask('sprite', 'Spritesheet making utility', gruntSpritesmithFn); };