From 0ae3f06b2fbe2cc6619c79f181ba0a5a2283a79f Mon Sep 17 00:00:00 2001 From: Tim Sebastian Date: Sun, 18 Aug 2013 12:07:28 +0200 Subject: [PATCH 1/3] switch to graceful-fs to handle large number of images --- package.json | 47 ++++++++++++------------ src/builder.coffee | 90 +++++++++++++++++++++++----------------------- 2 files changed, 69 insertions(+), 68 deletions(-) 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..28af5e9 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 @@ -24,7 +24,7 @@ class SpriteSheetBuilder @supportsPngcrush: ( callback ) -> exec "which pngcrush", ( error, stdout, stderr ) => callback stdout and !error and !stderr - + @pngcrush: ( image, callback ) -> SpriteSheetBuilder.supportsPngcrush ( supported ) -> if supported @@ -37,23 +37,23 @@ 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 ) - + if options.outputCss @outputStyleFilePath = [ @outputDirectory, options.outputCss ].join( separator ) @outputStyleDirectoryPath = path.dirname( @outputStyleFilePath ) @@ -65,53 +65,53 @@ 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 @@ -124,26 +124,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 +156,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 "--------------------------------------------------------------" - + # 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() - + @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 +195,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." ) - + image.baseRatio = @baseRatio - + @images.push image callback null, image - + generateCSS: => @css = @style.generate relativeImagePath: @httpImagePath @@ -228,13 +228,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 +243,7 @@ class SpriteSheetConfiguration output += "\n Output size: #{ @layout.width }x#{ @layout.height } \n" - + return output reportPath: ( path ) -> From 13d94f82c894dabef690d42ec11e99fdae7af21a Mon Sep 17 00:00:00 2001 From: Tim Sebastian Date: Sun, 18 Aug 2013 12:08:00 +0200 Subject: [PATCH 2/3] switch from composite to convert(-composite) to create the final sprite in bulks --- src/imagemagick.coffee | 63 +++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/imagemagick.coffee b/src/imagemagick.coffee index f08c896..ce23a32 100644 --- a/src/imagemagick.coffee +++ b/src/imagemagick.coffee @@ -2,33 +2,33 @@ exec = require( 'child_process' ).exec async = require( 'async' ) 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...' - + command = " convert -size #{ width }x#{ height } @@ -36,44 +36,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 ) => + console.log " Composing bulk of #{ images.length } images" + @composeImages filepath, images, downsampling, next + + async.forEachSeries imageBlocks, compose, callback exec: ( command, callback ) -> #console.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() From cbfb7a3399baf2d17e311df82bcd7fb12287838f Mon Sep 17 00:00:00 2001 From: Tim Sebastian Date: Wed, 28 Aug 2013 17:01:46 +0200 Subject: [PATCH 3/3] added logger to be able to switchoff the debug messages --- gruntfile.js | 3 ++- src/builder.coffee | 16 +++++++++------- src/imagemagick.coffee | 7 ++++--- src/logger.coffee | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 src/logger.coffee 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/src/builder.coffee b/src/builder.coffee index 28af5e9..416e700 100644 --- a/src/builder.coffee +++ b/src/builder.coffee @@ -7,6 +7,7 @@ _ = require( "underscore" ) ImageMagick = require( './imagemagick' ) Layout = require( './layout' ) Style = require( './style' ) +logger = require( './logger' ) separator = path.sep || "/" @@ -29,7 +30,7 @@ class SpriteSheetBuilder 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 @@ -53,6 +54,7 @@ class SpriteSheetBuilder @files = options.images @outputConfigurations = {} @outputDirectory = path.normalize( options.outputDirectory ) + logger.disable() unless options.log if options.outputCss @outputStyleFilePath = [ @outputDirectory, options.outputCss ].join( separator ) @@ -116,7 +118,7 @@ class SpriteSheetBuilder if err throw err else - console.log "CSS file written to", @outputStyleFilePath, "\n" + logger.log "CSS file written to", @outputStyleFilePath, "\n" callback() @@ -157,9 +159,9 @@ 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. @@ -173,7 +175,7 @@ class SpriteSheetConfiguration if @images.length is 0 throw "No image files specified" - console.log @summary() + logger.log @summary() @generateCSS() @@ -201,7 +203,7 @@ class SpriteSheetConfiguration 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 diff --git a/src/imagemagick.coffee b/src/imagemagick.coffee index ce23a32..6f6f16e 100644 --- a/src/imagemagick.coffee +++ b/src/imagemagick.coffee @@ -1,5 +1,6 @@ exec = require( 'child_process' ).exec async = require( 'async' ) +logger = require( './logger' ) class ImageMagick @@ -27,7 +28,7 @@ class ImageMagick 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 @@ -46,13 +47,13 @@ class ImageMagick throw "Error in creating canvas (#{ filepath }): #{ error || stderr }" compose = ( images, next ) => - console.log " Composing bulk of #{ images.length } images" + 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 composeImages: ( filepath, images, downsampling, callback ) -> 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()