yahoo/pngjs-image

View on GitHub
index.js

Summary

Maintainability
A
1 hr
Test Coverage
// Copyright 2014-2015, Yahoo! Inc.
// Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms.

var fs = require('fs'),
    _ = require('underscore'),
    PNG = require('pngjs').PNG,
    pixel = require('./lib/pixel'),
    modify = require('./lib/modify'),
    conversion = require('./lib/conversion'),
    filters = require('./lib/filters'),
    streamBuffers = require("stream-buffers"),
    MemoryStream = require('./lib/memoryStream'),
    request = require('request');

var Decoder = require('./lib/png/decoder');
var Encoder = require('./lib/png/encoder');

/**
 * PNGjs-image class
 *
 * @class PNGImage
 * @submodule Core
 * @param {PNG} image png-js object
 * @constructor
 */
function PNGImage (image) {
    image.on('error', function (err) {
        PNGImage.log(err.message);
    });
    this._image = image;
}

/**
 * Project version
 *
 * @property version
 * @static
 * @type {string}
 */
PNGImage.version = require('./package.json').version;


/**
 * Filter dictionary
 *
 * @property filters
 * @static
 * @type {object}
 */
PNGImage.filters = {};

/**
 * Sets a filter to the filter list
 *
 * @method setFilter
 * @param {string} key
 * @param {function} [fn]
 */
PNGImage.setFilter = function (key, fn) {
    if (fn) {
        this.filters[key] = fn;
    } else {
        delete this.filters[key];
    }
};


/**
 * Creates an image by dimensions
 *
 * @static
 * @method createImage
 * @param {int} width
 * @param {int} height
 * @return {PNGImage}
 */
PNGImage.createImage = function (width, height) {
    var image = new PNG({
        width: width,
        height: height
    });
    return new PNGImage(image);
};


/**
 * Copies an already existing image
 *
 * @static
 * @method copyImage
 * @param {PNGImage} image
 * @return {PNGImage}
 */
PNGImage.copyImage = function (image) {
    var newImage = this.createImage(image.getWidth(), image.getHeight());
    image.getImage().bitblt(newImage.getImage(), 0, 0, image.getWidth(), image.getHeight(), 0, 0);
    return newImage;
};


/**
 * Reads an image from the filesystem
 *
 * @static
 * @method readImage
 * @param {string} path Url or file-path
 * @param {function} fn
 * @return {PNGImage}
 */
PNGImage.readImage = function (path, fn) {
    if (path.indexOf('http') === 0) {
        return this._readImageFromUrl(path, fn);
    } else {
        return this._readImageFromFs(path, fn);
    }
};
PNGImage._readImageFromFs = function (filename, fn) {
    var image = new PNG(),
        resultImage = new PNGImage(image);

    fn = fn || function () {};

    fs.createReadStream(filename).once('error', function(err) {
        fn(err, undefined);
    }).pipe(image).once('parsed', function () {
        image.removeListener('error', fn);
        fn(undefined, resultImage);
    }).once('error', function (err) {
        image.removeListener('parsed', fn);
        fn(err, resultImage);
    }).pipe(image);

    return resultImage;
};
PNGImage._readImageFromUrl = function (url, fn) {

    var stream, req;

    request.head(url, function (err, res) {

        var contentType = (res.headers['content-type'] || '').toLowerCase();

        if (contentType !== 'image/png') {
            fn(new Error('Unsupported image format: ' + contentType));

        } else {

            stream = new MemoryStream({size: res.headers['content-length']});
            req = request(url).pipe(stream);

            req.on('error', function (err) {
                fn(err);
            });

            req.on('finish', function () {
                var buffer = stream.getBuffer();

                PNGImage.loadImage(buffer, fn);
            });
        }
    });

    return null; // This will be deprecated
};

/**
 * Reads an image from the filesystem synchronously
 *
 * @static
 * @method readImageSync
 * @param {string} filename
 * @return {PNGImage}
 */
PNGImage.readImageSync = function (filename) {
    return this.loadImageSync(fs.readFileSync(filename));
};


/**
 * Loads an image from a blob
 *
 * @static
 * @method loadImage
 * @param {Buffer} blob
 * @param {function} fn
 * @return {PNGImage}
 */
PNGImage.loadImage = function (blob, fn) {
    var image = new PNG(),
        resultImage = new PNGImage(image);

    fn = fn || function () {};

    image.once('error', function (err) {
        fn(err, resultImage);
    });
    image.parse(blob, function () {
        image.removeListener('error', fn);
        fn(undefined, resultImage);
    });

    return resultImage;
};


/**
 * Loads an image synchronously from a blob
 *
 * @static
 * @method loadImageSync
 * @param {Buffer} blob
 * @return {PNGImage}
 */
PNGImage.loadImageSync = function (blob) {
    var decoder,
        data,
        headerChunk,
        width, height;

    decoder = new Decoder();
    data = decoder.decode(blob, { strict: false });

    headerChunk = decoder.getHeaderChunk();
    width = headerChunk.getWidth();
    height = headerChunk.getHeight();

    var image = new PNG({
        width: width,
        height: height
    });
    data.copy(image.data, 0, 0, data.length);
    return new PNGImage(image);
};

/**
 * Log method that can be overwritten to modify the logging behavior
 *
 * @static
 * @method log
 * @param {string} text
 */
PNGImage.log = function (text) {
    // Nothing yet; Overwrite this when needed
};

PNGImage.prototype = {

    /**
     * Gets the original png-js object
     *
     * @method getImage
     * @return {PNG}
     */
    getImage: function () {
        return this._image;
    },

    /**
     * Gets the image as a blob
     *
     * @method getBlob
     * @return {Buffer}
     */
    getBlob: function () {
        return this._image.data;
    },


    /**
     * Gets the width of the image
     *
     * @method getWidth
     * @return {int}
     */
    getWidth: function () {
        return this._image.width;
    },

    /**
     * Gets the height of the image
     *
     * @method getHeight
     * @return {int}
     */
    getHeight: function () {
        return this._image.height;
    },


    /**
     * Clips the current image by modifying it in-place
     *
     * @method clip
     * @param {int} x Starting x-coordinate
     * @param {int} y Starting y-coordinate
     * @param {int} width Width of area relative to starting coordinate
     * @param {int} height Height of area relative to starting coordinate
     */
    clip: function (x, y, width, height) {

        var image;

        width = Math.min(width, this.getWidth() - x);
        height = Math.min(height, this.getHeight() - y);

        if ((width < 0) || (height < 0)) {
            throw new Error('Width and height cannot be negative.');
        }

        image = new PNG({
            width: width, height: height
        });

        this._image.bitblt(image, x, y, width, height, 0, 0);
        this._image = image;
    },

    /**
     * Fills an area with the specified color
     *
     * @method fillRect
     * @param {int} x Starting x-coordinate
     * @param {int} y Starting y-coordinate
     * @param {int} width Width of area relative to starting coordinate
     * @param {int} height Height of area relative to starting coordinate
     * @param {object} color
     * @param {int} [color.red] Red channel of color to set
     * @param {int} [color.green] Green channel of color to set
     * @param {int} [color.blue] Blue channel of color to set
     * @param {int} [color.alpha] Alpha channel for color to set
     * @param {float} [color.opacity] Opacity of color
     */
    fillRect: function (x, y, width, height, color) {

        var i,
            iLen = x + width,
            j,
            jLen = y + height,
            index;

        for (i = x; i < iLen; i++) {
            for (j = y; j < jLen; j++) {
                index = this.getIndex(i, j);
                this.setAtIndex(index, color);
            }
        }
    },


    /**
     * Applies a list of filters to the image
     *
     * @method applyFilters
     * @param {string|object|object[]} filters Names of filters in sequence `{key:<string>, options:<object>}`
     * @param {boolean} [returnResult=false]
     * @return {PNGImage}
     */
    applyFilters: function (filters, returnResult) {

        var image,
            newFilters;

        // Convert to array
        if (_.isString(filters)) {
            filters = [filters];
        } else if (!_.isArray(filters) && _.isObject(filters)) {
            filters = [filters];
        }

        // Format array as needed by the function
        newFilters = [];
        (filters || []).forEach(function (filter) {

            if (_.isString(filter)) {
                newFilters.push({key: filter, options: {}});

            } else if (_.isObject(filter)) {
                newFilters.push(filter);
            }
        });
        filters = newFilters;

        // Process filters
        image = this;
        (filters || []).forEach(function (filter) {

            var currentFilter = PNGImage.filters[filter.key];

            if (!currentFilter) {
                throw new Error('Unknown filter ' + filter.key);
            }

            filter.options = filter.options || {};
            filter.options.needsCopy = !!returnResult;

            image = currentFilter(this, filter.options);

        }.bind(this));

        // Overwrite current image, or just returning it
        if (!returnResult) {
            this._image = image.getImage();
        }

        return image;
    },


    /**
     * Gets index of a specific coordinate
     *
     * @method getIndex
     * @param {int} x X-coordinate of pixel
     * @param {int} y Y-coordinate of pixel
     * @return {int} Index of pixel
     */
    getIndex: function (x, y) {
        return (this.getWidth() * y) + x;
    },


    /**
     * Writes the image to the filesystem
     *
     * @method writeImage
     * @param {string} filename Path to file
     * @param {function} fn Callback
     */
    writeImage: function (filename, fn) {

        fn = fn || function () {};

        this._image.pack().pipe(fs.createWriteStream(filename)).once('close', function () {
            this._image.removeListener('error', fn);
            fn(undefined, this);
        }.bind(this)).once('error', function (err) {
            this._image.removeListener('close', fn);
            fn(err, this);
        }.bind(this));
    },

    writeImageSync: function (filename) {
        fs.writeFileSync(filename, this.toBlobSync());
    },

    toBlobSync: function (options) {
        var encoder = new Encoder();
        return encoder.encode(this.getBlob(), this.getWidth(), this.getHeight(), options);
    },

    /**
     * Writes the image to a buffer
     *
     * @method toBlob
     * @param {function} fn Callback
     */
    toBlob: function (fn) {

        var writeBuffer = new streamBuffers.WritableStreamBuffer({
            initialSize: (100 * 1024), incrementAmount: (10 * 1024)
        });

        fn = fn || function () {};

        this._image.pack().pipe(writeBuffer).once('close', function () {
            this._image.removeListener('error', fn);
            fn(undefined, writeBuffer.getContents());
        }.bind(this)).once('error', function (err) {
            this._image.removeListener('close', fn);
            fn(err);
        }.bind(this));
    }
};
PNGImage.prototype.constructor = PNGImage;


// Add standard methods to the prototype
_.extend(PNGImage.prototype, pixel);
_.extend(PNGImage.prototype, modify);
_.extend(PNGImage.prototype, conversion);


// Adds all standard filters
_.keys(filters).forEach(function (key) {
    PNGImage.setFilter(key, filters[key]);
});

/**
 * Instruments the node environment so that PNG files can be loaded through require calls
 *
 * @static
 * @method instrument
 */
PNGImage.instrument = function () {
    require.extensions['.png'] = function(module, filename) {
        var image = PNGImage.readImageSync(filename);
        module.exports = image;
    };
};

PNGImage.Decoder = Decoder;
PNGImage.Encoder = Encoder;

module.exports = PNGImage;