├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── DEPENDENCIES.md
├── README.md
├── example
├── Gruntfile.js
├── package.json
└── src
│ └── images
│ └── evolution_of_guybrush_threepwood.png
├── package.json
└── tasks
└── imageoptim.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: fold_left
2 | custom: ['https://www.paypal.me/foldleft']
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Explain how to reproduce a Bug
4 | ---
5 |
6 | ## Description
7 |
8 |
13 |
14 | ## Suggested Solution
15 |
16 |
20 |
21 | ## Help Needed
22 |
23 |
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | ---
5 |
6 | ## Description
7 |
8 |
14 |
15 | ## Suggested Solution
16 |
17 |
21 |
22 | ## Help Needed
23 |
24 |
28 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description (What)
2 |
3 |
9 |
10 | ## Justification (Why)
11 |
12 |
18 |
19 | ## How Can This Be Tested?
20 |
21 |
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.log.*
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .git*
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [1.4.4](https://github.com/JamieMason/grunt-imageoptim/compare/1.4.3...v1.4.4) (2017-07-09)
3 |
4 |
5 | ### Bug Fixes
6 |
7 | * **npm:** update dependencies ([0ed033e](https://github.com/JamieMason/grunt-imageoptim/commit/0ed033e))
8 |
9 |
10 |
11 |
12 | ## [1.4.3](https://github.com/JamieMason/grunt-imageoptim/compare/1.4.2...1.4.3) (2017-02-07)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * **cli:** update imageoptim-cli to 1.14.9 ([53ea333](https://github.com/JamieMason/grunt-imageoptim/commit/53ea333))
18 |
19 |
20 |
21 |
22 | ## [1.4.2](https://github.com/JamieMason/grunt-imageoptim/compare/1.4.1...1.4.2) (2017-01-12)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * **npm:** update dependencies ([28bcfc4](https://github.com/JamieMason/grunt-imageoptim/commit/28bcfc4))
28 |
29 |
30 |
31 |
32 | ## [1.4.1](https://github.com/JamieMason/grunt-imageoptim/compare/1.4.0...1.4.1) (2013-12-13)
33 |
34 |
35 |
36 |
37 | # [1.4.0](https://github.com/JamieMason/grunt-imageoptim/compare/1.3.13...1.4.0) (2013-10-12)
38 |
39 |
40 |
41 |
42 | ## [1.3.13](https://github.com/JamieMason/grunt-imageoptim/compare/1.2.12...1.3.13) (2013-09-22)
43 |
44 |
45 |
46 |
47 | ## [1.2.12](https://github.com/JamieMason/grunt-imageoptim/compare/1.2.11...1.2.12) (2013-08-28)
48 |
49 |
50 |
51 |
52 | ## [1.2.11](https://github.com/JamieMason/grunt-imageoptim/compare/1.2.10...1.2.11) (2013-08-10)
53 |
54 |
55 |
56 |
57 | ## [1.2.10](https://github.com/JamieMason/grunt-imageoptim/compare/1.2.6...1.2.10) (2013-08-04)
58 |
59 |
60 |
61 |
62 | ## [1.2.6](https://github.com/JamieMason/grunt-imageoptim/compare/1.2.4...1.2.6) (2013-06-13)
63 |
64 |
65 |
66 |
67 | ## [1.2.4](https://github.com/JamieMason/grunt-imageoptim/compare/1.2.2...1.2.4) (2013-06-10)
68 |
69 |
70 |
71 |
72 | ## [1.2.2](https://github.com/JamieMason/grunt-imageoptim/compare/1.0.0...1.2.2) (2013-06-10)
73 |
74 |
75 |
76 |
77 | # [1.0.0](https://github.com/JamieMason/grunt-imageoptim/compare/0.1.1...1.0.0) (2013-06-08)
78 |
79 |
80 |
81 |
82 | ## 0.1.1 (2013-05-26)
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/DEPENDENCIES.md:
--------------------------------------------------------------------------------
1 | # grunt-imageoptim
2 |
3 | Automate ImageOptim, ImageAlpha, and JPEGmini
4 |
5 | ## Installation
6 |
7 | Download node at [nodejs.org](http://nodejs.org) and install it, if you haven't already.
8 |
9 | ```sh
10 | npm install grunt-imageoptim --save
11 | ```
12 |
13 |
14 |
15 | ## Dependencies
16 |
17 | - [imageoptim-cli](https://github.com/JamieMason/ImageOptim-CLI): Automates ImageOptim, ImageAlpha, and JPEGmini for Mac to make batch optimisation of images part of your automated build process.
18 | - [q](https://github.com/kriskowal/q): A library for promises (CommonJS/Promises/A,B,D)
19 |
20 | ## Dev Dependencies
21 |
22 | - [xo](https://github.com/sindresorhus/xo): JavaScript happiness style linter ❤️
23 |
24 |
25 | ## License
26 |
27 | MIT
28 |
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # grunt-imageoptim
2 |
3 | > The companion [Grunt](http://gruntjs.com/) plugin for
4 | > [ImageOptim-CLI](http://jamiemason.github.io/ImageOptim-CLI/), which automates
5 | > batch optimisation of images with [ImageOptim](http://imageoptim.com),
6 | > [ImageAlpha](http://pngmini.com) and
7 | > [JPEGmini for Mac](http://jpegmini.com/mac).
8 |
9 | [](https://www.npmjs.com/package/grunt-imageoptim)
10 | [](https://www.npmjs.com/package/grunt-imageoptim)
11 | [](https://david-dm.org/JamieMason/grunt-imageoptim)
12 | [](https://github.com/JamieMason)
13 | [](https://twitter.com/fold_left)
14 |
15 | ## 🌩 Installation
16 |
17 | ```
18 | npm install grunt-imageoptim --save-dev
19 | ```
20 |
21 | ## 🔗 Dependencies
22 |
23 | Since this project automates three Mac Applications, you will need them to be
24 | installed on your machine for us to be able to reach them.
25 |
26 | - [ImageOptim](http://imageoptim.com)
27 | - [ImageAlpha](http://pngmini.com)
28 | - [JPEGmini for Mac](https://itunes.apple.com/us/app/jpegmini/id498944723) (App
29 | Store)
30 |
31 | A local copy of [ImageOptim-CLI](http://jamiemason.github.io/ImageOptim-CLI/)
32 | will be installed, you won't need to install that separately.
33 |
34 | ## ⚖️ Configuration
35 |
36 | As with all Grunt plugins, grunt-imageoptim is configured using a Gruntfile.js
37 | in the root of your project.
38 |
39 | Grunt provide a short
40 | [walkthrough of a sample Gruntfile](http://gruntjs.com/sample-gruntfile) which
41 | explains how they work, but the general structure is this;
42 |
43 | ```js
44 | module.exports = function(grunt) {
45 | grunt.initConfig({
46 | /* your grunt-imageoptim configuration goes here */
47 | });
48 | grunt.loadNpmTasks("grunt-imageoptim");
49 | };
50 | ```
51 |
52 | ### Use defaults
53 |
54 | Here we want to optimise two directories using default options.
55 |
56 | ```js
57 | imageoptim: {
58 | myTask: {
59 | src: ["www/images", "css/images"];
60 | }
61 | }
62 | ```
63 |
64 | ### Override defaults
65 |
66 | Here we want to optimise two directories using only ImageAlpha and ImageOptim,
67 | then close them once we're done.
68 |
69 | ```js
70 | imageoptim: {
71 | myTask: {
72 | options: {
73 | jpegMini: false,
74 | imageAlpha: true,
75 | quitAfter: true
76 | },
77 | src: ['www/images', 'css/images']
78 | }
79 | }
80 | ```
81 |
82 | ### Custom options for each task
83 |
84 | Here we have a task for a folder of PNGs and another for JPGs. Since we use
85 | ImageAlpha to optimise PNGs but not JPGs and vice versa with JPEGmini, here we
86 | toggle their availability between the two tasks.
87 |
88 | ```js
89 | imageoptim: {
90 | myPngs: {
91 | options: {
92 | jpegMini: false,
93 | imageAlpha: true,
94 | quitAfter: true
95 | },
96 | src: ['img/png']
97 | },
98 | myJpgs: {
99 | options: {
100 | jpegMini: true,
101 | imageAlpha: false,
102 | quitAfter: true
103 | },
104 | src: ['img/jpg']
105 | }
106 | }
107 | ```
108 |
109 | ### Option inheritance
110 |
111 | This example is equivalent to the _custom options for each task_ example, except
112 | we're setting some base options then overriding those we want to change within
113 | each task.
114 |
115 | ```js
116 | imageoptim: {
117 | options: {
118 | quitAfter: true
119 | },
120 | allPngs: {
121 | options: {
122 | imageAlpha: true,
123 | jpegMini: false
124 | },
125 | src: ['img/png']
126 | },
127 | allJpgs: {
128 | options: {
129 | imageAlpha: false,
130 | jpegMini: true
131 | },
132 | src: ['img/jpg']
133 | }
134 | }
135 | ```
136 |
137 | ## ⚖️ Configuration
138 |
139 | All options can be either `true` or `false` and default to `false`.
140 |
141 | - `quitAfter` Whether to exit each application after we're finished optimising
142 | your images.
143 | - `jpegMini` Whether to process your images using a copy of
144 | [JPEGmini.app](https://itunes.apple.com/us/app/jpegmini/id498944723) installed
145 | on your Mac.
146 | - `imageAlpha` Whether to process your images using a copy of
147 | [ImageAlpha.app](http://pngmini.com) installed on your Mac.
148 |
149 | ## 🙋🏾♀️ Getting Help
150 |
151 | - Get help with issues by creating a
152 | [Bug Report](https://github.com/JamieMason/grunt-imageoptim/issues/new?template=bug_report.md).
153 | - Discuss ideas by opening a
154 | [Feature Request](https://github.com/JamieMason/grunt-imageoptim/issues/new?template=feature_request.md).
155 |
--------------------------------------------------------------------------------
/example/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | grunt.initConfig({
3 | imageoptim: {
4 | imageAssets: {
5 | options: {
6 | jpegMini: false,
7 | imageAlpha: true,
8 | quitAfter: true
9 | },
10 | src: [
11 | 'src/images'
12 | ]
13 | }
14 | }
15 | });
16 | grunt.loadNpmTasks('grunt-imageoptim');
17 | };
18 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grunt-imageoptim-example",
3 | "description": "An example project using grunt-imageoptim",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "main": "Gruntfile.js",
7 | "private": true,
8 | "devDependencies": {
9 | "grunt": "1.0.1",
10 | "grunt-cli": "1.2.0",
11 | "grunt-imageoptim": ".."
12 | },
13 | "scripts": {
14 | "optimise-images": "grunt imageoptim"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/src/images/evolution_of_guybrush_threepwood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JamieMason/grunt-imageoptim/efe1d5d9e5c2b5256da4f8eac9dcedc594268842/example/src/images/evolution_of_guybrush_threepwood.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grunt-imageoptim",
3 | "description": "Automate ImageOptim, ImageAlpha, and JPEGmini",
4 | "version": "1.4.4",
5 | "author": "Jamie Mason (https://github.com/JamieMason)",
6 | "bugs": {
7 | "url": "https://github.com/JamieMason/grunt-imageoptim/issues"
8 | },
9 | "contributors": [
10 | "Jamie Mason (https://github.com/JamieMason)",
11 | "David Bushell (https://github.com/dbushell)",
12 | "Adam Simpson ((https://github.com/asimpson)"
13 | ],
14 | "dependencies": {
15 | "imageoptim-cli": "1.15.1",
16 | "q": "1.5.0"
17 | },
18 | "devDependencies": {
19 | "xo": "0.18.2"
20 | },
21 | "homepage": "https://github.com/JamieMason/grunt-imageoptim",
22 | "keywords": [
23 | "ImageOptim",
24 | "JPEGmini",
25 | "advpng",
26 | "build",
27 | "compress",
28 | "compression",
29 | "gifsicle",
30 | "gruntplugin",
31 | "images",
32 | "jpegoptim",
33 | "jpegtran",
34 | "optimization",
35 | "optimize",
36 | "optipng",
37 | "pngcrush",
38 | "pngout"
39 | ],
40 | "license": "MIT",
41 | "main": "tasks/imageoptim.js",
42 | "repository": "JamieMason/grunt-imageoptim",
43 | "scripts": {
44 | "lint": "xo --fix"
45 | },
46 | "xo": {
47 | "envs": [
48 | "node"
49 | ],
50 | "esnext": false,
51 | "space": 2
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tasks/imageoptim.js:
--------------------------------------------------------------------------------
1 | /*
2 | * grunt-imageoptim
3 | * https://github.com/JamieMason/grunt-imageoptim
4 | * Copyright © 2013 Jamie Mason, @fold_left,
5 | * https://github.com/JamieMason
6 | *
7 | * Permission is hereby granted, free of charge, to any person
8 | * obtaining a copy of this software and associated documentation files
9 | * (the "Software"), to deal in the Software without restriction,
10 | * including without limitation the rights to use, copy, modify, merge,
11 | * publish, distribute, sublicense, and/or sell copies of the Software,
12 | * and to permit persons to whom the Software is furnished to do so,
13 | * subject to the following conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be
16 | * included in all copies or substantial portions of the Software.
17 | *
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
22 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
23 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | * SOFTWARE.
26 | */
27 |
28 | module.exports = function (grunt) {
29 | 'use strict';
30 |
31 | var q = require('q');
32 | var path = require('path');
33 | var childProcess = require('child_process');
34 | var exec = childProcess.exec;
35 | var spawn = childProcess.spawn;
36 | var gruntFile = path.resolve();
37 | var taskName = 'imageoptim';
38 | var issuesPage = 'https://github.com/JamieMason/grunt-imageoptim/issues/new';
39 | var taskDescription = 'Losslessly compress images from the command line';
40 | var cliPaths = [
41 | '../node_modules/imageoptim-cli/bin',
42 | '../../imageoptim-cli/bin'
43 | ].map(function (dir) {
44 | return path.resolve(__dirname, dir);
45 | });
46 |
47 | (function () {
48 | var config = grunt.config.data.imageoptim || {};
49 | var tasks = config ? [config] : [];
50 | var hasDeprecatedConfig = false;
51 | var error = '';
52 |
53 | Object.keys(config).forEach(function (key) {
54 | var value = config[key];
55 | var isObject = value && typeof value === 'object';
56 | var isTask = isObject && key !== 'options' && key !== 'files';
57 | if (isTask) {
58 | tasks.push(value);
59 | }
60 | });
61 |
62 | hasDeprecatedConfig = tasks.some(function (task) {
63 | return 'files' in task && task.files.some(function (member) {
64 | return typeof member === 'string';
65 | });
66 | });
67 |
68 | if (hasDeprecatedConfig) {
69 | error += '\n';
70 | error += 'grunt-imageoptim 1.4.0 brought a change to it\'s configuration to bring full ';
71 | error += 'support for Grunt\'s file pattern matching.';
72 | error += '\n';
73 | error += 'In most cases all this means is renaming the "files" property to "src", but ';
74 | error += 'updated examples can be found at https://github.com/JamieMason/grunt-imageoptim.';
75 | grunt.fail.fatal(error);
76 | }
77 | })();
78 |
79 | /**
80 | * Get the ImageOptim-CLI Terminal command to be run for a given directory and task options
81 | * @param {String} directory
82 | * @param {Object} options
83 | * @return {String}
84 | */
85 |
86 | function getCommandByDirectory(directory, options) {
87 | var command = './imageOptim';
88 | command += options.quitAfter ? ' --quit' : '';
89 | command += options.imageAlpha ? ' --image-alpha' : '';
90 | command += options.jpegMini ? ' --jpeg-mini' : '';
91 | return command + ' --directory ' + directory.replace(/\s/g, '\\ ');
92 | }
93 |
94 | /**
95 | * @param {String} command
96 | * @param {String} cwd
97 | * @return {Promise}
98 | */
99 |
100 | function executeDirectoryCommand(command, cwd) {
101 | var deferred = q.defer();
102 | var errorMessage = 'ImageOptim-CLI exited with a failure status';
103 | var imageOptimCli = exec(command, {
104 | cwd: cwd
105 | });
106 |
107 | imageOptimCli.stdout.on('data', function (message) {
108 | console.log(String(message || '').replace(/\n+$/, ''));
109 | });
110 |
111 | imageOptimCli.on('exit', function (code) {
112 | return code === 0 ? deferred.resolve(true) : deferred.reject(new Error(errorMessage));
113 | });
114 |
115 | return deferred.promise;
116 | }
117 |
118 | /**
119 | * @param {String[]} files Array of paths to directories from src: in config.
120 | * @param {Boolean} opts.jpegMini Whether to run JPEGmini.app.
121 | * @param {Boolean} opts.imageAlpha Whether to run ImageAlpha.app.
122 | * @param {Boolean} opts.quitAfter Whether to quit apps after running.
123 | * @return {Promise}
124 | */
125 |
126 | function processDirectories(files, opts) {
127 | return files.map(function (directory) {
128 | return getCommandByDirectory(directory, opts);
129 | }).reduce(function (promise, command) {
130 | return promise.then(function () {
131 | return executeDirectoryCommand(command, opts.cliPath);
132 | });
133 | }, q());
134 | }
135 |
136 | /**
137 | * include necessary command line flags based on the current task's options
138 | * @param {Object} opts
139 | * @return {Array}
140 | */
141 |
142 | function getCliFlags(opts) {
143 | var cliOptions = [];
144 | if (opts.quitAfter) {
145 | cliOptions.push('--quit');
146 | }
147 | if (opts.imageAlpha) {
148 | cliOptions.push('--image-alpha');
149 | }
150 | if (opts.jpegMini) {
151 | cliOptions.push('--jpeg-mini');
152 | }
153 | return cliOptions;
154 | }
155 |
156 | /**
157 | * @param {String[]} files Array of paths to files from src: in config.
158 | * @param {Boolean} opts.jpegMini Whether to run JPEGmini.app.
159 | * @param {Boolean} opts.imageAlpha Whether to run ImageAlpha.app.
160 | * @param {Boolean} opts.quitAfter Whether to quit apps after running.
161 | * @return {Promise}
162 | */
163 |
164 | function processFiles(files, opts) {
165 | var imageOptimCli;
166 | var deferred = q.defer();
167 | var errorMessage = 'ImageOptim-CLI exited with a failure status';
168 |
169 | imageOptimCli = spawn('./imageOptim', getCliFlags(opts), {
170 | cwd: opts.cliPath
171 | });
172 |
173 | imageOptimCli.stdout.on('data', function (message) {
174 | console.log(String(message || '').replace(/\n+$/, ''));
175 | });
176 |
177 | imageOptimCli.on('exit', function (code) {
178 | return code === 0 ? deferred.resolve(true) : deferred.reject(new Error(errorMessage));
179 | });
180 |
181 | imageOptimCli.stdin.setEncoding('utf8');
182 | imageOptimCli.stdin.end(files.join('\n') + '\n');
183 |
184 | return deferred.promise;
185 | }
186 |
187 | /**
188 | * @param {String} str "hello"
189 | * @return {String} "Hello"
190 | */
191 |
192 | function firstUp(str) {
193 | return str.charAt(0).toUpperCase() + str.slice(1);
194 | }
195 |
196 | /**
197 | * @param {String} fileType "file" or "Dir"
198 | * @return {Function}
199 | */
200 |
201 | function isFileType(fileType) {
202 | var methodName = 'is' + firstUp(fileType);
203 | return function (file) {
204 | return grunt.file[methodName](file);
205 | };
206 | }
207 |
208 | /**
209 | * Ensure the ImageOptim-CLI binary is accessible
210 | * @return {String}
211 | */
212 |
213 | function getPathToCli() {
214 | return cliPaths.filter(function (cliPath) {
215 | return grunt.file.exists(cliPath);
216 | })[0];
217 | }
218 |
219 | /**
220 | * Convert a relative path to an absolute file system path
221 | * @param {String} relativePath
222 | * @return {String}
223 | */
224 |
225 | function toAbsolute(relativePath) {
226 | return path.resolve(gruntFile, relativePath);
227 | }
228 |
229 | /**
230 | * Given a collection of files to be run in a task, seperate the files from the directories to
231 | * handle them in their own way.
232 | * @param {String} fileType "dir" or "file"
233 | * @param {String} cliPath
234 | * @param {Object} options
235 | * @param {Array} taskFiles
236 | * @param {Promise} promise
237 | * @return {Promise}
238 | */
239 |
240 | function processBatch(fileType, cliPath, options, taskFiles, promise) {
241 | var files = taskFiles.filter(isFileType(fileType)).map(toAbsolute);
242 | var processor = fileType === 'dir' ? processDirectories : processFiles;
243 | return files.length === 0 ? promise : promise.then(function () {
244 | return processor(files, {
245 | cliPath: cliPath,
246 | jpegMini: options.jpegMini,
247 | imageAlpha: options.imageAlpha,
248 | quitAfter: options.quitAfter
249 | });
250 | });
251 | }
252 |
253 | grunt.registerMultiTask(taskName, taskDescription, function () {
254 | var task = this;
255 | var done = task.async();
256 | var promise = q();
257 | var cliPath = getPathToCli();
258 | var options = task.options({
259 | jpegMini: false,
260 | imageAlpha: false,
261 | quitAfter: false
262 | });
263 |
264 | if (!cliPath) {
265 | throw new Error('Unable to locate ImageOptim-CLI. Please raise issue at ' + issuesPage);
266 | }
267 |
268 | task.files.forEach(function (file) {
269 | promise = processBatch('file', cliPath, options, file.src, promise);
270 | promise = processBatch('dir', cliPath, options, file.src, promise);
271 | });
272 |
273 | promise.done(done);
274 | });
275 | };
276 |
--------------------------------------------------------------------------------