<repo_name>Teissere/chrome-manifest-iconify<file_sep>/README.md
# chrome-manifest-iconify

[![NPM version](https://img.shields.io/npm/v/chrome-manifest-iconify.svg)](https://www.npmjs.com/package/chrome-manifest-iconify)
[![node](https://img.shields.io/node/v/chrome-manifest-iconify.svg)](https://www.npmjs.com/package/chrome-manifest-iconify)
[![Build Status](https://travis-ci.org/Steven-Roberts/chrome-manifest-iconify.svg?branch=master)](https://travis-ci.org/Steven-Roberts/chrome-manifest-iconify)
[![dependencies Status](https://david-dm.org/Steven-Roberts/chrome-manifest-iconify/status.svg)](https://david-dm.org/Steven-Roberts/chrome-manifest-iconify)
[![devDependencies Status](https://david-dm.org/Steven-Roberts/chrome-manifest-iconify/dev-status.svg)](https://david-dm.org/Steven-Roberts/chrome-manifest-iconify?type=dev)
[![Test Coverage](https://codeclimate.com/github/Steven-Roberts/chrome-manifest-iconify/badges/coverage.svg)](https://codeclimate.com/github/Steven-Roberts/chrome-manifest-iconify/coverage)
[![Code Climate](https://codeclimate.com/github/Steven-Roberts/chrome-manifest-iconify/badges/gpa.svg)](https://codeclimate.com/github/Steven-Roberts/chrome-manifest-iconify)

When creating a Chrome extension or app, you need to provide a set of icons for
context menus, browser actions, page actions, and the Chrome Web Store. Usually,
these are just resized versions of the same image. The goal of
chrome-manifest-iconify is to intelligently handle the tedious process of
generated all these resized clones. All you need to do is provide it a master
icon and [v2 manifest](https://developer.chrome.com/extensions/manifest) file.
It will parse the manifest to determine the sizes, names, types, and paths of
the icons it needs to generate. You can choose from several resizing algorithms
as provide by [JIMP](https://github.com/oliver-moran/jimp) so your entire icon
set looks awesome.

## Installation

```shell
npm install --save-dev chrome-manifest-iconify
```

## Gulp

Instead of directly using this API, you might find it easier to use the
[Gulp](https://github.com/gulpjs/gulp) plugin
[gulp-chrome-manifest-iconify](https://github.com/Steven-Roberts/gulp-chrome-manifest-iconify)
for your project.

## API

<!-- Disable linting for the markdown generated by jsdoc-to-markdown -->
<!-- markdownlint-disable-->
<a name="module_chrome-manifest-iconify"></a>

### chrome-manifest-iconify
The chrome-manifest-iconify module

**Example**
```js
const chromeManifestIconify = require('chrome-manifest-iconify');

chromeManifestIconify.async({
    manifest: 'src/manifest.json',
    masterIcon: 'master.png',
    resizeMode: chromeManifestIconify.ResizeMode.HERMITE
}).then((icons) => {
    // Do stuff with icons
    icons.forEach((i) => console.log(i.toString()));
}).catch((err) => {
    // Oh, no! Something bad happened
    console.log(err);
});
```

* [chrome-manifest-iconify](#module_chrome-manifest-iconify)
    * [.Icon](#module_chrome-manifest-iconify.Icon)
        * [new Icon(size, path, jimpData, contents)](#new_module_chrome-manifest-iconify.Icon_new)
        * [.size](#module_chrome-manifest-iconify.Icon.Icon+size) : <code>number</code>
        * [.path](#module_chrome-manifest-iconify.Icon.Icon+path) : <code>string</code>
        * [.contents](#module_chrome-manifest-iconify.Icon.Icon+contents) : <code>Buffer</code>
        * [.mimeType](#module_chrome-manifest-iconify.Icon+mimeType) : <code>string</code>
        * [.toString()](#module_chrome-manifest-iconify.Icon+toString) ⇒ <code>string</code>
    * [.ResizeMode](#module_chrome-manifest-iconify.ResizeMode)
    * [.async(options)](#module_chrome-manifest-iconify.async) ⇒ [<code>Promise.&lt;Icon&gt;</code>](#module_chrome-manifest-iconify.Icon)

<a name="module_chrome-manifest-iconify.Icon"></a>

#### chrome-manifest-iconify.Icon
Class representing a Chrome extension or app icon

**Kind**: static class of [<code>chrome-manifest-iconify</code>](#module_chrome-manifest-iconify)

* [.Icon](#module_chrome-manifest-iconify.Icon)
    * [new Icon(size, path, jimpData, contents)](#new_module_chrome-manifest-iconify.Icon_new)
    * [.size](#module_chrome-manifest-iconify.Icon.Icon+size) : <code>number</code>
    * [.path](#module_chrome-manifest-iconify.Icon.Icon+path) : <code>string</code>
    * [.contents](#module_chrome-manifest-iconify.Icon.Icon+contents) : <code>Buffer</code>
    * [.mimeType](#module_chrome-manifest-iconify.Icon+mimeType) : <code>string</code>
    * [.toString()](#module_chrome-manifest-iconify.Icon+toString) ⇒ <code>string</code>

<a name="new_module_chrome-manifest-iconify.Icon_new"></a>

##### new Icon(size, path, jimpData, contents)
Create an Icon


| Param | Type | Description |
| --- | --- | --- |
| size | <code>string</code> \| <code>number</code> | The size of the Icon in pixels |
| path | <code>string</code> | The file path to the Icon |
| jimpData | <code>object</code> | The JIMP image data |
| contents | <code>Buffer</code> | A Buffer of the Icon data |

<a name="module_chrome-manifest-iconify.Icon.Icon+size"></a>

##### icon.size : <code>number</code>
The size of the Icon in pixels

**Kind**: instance property of [<code>Icon</code>](#module_chrome-manifest-iconify.Icon)
**Read only**: true
<a name="module_chrome-manifest-iconify.Icon.Icon+path"></a>

##### icon.path : <code>string</code>
The file path to the Icon

**Kind**: instance property of [<code>Icon</code>](#module_chrome-manifest-iconify.Icon)
**Read only**: true
<a name="module_chrome-manifest-iconify.Icon.Icon+contents"></a>

##### icon.contents : <code>Buffer</code>
A Buffer of the Icon data

**Kind**: instance property of [<code>Icon</code>](#module_chrome-manifest-iconify.Icon)
**Read only**: true
<a name="module_chrome-manifest-iconify.Icon+mimeType"></a>

##### icon.mimeType : <code>string</code>
Gets the MIME type of the Icon

**Kind**: instance property of [<code>Icon</code>](#module_chrome-manifest-iconify.Icon)
<a name="module_chrome-manifest-iconify.Icon+toString"></a>

##### icon.toString() ⇒ <code>string</code>
Gets a human-friendly string representation of the Icon

**Kind**: instance method of [<code>Icon</code>](#module_chrome-manifest-iconify.Icon)
**Returns**: <code>string</code> - A string representation of the Icon
<a name="module_chrome-manifest-iconify.ResizeMode"></a>

#### chrome-manifest-iconify.ResizeMode
Enum for resize algorithms

**Kind**: static enum of [<code>chrome-manifest-iconify</code>](#module_chrome-manifest-iconify)
**Read only**: true
**Properties**

| Name | Default |
| --- | --- |
| NEAREST_NEIGHBOR | <code>jimp.RESIZE_NEAREST_NEIGHBOR</code> |
| BILINEAR | <code>jimp.RESIZE_BILINEAR</code> |
| BICUBIC | <code>jimp.RESIZE_BICUBIC</code> |
| HERMITE | <code>jimp.RESIZE_HERMITE</code> |
| BEZIER | <code>jimp.RESIZE_BEZIER</code> |

<a name="module_chrome-manifest-iconify.async"></a>

#### chrome-manifest-iconify.async(options) ⇒ [<code>Promise.&lt;Icon&gt;</code>](#module_chrome-manifest-iconify.Icon)
Generates icon set for a Chrome extension or app by parsing the v2 manifest.
Note that this function does not actually write the files.

**Kind**: static method of [<code>chrome-manifest-iconify</code>](#module_chrome-manifest-iconify)
**Returns**: [<code>Promise.&lt;Icon&gt;</code>](#module_chrome-manifest-iconify.Icon) - A promise that
resolves with the generated Icons

| Param | Type | Default | Description |
| --- | --- | --- | --- |
| options | <code>object</code> |  | The options for generating the Icons |
| options.manifest | <code>string</code> |  | The path to the v2 manifest.json |
| options.masterIcon | <code>string</code> \| <code>Buffer</code> |  | Either a path or Buffer of the master icon from which all the generated icons will be reseized |
| [options.resizeMode] | [<code>ResizeMode</code>](#module_chrome-manifest-iconify.ResizeMode) | <code>ResizeMode.BILINEAR</code> | The algorithm for resizing the master Icon |


<!-- markdownlint-enable-->
<file_sep>/index.js
'use strict';

/**
 * The chrome-manifest-iconify module
 * @module chrome-manifest-iconify
 * @example
 * const chromeManifestIconify = require('chrome-manifest-iconify');
 *
 * chromeManifestIconify.async({
 *     manifest: 'src/manifest.json',
 *     masterIcon: 'master.png',
 *     resizeMode: chromeManifestIconify.ResizeMode.HERMITE
 * }).then((icons) => {
 *     // Do stuff with icons
 *     icons.forEach((i) => console.log(i.toString()));
 * }).catch((err) => {
 *     // Oh, no! Something bad happened
 *     console.log(err);
 * });
 */

// Local imports
const Manifest = require('./lib/manifest');
const Icon = require('./lib/icon');
const ResizeMode = require('./lib/resizeMode');

// Node imports
const Promise = require('bluebird');
const _ = require('lodash');

exports.Icon = Icon;
exports.ResizeMode = ResizeMode;

/**
 * Generates icon set for a Chrome extension or app by parsing the v2 manifest.
 * Note that this function does not actually write the files.
 * @param {object} options - The options for generating the Icons
 * @param {string} options.manifest - The path to the v2 manifest.json
 * @param {string|Buffer} options.masterIcon - Either a path or Buffer of the
 * master icon from which all the generated icons will be reseized
 * @param {module:chrome-manifest-iconify.ResizeMode}
 * [options.resizeMode=ResizeMode.BILINEAR] - The algorithm for resizing the
 * master Icon
 * @returns {Promise<module:chrome-manifest-iconify.Icon>} A promise that
 * resolves with the generated Icons
 */
exports.async = (options) => {
    // Validate options object
    if (!_.isObject(options)) {
        throw new TypeError('Options must be an object');
    }

    // Return a promise of the Icons
    return Promise.join(
        Manifest.load(options.manifest),
        Icon.load(options.masterIcon),
        (manifest, masterIcon) =>
            manifest.getIcons(masterIcon, options.resizeMode)
    );
};
<file_sep>/test/test.js
'use strict';

const chromeManifestIconify = require('../');
const Promise = require('bluebird');
const readFile = Promise.promisify(require('fs').readFile);
const path = require('path');
const jimp = require('jimp');

// <NAME>
const chai = require('chai');

chai.use(require('chai-as-promised'));
chai.use(require('chai-string'));
chai.should();


const getPath = path.join.bind(null, __dirname);
const getIconPath = getPath.bind(null, 'icons');
const getManifestPath = getPath.bind(null, 'manifests');

describe('chrome-manifest-iconify', () => {
    describe('#async', () => {
        const async = chromeManifestIconify.async;

        it(
            'should require an options object',
            () => {
                async.should.throw(TypeError, 'Options must be an object');
            }
        );

        it(
            'should not fail for valid resize mode',
            () => {
                const fn = () => async({
                    manifest: getManifestPath('minimal.json'),
                    masterIcon: getIconPath('test-icon.png'),
                    resizeMode: chromeManifestIconify.ResizeMode.HERMITE
                });

                fn.should.not.throw(Error);
            }
        );

        it(
            'should require a manifest path be provided in options',
            () => {
                const fn = () => async({
                    masterIcon: getIconPath('test-icon.png')
                });

                fn.should.throw(Error, 'The manifest path must be a string');
            }
        );

        it(
            'should require a master icon be provided in options',
            () => {
                const fn = () => async({
                    manifest: getManifestPath('minimal.json')
                });

                fn.should.throw(Error,
                    'The icon must be a file path or Buffer');
            }
        );

        it(
            'should reject promise when the manifest does not exist',
            () => async({
                manifest: getManifestPath('does-not-exist.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(Error, new RegExp('ENOENT: ' +
                'no such file or directory, open \'.*does-not-exist.json\''))
        );

        it(
            'should reject promise when the master icon does not exist',
            () => async({
                manifest: getManifestPath('minimal.json'),
                masterIcon: getIconPath('does-not-exist.jpg')
            }).should.eventually.be.rejectedWith(Error, new RegExp('ENOENT: ' +
                'no such file or directory, open \'.*does-not-exist.jpg\''))
        );

        it(
            'should reject promise when the master icon is not square',
            () => async({
                manifest: getManifestPath('minimal.json'),
                masterIcon: getIconPath('not-square.png')
            }).should.eventually.be.rejectedWith(Error,
                'The icon has size 128x64 which is not square')
        );

        it(
            'should resolve promise when the master icon is a valid buffer',
            () => readFile(getIconPath('test-icon.png'))
                .then((masterIconBuffer) => async({
                    manifest: getManifestPath('minimal.json'),
                    masterIcon: masterIconBuffer
                }))
                .should.eventually.be.fulfilled
        );

        it(
            'should reject promise when the manifest contains malformed JSON',
            () => async({
                manifest: getManifestPath('malformed.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(SyntaxError,
                'Unexpected token')
        );

        it(
            'should reject promise when the manifest contains an invalid ' +
                'icon extension',
            () => async({
                manifest: getManifestPath('invalid-extension.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(Error,
                'icon-128.invalid has no MIME type')
        );

        it(
            'should reject promise when the manifest has a non-positve icon' +
                'size',
            () => async({
                manifest: getManifestPath('negative-size.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(Error,
                'The icon size -42 is not a positive integer')
        );

        it(
            'should reject promise when the manifest has a NaN icon size',
            () => async({
                manifest: getManifestPath('nan-size.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(Error,
                'The icon size abc is not a positive integer')
        );

        it(
            'should reject promise when the manifest has an invalid icon path',
            () => async({
                manifest: getManifestPath('invalid-path.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(Error,
                'Path must be a string. Received 3.14')
        );

        it(
            'should reject promise when the manifest has two icons with the ' +
                'same path but different sizes',
            () => async({
                manifest: getManifestPath('same-path-different-size.json'),
                masterIcon: getIconPath('test-icon.png')
            }).should.eventually.be.rejectedWith(Error, 'The manifest ' +
                'contains icons with sizes 128 and 16 from the same path ' +
                'icon.png')
        );

        it(
            'should resolve promise with icons for a valid manifest',
            () => async({
                manifest: getManifestPath('manifest.json'),
                masterIcon: getIconPath('test-icon.png')
            })
                .then((icons) => {
                    icons.should.be.an('array').with.length(4);
                    icons.forEach((i) => i.should.have.property('contents')
                        .that.has.length.at.least(0));

                    icons[0].toString().should.startWith(`Icon \
${getManifestPath('icon-16.png')} of size 16x16 with data `);
                    icons[1].toString().should.startWith(`Icon \
${getManifestPath('icon-128.bmp')} of size 128x128 with data `);
                    icons[2].toString().should.startWith(`Icon \
${getManifestPath('a', 'icon-19.png')} of size 19x19 with data `);
                    icons[3].toString().should.startWith(`Icon \
${getManifestPath('img.jpg')} of size 38x38 with data `);

                    return Promise.map(icons, (i) => jimp.read(i.contents));
                })
                .then((icons) => {
                    icons[0].bitmap.width.should.equal(16);
                    icons[0].bitmap.height.should.equal(16);
                    icons[0].getMIME().should.equal('image/png');

                    icons[1].bitmap.width.should.equal(128);
                    icons[1].bitmap.height.should.equal(128);
                    icons[1].getMIME().should.equal('image/bmp');

                    icons[2].bitmap.width.should.equal(19);
                    icons[2].bitmap.height.should.equal(19);
                    icons[2].getMIME().should.equal('image/png');

                    icons[3].bitmap.width.should.equal(38);
                    icons[3].bitmap.height.should.equal(38);
                    icons[3].getMIME().should.equal('image/jpeg');
                })
        );

        it(
            'should resolve promise when manifest has duplicate icon',
            () => async({
                manifest: getManifestPath('duplicate-icon.json'),
                masterIcon: getIconPath('test-icon.png'),
                resizeMode: chromeManifestIconify.ResizeMode.NEAREST_NEIGHBOR
            })
                .then((icons) => {
                    icons.should.be.an('array').with.length(1);
                    icons[0].toString().should.startWith(`Icon \
${getManifestPath('icon.png')} of size 19x19 with data `);

                    return jimp.read(icons[0].contents);
                })
                .then((icon) => {
                    icon.bitmap.width.should.equal(19);
                    icon.bitmap.height.should.equal(19);
                    icon.getMIME().should.equal('image/png');
                })
        );
    });
});
<file_sep>/lib/manifest.js
'use strict';

const Promise = require('bluebird');
const readFile = Promise.promisify(require('fs').readFile);
const _ = require('lodash');
const pathUtil = require('path');

const iconManifestPaths = [
    'icons',
    'browser_action.default_icon',
    'page_action.default_icon'
];

/**
 * Class representing a Chrome extension manifest file
 */
class Manifest {

    /**
     * A static helper function that loads a Manifest from a file
     * @param {string} path The path from which to load the Manifest
     * @returns {Promise} A Promise that resolves when the Manifest loads
     */
    static load (path) {
        if (!_.isString(path)) {
            throw new TypeError('The manifest path must be a string');
        }

        return readFile(path)
            .then((data) => new Manifest(path, JSON.parse(data)));
    }

    /**
     * Create a Manifest
     * @param {string} path The path at which the Manifest is located
     * @param {object} content - The Manifest data
     */
    constructor (path, content) {
        this.path = path;
        this.content = content;
    }

    /**
     * Generated properly resized icons by parsing the Manifest contents
     * @param {module:chrome-manifest-iconify.Icon} masterIcon The Icon from
     * which the others will be generated
     * @param {module:chrome-manifest-iconify.ResizeMode}
     * [resizeMode=ResizeMode.BILINEAR] - The algorithm for resizing the master
     * Icon
     * @returns {Promise<Icon>} A promise that resolves when the Icons are
     * created
     */
    getIcons (masterIcon, resizeMode) {
        const icons = _(iconManifestPaths)
            .flatMap((manifestPath) => _.map(_.get(this.content, manifestPath),
                (path, size) => ({
                    path,
                    size
                })))
            .uniqWith((i1, i2) => {
                const samePath = i1.path === i2.path;
                const sameSize = i1.size === i2.size;

                if (samePath && !sameSize) {
                    throw Error(`The manifest contains icons with sizes \
${i1.size} and ${i2.size} from the same path ${i1.path}`);
                }

                return samePath && sameSize;
            })
            .map((i) => masterIcon.resize(
                i.size,
                pathUtil.join(pathUtil.dirname(this.path), i.path),
                resizeMode
            ))
            .value();

        return Promise.all(icons);
    }

}

module.exports = Manifest;
<file_sep>/lib/resizeMode.js
'use strict';

const jimp = require('jimp');

/**
 * Enum for resize algorithms
 * @readonly
 * @enum
 * @memberof module:chrome-manifest-iconify
 */
const ResizeMode = {
    NEAREST_NEIGHBOR: jimp.RESIZE_NEAREST_NEIGHBOR,
    BILINEAR: jimp.RESIZE_BILINEAR,
    BICUBIC: jimp.RESIZE_BICUBIC,
    HERMITE: jimp.RESIZE_HERMITE,
    BEZIER: jimp.RESIZE_BEZIER
};

module.exports = Object.freeze(ResizeMode);
<file_sep>/lib/icon.js
'use strict';

const mimeTypes = require('mime-types');
const Promise = require('bluebird');
const jimp = require('jimp');
const _ = require('lodash');
const nodePath = require('path');

/**
 * Class representing a Chrome extension or app icon
 * @memberof module:chrome-manifest-iconify
 */
class Icon {

    /**
     * Create an Icon
     * @ignore
     * @private
     * @param {string|number} size - The size of the Icon in pixels
     * @param {string} path - The file path to the Icon
     * @param {object} jimpData - The JIMP image data
     * @param {Buffer} contents - A Buffer of the Icon data
     */
    constructor (size, path, jimpData, contents) {
        // Assign the params as class properties
        /**
         * The size of the Icon in pixels
         * @readonly
         * @type {number}
         */
        this.size = parseInt(size, 10);
        if (isNaN(this.size) || this.size <= 0) {
            throw new Error(`The icon size ${size} is not a positive integer`);
        }

        /**
         * The file path to the Icon
         * @readonly
         * @type {string}
         */
        this.path = path;

        /**
         * @private
         * The JIMP image data
         * @readonly
         * @type {object}
         */
        this.jimpData = jimpData;

        /**
         * A Buffer of the Icon data
         * @readonly
         * @type {Buffer}
         */
        this.contents = contents;
    }

    /**
     * A static helper function that loads an Icon from a file
     * @private
     * @param {string|Buffer} file - Either a path or Buffer of the Icon
     * @returns {Promise<Icon>} A promise that resolves when the Icon loads
     */
    static load (file) {
        let path = '';
        let contents = null;

        if (_.isString(file)) {
            path = file;
        } else if (Buffer.isBuffer(file)) {
            contents = file;
        } else {
            throw new TypeError('The icon must be a file path or Buffer');
        }

        return jimp.read(file)
            .then((jimpData) => {
                const width = jimpData.bitmap.width;
                const height = jimpData.bitmap.height;

                if (width !== height) {
                    throw new Error(`The icon has size ${width}x${height} \
which is not square`);
                }

                return new Icon(width, path, jimpData, contents);
            });
    }

    /**
     * Gets the MIME type of the Icon
     * @type {string}
     */
    get mimeType () {
        const mimeType = mimeTypes.lookup(this.path);

        if (!mimeType) {
            throw new Error(`${nodePath.basename(this.path)} has no MIME type`);
        }

        return mimeType;
    }

    /**
     * Create a new Icon that is a resized version of this Icon
     * @private
     * @param {string|number} size - The size of the new Icon in pixels
     * @param {string} path - The file path to the new Icon
     * @param {module:chrome-manifest-iconify.ResizeMode}
     * [resizeMode=ResizeMode.BILINEAR] - The algorithm for resizing the master
     * Icon
     * @returns {Promise<Icon>} A promise that resolves when the Icon is resized
     */
    resize (size, path, resizeMode) {
        const resizedIcon = new Icon(size, path);

        const resizedJimpData = this.jimpData.clone()
            .resize(resizedIcon.size, resizedIcon.size, resizeMode);

        resizedIcon.jimpData = resizedJimpData;

        return Promise.fromCallback(
            (cb) => resizedJimpData.getBuffer(resizedIcon.mimeType, cb)
        ).then((resizedContents) => {
            resizedIcon.contents = resizedContents;

            return resizedIcon;
        });
    }

    /**
     * Gets a human-friendly string representation of the Icon
     * @returns {string} A string representation of the Icon
     */
    toString () {
        return `Icon ${this.path} of size ${this.size}x${this.size} with data \
${this.contents}`;
    }

}

module.exports = Icon;
