Source: image/index.js

/**
 * @name index.js<lib/image>
 * @author Kei Funagayama <funagayama_kei@cyberagent.co.jp>
 * copyright (c) Cyberagent Inc.
 * @overview image library
 */

var path = require('path');
var spawn = require('child_process').spawn;

var _ = require('lodash');

var Bucks = require('bucks');

var logger = require('../logger');
var constant = require('../constant');
var fsys = require('../fsys');

var imagemagick = require('./imagemagick');

var RESIZE_DEFAULT_OPTIONS = constant.IMAGE_RATIORESIZE_DEFAULT_OPTIONS;
var OPTIM_DEFAULT_OPTIONS = constant.IMAGE_OPTIM_DEFAULT_OPTIONS;
var REG_OPTIPNG_FILE = constant.REG_OPTIPNG_FILE;
var REG_JPEGOPTIM_FILE = constant.REG_JPEGOPTIM_FILE;


/**
 * @name image
 * @namespace image
 */
module.exports = {

    /**
     * imagemagick library
     * @name imagemagick
     * @memberof image
     */
    imagemagick: imagemagick,

    /**
     * optipng
     *
     * @name optipng
     * @memberof image
     * @method
     * @param {String} filepath PING file path.
     * @param {String} options optipng options.
     * @param {function} callback
     * @example
     * var filepath = './test/image/logo.png';
     */
    optipng: function (filepath, options, callback) {
        if (typeof options === 'function') {
            callback = options;
            options = undefined;
        }

        options = options || '';
        var args = _.compact([filepath].concat(options.replace(/\ \ /g, ' ').split(' ')));

        logger.debug('cmd:', constant.PROG_OPTIPNG, args.join(' '));

        var proc = spawn(constant.PROG_OPTIPNG, args);

        proc.stdout.on('data', function (data) {
            logger.debug(data.toString());
        });

        proc.stderr.on('data', function (data) {
            logger.debug(data.toString());
        });

        proc.addListener('exit', function (e) {
            if (e) {
                return callback(e);
            }
            callback();
        });
    },

    /**
     * jpegoptim
     *
     * @name jpegoptim
     * @memberof image
     * @method
     * @param {String} filepath JPEG file path.
     * @param {String} options jpegoptim options.
     * @param {function} callback
     * @example
     * var filepath = './test/image/logo.jpg';
     */
    jpegoptim: function (filepath, options, callback) {
        if (typeof options === 'function') {
            callback = options;
            options = undefined;
        }

        options = options || '--strip-all';

        var args = _.compact([filepath].concat(options.replace(/\ \ /g, ' ').split(' ')));

        logger.debug('cmd:', constant.PROG_JPEGOPTIM, args.join(' '));

        var proc = spawn(constant.PROG_JPEGOPTIM, args);
        var err = '';
        proc.stdin.pause();

        proc.stdout.on('data', function (data) {
            logger.debug(data.toString());
        });

        proc.stderr.on('data', function (data) {
            err += data;
            logger.debug(data.toString());
        });

        proc.addListener('exit', function (e) {
            proc.stdin.resume();
            if (e || err) {
                return callback(e || err);
            }
            callback();
        });
    },

    /**
     * pngquant
     *
     * @name pngquant
     * @memberof image
     * @method
     * @param {String} filepath PNG file path.
     * @param {String} options pngquant options.
     * @param {function} callback
     * @example
     * var filepath = './test/image/logo.png';
     */
    pngquant: function (filepath, options, callback) {
        if (typeof options === 'function') {
            callback = options;
            options = undefined;
        }

        options = options || '--ext .png -f -v'; // defualt) overwrite file.
        var args = _.compact([filepath].concat(options.replace(/\ \ /g, ' ').split(' ')));

        logger.debug('cmd:', constant.PROG_PNGQUANT, args.join(' '));

        var proc = spawn(constant.PROG_PNGQUANT, args);
        var err = '';
        proc.stdin.pause();

        proc.stdout.on('data', function (data) {
            logger.debug(data.toString());
        });

        proc.stderr.on('data', function (data) {
            err += data;
            logger.debug(data.toString());
        });

        proc.addListener('exit', function (e) {
            proc.stdin.resume();
            if (0 !== e) {
                return callback(e, err);
            }
            callback();
        });
    },

    /**
     *
     * @param {String} filepath Image file path.
     * @param {Object} options
     * @example
     * var filepath = './test/image/xxx.png'
     * var options = { extnames: ['png', 'jpg'], include: ['test/image'], exclude: [] }
     * var out = beezlib.image.isResizeImage(filepath, options)
     * console.log(out)
     * >> true
     * @returns {Boolean}
     */
    isResizeImage: function (filepath, options) {
        _.defaults(options || (options = {}), RESIZE_DEFAULT_OPTIONS);
        var extnames = options.extnames;
        var include = options.include;
        var exclude = options.exclude;
        var separator = options.separator;
        var extname = path.extname(filepath);
        var reg = new RegExp(separator + '\\d+\\' + extname + '$');
        if (!~extnames.indexOf(extname) || reg.test(filepath) || /^sprite/.test(path.basename(filepath))) {
            return false;
        }

        for (var i = 0; i < exclude.length; i++) {
            if (~filepath.indexOf(exclude[i])) {
                return false;
            }
        }

        return _.some(_.map(include, function (inc) {
            return ~filepath.indexOf(inc);
        }));
    },

    /**
     * run the optipng and jpegoptim
     *
     * @name optim
     * @memberof image
     * @method
     * @param {String} path JPEG|PNG file path.
     * @param {String} options options.
     * @param {function} callback
     * @example
     * var filepath = './test/image/logo.jpg';
     * var options = {
     *     optipng:   { use: true, options: "-o 2"},
     *     jpegoptim: { use: true, options: "--strip-all"}
     *     pngquant: { use: true, options: "--ext .png -f -v"}
     * }
     */
    optim: function (path, options, callback) {
        var self = this;

        if (typeof options === 'function') {
            callback = options;
            options = undefined;
        }

        _.defaults(options || (options = {}), OPTIM_DEFAULT_OPTIONS);

        var bucks = new Bucks();
        bucks.empty();

        if (REG_OPTIPNG_FILE.test(path) && options.optipng.use) { // optipng
            bucks.add(function(err, res, next) {
                self.optipng(path, options.optipng.options, function(err) {
                    if (err) {
                        logger.error('optipng error.', err);
                        next(err);
                        return;
                    }
                    logger.info('execute optipng:', path);
                    next();
                });
            });
        }

        if (REG_OPTIPNG_FILE.test(path) && options.pngquant.use) { // pngquant
            bucks.add(function(err, res, next) {
                self.pngquant(path, options.pngquant.options, function(err) { // pngquant

                    if (err) {
                        logger.error('pngquant error.', err);
                        next(err);
                        return;
                    }
                    logger.info('execute pngquant:', path);
                    next();

                });
            });
        }

        if (REG_JPEGOPTIM_FILE.test(path) && options.jpegoptim.use) { // jpegoptim
            bucks.add(function(err, res, next) {
                self.jpegoptim(path, options.jpegoptim.options, function(err) {

                    if (err) {
                        logger.error('jpegoptim error.', err);
                        next(err);
                        return;
                    }

                    logger.info('executejpegoptim:', path);
                    next();

                });
            });
        }


        if (bucks._tasks.length === 0) {
            logger.info('no match extension. path: ' + path);
        }

        bucks.end(function(err, ress) {
            callback && callback(err, path);
        });

    },

    /**
     * run the recursive directory of  optipng and jpegoptim
     *
     * @name optimdir
     * @memberof image
     * @method
     * @param {String} dirpath directory root path
     * @param {String} options options.
     * @param {function} callback
     * @example
     * var filepath = './test/image/logo.jpg';
     * var options = {
     *     optipng:   { use: true, options: "-o 2"},
     *     jpegoptim: { use: true, options: "--strip-all"}
     *     pngquant: { use: true, options: "--ext .png -f -v"},
     *     limit: 10
     * }
     */
    optimdir: function (dirpath, options, callback) {
        var self = this;

        if (typeof options === 'function') {
            callback = options;
            options = undefined;
        }

        if (!fsys.isDirectorySync(dirpath)) {
            logger.error('not a directory. path:', dirpath);
            callback && callback(new Error('not a directory. path: ' + dirpath));
            return;
        }

        var bucks = new Bucks();
        bucks.empty();

        var tasks = [];
        fsys.walk(dirpath, function filefn(prefix, dir, file, stats) {
            var src = path.join(dir, file);
            tasks.push(function (err, res, next) {
                self.optim(src, options, next);
            });

        });

        var range = options.limit || 10;
        var gtasks = [];

        for(var i = 0; i < Math.ceil(tasks.length / range); i++) {
            var j = i * range;
            var p = tasks.slice(j, j + range);
            gtasks.push(p);
        }

        logger.warn('tasks:', tasks.length); // all task num

        for (var k = 0; k < gtasks.length; k++) {
            bucks.parallel(gtasks[k]);
        }

        bucks.end(function(err, ress) {
            callback && callback(err, ress);
        });


    }
};