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);