diff --git a/gruntfile.js b/gruntfile.js index a124155..4bbf531 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -11,7 +11,8 @@ module.exports = function(grunt) { 'lib/builder.js': 'src/builder.coffee', 'lib/imagemagick.js': 'src/imagemagick.coffee', 'lib/style.js': 'src/style.coffee', - 'lib/layout.js': 'src/layout.coffee' + 'lib/layout.js': 'src/layout.coffee', + 'lib/logger.js': 'src/logger.coffee' } } }, diff --git a/package.json b/package.json index 6a6fad9..ac9a1f3 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,26 @@ { - "name": "node-spritesheet", - "description": "Sprite sheet generator for node.js", - "version": "0.4.2", - "author": { - "name": "Richard Butler", - "email": "rich@aspectvision.com" - }, - "repository": { - "type": "git", - "url": "http://github.com/richardbutler/node-spritesheet.git" - }, - "main": "./index", - "scripts": { - "install": "grunt" - }, - "dependencies": { - "async": "~0.1.22", - "q-fs": "~0.1.32", - "underscore": "~1.4.2", - "grunt": "~0.4.1", - "grunt-contrib-coffee": "~0.7.0", - "grunt-contrib-clean": "~0.5.0" - } + "name": "node-spritesheet", + "description": "Sprite sheet generator for node.js", + "version": "0.4.2", + "author": { + "name": "Richard Butler", + "email": "rich@aspectvision.com" + }, + "repository": { + "type": "git", + "url": "http://github.com/richardbutler/node-spritesheet.git" + }, + "main": "./index", + "scripts": { + "install": "grunt" + }, + "dependencies": { + "async": "~0.1.22", + "q-fs": "~0.1.32", + "underscore": "~1.4.2", + "grunt": "~0.4.1", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-clean": "~0.5.0", + "graceful-fs": "~2.0.0" + } } diff --git a/src/builder.coffee b/src/builder.coffee index b2bd2d0..416e700 100644 --- a/src/builder.coffee +++ b/src/builder.coffee @@ -1,4 +1,4 @@ -fs = require( 'fs' ) +fs = require( 'graceful-fs' ) path = require( 'path' ) qfs = require( 'q-fs' ) exec = require( 'child_process' ).exec @@ -7,6 +7,7 @@ _ = require( "underscore" ) ImageMagick = require( './imagemagick' ) Layout = require( './layout' ) Style = require( './style' ) +logger = require( './logger' ) separator = path.sep || "/" @@ -24,12 +25,12 @@ class SpriteSheetBuilder @supportsPngcrush: ( callback ) -> exec "which pngcrush", ( error, stdout, stderr ) => callback stdout and !error and !stderr - + @pngcrush: ( image, callback ) -> SpriteSheetBuilder.supportsPngcrush ( supported ) -> if supported crushed = "#{ image }.crushed" - console.log "\n pngcrushing, this may take a few moments...\n" + logger.log "\n pngcrushing, this may take a few moments...\n" exec "pngcrush -reduce #{ image } #{ crushed } && mv #{ crushed } #{ image }", ( error, stdout, stderr ) => callback() else @@ -37,23 +38,24 @@ class SpriteSheetBuilder @fromGruntTask: ( options ) -> builder = new SpriteSheetBuilder( options ) - + outputConfigurations = options.output delete options.output - + if outputConfigurations && Object.keys( outputConfigurations ).length > 0 - + for key of outputConfigurations config = outputConfigurations[ key ] builder.addConfiguration( key, config ) - + return builder constructor: ( @options ) -> @files = options.images @outputConfigurations = {} @outputDirectory = path.normalize( options.outputDirectory ) - + logger.disable() unless options.log + if options.outputCss @outputStyleFilePath = [ @outputDirectory, options.outputCss ].join( separator ) @outputStyleDirectoryPath = path.dirname( @outputStyleFilePath ) @@ -65,58 +67,58 @@ class SpriteSheetBuilder outputStyleDirectoryPath: @outputStyleDirectoryPath ssc = new SpriteSheetConfiguration( options.images || @files, config ) - + @outputConfigurations[ name ] = ssc - + # Ascertain the "base" configuration, i.e. the highest pixel density # images, to scale down to other ratios if !baseConfig || config.pixelRatio > baseConfig.pixelRatio baseConfig = config - + return ssc build: ( done ) -> throw "no output style file specified" if !@outputStyleFilePath - + if Object.keys( @outputConfigurations ).length is 0 # If no configurations are supplied, we need to supply a default. @addConfiguration( "default", { pixelRatio: 1 } ) - + @configs = [] baseConfig = null - + for key of @outputConfigurations config = @outputConfigurations[ key ] - + # Ascertain the "base" configuration, i.e. the highest pixel density # images, to scale down to other ratios if !baseConfig || config.pixelRatio > baseConfig.pixelRatio baseConfig = config - + @configs.push( config ) - + SpriteSheetConfiguration.baseConfiguration = baseConfig - + async.series [ ( callback ) => async.forEachSeries @configs, @buildConfig, callback - + ensureDirectory( @outputStyleDirectoryPath ) @writeStyleSheet ], done - + buildConfig: ( config, callback ) => config.build( callback ) writeStyleSheet: ( callback ) => css = @configs.map ( config ) -> config.css - + fs.writeFile @outputStyleFilePath, css.join( "\n\n" ), ( err ) => if err throw err else - console.log "CSS file written to", @outputStyleFilePath, "\n" + logger.log "CSS file written to", @outputStyleFilePath, "\n" callback() @@ -124,26 +126,26 @@ class SpriteSheetConfiguration constructor: ( files, options ) -> throw "no selector specified" if !options.selector - + @images = [] @filter = options.filter @outputDirectory = path.normalize options.outputDirectory - + # Use the .filter() function, if applicable @files = if @filter then files.filter( @filter ) else files - + # The ImageMagick filter method to use for resizing images. @downsampling = options.downsampling - + # The target pixel density ratio for this configuration. @pixelRatio = options.pixelRatio || 1 - + # The pseudonym for which this configuration should be referenced, e.g. "retina". @name = options.name || "default" if options.outputStyleDirectoryPath @outputStyleDirectoryPath = options.outputStyleDirectoryPath - + if options.outputImage @outputImageFilePath = [ @outputDirectory, options.outputImage ].join( separator ) @outputImageDirectoryPath = path.dirname( @outputImageFilePath ) @@ -156,38 +158,38 @@ class SpriteSheetConfiguration build: ( callback ) => throw "No output image file specified" if !@outputImageFilePath - - console.log "--------------------------------------------------------------" - console.log "Building '#{ @name }' at pixel ratio #{ @pixelRatio }" - console.log "--------------------------------------------------------------" - + + logger.log "--------------------------------------------------------------" + logger.log "Building '#{ @name }' at pixel ratio #{ @pixelRatio }" + logger.log "--------------------------------------------------------------" + # Whether the images in this configuration should be resized, based on the # highest-density pixel ratio. @derived = ( !@filter and SpriteSheetConfiguration.baseConfiguration.name isnt @name ) or @files.length is 0 - + # The multiplier for any image resizing that needs to take place against # the base configuration. @baseRatio = @pixelRatio / SpriteSheetConfiguration.baseConfiguration.pixelRatio - + @layoutImages => if @images.length is 0 throw "No image files specified" - - console.log @summary() - + + logger.log @summary() + @generateCSS() - + async.series [ ensureDirectory( @outputImageDirectoryPath ) @createSprite ], callback - + layoutImages: ( callback ) => async.forEachSeries @files, @identify, => layout = new Layout() @layout = layout.layout @images, @options - + callback() identify: ( filepath, callback ) => @@ -195,19 +197,19 @@ class SpriteSheetConfiguration if @derived image.width = image.width * @baseRatio image.height = image.height * @baseRatio - + if Math.round( image.width ) isnt image.width or Math.round( image.height ) isnt 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." ) - + + logger.log( " WARN: Dimensions for #{ image.filename } don't use multiples of the pixel ratio, so they've been rounded." ) + image.baseRatio = @baseRatio - + @images.push image callback null, image - + generateCSS: => @css = @style.generate relativeImagePath: @httpImagePath @@ -228,13 +230,13 @@ class SpriteSheetConfiguration summary: -> output = "\n Creating a sprite from following images:\n" - + for i in @images output += " #{ @reportPath( i.path ) } (#{ i.width }x#{ i.height }" - + if @derived output += " - derived from #{ SpriteSheetConfiguration.baseConfiguration.name }" - + output += ")\n" output += "\n Output files: @@ -243,7 +245,7 @@ class SpriteSheetConfiguration output += "\n Output size: #{ @layout.width }x#{ @layout.height } \n" - + return output reportPath: ( path ) -> diff --git a/src/imagemagick.coffee b/src/imagemagick.coffee index f08c896..6f6f16e 100644 --- a/src/imagemagick.coffee +++ b/src/imagemagick.coffee @@ -1,34 +1,35 @@ exec = require( 'child_process' ).exec async = require( 'async' ) +logger = require( './logger' ) class ImageMagick - + identify: ( filepath, callback ) -> @exec "identify #{ filepath }", ( error, stdout, stderr ) -> if error or 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 - + callback image composite: ( options, callback ) -> { filepath, images, width, height, downsampling } = options - - console.log ' Writing images to sprite sheet...' - + + logger.log ' Writing images to sprite sheet...' + command = " convert -size #{ width }x#{ height } @@ -36,44 +37,43 @@ class ImageMagick -alpha transparent #{ filepath } " - + + imageBlocks = [] + while images.length + imageBlocks.push images.splice(0,50) + @exec command, ( error, stdout, stderr ) => if error or stderr throw "Error in creating canvas (#{ filepath }): #{ error || stderr }" - - compose = ( image, next ) => - console.log " Composing #{ image.path }" - @composeImage filepath, image, downsampling, next - - async.forEachSeries images, compose, callback + + compose = ( images, next ) => + logger.log " Composing bulk of #{ images.length } images" + @composeImages filepath, images, downsampling, next + + async.forEachSeries imageBlocks, compose, callback exec: ( command, callback ) -> - #console.log "Exec: #{ command }" + #logger.log "Exec: #{ command }" exec command, callback - - composeImage: ( filepath, image, downsampling, callback ) -> + + composeImages: ( filepath, images, downsampling, callback ) -> # No need - ImageMagick defaults to Mitchell or Lanczos, where appropriate. # downsampling ||= "Lanczos" - - command = " - composite - -geometry #{ image.width }x#{ image.height }+#{ image.cssx }+#{ image.cssy } - " - - if downsampling - command += "-filter #{ downsampling }" - - command += " - #{ image.path } #{ filepath } #{ filepath }.tmp - - && - mv #{ filepath }.tmp #{ filepath } - " - + + command = " convert #{filepath} " + + images.forEach (image)-> + + command += " #{image.path} -geometry #{ image.width }x#{ image.height }+#{ image.cssx }+#{ image.cssy } " + command += "-filter #{ downsampling }" if downsampling + command += " -composite" + + command += " #{filepath}" + exec command, ( error, stdout, stderr ) -> if error or stderr throw "Error in composite (#{ filepath }): #{ error || stderr }" - + callback() module.exports = new ImageMagick() diff --git a/src/logger.coffee b/src/logger.coffee new file mode 100644 index 0000000..d1ac893 --- /dev/null +++ b/src/logger.coffee @@ -0,0 +1,15 @@ + +class Logger + constructor: -> + @_log = true + +Logger::disable = -> + @_log = false + +Logger::enable = -> + @_log = true + +Logger::log = -> + console.log.apply console, arguments if @_log + +module.exports = new Logger()