(function(global) { "use strict"; /** * @name fabric * @namespace */ var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, capitalize = fabric.util.string.capitalize, clone = fabric.util.object.clone; var attributesMap = { 'cx': 'left', 'x': 'left', 'cy': 'top', 'y': 'top', 'r': 'radius', 'fill-opacity': 'opacity', 'fill-rule': 'fillRule', 'stroke-width': 'strokeWidth', 'transform': 'transformMatrix' }; /** * Returns an object of attributes' name/value, given element and an array of attribute names; * Parses parent "g" nodes recursively upwards. * @static * @memberOf fabric * @method parseAttributes * @param {DOMElement} element Element to parse * @param {Array} attributes Array of attributes to parse * @return {Object} object containing parsed attributes' names/values */ function parseAttributes(element, attributes) { if (!element) { return; } var value, parsed, parentAttributes = { }; // if there's a parent container (`g` node), parse its attributes recursively upwards if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { parentAttributes = fabric.parseAttributes(element.parentNode, attributes); } var ownAttributes = attributes.reduce(function(memo, attr) { value = element.getAttribute(attr); parsed = parseFloat(value); if (value) { // "normalize" attribute values if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; } if (attr === 'fill-rule') { value = (value === 'evenodd') ? 'destination-over' : value; } if (attr === 'transform') { value = fabric.parseTransformAttribute(value); } // transform attribute names if (attr in attributesMap) { attr = attributesMap[attr]; } memo[attr] = isNaN(parsed) ? value : parsed; } return memo; }, { }); // add values parsed from style, which take precedence over attributes // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); return extend(parentAttributes, ownAttributes); }; /** * Parses "transform" attribute, returning an array of values * @static * @function * @memberOf fabric * @method parseTransformAttribute * @param attributeValue {String} string containing attribute value * @return {Array} array of 6 elements representing transformation matrix */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { var angle = args[0]; matrix[0] = Math.cos(angle); matrix[1] = Math.sin(angle); matrix[2] = -Math.sin(angle); matrix[3] = Math.cos(angle); } function scaleMatrix(matrix, args) { var multiplierX = args[0], multiplierY = (args.length === 2) ? args[1] : args[0]; matrix[0] = multiplierX; matrix[3] = multiplierY; } function skewXMatrix(matrix, args) { matrix[2] = args[0]; } function skewYMatrix(matrix, args) { matrix[1] = args[0]; } function translateMatrix(matrix, args) { matrix[4] = args[0]; if (args.length === 2) { matrix[5] = args[1]; } } // identity matrix var iMatrix = [ 1, // a 0, // b 0, // c 1, // d 0, // e 0 // f ], // == begin transform regexp number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)', comma_wsp = '(?:\\s+,?\\s*|,\\s*)', skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))', scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', matrix = '(?:(matrix)\\s*\\(\\s*' + '(' + number + ')' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + ')' + '\\s*\\))', transform = '(?:' + matrix + '|' + translate + '|' + scale + '|' + rotate + '|' + skewX + '|' + skewY + ')', transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')', transform_list = '^\\s*(?:' + transforms + '?)\\s*$', // http://www.w3.org/TR/SVG/coords.html#TransformAttribute reTransformList = new RegExp(transform_list), // == end transform regexp reTransform = new RegExp(transform); return function(attributeValue) { // start with identity matrix var matrix = iMatrix.concat(); // return if no argument was given or // an argument does not match transform attribute regexp if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { return matrix; } attributeValue.replace(reTransform, function(match) { var m = new RegExp(transform).exec(match).filter(function (match) { return (match !== '' && match != null); }), operation = m[1], args = m.slice(2).map(parseFloat); switch(operation) { case 'translate': translateMatrix(matrix, args); break; case 'rotate': rotateMatrix(matrix, args); break; case 'scale': scaleMatrix(matrix, args); break; case 'skewX': skewXMatrix(matrix, args); break; case 'skewY': skewYMatrix(matrix, args); break; case 'matrix': matrix = args; break; } }) return matrix; } })(); /** * Parses "points" attribute, returning an array of values * @static * @memberOf fabric * @method parsePointsAttribute * @param points {String} points attribute string * @return {Array} array of points */ function parsePointsAttribute(points) { // points attribute is required and must not be empty if (!points) return null; points = points.trim(); var asPairs = points.indexOf(',') > -1; points = points.split(/\s+/); var parsedPoints = [ ]; // points could look like "10,20 30,40" or "10 20 30 40" if (asPairs) { for (var i = 0, len = points.length; i < len; i++) { var pair = points[i].split(','); parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); } } else { for (var i = 0, len = points.length; i < len; i+=2) { parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); } } // odd number of points is an error if (parsedPoints.length % 2 !== 0) { // return null; } return parsedPoints; }; /** * Parses "style" attribute, retuning an object with values * @static * @memberOf fabric * @method parseStyleAttribute * @param {SVGElement} element Element to parse * @return {Object} Objects with values parsed from style attribute of an element */ function parseStyleAttribute(element) { var oStyle = { }, style = element.getAttribute('style'); if (style) { if (typeof style == 'string') { style = style.replace(/;$/, '').split(';'); oStyle = style.reduce(function(memo, current) { var attr = current.split(':'), key = attr[0].trim(), value = attr[1].trim(); memo[key] = value; return memo; }, { }); } else { for (var prop in style) { if (typeof style[prop] !== 'undefined') { oStyle[prop] = style[prop]; } } } } return oStyle; }; function resolveGradients(instances) { var activeInstance = fabric.Canvas.activeInstance, ctx = activeInstance ? activeInstance.getContext() : null; if (!ctx) return; for (var i = instances.length; i--; ) { var instanceFillValue = instances[i].get('fill'); if (/^url\(/.test(instanceFillValue)) { var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); if (fabric.gradientDefs[gradientId]) { instances[i].set('fill', fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], ctx, instances[i])); } } } } /** * Transforms an array of svg elements to corresponding fabric.* instances * @static * @memberOf fabric * @method parseElements * @param {Array} elements Array of elements to parse * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) * @param {Object} options Options object */ function parseElements(elements, callback, options) { var instances = Array(elements.length), i = elements.length; function checkIfDone() { if (--i === 0) { instances = instances.filter(function(el) { return el != null; }); resolveGradients(instances); callback(instances); } } for (var index = 0, el, len = elements.length; index < len; index++) { el = elements[index]; var klass = fabric[capitalize(el.tagName)]; if (klass && klass.fromElement) { try { if (klass.fromElement.async) { klass.fromElement(el, (function(index) { return function(obj) { instances.splice(index, 0, obj); checkIfDone(); }; })(index), options); } else { instances.splice(index, 0, klass.fromElement(el, options)); checkIfDone(); } } catch(e) { fabric.log(e.message || e); } } else { checkIfDone(); } } }; /** * Returns CSS rules for a given SVG document * @static * @function * @memberOf fabric * @method getCSSRules * @param {SVGDocument} doc SVG document to parse * @return {Object} CSS rules of this document */ function getCSSRules(doc) { var styles = doc.getElementsByTagName('style'), allRules = { }, rules; // very crude parsing of style contents for (var i = 0, len = styles.length; i < len; i++) { var styleContents = styles[0].textContent; // remove comments styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); rules = rules.map(function(rule) { return rule.trim() }); rules.forEach(function(rule) { var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), rule = match[1], declaration = match[2].trim(), propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); if (!allRules[rule]) { allRules[rule] = { }; } for (var i = 0, len = propertyValuePairs.length; i < len; i++) { var pair = propertyValuePairs[i].split(/\s*:\s*/), property = pair[0], value = pair[1]; allRules[rule][property] = value; } }); } return allRules; } function getGlobalStylesForElement(element) { var nodeName = element.nodeName, className = element.getAttribute('class'), id = element.getAttribute('id'), styles = { }; for (var rule in fabric.cssRules) { var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || (id && new RegExp('^#' + id).test(rule)) || (new RegExp('^' + nodeName).test(rule)); if (ruleMatchesElement) { for (var property in fabric.cssRules[rule]) { styles[property] = fabric.cssRules[rule][property]; } } } return styles; } /** * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback * @static * @function * @memberOf fabric * @method parseSVGDocument * @param {SVGDocument} doc SVG document to parse * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). */ fabric.parseSVGDocument = (function() { var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image)$/; // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute // \d doesn't quite cut it (as we need to match an actual float number) // matches, e.g.: +14.56e-12, etc. var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; var reViewBoxAttrValue = new RegExp( '^' + '\\s*(' + reNum + '+)\\s*,?' + '\\s*(' + reNum + '+)\\s*,?' + '\\s*(' + reNum + '+)\\s*,?' + '\\s*(' + reNum + '+)\\s*' + '$' ); function hasAncestorWithNodeName(element, nodeName) { while (element && (element = element.parentNode)) { if (nodeName.test(element.nodeName)) { return true; } } return false; } return function(doc, callback) { if (!doc) return; var startTime = new Date(), descendants = fabric.util.toArray(doc.getElementsByTagName('*')); var elements = descendants.filter(function(el) { return reAllowedSVGTagNames.test(el.tagName) && !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement }); if (!elements || (elements && !elements.length)) return; var viewBoxAttr = doc.getAttribute('viewBox'), widthAttr = doc.getAttribute('width'), heightAttr = doc.getAttribute('height'), width = null, height = null, minX, minY; if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) { minX = parseInt(viewBoxAttr[1], 10); minY = parseInt(viewBoxAttr[2], 10); width = parseInt(viewBoxAttr[3], 10); height = parseInt(viewBoxAttr[4], 10); } // values of width/height attributes overwrite those extracted from viewbox attribute width = widthAttr ? parseFloat(widthAttr) : width; height = heightAttr ? parseFloat(heightAttr) : height; var options = { width: width, height: height }; fabric.gradientDefs = fabric.getGradientDefs(doc); fabric.cssRules = getCSSRules(doc); // Precedence of rules: style > class > attribute fabric.parseElements(elements, function(instances) { fabric.documentParsingTime = new Date() - startTime; if (callback) { callback(instances, options); } }, clone(options)); }; })(); extend(fabric, { parseAttributes: parseAttributes, parseElements: parseElements, parseStyleAttribute: parseStyleAttribute, parsePointsAttribute: parsePointsAttribute, getCSSRules: getCSSRules }); })(this);