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