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