UNPKG

pngjs-image

Version:

JavaScript-based PNG image encoder, decoder, and manipulator

505 lines (416 loc) 10.7 kB
// 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;