throrin19/svgutils

View on GitHub
libs/svg.js

Summary

Maintainability
F
1 wk
Test Coverage
"use strict";

var fs          = require('fs'),
    xml2js      = require('xml2js'),
    _           = require('underscore'),
    builder     = require('xmlbuilder'),
    async       = require('async'),
    SvgParser   = require(__dirname + '/parser'),
    Matrix      = require(__dirname + '/matrix/extends'),
    gm          = require('gm').subClass({ imageMagick: true }),
    utils       = require(__dirname + '/matrix/utils');

var Svg = function Svg() {
    if (!(this instanceof Svg))
        throw 'this function in a constructor. Use new to call it';

    this.elements       = [];
    this.size           = { width: 100, height : 100 };
    this.stylesheets    = [];
};

/**
 * Set Svg Elements
 * @param {Array}       elements            SvgObject Array (rect|polygon|polyline|...)
 */
Svg.prototype.setElements = function setElements(elements) {
    this.elements = elements;
};

/**
 * Add SvgObject element to current SVG
 * @param {SvgObject}   element             SvgObject Element
 */
Svg.prototype.addElement = function toJSON(element) {
    this.elements.push(element);
};

/**
 * Add Specific stylesheet in SVG
 * @param {string}      cssFilePath         Css File Path to add
 */
Svg.prototype.addStyleSheet = function addStyleSheet(cssFilePath) {
    this.stylesheets.push(cssFilePath);
};

/**
 * Convert Svg to Json format
 * @param {boolean}     matrix              String representation without transform attribute
 * @returns {object}                        Svg Json Object representation
 */
Svg.prototype.toJSON = function toXml(matrix) {
    if(typeof matrix == 'undefined') matrix = false;

    var json = {
        elements    : [],
        stylesheets : this.stylesheets,
        size        : this.size
    };

    _.each(this.elements, function (element) {
        json.elements.push(element.toJSON(matrix));
    });

    return json;
};

/**
 * Convert Svg to Xml format
 * @param {boolean}     matrix              String representation without transform attribute
 * @returns {object}                        XMLBuilder Svg representation
 */
Svg.prototype.toXml = function toXml(matrix) {
    if(typeof matrix == 'undefined') matrix = false;

    var xml = builder.create('svg');
    xml.att('version', '1.1');
    xml.att('xmlns', 'http://www.w3.org/2000/svg');
    xml.att('width', this.size.width+'px');
    xml.att('height', this.size.height+'px');

    _.each(this.stylesheets, function (styleSheet) {
        xml.ins('xml-stylesheet', 'type="text/xsl" href="'+ styleSheet +'"');
    });

    _.each(this.elements, function (element) {
        xml.importXMLBuilder(element.toXml(matrix));
    });

    return xml;
};

/**
 * Convert SVG to String :
 *     '<svg>...</svg>'
 * @param {boolean}     content             false : return only svg content, true : return all svg in <svg> tag
 * @param {boolean}     [matrix]            String representation without transform attribute
 * @returns {string}                        Svg String representation
 */
Svg.prototype.toString = function toString(content, matrix) {
    if(typeof matrix == 'undefined') matrix = false;

    if (content == true) {
        return this.toXml(matrix).toString();
    } else {
        var string = '';
        _.each(this.elements, function(element){
            string += element.toXml(matrix).toString();
        });
        return string;
    }
};

/**
 * Find elements in SVG and return new Svg object with all elements by selected type
 *
 * @param   {string}    type                Selected type (rect|polygon|g|...)
 * @param   {boolean}   all                 true : find all type in groups and root, false : find only in root
 * @returns {Svg}                           new Svg object with selected types elements
 */
Svg.prototype.findByType = function findByType(type, all) {
    var svg = new Svg();

    _.each(this.elements, function (elem) {
        if (elem.type == type) {
            svg.addElement(elem);
        }

        if (all && elem.type == 'g') {
            var group = elem.findByType(type, all);
            _.each(group.childs, function (child) {
                svg.addElement(child);
            });
        }
    });

    return svg;
};

/**
 * Find one element by id in SVG
 *
 * @param   {string}    id                  Item id
 * @returns {SvgObject}                     SvgObject element
 */
Svg.prototype.findById = function (id) {
    var returnElem = null;

    _.each(this.elements, function (elem) {
        if (elem.id == id) {
            returnElem = elem;
        }else if (elem.type == 'g') {
            returnElem = elem.findById(id);
        }
    });

    return returnElem;
};

/**
 * Find one element by id and type in SVG
 *
 * @param   {string}    id                  Item id
 * @param   {string}    type                Item type (rect, path, ...)
 * @returns {SvgObject}                     SvgObject element
 */
Svg.prototype.findByIdAndType = function findByIdAndType(id, type) {
    var returnElem = null;

    _.each(this.elements, function (elem) {
        if (elem.id == id && elem.type == type) {
            returnElem = elem;
        }else if (elem.type == 'g') {
            returnElem = elem.findByIdAndType(id, type);
        }
    });

    return returnElem;
};

/**
 * Find one element by id without param type in SVG
 *
 * @param   {string}    id                  Item id
 * @param   {string}    type                Item type (rect, path, ...)
 * @returns {SvgObject}                     SvgObject element
 */
Svg.prototype.findByIdWithoutType = function findByIdWithoutType(id, type) {
    var returnElem = null;

    _.each(this.elements, function (elem) {
        if (elem.id === id && elem.type !== type) {
            returnElem = elem;
        }else if (elem.type === 'g') {
            returnElem = elem.findByIdWithoutType(id, type);
        }
    });

    return returnElem;
};

/**
 * Remove All elements by type. If type is 'g', all childs elements are moved on svg root node.
 * @param {string}      type                Type to remove
 */
Svg.prototype.removeAllByType = function removeAllByType(type) {
    var elements = [];
    _.each(this.elements, function (elem) {
        if (elem.type === 'g') {
            var elms = elem.removeAllByType(type);
            if (type === 'g') {
                elements = _.union(elements, elms);
            }
        }
        if (elem.type !== type) {
            elements.push(elem);
        }
    });

    this.elements = elements;
};

/**
 * Generate new Svg element with all applied matrix to all elements.
 * Convert rect into polygon
 * @param {array|Matrix}        matrix              Matrix to be applied in addition to those elements.
 * @param {function}            callback            Callback Function
 */
Svg.prototype.applyMatrix = function applyMatrix(matrix, callback) {
    var svg = new Svg();

    var applyMatrix = new Matrix();
    if (matrix != null) {
        if(matrix instanceof Array){
            _.each(matrix, function(mat){
                applyMatrix.add(mat);
            });
        }else{
            applyMatrix = matrix;
        }
    }

    async.each(this.elements, function (elem, c) {
        elem.getBBox( function (bbox) {
            var applyCloneMatrix = applyMatrix.clone();
            var matrix = applyCloneMatrix.add(Matrix.fromElement(bbox, elem));
            elem.applyMatrix(matrix, function(e){
                svg.addElement(e);
                c();
            });
        });
    }, function () {
        callback(svg);
    });
};

/**
 * Save Svg file
 * @param {object}      params              Functon Params
 * @param {string}      [params.output]     Output file
 * @param {function}    callback            Callback Function
 */
Svg.prototype.save = function save(params, callback) {
    var defOpts = {
        output : '/tmp/export_' + new Date().getTime() + '.svg'
    };

    params = _.extend({}, defOpts, params);

    fs.writeFile(params.output, this.toString(true), function (err) {
        if (err) {
            callback(err);
            return;
        }
        callback(null, params.output);
    });
};

/**
 * Save Svg file in Png Format
 * @param {object}      params              Functon Params
 * @param {string}      [params.output]     Output file
 * @param {function}    callback            Callback Function
 */
Svg.prototype.savePng = function savePng(params, callback) {
    var defOpts = {
        output : '/tmp/export_' + new Date().getTime() + '.png'
    };

    params = _.extend({}, defOpts, params);

    this.save({}, function (err, file) {
        if (err) {
            callback(err);
            return;
        }

        gm(file).write(params.output, function (err) {
            if (err) {
                callback(err);
                return;
            }

            callback(null, params.output);
        });
    });
};

/**
 * Refresh SVG size
 * @param {function}    callback        Callback Function
 */
Svg.prototype.getSize = function getSize(callback) {
    var minX = +Infinity,
        maxX = -Infinity,
        minY = +Infinity,
        maxY = -Infinity,
        self = this;

    async.each(this.elements, function (child, done) {
        child.getBBox(function (bbox) {
            minX = Math.min(minX, bbox.x);
            minY = Math.min(minY, bbox.y);
            maxX = Math.max(maxX, bbox.x2);
            maxY = Math.max(maxY, bbox.y2);
            done();
        });
    }, function () {
        var bbox  = utils.bbox(minX,minY,maxX,maxY);
        self.size = { width : bbox.w, height : bbox.h  };

        callback(self.size);
    });
};

/**
 * Get SVG bbox
 * @param {function}    callback        Callback Function
 */
Svg.prototype.getBBox = function getBBox(callback) {
    var minX = +Infinity,
        maxX = -Infinity,
        minY = +Infinity,
        maxY = -Infinity;

    async.each(this.elements, function (child, done) {
        child.getBBox(function (bbox) {
            minX = Math.min(minX, bbox.x);
            minY = Math.min(minY, bbox.y);
            maxX = Math.max(maxX, bbox.x2);
            maxY = Math.max(maxY, bbox.y2);
            done();
        });
    }, function () {
        callback(utils.bbox(minX,minY,maxX,maxY));
    });
};

/**
 * Calculate all innerboxes in SVG. Return copy of current svg with elements with data attribute innerbox.
 * @param {function}    callback                    Callback Function
 */
Svg.prototype.calculateAllInnerBoxes = function calculateAllInnerBoxes(callback) {
    var svg = new Svg();
    async.each(this.elements, function (child, done) {
        switch (child.type) {
            case 'rect' :
            case 'polygon' :
            case 'polyline' :
            case 'circle' :
                child.getInnerBox(function (innerBox) {
                    child.data.innerbox = innerBox;
                    svg.addElement(child);
                    done();
                });
                break;
            case 'g' :
                child.calculateAllInnerBoxes( function (group) {
                    svg.addElement(child);
                    done();
                });
                break;
            default :
                svg.addElement(child);
                done();
        }
    }, function () {
        svg.getSize(function () {
            callback(svg);
        });
    });
};

/**
 * Remove specifics types of objetcs in svg
 * @param {object}          params              Function params
 * @param {string|Array}    params.type         target type(s) (g, rect, ...)
 */
Svg.prototype.removeByType = function removeByType(params) {
    this.elements = _.filter(this.elements, function (element) {
        if (!_.isArray(params.type)) {
            var t = params.type;
            params.type = [];
            params.type.push(t);
        }

        if (element.type === 'g') {
            // remove elements for group
            element.removeByType(params);
            // if remove group type, add all elements in svg
            if (_.contains(params.type, 'g')) {
                _.union(this.elements, element.childs);
            }
        }

        return !_.contains(params.type, element.type);
    }, this);
};

/**
 * Convert Svg elements into Path.
 * Works only on rect, polygon and polyline
 */
Svg.prototype.convertElementsToPath = function convertElementsToPath() {
    var elements = [];

    _.each(this.elements, function (element) {
        switch(element.type) {
            case 'rect' :
            case 'polygon' :
            case 'polyline' :
                elements.push(element.toPath());
                break;
            case 'g' :
                element.convertElementsToPath();
                elements.push(element);
                break;
            default :
                elements.push(element);
        }
    }, this);
    this.elements = elements;
};

module.exports = Svg;

/**
 * Create Svg from Svg Document
 * @param {string}      path                Uri of source file
 * @param {function}    callback            Callback Function
 */
module.exports.fromSvgDocument = function fromSvgDocument(path, callback) {
    fs.readFile(path, function (error, data) {
        if (error) {
            callback(error);
            return;
        }
        Svg.fromXmlString(data.toString(), callback);
    });
};

/**
 * Create Svg from Xml String representation
 * @param {string}      string              Svg string representation
 * @param {function}    callback            Callback Function
 */
module.exports.fromXmlString = function fromXmlString(string, callback) {
    var parser  = new xml2js.Parser();

    parser.addListener('end', function (result) {
        SvgParser.convertXml(result, function (err, elements) {
            if (err) {
                callback(err);
                return;
            }

            var svg = new Svg();
            svg.setElements(elements);
            svg.getSize(function () {
                callback(null, svg);
            });
        });
    });

    parser.parseString(string);
};


module.exports.fromJsonFile = function fromJsonFile(path, callback) {
    fs.readFile(path, function (error, data) {
        if (error) {
            callback(error);
            return;
        }
        Svg.fromJsonString(data.toString(), callback);
    });
};

module.exports.fromJsonString = function fromJsonString(string, callback) {
    var json = JSON.parse(string);

    Svg.fromJson(json, callback);
};

module.exports.fromJson = function fromJson(json, callback) {
    SvgParser.convertJson(json, function (err, elements) {
        if (err) {
            callback(err);
            return;
        }

        var svg = new Svg();
        svg.setElements(elements);

        svg.getSize(function () {
            callback(null, svg);
        });
    });
};

/**
 * Create SVG from dxf file
 * @param {object}      params              Function params
 * @param {string}      params.path         DXF File path
 * @param {Array}       params.layers       Import element for passed layers names
 * @param {function}    callback            Callback Function
 */
module.exports.fromDxfFile = function fromDxfFile(params, callback) {
    SvgParser.convertDxf(params, function (err, result) {
        if (err) {
            callback (err);
            return;
        }

        var svg = new Svg();
        _.each(result.polygons, function (polygon) {
            // test if params.layers is set and if polygon is in specified layers
            if (!params.layers || params.layers.length == 0 || _.contains(params.layers, polygon.layer)) {
                svg.addElement(SvgParser.parseJsonPolygon({
                    points : polygon.points,
                    fill : 'white',
                    stroke : 'black'
                }));
            }
        }, this);
        _.each(result.circles, function (circle) {
            if (!params.layers || params.layers.length == 0 || _.contains(params.layers, circle.layer)) {
                svg.addElement(SvgParser.parseJsonPolygon({
                    points : circle.points,
                    fill : 'white',
                    stroke : 'black'
                }));
            }
        });
        _.each(result.texts, function (text) {
            if (!params.layers || params.layers.length == 0 || _.contains(params.layers, text.layer)) {
                svg.addElement(SvgParser.parseJsonText({
                    fill : 'black',
                    value : text.contents,
                    x : text.point.x,
                    y : text.point.y
                }));
            }
        });
        svg.getBBox(function (bbox) {
            var matrix = new Matrix(1, 0, 0, -1, Math.abs(Math.min(bbox.x, bbox.x2))+10, Math.abs(Math.min(bbox.y, bbox.y2))+10);
            svg.applyMatrix(matrix, function (svg) {
                svg.getSize(function () {
                    callback(null, svg);
                });
            });
        });
    });
};