├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── encode.js └── fetch.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Wenqer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gulp-base64 2 | =========== 3 | 4 | Gulp task for converting all files found within a stylesheet (those within a url( ... ) declaration) into base64-encoded data URI strings. 5 | 6 | ## Install 7 | 8 | Install with [npm](https://npmjs.org) 9 | 10 | ``` 11 | npm install gulp-base64 --save-dev 12 | ``` 13 | 14 | ## Example usage 15 | ```js 16 | var gulp = require('gulp'); 17 | var base64 = require('./build/gulp-base64'); 18 | 19 | //basic example 20 | gulp.task('build', function () { 21 | return gulp.src('./css/*.css') 22 | .pipe(base64()) 23 | .pipe(concat('main.css')) 24 | .pipe(gulp.dest('./public/css')); 25 | }); 26 | ... 27 | //example with options 28 | gulp.task('build', function () { 29 | return gulp.src('./css/*.css') 30 | .pipe(base64({ 31 | baseDir: 'public', 32 | extensions: ['svg', 'png', /\.jpg#datauri$/i], 33 | exclude: [/\.server\.(com|net)\/dynamic\//, '--live.jpg'], 34 | maxImageSize: 8*1024, // bytes, 35 | deleteAfterEncoding: false, 36 | debug: true 37 | })) 38 | .pipe(concat('main.css')) 39 | .pipe(gulp.dest('./public/css')); 40 | }); 41 | 42 | ``` 43 | ## Options 44 | 45 | - `baseDir` (String) 46 | If you have absolute image paths in your stylesheet, the path specified 47 | in this option will be used as the base directory (relative to gulpfile). 48 | 49 | - `deleteAfterEncoding` (Boolean) 50 | Set this to true to delete images after they've been encoded. 51 | You'll want to do this in a staging area, and not in your source directories. Be careful. 52 | 53 | - `extensions` (Array of `String` or `RegExp`s) 54 | Proccess only specified extensions. 55 | Strings are matched against file-extension only, while RegExps are tested against the raw URL value. 56 | 57 | - `exclude` (Array of `String` or `RegExp`s) 58 | Skip files with URLs that match these patterns. 59 | Unlike with the `extensions` option Strings are sub-string matched against the whole URL value. 60 | 61 | - `maxImageSize` (Number) 62 | Maximum filesize in bytes for changing image to base64. 63 | 64 | - `debug` (Boolean) 65 | Enable log to console. 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var through = require('through2'); 3 | var encode = require('./lib/encode'); 4 | 5 | module.exports = function (opts) { 6 | 7 | function rebase(file, encoding, callback) { 8 | var self = this; 9 | 10 | encode.stylesheet(file, opts, function (err, src) { 11 | if (err) { 12 | console.error(err); 13 | } 14 | file.contents = new Buffer(src); 15 | 16 | self.push(file); 17 | callback(); 18 | }); 19 | 20 | } 21 | 22 | return through.obj(rebase); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /lib/encode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Grunt Image Embed 3 | * https://github.com/ehynds/grunt-image-embed 4 | * 5 | * Copyright (c) 2012 Eric Hynds 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | // Node libs 10 | var fs = require("fs"); 11 | var path = require("path"); 12 | var mime = require("mime"); 13 | 14 | var extend = require("extend"); 15 | var async = require("async"); 16 | 17 | // Internal Libs 18 | var fetch = require("./fetch"); 19 | 20 | // Cache regex's 21 | var rImages = /([\s\S]*?)(url\(([^)]+)\))(?!\s*[;,]?\s*\/\*\s*base64:skip\s*\*\/)|([\s\S]+)/img; 22 | var rExternal = /^(http|https|\/\/)/; 23 | var rSchemeless = /^\/\//; 24 | var rData = /^data:/; 25 | var rQuotes = /['"]/g; 26 | var rParams = /([?#].*)$/g; 27 | 28 | // Grunt export wrapper 29 | module.exports = (function () { 30 | "use strict"; 31 | 32 | var exports = {}; 33 | 34 | /** 35 | * Takes a CSS file as input, goes through it line by line, and base64 36 | * encodes any images it finds. 37 | * 38 | * @param srcFile Relative or absolute path to a source stylesheet file. 39 | * @param opts Options object 40 | * @param done Function to call once encoding has finished. 41 | */ 42 | exports.stylesheet = function(file, opts, done) { 43 | opts = opts || {}; 44 | 45 | // Cache of already converted images 46 | var cache = {}; 47 | 48 | // Shift args if no options object is specified 49 | if(typeof opts === "function") { 50 | done = opts; 51 | opts = {}; 52 | } 53 | 54 | var deleteAfterEncoding = opts.deleteAfterEncoding; 55 | var src = file.contents.toString(); 56 | var result = ""; 57 | var match, img, line, tasks, group; 58 | 59 | async.whilst(function() { 60 | group = rImages.exec(src); 61 | return group != null; 62 | }, 63 | function(complete) { 64 | // if there is another url to be processed, then: 65 | // group[1] will hold everything up to the url declaration 66 | // group[2] will hold the complete url declaration (useful if no encoding will take place) 67 | // group[3] will hold the contents of the url declaration 68 | // group[4] will be undefined 69 | // if there is no other url to be processed, then group[1-3] will be undefined 70 | // group[4] will hold the entire string 71 | 72 | // console.log(group[2]); 73 | 74 | if(group[4] == null) { 75 | result += group[1]; 76 | 77 | var rawUrl = group[3].trim(); 78 | img = rawUrl 79 | .replace(rQuotes, "") 80 | .replace(rParams, ""); // remove query string/hash parmams in the filename, like foo.png?bar or foo.png#bar 81 | 82 | var test = true; 83 | if (opts.extensions) { //test for extensions if it provided 84 | var imgExt = img.split('.').pop(); 85 | if (typeof opts.extensions === 'function') { 86 | test = opts.extensions(imgExt, rawUrl); 87 | } else { 88 | test = opts.extensions.some(function (ext) { 89 | return (ext instanceof RegExp) ? ext.test(rawUrl) : (ext === imgExt); 90 | }); 91 | } 92 | } 93 | if (test && opts.exclude) { //test for extensions to exclude if it provided 94 | if (typeof opts.exclude === 'function') { 95 | test = !opts.exclude(rawUrl); 96 | } else { 97 | test = !opts.exclude.some(function (pattern) { 98 | return (pattern instanceof RegExp) ? pattern.test(rawUrl) : (rawUrl.indexOf(pattern) > -1); 99 | }); 100 | } 101 | } 102 | if (!test) { 103 | if (opts.debug) { 104 | console.log(img + ' skipped by extension or exclude filters'); 105 | } 106 | result += group[2]; 107 | return complete(); 108 | } 109 | // see if this img was already processed before... 110 | if(cache[img]) { 111 | // grunt.log.error("The image " + img + " has already been encoded elsewhere in your stylesheet. I'm going to do it again, but it's going to make your stylesheet a lot larger than it needs to be."); 112 | result = result += cache[img]; 113 | complete(); 114 | } else { 115 | // process it and put it into the cache 116 | var loc = img, 117 | is_local_file = !rData.test(img) && !rExternal.test(img); 118 | 119 | // Resolve the image path relative to the CSS file 120 | if (is_local_file) { 121 | // local file system.. fix up the path 122 | // loc = path.join(path.dirname(file.path), img); 123 | 124 | loc = opts.baseDir ? path.join(opts.baseDir, img) : 125 | path.join(path.dirname(file.path), img); 126 | 127 | // If that didn't work, try finding the image relative to 128 | // the current file instead. 129 | if(!fs.existsSync(loc)) { 130 | if (opts.debug) { 131 | console.log('in ' + loc + ' file doesn\'t exist'); 132 | } 133 | loc = path.join(file.cwd, img); 134 | } 135 | } 136 | 137 | // Test for scheme less URLs => "//example.com/image.png" 138 | if (!is_local_file && rSchemeless.test(loc)) { 139 | loc = 'http:' + loc; 140 | } 141 | 142 | exports.image(loc, opts, function (err, resp, cacheable) { 143 | if (err == null) { 144 | var url = "url(" + resp + ")"; 145 | result += url; 146 | 147 | if(cacheable !== false) { 148 | cache[img] = url; 149 | } 150 | 151 | if(deleteAfterEncoding && is_local_file) { 152 | if (opts.debug) { 153 | console.info("Deleting file: " + loc); 154 | } 155 | fs.unlinkSync(loc); 156 | } 157 | } else { 158 | result += group[2]; 159 | } 160 | 161 | complete(); 162 | }); 163 | } 164 | } else { 165 | result += group[4]; 166 | complete(); 167 | } 168 | }, 169 | function() { 170 | done(null, result); 171 | }); 172 | }; 173 | 174 | 175 | /** 176 | * Takes an image (absolute path or remote) and base64 encodes it. 177 | * 178 | * @param img Absolute, resolved path to an image 179 | * @param opts Options object 180 | * @return A data URI string (mime type, base64 img, etc.) that a browser can interpret as an image 181 | */ 182 | exports.image = function(img, opts, done) { 183 | 184 | // Shift args 185 | if(typeof opts === "function") { 186 | done = opts; 187 | opts = {}; 188 | } 189 | 190 | // Set default, helper-specific options 191 | opts = extend({ 192 | maxImageSize: 32768 193 | }, opts); 194 | 195 | var complete = function(err, encoded, cacheable) { 196 | // Too long? 197 | if(cacheable && encoded && opts.maxImageSize && encoded.length > opts.maxImageSize) { 198 | err = "Skipping " + img + " (greater than " + opts.maxImageSize + " bytes)"; 199 | } 200 | 201 | // Return the original source if an error occurred 202 | if(err) { 203 | // grunt.log.error(err); 204 | done(err, img, false); 205 | 206 | // Otherwise cache the processed image and return it 207 | } else { 208 | done(null, encoded, cacheable); 209 | } 210 | }; 211 | 212 | // Already base64 encoded? 213 | if(rData.test(img)) { 214 | complete(null, img, false); 215 | 216 | // External URL? 217 | } else if(rExternal.test(img)) { 218 | // grunt.log.writeln("Encoding file: " + img); 219 | fetch.image(img, function(err, src, cacheable) { 220 | var encoded, type; 221 | if (err == null) { 222 | type = mime.lookup(img); 223 | encoded = exports.getDataURI(type, src); 224 | } 225 | complete(err, encoded, cacheable); 226 | } ); 227 | 228 | // Local file? 229 | } else { 230 | // Does the image actually exist? 231 | if(!fs.existsSync(img) || !fs.lstatSync(img).isFile()) { 232 | // grunt.fail.warn("File " + img + " does not exist"); 233 | if (opts.debug) { 234 | console.warn("File " + img + " does not exist"); 235 | } 236 | complete(true, img, false); 237 | return; 238 | } 239 | 240 | // grunt.log.writeln("Encoding file: " + img); 241 | if (opts.debug) { 242 | console.info("Encoding file: " + img); 243 | } 244 | 245 | // Read the file in and convert it. 246 | var src = fs.readFileSync(img); 247 | var type = mime.lookup(img); 248 | var encoded = exports.getDataURI(type, src); 249 | complete(null, encoded, true); 250 | } 251 | }; 252 | 253 | 254 | /** 255 | * Base64 encodes an image and builds the data URI string 256 | * 257 | * @param mimeType Mime type of the image 258 | * @param img The source image 259 | * @return Data URI string 260 | */ 261 | exports.getDataURI = function(mimeType, img) { 262 | var ret = "data:"; 263 | ret += mimeType; 264 | ret += ";base64,"; 265 | ret += img.toString("base64"); 266 | return ret; 267 | }; 268 | 269 | return exports; 270 | })(); 271 | -------------------------------------------------------------------------------- /lib/fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Grunt Image Embed 3 | * https://github.com/ehynds/grunt-image-embed 4 | * 5 | * Copyright (c) 2012 Eric Hynds 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | // Node libs 10 | var request = require("request"); 11 | var stream = require("stream"); 12 | var buffers = require("buffers"); 13 | 14 | // Grunt export wrapper 15 | module.exports = (function () { 16 | "use strict"; 17 | 18 | var exports = {}; 19 | 20 | /** 21 | * Fetches a remote image. 22 | * 23 | * @param img Remote path, like http://url.to/an/image.png 24 | * @param done Function to call once done 25 | */ 26 | exports.image = function (url, done) { 27 | var resultBuffer; 28 | var buffList = buffers(); 29 | var imageStream = new stream.Stream(); 30 | 31 | imageStream.writable = true; 32 | imageStream.write = function (data) { buffList.push(new Buffer(data)); }; 33 | imageStream.end = function () { resultBuffer = buffList.toBuffer(); }; 34 | 35 | request(url, function (error, response/*, body*/) { 36 | if (error) { 37 | done("Unable to get " + url + ". Error: " + error.message); 38 | return; 39 | } 40 | 41 | // Bail if we get anything other than 200 42 | if (response.statusCode !== 200) { 43 | done("Unable to get " + url + " because the URL did not return an image. Status code " + response.statusCode + " received"); 44 | return; 45 | } 46 | 47 | done(null, resultBuffer, true); 48 | }).pipe(imageStream); 49 | }; 50 | 51 | return exports; 52 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-base64", 3 | "version": "0.1.3", 4 | "description": "gulp task to encode images to data URI", 5 | "main": "index.js", 6 | "homepage": "http://github.com/Wenqer/gulp-base64", 7 | "repository": "git://github.com/Wenqer/gulp-base64.git", 8 | "keywords": [ 9 | "gulpplugin", 10 | "data-uri", 11 | "base64" 12 | ], 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Yaroslav 2gis", 17 | "license": "MIT", 18 | "dependencies": { 19 | "through2": "~0.4.1", 20 | "extend": "~1.2.1", 21 | "async": "~0.2.10", 22 | "mime": "~1.2.11", 23 | "request": "~2.33.0", 24 | "buffers": "~0.1.1" 25 | } 26 | } 27 | --------------------------------------------------------------------------------