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, which take precedence over attributes
 76     // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes)
 77     
 78     ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element)));
 79     return extend(parentAttributes, ownAttributes);
 80   };
 81   
 82   /**
 83    * Parses "transform" attribute, returning an array of values
 84    * @static
 85    * @function
 86    * @memberOf fabric
 87    * @method parseTransformAttribute
 88    * @param attributeValue {String} string containing attribute value
 89    * @return {Array} array of 6 elements representing transformation matrix
 90    */
 91   fabric.parseTransformAttribute = (function() {
 92     function rotateMatrix(matrix, args) {
 93       var angle = args[0];
 94       
 95       matrix[0] = Math.cos(angle);
 96       matrix[1] = Math.sin(angle);
 97       matrix[2] = -Math.sin(angle);
 98       matrix[3] = Math.cos(angle);
 99     }
100     
101     function scaleMatrix(matrix, args) {
102       var multiplierX = args[0],
103           multiplierY = (args.length === 2) ? args[1] : args[0];
104 
105       matrix[0] = multiplierX;
106       matrix[3] = multiplierY;
107     }
108     
109     function skewXMatrix(matrix, args) {
110       matrix[2] = args[0];
111     }
112     
113     function skewYMatrix(matrix, args) {
114       matrix[1] = args[0];
115     }
116     
117     function translateMatrix(matrix, args) {
118       matrix[4] = args[0];
119       if (args.length === 2) {
120         matrix[5] = args[1];
121       }
122     }
123     
124     // identity matrix
125     var iMatrix = [
126           1, // a
127           0, // b
128           0, // c
129           1, // d
130           0, // e
131           0  // f
132         ],
133     
134         // == begin transform regexp
135         number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)',
136         comma_wsp = '(?:\\s+,?\\s*|,\\s*)',
137         
138         skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))',
139         skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))',
140         rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))',
141         scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
142         translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
143         
144         matrix = '(?:(matrix)\\s*\\(\\s*' + 
145                   '(' + number + ')' + comma_wsp + 
146                   '(' + number + ')' + comma_wsp + 
147                   '(' + number + ')' + comma_wsp +
148                   '(' + number + ')' + comma_wsp +
149                   '(' + number + ')' + comma_wsp +
150                   '(' + number + ')' + 
151                   '\\s*\\))',
152         
153         transform = '(?:' +
154                     matrix + '|' +
155                     translate + '|' +
156                     scale + '|' +
157                     rotate + '|' +
158                     skewX + '|' +
159                     skewY + 
160                     ')',
161         
162         transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')',
163         
164         transform_list = '^\\s*(?:' + transforms + '?)\\s*$',
165     
166         // http://www.w3.org/TR/SVG/coords.html#TransformAttribute
167         reTransformList = new RegExp(transform_list),
168         // == end transform regexp
169         
170         reTransform = new RegExp(transform);
171     
172     return function(attributeValue) {
173       
174       // start with identity matrix
175       var matrix = iMatrix.concat();
176       
177       // return if no argument was given or 
178       // an argument does not match transform attribute regexp
179       if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) {
180         return matrix;
181       }
182       
183       attributeValue.replace(reTransform, function(match) {
184           
185         var m = new RegExp(transform).exec(match).filter(function (match) {
186               return (match !== '' && match != null);
187             }),
188             operation = m[1],
189             args = m.slice(2).map(parseFloat);
190         
191         switch(operation) {
192           case 'translate':
193             translateMatrix(matrix, args);
194             break;
195           case 'rotate':
196             rotateMatrix(matrix, args);
197             break;
198           case 'scale':
199             scaleMatrix(matrix, args);
200             break;
201           case 'skewX':
202             skewXMatrix(matrix, args);
203             break;
204           case 'skewY':
205             skewYMatrix(matrix, args);
206             break;
207           case 'matrix':
208             matrix = args;
209             break;
210         }
211       })
212       return matrix;
213     }
214   })();
215   
216   /**
217    * Parses "points" attribute, returning an array of values
218    * @static
219    * @memberOf fabric
220    * @method parsePointsAttribute
221    * @param points {String} points attribute string
222    * @return {Array} array of points
223    */
224   function parsePointsAttribute(points) {
225         
226     // points attribute is required and must not be empty
227     if (!points) return null;
228     
229     points = points.trim();
230     var asPairs = points.indexOf(',') > -1;
231     
232     points = points.split(/\s+/);
233     var parsedPoints = [ ];
234     
235     // points could look like "10,20 30,40" or "10 20 30 40"
236     if (asPairs) {  
237      for (var i = 0, len = points.length; i < len; i++) {
238        var pair = points[i].split(',');
239        parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) });
240      } 
241     }
242     else {
243       for (var i = 0, len = points.length; i < len; i+=2) {
244         parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) });
245       }
246     }
247     
248     // odd number of points is an error
249     if (parsedPoints.length % 2 !== 0) {
250       // return null;
251     }
252     
253     return parsedPoints;
254   };
255 
256   /**
257    * Parses "style" attribute, retuning an object with values
258    * @static
259    * @memberOf fabric
260    * @method parseStyleAttribute
261    * @param {SVGElement} element Element to parse
262    * @return {Object} Objects with values parsed from style attribute of an element
263    */
264   function parseStyleAttribute(element) {
265     var oStyle = { },
266         style = element.getAttribute('style');
267     if (style) {
268       if (typeof style == 'string') {
269         style = style.replace(/;$/, '').split(';');
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   function resolveGradients(instances) {
290     var activeInstance = fabric.Canvas.activeInstance,
291         ctx = activeInstance ? activeInstance.getContext() : null;
292         
293     if (!ctx) return;
294 
295     for (var i = instances.length; i--; ) {
296       var instanceFillValue = instances[i].get('fill');
297 
298       if (/^url\(/.test(instanceFillValue)) {
299 
300         var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1);
301 
302         if (fabric.gradientDefs[gradientId]) {
303           instances[i].set('fill',
304             fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], ctx, instances[i]));
305         }
306       }
307     }
308   }
309 
310   /**
311    * Transforms an array of svg elements to corresponding fabric.* instances
312    * @static
313    * @memberOf fabric
314    * @method parseElements
315    * @param {Array} elements Array of elements to parse
316    * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements)
317    * @param {Object} options Options object
318    */
319   function parseElements(elements, callback, options) {
320     var instances = Array(elements.length), i = elements.length;
321 
322     function checkIfDone() {
323       if (--i === 0) {
324         instances = instances.filter(function(el) {
325           return el != null;
326         });
327         resolveGradients(instances);
328         callback(instances);
329       }
330     }
331     
332     for (var index = 0, el, len = elements.length; index < len; index++) {
333       el = elements[index];
334       var klass = fabric[capitalize(el.tagName)];
335       if (klass && klass.fromElement) {
336         try {
337           if (klass.fromElement.async) {
338             klass.fromElement(el, (function(index) { 
339               return function(obj) {
340                 instances.splice(index, 0, obj);
341                 checkIfDone();
342               };
343             })(index), options);
344           }
345           else {
346             instances.splice(index, 0, klass.fromElement(el, options));
347             checkIfDone();
348           }
349         }
350         catch(e) {
351           fabric.log(e.message || e);
352         }
353       }
354       else {
355         checkIfDone();
356       }
357     }
358   };
359   
360   /**
361    * Returns CSS rules for a given SVG document
362    * @static
363    * @function
364    * @memberOf fabric
365    * @method getCSSRules
366    * @param {SVGDocument} doc SVG document to parse
367    * @return {Object} CSS rules of this document
368    */
369   function getCSSRules(doc) {
370     var styles = doc.getElementsByTagName('style'),
371         allRules = { },
372         rules;
373     
374     // very crude parsing of style contents  
375     for (var i = 0, len = styles.length; i < len; i++) {
376       var styleContents = styles[0].textContent;
377       
378       // remove comments
379       styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, '');
380                          
381       rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g);
382       rules = rules.map(function(rule) { return rule.trim() });
383       
384       rules.forEach(function(rule) {
385         var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/),
386             rule = match[1],
387             declaration = match[2].trim(),
388             propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/);
389         
390         if (!allRules[rule]) {
391           allRules[rule] = { };
392         }
393         
394         for (var i = 0, len = propertyValuePairs.length; i < len; i++) {
395           var pair = propertyValuePairs[i].split(/\s*:\s*/),
396               property = pair[0],
397               value = pair[1];
398               
399           allRules[rule][property] = value;
400         }
401       });
402     }
403     
404     return allRules;
405   }
406   
407   function getGlobalStylesForElement(element) {
408     var nodeName = element.nodeName,
409         className = element.getAttribute('class'),
410         id = element.getAttribute('id'),
411         styles = { };
412     
413     for (var rule in fabric.cssRules) {
414       var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) ||
415                                (id && new RegExp('^#' + id).test(rule)) ||
416                                (new RegExp('^' + nodeName).test(rule));
417                                
418       if (ruleMatchesElement) {
419         for (var property in fabric.cssRules[rule]) {
420           styles[property] = fabric.cssRules[rule][property];
421         }
422       }
423     }
424     
425     return styles;
426   }
427   
428   /**
429    * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback
430    * @static
431    * @function
432    * @memberOf fabric
433    * @method parseSVGDocument
434    * @param {SVGDocument} doc SVG document to parse
435    * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document).
436    */
437   fabric.parseSVGDocument = (function() {
438 
439     var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image)$/;
440 
441     // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
442     // \d doesn't quite cut it (as we need to match an actual float number)
443 
444     // matches, e.g.: +14.56e-12, etc.
445     var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)';
446 
447     var reViewBoxAttrValue = new RegExp(
448       '^' +
449       '\\s*(' + reNum + '+)\\s*,?' +
450       '\\s*(' + reNum + '+)\\s*,?' +
451       '\\s*(' + reNum + '+)\\s*,?' +
452       '\\s*(' + reNum + '+)\\s*' +
453       '$'
454     );
455     
456     function hasAncestorWithNodeName(element, nodeName) {
457       while (element && (element = element.parentNode)) {
458         if (nodeName.test(element.nodeName)) {
459           return true;
460         }
461       }
462       return false;
463     }
464 
465     return function(doc, callback) {
466       if (!doc) return;
467       
468       var startTime = new Date(),
469           descendants = fabric.util.toArray(doc.getElementsByTagName('*'));
470       
471       var elements = descendants.filter(function(el) {
472         return reAllowedSVGTagNames.test(el.tagName) && 
473               !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement
474       });
475 
476       if (!elements || (elements && !elements.length)) return;
477 
478       var viewBoxAttr = doc.getAttribute('viewBox'),
479           widthAttr = doc.getAttribute('width'),
480           heightAttr = doc.getAttribute('height'),
481           width = null,
482           height = null,
483           minX,
484           minY;
485       
486       if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) {
487         minX = parseInt(viewBoxAttr[1], 10);
488         minY = parseInt(viewBoxAttr[2], 10);
489         width = parseInt(viewBoxAttr[3], 10);
490         height = parseInt(viewBoxAttr[4], 10);
491       }
492 
493       // values of width/height attributes overwrite those extracted from viewbox attribute
494       width = widthAttr ? parseFloat(widthAttr) : width;
495       height = heightAttr ? parseFloat(heightAttr) : height;
496 
497       var options = { 
498         width: width, 
499         height: height
500       };
501       
502       fabric.gradientDefs = fabric.getGradientDefs(doc);
503       fabric.cssRules = getCSSRules(doc);
504       
505       // Precedence of rules:   style > class > attribute
506       
507       fabric.parseElements(elements, function(instances) {
508         fabric.documentParsingTime = new Date() - startTime;
509         if (callback) {
510           callback(instances, options);
511         }
512       }, clone(options));
513     };
514   })();
515   
516   extend(fabric, {
517     parseAttributes:        parseAttributes,
518     parseElements:          parseElements,
519     parseStyleAttribute:    parseStyleAttribute,
520     parsePointsAttribute:   parsePointsAttribute,
521     getCSSRules:            getCSSRules
522   });
523   
524 })(this);