yahoo/pngjs-image

View on GitHub
lib/png/chunks/tRNS.js

Summary

Maintainability
D
2 days
Test Coverage
// Copyright 2015 Yahoo! Inc.
// Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms.

var colorTypes = require('../utils/constants').colorTypes;

/**
 * @class tRNS
 * @module PNG
 * @submodule PNGChunks
 *
 * Options:
 * - transparent {boolean} - Should this chunk be applied to the image? (default: true)
 */
module.exports = {

    /**
     * Gets the sequence
     *
     * @method getSequence
     * @return {int}
     */
    getSequence: function () {
        return 300;
    },


    /**
     * Searches the alpha values of a color
     *
     * @method findAlpha
     * @param {int} red
     * @param {int} green
     * @param {int} blue
     * @return {int|null}
     */
    findAlpha: function (red, green, blue) {

        if (!this._lookup) {
            this._createLookUpTable();
        }

        return this._lookup[red + '_' + green + '_' + blue];
    },

    /**
     * Creates a look-up table for faster finds
     *
     * @method _createLookUpTable
     * @private
     */
    _createLookUpTable: function () {

        var i, len,
            lookup = {},
            colors = this.getColors();

        for (i = 0, len = colors.length; i < len; i++) {
            lookup[colors[i].red + '_' + colors[i].green + '_' + colors[i].blue] = colors[i].alpha;
        }

        this._lookup = lookup;
    },


    /**
     * Gets all transparent colors
     *
     * @method getColors
     * @return {object[]}
     */
    getColors: function () {
        return this._colors || [];
    },

    /**
     * Sets all transparent colors
     *
     * @method setColors
     * @return {object[]}
     */
    setColors: function (colors) {
        this._colors = colors;
        this._lookup = null;
    },


    /**
     * Parsing of chunk data
     *
     * Phase 1
     *
     * @method parse
     * @param {BufferedStream} stream Data stream
     * @param {int} length Length of chunk data
     * @param {boolean} strict Should parsing be strict?
     * @param {object} options Decoding options
     */
    parse: function (stream, length, strict, options) {

        var headerChunk,
            paletteChunk,
            colorType,
            color,
            colors = [],
            i,
            maxLength;

        // Validation
        if (!this.getFirstChunk('IHDR', false) === null) {
            throw new Error('Chunk ' + this.getType() + ' requires the IHDR chunk.');
        }

        if (strict && (this.getFirstChunk(this.getType(), false) !== null)) {
            throw new Error('Only one ' + this.getType() + ' is allowed in the data.');
        }

        headerChunk = this.getHeaderChunk();
        colorType = headerChunk.getColorType();

        // Check for valid palette color-types
        if (strict && headerChunk.hasAlphaChannel()) {
            throw new Error('A transparency chunk is not allowed to appear with this color-type: ' + colorType);
        }

        switch (colorType) {

            case colorTypes.GREY_SCALE:

                for (i = 0; i < length; i += 2) {
                    color = stream.readUInt16BE();
                    colors.push({
                        red: color,
                        green: color,
                        blue: color,
                        alpha: 0
                    });
                }
                break;

            case colorTypes.TRUE_COLOR:

                for (i = 0; i < length; i += 6) {
                    colors.push({
                        red: stream.readUInt16BE(),
                        green: stream.readUInt16BE(),
                        blue: stream.readUInt16BE(),
                        alpha: 0
                    });
                }
                break;

            case colorTypes.INDEXED_COLOR: // Indexed-Color

                paletteChunk = this.getFirstChunk('PLTE', true);

                // Cut-off length if it goes over in non-strict mode - losing some colors alpha channel
                if (strict) {
                    maxLength = length;
                } else {
                    maxLength = Math.max(length, paletteChunk.getColors().length);
                }

                if (maxLength > paletteChunk.getColors().length) {
                    throw new Error('There are more transparent colors than available in the palette.');
                }

                for (i = 0; i < maxLength; i++) {
                    color = paletteChunk.getColors()[i];
                    color.alpha = stream.readUInt8();
                    colors.push(color);
                }
                break;
        }

        this.setColors(colors);
    },

    /**
     * Decoding of chunk data before scaling
     *
     * Phase 2
     *
     * @method decode
     * @param {Buffer} image
     * @param {boolean} strict Should parsing be strict?
     * @param {object} options Decoding options
     * @param {boolean} [options.transparent=false] Apply transparency?
     * @return {Buffer}
     */
    decode: function (image, strict, options) {

        var i, len, index,
            r, g, b, alpha,
            headerChunk,
            isIndexed,
            paletteChunk,
            colors;

        if (options.transparent || (options.transparent === undefined)) {
            headerChunk = this.getHeaderChunk();
            isIndexed = headerChunk.isColorTypeIndexedColor();

            if (isIndexed) {
                paletteChunk = this.getFirstChunk('PLTE', true);
                colors = paletteChunk.getColors();
            }

            for (i = 0, len = image.length; i < len; i += 8) {

                index = image.readUInt16BE(i, false);
                if (isIndexed) {

                    alpha = colors[index].alpha;
                    if (alpha !== undefined) {
                        image.writeUInt16BE(alpha, i + 6);
                    }

                } else { // All other types

                    r = index;
                    g = image.readUInt16BE(i + 2, false);
                    b = image.readUInt16BE(i + 4, false);

                    alpha = this.findAlpha(r, g, b);

                    if (alpha !== undefined) {
                        image.writeUInt16BE(alpha, i + 6);
                    }
                }
            }
        }

        return image;
    },

    /**
     * Gathers chunk-data from decoded chunks
     *
     * Phase 5
     *
     * @static
     * @method decodeData
     * @param {object} data Data-object that will be used to export values
     * @param {boolean} strict Should parsing be strict?
     * @param {object} options Decoding options
     */
    decodeData: function (data, strict, options) {

        var chunks = this.getChunksByType(this.getType());

        if (!chunks) {
            return ;
        }

        if (strict && (chunks.length !== 1)) {
            throw new Error('Not more than one chunk allowed for ' + this.getType() + '.');
        }

        data.volatile = data.volatile || {};
        data.volatile.transparentColors = chunks[0].getColors();
    },


    /**
     * Returns a list of chunks to be added to the data-stream
     *
     * Phase 1
     *
     * @static
     * @method encodeData
     * @param {Buffer} image Image data
     * @param {object} options Encoding options
     * @return {Chunk[]} List of chunks to encode
     */
    encodeData: function (image, options) {

        if (options.transparentColors) {

            var chunk = this.createChunk(this.getType(), this.getChunks());

            chunk.setColors(options.transparentColors);

            return [chunk];
        } else {
            return [];
        }
    },

    /**
     * Composing of chunk data
     *
     * Phase 4
     *
     * @method compose
     * @param {BufferedStream} stream Data stream
     * @param {object} options Encoding options
     */
    compose: function (stream, options) {

        var headerChunk, paletteChunk,
            colorType, color,
            i,
            colors = this.getColors(),
            length = colors.length;

        headerChunk = this.getHeaderChunk();
        colorType = headerChunk.getColorType();

        // Check for valid palette color-types
        if (headerChunk.hasAlphaChannel()) {
            throw new Error('A transparency chunk is not allowed to appear with this color-type: ' + colorType);
        }

        switch (colorType) {

            case colorTypes.GREY_SCALE:
                for (i = 0; i < length; i++) {
                    stream.writeUInt16BE(colors[i].red);
                }
                break;

            case colorTypes.TRUE_COLOR:
                for (i = 0; i < length; i++) {
                    stream.writeUInt16BE(colors[i].red);
                    stream.writeUInt16BE(colors[i].green);
                    stream.writeUInt16BE(colors[i].blue);
                }
                break;

            case colorTypes.INDEXED_COLOR: // Indexed-Color

                paletteChunk = this.getFirstChunk('PLTE', true);
                for (i = 0; i < length; i++) {
                    color = paletteChunk.findColor(colors[i]);
                    if (color === null) {
                        throw new Error('Cannot find color in palette.');
                    }
                    stream.writeUInt8(color);
                }
                break;
        }
    }
};