      1 (function(){
      2
      3   /** @name fabric */
      4
      5   var fabric = this.fabric || (this.fabric = { }),
      6       extend = fabric.util.object.extend,
      7       capitalize = fabric.util.string.capitalize,
      8       clone = fabric.util.object.clone;
      9
     10   var attributesMap = {
     11     'cx':             'left',
     12     'x':              'left',
     13     'cy':             'top',
     14     'y':              'top',
     15     'r':              'radius',
     16     'fill-opacity':   'opacity',
     17     'fill-rule':      'fillRule',
     18     'stroke-width':   'strokeWidth',
     19     'transform':      'transformMatrix'
     20   };
     21
     22   /**
     23    * Returns an object of attributes' name/value, given element and an array of attribute names;
     24    * Parses parent "g" nodes recursively upwards.
     25    *
     26    * @static
     27    * @memberOf fabric
     28    * @method parseAttributes
     29    * @param {DOMElement} element Element to parse
     30    * @param {Array} attributes Array of attributes to parse
     31    * @return {Object} object containing parsed attributes' names/values
     32    */
     33   function parseAttributes(element, attributes) {
     34
     35     if (!element) {
     36       return;
     37     }
     38
     39     var value,
     40         parsed,
     41         parentAttributes = { };
     42
     43     // if there's a parent container (`g` node), parse its attributes recursively upwards
     44     if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) {
     45       parentAttributes = fabric.parseAttributes(element.parentNode, attributes);
     46     }
     47
     48     var ownAttributes = attributes.reduce(function(memo, attr) {
     49       value = element.getAttribute(attr);
     50       parsed = parseFloat(value);
     51       if (value) {
     52         // "normalize" attribute values
     53         if ((attr === 'fill' || attr === 'stroke') && value === 'none') {
     54           value = '';
     55         }
     56         if (attr === 'fill-rule') {
     57           value = (value === 'evenodd') ? 'destination-over' : value;
     58         }
     59         if (attr === 'transform') {
     60           value = fabric.parseTransformAttribute(value);
     61         }
     62         // transform attribute names
     63         if (attr in attributesMap) {
     64           attr = attributesMap[attr];
     65         }
     66         memo[attr] = isNaN(parsed) ? value : parsed;
     67       }
     68       return memo;
     69     }, { });
     70
     71     // add values parsed from style
     72     // TODO (kangax): check the presedence of values from the style attribute
     73     ownAttributes = extend(fabric.parseStyleAttribute(element), ownAttributes);
     74     return extend(parentAttributes, ownAttributes);
     75   };
     76
     77   /**
     78    * @static
     79    * @function
     80    * @memberOf fabric
     81    * @method parseTransformAttribute
     82    * @param attributeValue {String} string containing attribute value
     83    * @return {Array} array of 6 elements representing transformation matrix
     84    */
     85   fabric.parseTransformAttribute = (function() {
     86     function rotateMatrix(matrix, args) {
     87       var angle = args[0];
     88
     89       matrix[0] = Math.cos(angle);
     90       matrix[1] = Math.sin(angle);
     91       matrix[2] = -Math.sin(angle);
     92       matrix[3] = Math.cos(angle);
     93     }
     94
     95     function scaleMatrix(matrix, args) {
     96       var multiplierX = args[0],
     97           multiplierY = (args.length === 2) ? args[1] : args[0];
     98
     99       matrix[0] = multiplierX;
    100       matrix[3] = multiplierY;
    101     }
    102
    103     function skewXMatrix(matrix, args) {
    104       matrix[2] = args[0];
    105     }
    106
    107     function skewYMatrix(matrix, args) {
    108       matrix[1] = args[0];
    109     }
    110
    111     function translateMatrix(matrix, args) {
    112       matrix[4] = args[0];
    113       if (args.length === 2) {
    114         matrix[5] = args[1];
    115       }
    116     }
    117
    118     // identity matrix
    119     var iMatrix = [
    120           1, // a
    121           0, // b
    122           0, // c
    123           1, // d
    124           0, // e
    125           0  // f
    126         ],
    127
    128         // == begin transform regexp
    129         number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)',
    130         comma_wsp = '(?:\\s+,?\\s*|,\\s*)',
    131
    132         skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))',
    133         skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))',
    134         rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))',
    135         scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
    136         translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
    137
    138         matrix = '(?:(matrix)\\s*\\(\\s*' +
    139                   '(' + number + ')' + comma_wsp +
    140                   '(' + number + ')' + comma_wsp +
    141                   '(' + number + ')' + comma_wsp +
    142                   '(' + number + ')' + comma_wsp +
    143                   '(' + number + ')' + comma_wsp +
    144                   '(' + number + ')' +
    145                   '\\s*\\))',
    146
    147         transform = '(?:' +
    148                     matrix + '|' +
    149                     translate + '|' +
    150                     scale + '|' +
    151                     rotate + '|' +
    152                     skewX + '|' +
    153                     skewY +
    154                     ')',
    155
    156         transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')',
    157
    158         transform_list = '^\\s*(?:' + transforms + '?)\\s*$',
    159
    160         // http://www.w3.org/TR/SVG/coords.html#TransformAttribute
    161         reTransformList = new RegExp(transform_list),
    162         // == end transform regexp
    163
    164         reTransform = new RegExp(transform);
    165
    166     return function(attributeValue) {
    167
    168       // start with identity matrix
    169       var matrix = iMatrix.concat();
    170
    171       // return if no argument was given or
    172       // an argument does not match transform attribute regexp
    173       if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) {
    174         return matrix;
    175       }
    176
    177       attributeValue.replace(reTransform, function(match) {
    178
    179         var m = new RegExp(transform).exec(match).filter(function (match) {
    180               return (match !== '' && match != null);
    181             }),
    182             operation = m[1],
    183             args = m.slice(2).map(parseFloat);
    184
    185         switch(operation) {
    186           case 'translate':
    187             translateMatrix(matrix, args);
    188             break;
    189           case 'rotate':
    190             rotateMatrix(matrix, args);
    191             break;
    192           case 'scale':
    193             scaleMatrix(matrix, args);
    194             break;
    195           case 'skewX':
    196             skewXMatrix(matrix, args);
    197             break;
    198           case 'skewY':
    199             skewYMatrix(matrix, args);
    200             break;
    201           case 'matrix':
    202             matrix = args;
    203             break;
    204         }
    205       })
    206       return matrix;
    207     }
    208   })();
    209
    210   /**
    211    * @static
    212    * @memberOf fabric
    213    * @method parsePointsAttribute
    214    * @param points {String} points attribute string
    215    * @return {Array} array of points
    216    */
    217   function parsePointsAttribute(points) {
    218
    219     // points attribute is required and must not be empty
    220     if (!points) return null;
    221
    222     points = points.trim();
    223     var asPairs = points.indexOf(',') > -1;
    224
    225     points = points.split(/\s+/);
    226     var parsedPoints = [ ];
    227
    228     // points could look like "10,20 30,40" or "10 20 30 40"
    229     if (asPairs) {
    230      for (var i = 0, len = points.length; i < len; i++) {
    231        var pair = points[i].split(',');
    232        parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) });
    233      }
    234     }
    235     else {
    236       for (var i = 0, len = points.length; i < len; i+=2) {
    237         parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) });
    238       }
    239     }
    240
    241     // odd number of points is an error
    242     if (parsedPoints.length % 2 !== 0) {
    243       // return null;
    244     }
    245
    246     return parsedPoints;
    247   };
    248
    249   /**
    250    * @static
    251    * @memberOf fabric
    252    * @method parseStyleAttribute
    253    * @param {SVGElement} element Element to parse
    254    * @return {Object} Objects with values parsed from style attribute of an element
    255    */
    256   function parseStyleAttribute(element) {
    257     var oStyle = { },
    258         style = element.getAttribute('style');
    259     if (style) {
    260       if (typeof style == 'string') {
    261         style = style.split(';');
    262         style.pop();
    263         oStyle = style.reduce(function(memo, current) {
    264           var attr = current.split(':'),
    265               key = attr[0].trim(),
    266               value = attr[1].trim();
    267           memo[key] = value;
    268           return memo;
    269         }, { });
    270       }
    271       else {
    272         for (var prop in style) {
    273           if (typeof style[prop] !== 'undefined') {
    274             oStyle[prop] = style[prop];
    275           }
    276         }
    277       }
    278     }
    279     return oStyle;
    280   };
    281
    282   /**
    283    * @static
    284    * @memberOf fabric
    285    * @method parseElements
    286    * @param {Array} elements Array of elements to parse
    287    * @param {Object} options Options object
    288    * @return {Array} Array of corresponding instances (transformed from SVG elements)
    289    */
    290    function parseElements(elements, options) {
    291     // transform svg elements to fabric.Path elements
    292     var _elements = elements.map(function(el) {
    293       var klass = fabric[capitalize(el.tagName)];
    294       if (klass && klass.fromElement) {
    295         try {
    296           return klass.fromElement(el, options);
    297         }
    298         catch(e) {
    299           fabric.log(e.message || e);
    300         }
    301       }
    302     });
    303     _elements = _elements.filter(function(el) {
    304       return el != null;
    305     });
    306     return _elements;
    307   };
    308
    309   /**
    310    * @static
    311    * @function
    312    * @memberOf fabric
    313    * @method parseSVGDocument
    314    * @param {SVGDocument} doc SVG document to parse
    315    * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document).
    316    */
    317   fabric.parseSVGDocument = (function() {
    318
    319     var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line)$/;
    320
    321     // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
    322     // \d doesn't quite cut it (as we need to match an actual float number)
    323
    324     // matches, e.g.: +14.56e-12, etc.
    325     var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)';
    326
    327     var reViewBoxAttrValue = new RegExp(
    328       '^' +
    329       '\\s*(' + reNum + '+)\\s*,?' +
    330       '\\s*(' + reNum + '+)\\s*,?' +
    331       '\\s*(' + reNum + '+)\\s*,?' +
    332       '\\s*(' + reNum + '+)\\s*' +
    333       '$'
    334     );
    335
    336     function hasParentWithNodeName(element, parentNodeName) {
    337       while (element && (element = element.parentNode)) {
    338         if (element.nodeName === parentNodeName) {
    339           return true;
    340         }
    341       }
    342       return false;
    343     }
    344
    345     return function(doc, callback) {
    346       if (!doc) return;
    347       var descendants = fabric.util.toArray(doc.getElementsByTagName('*'));
    348
    349       var elements = descendants.filter(function(el) {
    350         return reAllowedSVGTagNames.test(el.tagName) &&
    351           !hasParentWithNodeName(el, 'pattern');
    352       });
    353
    354       if (!elements || (elements && !elements.length)) return;
    355
    356       var viewBoxAttr = doc.getAttribute('viewBox'),
    357           widthAttr = doc.getAttribute('width'),
    358           heightAttr = doc.getAttribute('height'),
    359           width = null,
    360           height = null,
    361           minX,
    362           minY;
    363
    364       if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) {
    365         minX = parseInt(viewBoxAttr[1], 10);
    366         minY = parseInt(viewBoxAttr[2], 10);
    367         width = parseInt(viewBoxAttr[3], 10);
    368         height = parseInt(viewBoxAttr[4], 10);
    369       }
    370
    371       // values of width/height attributes overwrite those extracted from viewbox attribute
    372       width = widthAttr ? parseFloat(widthAttr) : width;
    373       height = heightAttr ? parseFloat(heightAttr) : height;
    374
    375       var options = {
    376         width: width,
    377         height: height
    378       };
    379
    380       var elements = fabric.parseElements(elements, clone(options));
    381       if (!elements || (elements && !elements.length)) return;
    382
    383       if (callback) {
    384         callback(elements, options);
    385       }
    386     };
    387   })();
    388
    389   extend(fabric, {
    390     parseAttributes:        parseAttributes,
    391     parseElements:          parseElements,
    392     parseStyleAttribute:    parseStyleAttribute,
    393     parsePointsAttribute:   parsePointsAttribute
    394   });
    395
    396 })();