Permalink
Browse files

initial checkin

  • Loading branch information...
0 parents commit de302fd9793a6e4cc98b844b4d0a6e56fc8a2fe6 @ehynds ehynds committed Sep 12, 2012
2 .gitignore
@@ -0,0 +1,2 @@
+node_modules/
+example/css/output.css
53 README.md
@@ -0,0 +1,53 @@
+This task converts all images found within a stylersheet (those within a `url( ... )` declaration) into base64-encoded data URI strings.
+
+# Features:
+
+* Supports both local & remote images.
+* Ability to specify a size limit. Default is 32kb, or IE8's limit.
+* Existing data URIs will be ignored.
+* Skip specific images by specifying a directive comment.
+* Includes two helpers: `encode_stylesheet` to encode a stylesheet, and `encode_image` to encode an image.
+
+# Getting Started
+
+Install this plugin with: `grunt install grunt-image-embed`
+
+Next, add this line to your project's `grunt.js` file:
+
+`grunt.loadNpmTasks("grunt-image-embed");`
+
+Lastly, add the configuration settings to your grunt.js file.
+
+# Documentation
+
+This task has two required properties, `src` and `dest`. `src` is the path to your stylesheet and `dest` is the file this task will write to (relative to the grunt.js file). If this file already exists **it will be overwritten**.
+
+An example configuration looks like this:
+
+```` javascript
+grunt.initConfig({
+ imageEmbed: {
+ dist: {
+ src: [ "css/styles.css" ],
+ dest: "css/output.css"
+ }
+ }
+});
+````
+
+## Optional Configuration Properties
+
+ImageEmbed can be customized by specifying the following options:
+
+* `maxImageSize`: The maximum size of the base64 string in bytes. This defaults to `32768`, or IE8's limit. Set this to `0` to remove the limit and allow any size string.
+* `baseDir`: If you have absolute image paths in your stylesheet, the path specified in this option will be used as the base directory.
+
+## Skipping Images
+
+Specify that an image should be skipped by adding the following comment *after* the image:
+
+`background: url(image.gif); /*ImageEmbed:skip*/`
+
+# Known Issues
+
+* Only one image per line can be read at the moment, so run this task before minifying your CSS file.
2 bin/grunt-image-embed
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require("grunt").npmTasks("grunt-image-embed").cli();
0 example/README.md
No changes.
15 example/css/styles.css
@@ -0,0 +1,15 @@
+body {
+ /* local images */
+ background-image: url('../images/test.png');
+
+ /* existing base64 images will be skipped */
+ background-image: url('data:image/png;base64,lolol');
+
+ /* skip images using a comment directive */
+ background-image: url(images/test.gif); /*ImageEmbed:skip*/
+
+ /* external images work too */
+ background-image: url('http://www.placehold.it/400x300');
+
+ /* not broken ones though. */
+}
23 example/grunt.js
@@ -0,0 +1,23 @@
+/*global module:false*/
+module.exports = function(grunt) {
+ "use strict";
+
+ grunt.loadTasks("../tasks");
+
+ grunt.initConfig({
+ imageEmbed: {
+ dist: {
+ src: [ "css/styles.css" ],
+ dest: "css/output.css",
+
+ // Specify a max image size. Default is 32768 (32kb is IE8's limit).
+ // maxImageSize: 0,
+
+ // Base directory if you use absolute paths in your stylesheet
+ // baseDir: "/Users/ehynds/projects/grunt-image-embed/"
+ }
+ }
+ });
+
+ grunt.registerTask("default", "imageEmbed");
+};
BIN example/images/test.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 grunt.js
@@ -0,0 +1,37 @@
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ test: {
+ files: ["test/**/*.js"]
+ },
+ lint: {
+ files: ["grunt.js", "tasks/**/*.js", "test/**/*.js"]
+ },
+ watch: {
+ files: "<config:lint.files>",
+ tasks: "default"
+ },
+ jshint: {
+ options: {
+ curly: true,
+ eqeqeq: true,
+ immed: true,
+ latedef: true,
+ newcap: true,
+ noarg: true,
+ sub: true,
+ undef: true,
+ boss: true,
+ eqnull: true,
+ node: true,
+ es5: true
+ },
+ globals: {}
+ }
+ });
+
+ grunt.loadTasks("tasks");
+
+ grunt.registerTask("default", "lint test");
+};
33 package.json
@@ -0,0 +1,33 @@
+{
+ "name" : "grunt-image-embed",
+ "description" : "Grunt task for embedding images as base64 data URIs inside your stylesheets.",
+ "version" : "0.0.1",
+ "homepage" : "",
+ "author" : {
+ "name" : "Eric Hynds",
+ "email" : "ehynds@gmail.com",
+ "url" : "http://erichynds.com"
+ },
+ "repository" : {
+ "type" : "git",
+ "url" : "git://github.com/ehynds/grunt-image-embed.git"
+ },
+ "bugs" : {
+ "url" : "https://github.com/ehynds/grunt-image-embed/issues"
+ },
+ "main" : "grunt.js",
+ "bin" : "bin/grunt-image-embed",
+ "engines" : {
+ "node" : "*"
+ },
+ "scripts" : {
+ "test" : "grunt test"
+ },
+ "dependencies" : {
+ "grunt" : "0.3.x",
+ "mime" : "1.2.x"
+ },
+ "keywords" : [
+ "gruntplugin"
+ ]
+}
253 tasks/grunt-image-embed.js
@@ -0,0 +1,253 @@
+module.exports = function(grunt) {
+ "use strict";
+
+ // Node modules
+ var fs = require("fs");
+ var path = require("path");
+ var mime = require("mime");
+ var url = require("url");
+ var http = require("http");
+
+ // Grunt utils
+ var utils = grunt.utils;
+ var file = grunt.file;
+ var _ = utils._;
+ var async = utils.async;
+
+ // Cache regex's
+ var rImages = /(?:url\()(.*)(?:\))(?!(?:.*)\/\*\s*ImageEmbed:skip\s*\*\/)/i;
+ var rExternal = /^http/;
+ var rData = /^data:/;
+ var rQuotes = /['"]/g;
+
+ // Cache of already converted images
+ var cache = {};
+
+ grunt.registerMultiTask("imageEmbed", "Base64 encode stylesheet", function() {
+ var opts = this.data;
+ var src = this.file.src;
+ var dest = this.file.dest;
+ var tasks, done, srcFiles;
+
+ if(!src) {
+ grunt.fatal("Missing src property.");
+ }
+
+ if(!dest) {
+ grunt.fatal("Missing dest property");
+ }
+
+ done = this.async();
+
+ // Process each src file
+ tasks = file.expandFiles(src).map(function(srcFile) {
+ return function(callback) {
+ grunt.helper("encode_stylesheet", srcFile, opts, callback);
+ };
+ });
+
+ // Once all files have been processed write them out.
+ async.parallel(tasks, function(err, output) {
+ grunt.file.write(dest, output);
+ grunt.log.writeln('File "' + dest + '" created.');
+ done();
+ });
+ });
+
+ /**
+ * Takes a CSS file as input, goes through it line by line, and base64
+ * encodes any images it finds.
+ *
+ * @param srcFile Relative or absolute path to a source stylesheet file.
+ * @param opts Options object
+ * @param done Function to call once encoding has finished.
+ */
+ grunt.registerHelper("encode_stylesheet", function(srcFile, opts, done) {
+ // Shift args if no options object is specified
+ if(utils.kindOf(opts) === "function") {
+ done = opts;
+ opts = {};
+ }
+
+ var src = file.read(srcFile);
+ var result, match, img, line, tasks;
+
+ tasks = src.split(grunt.utils.linefeed).map(function(line) {
+ var result = rImages.exec(line);
+
+ return function(callback) {
+ if(!result) {
+ callback(null, line);
+ return;
+ }
+
+ // The original image value
+ match = result[0];
+
+ // Image value with trailing whitespace/quotes removed
+ img = result[1].trim().replace(rQuotes, "");
+
+ // Check to see if this image has already been converted, and if
+ // so, warn the user.
+ if(cache[img]) {
+ 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.");
+ }
+
+ // Use image from cache if this image has been processed already
+ if(cache[img]) {
+ callback(null, line.replace(match, cache[img]));
+
+ // Otherwise encode the image
+ } else {
+ var loc = img;
+
+ // Resolve the image path relative to the CSS file
+ if(!rData.test(img) && !rExternal.test(img)) {
+ loc = img.charAt(0) === "/" ?
+ (opts.baseDir || "") + loc :
+ path.join(path.dirname(srcFile), img);
+
+ // If that didn't work, try finding the image relative to
+ // the current file instead.
+ if(!fs.existsSync(loc)) {
+ loc = path.resolve(__dirname + img);
+ }
+ }
+
+ grunt.helper("encode_image", loc, opts, function(err, resp) {
+ resp = "url(" + resp + ")";
+ callback(err, line.replace(match, resp));
+ });
+ }
+ };
+ });
+
+ // Once each line has been processed...
+ async.series(tasks, function(err, result) {
+ done(err, result.join("\n"));
+ });
+ });
+
+ /**
+ * Takes an image (absolute path or remote) and base64 encodes it.
+ *
+ * @param img Absolute, resolved path to an image
+ * @param opts Options object
+ * @return A data URI string (mime type, base64 img, etc.) that a browser can interpret as an image
+ */
+ grunt.registerHelper("encode_image", function(img, opts, done) {
+ // Shift args
+ if(utils.kindOf(opts) === "function") {
+ done = opts;
+ opts = {};
+ }
+
+ // Set default, helper-specific options
+ opts = _.extend({
+ maxImageSize: 32768
+ }, opts);
+
+ var complete = function(err, encoded, cacheable) {
+ // Too long?
+ if(encoded && opts.maxImageSize && encoded.length > opts.maxImageSize) {
+ err = "Image " + img + " not encoded because it is larger than " + opts.maxImageSize + " bytes";
+ }
+
+ // Return the original source if an error occurred
+ if(err) {
+ grunt.log.error(err);
+ done(null, img);
+
+ // Otherwise cache the processed image and return it
+ } else {
+ if(cacheable !== false) {
+ cache[img] = encoded;
+ }
+
+ done(null, encoded);
+ }
+ };
+
+ // Already base64 encoded?
+ if(rData.test(img)) {
+ complete(null, img, false);
+
+ // External URL?
+ } else if(rExternal.test(img)) {
+ fetchImage(img, complete);
+
+ // Local file?
+ } else {
+ // Does the image actually exist?
+ if(!fs.existsSync(img)) {
+ grunt.fail.warn(img + " does not exist");
+ complete(null, img, false);
+ return;
+ }
+
+ grunt.log.writeln("Encoding file: " + img);
+
+ // Read the file in and convert it.
+ fs.readFile(img, function(err, src) {
+ var type = mime.lookup(img);
+ var encoded = encode(type, src);
+ complete(err, encoded);
+ });
+ }
+ });
+
+ /**
+ * Fetches a remote image and encodes it.
+ *
+ * @param img Remote path, like http://url.to/an/image.png
+ * @param done Function to call once done
+ */
+ function fetchImage(img, done) {
+ var opts = url.parse(img);
+
+ var req = http.request(opts, function(res) {
+ res.setEncoding("binary");
+
+ var mime = res.headers["content-type"];
+ var data = "";
+
+ // Bail if we get anything other than 200
+ if(res.statusCode !== 200) {
+ done("Unable to convert " + img + " because the URL did not return an image. Staus code " + res.statusCode + " received");
+ return;
+ }
+
+ res.on("data", function(chunk) {
+ data += chunk;
+ });
+
+ res.on("end", function() {
+ data = new Buffer(data, "binary");
+ done(null, encode(mime, data));
+ });
+ });
+
+ req.on("error", function(err) {
+ done("Unable to convert " + img + ". Error: " + err.code);
+ });
+
+ req.end();
+ }
+
+ /**
+ * Base64 encodes an image and builds the data URI string
+ *
+ * @param mimeType Mime type of the image
+ * @param img The source image
+ * @return Data URI string
+ */
+ function encode(mimeType, img) {
+ var ret = "data:";
+ ret += mimeType;
+ ret += ";base64,";
+ ret += img.toString("base64");
+ return ret;
+ }
+
+
+};
BIN test/images/test.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/images/test.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/images/test.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 test/test.css
@@ -0,0 +1,26 @@
+body {
+ /* local images */
+ background-image: url(images/test.gif);
+ background-image: url("images/test.jpg");
+ background-image: url('images/test.png');
+ background-image: url( 'images/test.png' );
+ background-image: url( "images/test.png" );
+
+ /* this will be skipped */
+ background-image: url(images/test.gif); /*Base64:SKIP*/
+ background-image: url(images/test.gif); /* Base64:SKIP */
+ background-image: url(images/test.gif);/* Base64:SKIP */
+
+ /* images after a skip will still work though */
+ background-image: url(images/test.gif);
+
+ /* existing data URI's should not be encoded */
+ background-image: url(data:image/png;base64,lol);
+ background-image: url("data:image/png;base64,lol");
+ background-image: url('data:image/png;base64,lol');
+ background-image: url( 'data:image/png;base64,lol' );
+
+ /* external images */
+ background-image: url(http://www.placehold.it/400x300);
+ background-image: url(http://placehold.it/400x300);
+}
44 test/test.js
@@ -0,0 +1,44 @@
+var grunt = require("grunt");
+var task = require("../tasks/grunt-image-embed");
+var async = grunt.utils.async;
+
+// TODO: write more tests
+
+exports.base64stylesheet = {
+ setUp: function(done) {
+ done();
+ },
+
+ encode_image_helper: function(test) {
+ test.expect(3);
+
+ var tests = [
+ {
+ input: __dirname + "/images/test.gif",
+ output: "data:image/gif;base64,R0lGODlhAQABAPcAAMzMzJaWlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjNEQTlEQTcyRjEzQjExRTE5NEIzQjMwOTc5NTREMzQ2IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjNEQTlEQTczRjEzQjExRTE5NEIzQjMwOTc5NTREMzQ2Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6M0RBOURBNzBGMTNCMTFFMTk0QjNCMzA5Nzk1NEQzNDYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6M0RBOURBNzFGMTNCMTFFMTk0QjNCMzA5Nzk1NEQzNDYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4B//79/Pv6+fj39vX08/Lx8O/u7ezr6uno5+bl5OPi4eDf3t3c29rZ2NfW1dTT0tHQz87NzMvKycjHxsXEw8LBwL++vby7urm4t7a1tLOysbCvrq2sq6qpqKempaSjoqGgn56dnJuamZiXlpWUk5KRkI+OjYyLiomIh4aFhIOCgYB/fn18e3p5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhYF9eXVxbWllYV1ZVVFNSUVBPTk1MS0pJSEdGRURDQkFAPz49PDs6OTg3NjU0MzIxMC8uLSwrKikoJyYlJCMiISAfHh0cGxoZGBcWFRQTEhEQDw4NDAsKCQgHBgUEAwIBAAAh+QQAAAAAACwAAAAAAQABAAAIBAABBAQAOw=="
+ },
+ {
+ input: "http://www.placehold.it/10x10",
+ output: "data:image/gif;base64,R0lGODdhCgAKAOMAAMzMzJaWlrGxsZycnKOjo6qqqr6+vsXFxbe3twAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAACgAKAAAEHxDISau9mIpqBhAehQTAQA5EIZGB1x1ruQUCAQTklkUAOw=="
+ },
+ {
+ input: "data:image/gif;base64,R0lGODdhCgAKAOMAAMzMzJaWlrGxsZycnKOjo6qqqr6+vsXFxbe3twAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAACgAKAAAEHxDISau9mIpqBhAehQTAQA5EIZGB1x1ruQUCAQTklkUAOw==",
+ output: "data:image/gif;base64,R0lGODdhCgAKAOMAAMzMzJaWlrGxsZycnKOjo6qqqr6+vsXFxbe3twAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAACgAKAAAEHxDISau9mIpqBhAehQTAQA5EIZGB1x1ruQUCAQTklkUAOw=="
+ }
+ ];
+
+ // Create a bunch of functions for each test above
+ var fns = tests.map(function(obj) {
+ return function(complete) {
+ grunt.helper("encode_image", obj.input, function(err, str) {
+ test.equal(str, obj.output);
+ complete();
+ });
+ };
+ });
+
+ // Run tests
+ async.parallel(fns, test.done);
+ }
+
+};
BIN test/test.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit de302fd

Please sign in to comment.