|
| 1 | +var async = require( "async" ), |
| 2 | + fs = require( "fs" ), |
| 3 | + im = require( "gm" ).subClass({ imageMagick: true }), |
| 4 | + path = require( "path" ); |
| 5 | + |
| 6 | +function expandColor( color ) { |
| 7 | + if ( color.length === 3 ) { |
| 8 | + return [ 0, 0, 1, 1, 2, 2 ].map(function( i ) { |
| 9 | + return color[i]; |
| 10 | + }).join( "" ); |
| 11 | + } |
| 12 | + return color; |
| 13 | +} |
| 14 | + |
| 15 | +function hashColor( color ) { |
| 16 | + if ( ( color.length === 3 || color.length === 6 ) && /^[0-9a-f]+$/i.test( color ) ) { |
| 17 | + color = "#" + color; |
| 18 | + } |
| 19 | + return color; |
| 20 | +} |
| 21 | + |
| 22 | +// I don't know if there's a better solution, but without the below conversion to Buffer we're not able to use it. |
| 23 | +function stream2Buffer( callback ) { |
| 24 | + return function( err, data ) { |
| 25 | + if ( err ) { |
| 26 | + return callback( err ); |
| 27 | + } |
| 28 | + var chunks = [], |
| 29 | + dataLen = 0; |
| 30 | + |
| 31 | + data.on( "data", function( chunk ) { |
| 32 | + chunks.push( chunk ); |
| 33 | + dataLen += chunk.length; |
| 34 | + }); |
| 35 | + |
| 36 | + data.on( "end", function() { |
| 37 | + var i = 0, |
| 38 | + buffer = new Buffer( dataLen ); |
| 39 | + chunks.forEach(function ( chunk ) { |
| 40 | + chunk.copy( buffer, i, 0, chunk.length ); |
| 41 | + i += chunk.length; |
| 42 | + }); |
| 43 | + callback( null, buffer ); |
| 44 | + }); |
| 45 | + |
| 46 | + data.on( "error", function( err ) { |
| 47 | + callback( err ); |
| 48 | + }); |
| 49 | + }; |
| 50 | +} |
| 51 | + |
| 52 | +var generateIcon, generateImage, generateTexture, |
| 53 | + cacheDirectory = __dirname + "/../tmp/cache", |
| 54 | + concurrentQueues = 4, |
| 55 | + imageQueue = async.queue( function( task, callback ) { |
| 56 | + task( callback ); |
| 57 | + }, concurrentQueues ); |
| 58 | + |
| 59 | +if ( !fs.existsSync( cacheDirectory ) ) { |
| 60 | + throw new Error( "Missing " + cacheDirectory + " folder. Run `grunt prepare` first." ); |
| 61 | +} |
| 62 | + |
| 63 | +generateImage = function( params, callback ) { |
| 64 | + if ( params.icon ) { |
| 65 | + generateIcon( params.icon, callback ); |
| 66 | + } else { |
| 67 | + generateTexture( params.texture, callback ); |
| 68 | + } |
| 69 | +}; |
| 70 | + |
| 71 | +generateIcon = function( params, callback ) { |
| 72 | + var color; |
| 73 | + |
| 74 | + // Add '#' in the beginning of the colors if needed |
| 75 | + color = hashColor( params.color ); |
| 76 | + |
| 77 | + // http://www.imagemagick.org/Usage/masking/#shapes |
| 78 | + // $ convert <icons_mask_filename> -background <color> -alpha shape output.png |
| 79 | + |
| 80 | + imageQueue.push(function( innerCallback ) { |
| 81 | + im( __dirname + "/../template/themeroller/icon/mask.png" ) |
| 82 | + .background( color ) |
| 83 | + .out( "-alpha", "shape" ) |
| 84 | + .stream( "png", stream2Buffer( innerCallback ) ); |
| 85 | + }, callback ); |
| 86 | +}; |
| 87 | + |
| 88 | +generateTexture = function( params, callback ) { |
| 89 | + var color, filename; |
| 90 | + |
| 91 | + // Add '#' in the beginning of the colors if needed |
| 92 | + color = hashColor( params.color ); |
| 93 | + |
| 94 | + filename = params.type.replace( /-/g, "_" ).replace( /$/, ".png" ); |
| 95 | + |
| 96 | + // http://www.imagemagick.org/Usage/compose/#dissolve |
| 97 | + // $ convert -size <width>x<height> 'xc:<color>' <texture_filename> -compose dissolve -define compose:args=<opacity>,100 -composite output.png |
| 98 | + |
| 99 | + imageQueue.push(function( innerCallback ) { |
| 100 | + im( params.width, params.height, color ) |
| 101 | + .out( __dirname + "/../template/themeroller/texture/" + filename, "-compose", "dissolve", "-define", "compose:args=" + params.opacity + ",100", "-composite" ) |
| 102 | + .stream( "png", stream2Buffer( innerCallback ) ); |
| 103 | + }, callback ); |
| 104 | +}; |
| 105 | + |
| 106 | + |
| 107 | +/** |
| 108 | + * Image |
| 109 | + */ |
| 110 | +function Image( params ) { |
| 111 | + var missingParams, requiredParams; |
| 112 | + |
| 113 | + if ( typeof params === "string" ) { |
| 114 | + params = this._parse( params ); |
| 115 | + } |
| 116 | + |
| 117 | + params = params || {}; |
| 118 | + |
| 119 | + if ( params.icon ) { |
| 120 | + params.icon = params.icon || {}; |
| 121 | + |
| 122 | + // Validate Icon |
| 123 | + if ( !params.icon.color ) { |
| 124 | + throw new Error( "missing color" ); |
| 125 | + } |
| 126 | + |
| 127 | + } else if ( params.texture ) { |
| 128 | + params.texture = params.texture || {}; |
| 129 | + |
| 130 | + // Validate Texture |
| 131 | + requiredParams = [ "color", "height", "opacity", "type", "width" ]; |
| 132 | + |
| 133 | + missingParams = requiredParams.filter(function( param ) { |
| 134 | + return !params.texture[ param ]; |
| 135 | + }); |
| 136 | + |
| 137 | + if ( missingParams.length ) { |
| 138 | + throw new Error( "missing \"" + missingParams.join( "\", \"" ) + "\"" ); |
| 139 | + } |
| 140 | + |
| 141 | + } else { |
| 142 | + throw new Error( "invalid parameters ", JSON.stringify( params ) ); |
| 143 | + } |
| 144 | + |
| 145 | + this.params = params; |
| 146 | +} |
| 147 | + |
| 148 | +Image.prototype = { |
| 149 | + _parse: function( filename ) { |
| 150 | + var match, params; |
| 151 | + |
| 152 | + if ( /^ui-icons/i.test( filename ) ) { |
| 153 | + |
| 154 | + // ui-icons_<color>_256x240.png |
| 155 | + match = filename.match( /^ui-icons_(\w+)_256x240.png$/i ); |
| 156 | + if ( match == null ) { |
| 157 | + throw new Error( "Invalid format: " + filename ); |
| 158 | + } |
| 159 | + params = { |
| 160 | + icon: { color: match[ 1 ] } |
| 161 | + }; |
| 162 | + |
| 163 | + } else { |
| 164 | + |
| 165 | + // ui-bg_<type>_<opacity>_<color>_<width>x<height>.png |
| 166 | + match = filename.match( /^ui-bg_([a-z0-9\-]+)_(\w+)_(\w+)_(\d+)x(\d+).png$/i ); |
| 167 | + if ( match == null ) { |
| 168 | + throw new Error( "Invalid format: " + filename ); |
| 169 | + } |
| 170 | + params = { |
| 171 | + texture: { |
| 172 | + type: match[ 1 ], |
| 173 | + opacity: match[ 2 ], |
| 174 | + color: match[ 3 ], |
| 175 | + width: match[ 4 ], |
| 176 | + height: match[ 5 ] |
| 177 | + } |
| 178 | + }; |
| 179 | + } |
| 180 | + |
| 181 | + return params; |
| 182 | + }, |
| 183 | + |
| 184 | + filename: function() { |
| 185 | + var color, params; |
| 186 | + if ( !this._filename ) { |
| 187 | + if ( this.params.icon ) { |
| 188 | + params = this.params.icon; |
| 189 | + color = expandColor( params.color ).replace( /^#/, "" ); |
| 190 | + |
| 191 | + // ui-icons_<color>_256x240.png |
| 192 | + this._filename = "ui-icons_" + color + "_256x240.png"; |
| 193 | + |
| 194 | + } else { |
| 195 | + params = this.params.texture; |
| 196 | + color = expandColor( params.color ).replace( /^#/, "" ); |
| 197 | + |
| 198 | + // ui-bg_<type>_<opacity>_<color>_<width>x<height>.png |
| 199 | + this._filename = "ui-bg_" + params.type.replace( /_/g, "-" ) + "_" + params.opacity + "_" + color + "_" + params.width + "x" + params.height + ".png"; |
| 200 | + } |
| 201 | + } |
| 202 | + return this._filename; |
| 203 | + }, |
| 204 | + |
| 205 | + get: function( callback ) { |
| 206 | + var cacheFile = cacheDirectory + "/" + this.filename(), |
| 207 | + filename = this.filename(), |
| 208 | + params = this.params; |
| 209 | + |
| 210 | + fs.readFile( cacheFile, function( err, data ) { |
| 211 | + if ( err ) { |
| 212 | + generateImage( params, function( err, data ) { |
| 213 | + if ( !err ) { |
| 214 | + // Write file asynchronously while sending data to callback. |
| 215 | + fs.writeFile( cacheFile, data ); |
| 216 | + } |
| 217 | + callback( err, filename, data ); |
| 218 | + }); |
| 219 | + } else { |
| 220 | + callback( null, filename, data ); |
| 221 | + } |
| 222 | + }); |
| 223 | + } |
| 224 | +}; |
| 225 | + |
| 226 | +module.exports = Image; |
0 commit comments