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