Importing icons

This article explains how to convert icons from different sources to Iconify icons set.

For import and export you need to use Iconify Tools package: @iconify/tools.

Iconify Tools are available only for Node.js. Therefore you need Node.js to be installed and you need basic understanding of how to use it. Maybe later PHP versions will be available too, but for now its possible to do only with Node.

Iconify Tools has several import functions:

  • Import icons from directory full of SVG files.
  • Import icons from glyph fonts.
  • Import icons from WebIcon format.
  • Import Iconify JSON file.

Then it has several export functions:

  • Export to Iconify JSON file.
  • Export to SVG files.

There are also several functions for cleaning up and manipulating icons:

  • Optimizing icons (based on SVGO).
  • Validating icons to make sure it has no content that cannot be properly scaled: images, text. (text requires specific fonts to be installed and is often rendered incorrectly when icon is scaled - avoid it). It also checks for invalid tags and scripts.
  • Functions to extract and change icon palette.
  • Find icon content and crop icon (requires PhantomJS to be installed).

All those functions use JavaScript Promise class and are asynchronous.

Import SVG set

This example shows how to convert directory full of SVG files to Iconify JSON format.

As source lets take Material Design icons from Templarian/MaterialDesign-SVG repository that is also available as @mdi/svg NPM package.

Initialize project by running these commands:

npm init
npm install @iconify/tools
npm install @mdi/svg

Then create file "convert-mdi.js" and put this content:

"use strict";

const path = require('path');
const util = require('util');
const tools = require('@iconify/tools');

/*
    Locate directory where SVG files are

    Icons are located  in directory "svg" in @mdi/svg package

    require.resolve locates package.json
    path.dirname removes package.json from result, returning only directory
    + '/svg' adds 'svg' directory to result
*/

let source = path.dirname(require.resolve('@mdi/svg/package.json')) + '/svg';

// Target file name
let target = __dirname + '/mdi.json';

// Variable to store collection
let collection;

// Options for SVGO optimization
let SVGOOptions = {
    convertShapeToPath: true,
    mergePaths: false,
};

/**
* Import directory
*/

tools.ImportDir(source, {
    prefix: 'mdi'
}).then(result => {

    // Copy reference so it can be used in chain of promises
    // collection is instance of tools.Collection class
    collection = result;

    console.log('Imported', collection.length(), 'icons.');

    // Optimize SVG files
    //
    // collection.promiseEach() iterates all icons in collection and runs
    // promise for each icon, one at a time.
    return collection.promiseEach((svg, key) => new Promise((fulfill, reject) => {
        tools.SVGO(svg, SVGOOptions).then(res => {
            fulfill(res);
        }).catch(err => {
            reject('Error optimizing icon ' + key + '\n' + util.format(err));
        });
    }), true);

}).then(() => {

    // Clean up tags
    return collection.promiseEach((svg, key) => new Promise((fulfill, reject) => {
        tools.Tags(svg).then(res => {
            fulfill(res);
        }).catch(err => {
            reject('Error checking tags in icon ' + key + '\n' + util.format(err));
        });
    }), true);

}).then(() => {

    // Move icons origin to 0,0
    // This is not needed for most collections, but its useful to know how to do it
    let promises = [];
    collection.forEach((svg, key) => {
        if (svg.top !== 0 || svg.left !== 0) {
            let body = svg.getBody();
            if (body.indexOf('<defs') !== -1) {
                // Do not use this method to move icons with <defs> tags - sometimes results could be wrong
                return;
            }

            let content = '<svg';
            content += ' width="' + svg.width + '"';
            content += ' height="' + svg.height + '"';
            content += ' viewBox="0 0 ' + svg.width + ' ' + svg.height + '"';
            content += ' xmlns="http://www.w3.org/2000/svg">\n';
            content += '<g transform="translate(' + (0 - svg.left) + ' ' + (0 - svg.top) + ')">' + body + '</g>';
            content += '</svg>';

            svg.load(content);
            promises.push(new Promise((fulfill, reject) => {
                // Use SVGO to optimize icon. It will get apply transformation to shapes
                tools.SVGO(svg, SVGOOptions).then(res => {
                    fulfill(res);
                }).catch(err => {
                    reject('Error changing icon origin for ' + key + '\n' + util.format(err));
                });
            }));
        }
    });
    return Promise.all(promises);

}).then(() => {

    // Change color to "currentColor" to all icons
    // Use this only for monotone collections
    let options = {
        default: 'currentColor', // change all colors to "currentColor"
        add: 'currentColor' // add "currentColor" to shapes that are missing color value
    };

    /*
    // For icons that have palette use this instead:
    let options = {
        add: 'currentColor'
    };
    */


    return collection.promiseEach(svg => tools.ChangePalette(svg, options), true);

}).then(() => {

    // Export JSON collection
    console.log('Exporting collection to', target);
    return tools.ExportJSON(collection, target, {
        optimize: true
    });

}).catch(err => {
    console.error(err);
});

Run node convert-mdi to run that file. It will generate "mdi.json"

There are comments in code above that explain what is going on.

Process is simple:

  1. tools.ImportDir() imports all icons from directory "svg" of @mdi/svg package.
  2. Then tools.SVGO() is used to optimize icons.
  3. Then tools.Tags() is used to validate and clean up icons.
  4. Then icons origin is moved to 0,0 and tools.SVO() is used again to optimize icons.
  5. Then tools.ChangePalette() is used to add "currentColor" to shapes that are missing colors and change existing color to "currentColor" (colored icons use different sets of options. see comments in code).
  6. Then tools.ExportJSON() is used to export collection to json file.

Steps 3 and 4 are actually useless for this example because MDI icons are all well coded, but they are included in example because some SVG sets do require them.

Import SVG font

This example shows how to convert SVG font to Iconify JSON format.

As source lets take FontAwesome 4 from FortAwesome/Font-Awesome repository "fa-4" branch that is also available as "font-awesome" NPM package.

Initialize project by running these commands:

npm init
npm install @iconify/tools
npm install font-awesome

Then create file "convert-fa.js" and put this content:

"use strict";

const fs = require('fs');
const path = require('path');
const util = require('util');
const tools = require('@iconify/tools');

/*
    Locate directory where font files are

    require.resolve locates package.json
    path.dirname removes package.json from result, returning only directory
*/

let faDir = path.dirname(require.resolve('font-awesome/package.json'));

// Target file name
let target = __dirname + '/fa.json';

// Variable to store collection
let collection;

// Options for SVGO optimization
let SVGOOptions = {
    convertShapeToPath: true,
    mergePaths: false,
};

/**
* Import font
*/

tools.ImportFont(faDir + '/fonts/fontawesome-webfont.svg', {
    /*
        One major downside of importing fonts is glyphs are often misaligned.

        They tend to have wrong dimensions, sometimes extra space on left or right
        or even part of icon being cut.

        This is why importing font requires some manual work to find correct values.

        Below is configuration from FontAwesome 4 import function used to generate fa.json for @iconify/json package.
     */

    fontChanges: {
        // Adjustments for web font configuration
        height: 1792,
        bottom: value => Math.round(value / 16) * 16,
        left: value => Math.round(value / 16) * 16,
        width: value => Math.round(value / 16) * 16
    },
    characterChanges: {
        // Adjustments for web font characters
        f19c: { width: 1920 }, // bank
        f0fc: { left: 64, width: 1600 }, // beer
        f1ad: { width: 1408 }, // building
        f1ec: { width: 1664 }, // calculator
        f274: { width: 1664 }, // calculator-check-o
        f272: { width: 1664 }, // calendar-minus-o
        f271: { width: 1664 }, // calendar-plus-o
        f273: { width: 1664 }, // calendar-times-o
        f1b2: { width: 1664 }, // cube
        f1b3: { width: 2176 }, // cubes
        f210: { width: 1408 }, // dashcube
        f286: { width: 1664 }, // fort-awesome
        f0f4: { left: -64, width: 1984 },
        f21b: { width: 1408 },
        f21d: { width: 1408 },
        f22a: { width: 1152 },
        f22b: { width: 1920 },
        f22c: { width: 1152 },
        f22d: { width: 1152 },
        f24e: { width: 2176 },
        f25a: { width: 1664 },
        f26c: { width: 1920 },
        f089: { width: 1664 },
        f225: { width: 1664 },
        f245: { width: 1152 },
        f246: { width: 896 },
        f256: { width: 1664 },
        f259: { width: 1920 },
    },
}).then(result => {

    // Copy reference so it can be used in chain of promises
    // collection is instance of tools.Collection class
    collection = result;

    console.log('Imported', collection.length(), 'icons.');

    // Optimize SVG files
    //
    // collection.promiseEach() iterates all icons in collection and runs
    // promise for each icon, one at a time.
    return collection.promiseEach((svg, key) => new Promise((fulfill, reject) => {
        tools.SVGO(svg, SVGOOptions).then(res => {
            fulfill(res);
        }).catch(err => {
            reject('Error optimizing icon ' + key + '\n' + util.format(err));
        });
    }), true);

}).then(() => {

    // Add "currentColor" to all shapes
    return collection.promiseEach(svg => tools.ChangePalette(svg, 'currentColor'), true);

}).then(() => {

    // Export JSON collection
    console.log('Exporting collection to', target);
    return tools.ExportJSON(collection, target, {
        optimize: true
    });

}).catch(err => {
    console.error(err);
});

Run node convert-fa to run that file. It will generate "fa.json"

There are comments in code above that explain what is going on.

Process is simple:

  1. tools.ImportFont() imports SVG font.
  2. Then tools.SVGO() is used to optimize icons.
  3. Then tools.ChangePalette() is used to add "currentColor" to all shapes because imported SVG do not have any color values.
  4. Then tools.ExportJSON() is used to export collection to json file.

This code does not do one major part of importing font: it does not assign keywords to each icon. Keywords are not stored in font, they should be retrieved from CSS file. Problem with those files is they can be very different for each font, so there is no universal code to get those icon names.

However if you do get icon names for each icon, you can use code like this to rename icons:

let keywords = {
    f19c: 'bank',
    f0fc: 'beer',
    // and the rest of keywords
};
collection.forEach((svg, key) => {
    if (keywords[key] === void 0) {
        // No keyword for character - delete it
        collection.remove(key);
    } else {
        collection.rename(key, keywords[key]);
    }
});

Handling errors when processing icons

In all examples above if script encounters error, it will throw exception.

But what if you want bad file to be simply ignored? Then you can change stopOnError parameter to promiseEeach() to false or replace reject() with fulfill() while also removing bad icon.

For example, this is code that optimizes icons:

return collection.promiseEach((svg, key) => new Promise((fulfill, reject) => {
    tools.SVGO(svg, SVGOOptions).then(res => {
        fulfill(res);
    }).catch(err => {
        reject('Error optimizing icon ' + key + '\n' + util.format(err));
    });
}), true);

And this is same code that instead of throwing error simply removes bad icon:

return collection.promiseEach((svg, key) => new Promise((fulfill, reject) => {
    tools.SVGO(svg, SVGOOptions).then(res => {
        fulfill(res);
    }).catch(err => {
        console.error('Error optimizing icon ' + key, err);
        collection.remove(key);
        fulfill(null);
    });
}), false);