UNPKG

fabric-pure-browser

Version:

Fabric.js package with no node-specific dependencies (node-canvas, jsdom). The project is published once a day (in case if a new version appears) from 'master' branch of https://github.com/fabricjs/fabric.js repository. You can keep original imports in

1,080 lines (961 loc) 36.4 kB
(function(global) { 'use strict'; /** * @name fabric * @namespace */ var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, clone = fabric.util.object.clone, toFixed = fabric.util.toFixed, parseUnit = fabric.util.parseUnit, multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line', 'image', 'text'], svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'], svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'], svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'], attributesMap = { cx: 'left', x: 'left', r: 'radius', cy: 'top', y: 'top', display: 'visible', visibility: 'visible', transform: 'transformMatrix', 'fill-opacity': 'fillOpacity', 'fill-rule': 'fillRule', 'font-family': 'fontFamily', 'font-size': 'fontSize', 'font-style': 'fontStyle', 'font-weight': 'fontWeight', 'letter-spacing': 'charSpacing', 'paint-order': 'paintFirst', 'stroke-dasharray': 'strokeDashArray', 'stroke-dashoffset': 'strokeDashOffset', 'stroke-linecap': 'strokeLineCap', 'stroke-linejoin': 'strokeLineJoin', 'stroke-miterlimit': 'strokeMiterLimit', 'stroke-opacity': 'strokeOpacity', 'stroke-width': 'strokeWidth', 'text-decoration': 'textDecoration', 'text-anchor': 'textAnchor', opacity: 'opacity', 'clip-path': 'clipPath', 'clip-rule': 'clipRule', 'vector-effect': 'strokeUniform' }, colorAttributes = { stroke: 'strokeOpacity', fill: 'fillOpacity' }, fSize = 'font-size', cPath = 'clip-path'; fabric.svgValidTagNamesRegEx = getSvgRegex(svgValidTagNames); fabric.svgViewBoxElementsRegEx = getSvgRegex(svgViewBoxElements); fabric.svgInvalidAncestorsRegEx = getSvgRegex(svgInvalidAncestors); fabric.svgValidParentsRegEx = getSvgRegex(svgValidParents); fabric.cssRules = { }; fabric.gradientDefs = { }; fabric.clipPaths = { }; function normalizeAttr(attr) { // transform attribute names if (attr in attributesMap) { return attributesMap[attr]; } return attr; } function normalizeValue(attr, value, parentAttributes, fontSize) { var isArray = Object.prototype.toString.call(value) === '[object Array]', parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; } else if (attr === 'vector-effect') { value = value === 'non-scaling-stroke'; } else if (attr === 'strokeDashArray') { if (value === 'none') { value = null; } else { value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); } } else if (attr === 'transformMatrix') { if (parentAttributes && parentAttributes.transformMatrix) { value = multiplyTransformMatrices( parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); } else { value = fabric.parseTransformAttribute(value); } } else if (attr === 'visible') { value = value !== 'none' && value !== 'hidden'; // display=none on parent element always takes precedence over child element if (parentAttributes && parentAttributes.visible === false) { value = false; } } else if (attr === 'opacity') { value = parseFloat(value); if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { value *= parentAttributes.opacity; } } else if (attr === 'textAnchor' /* text-anchor */) { value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; } else if (attr === 'charSpacing') { // parseUnit returns px and we convert it to em parsed = parseUnit(value, fontSize) / fontSize * 1000; } else if (attr === 'paintFirst') { var fillIndex = value.indexOf('fill'); var strokeIndex = value.indexOf('stroke'); var value = 'fill'; if (fillIndex > -1 && strokeIndex > -1 && strokeIndex < fillIndex) { value = 'stroke'; } else if (fillIndex === -1 && strokeIndex > -1) { value = 'stroke'; } } else if (attr === 'href' || attr === 'xlink:href') { return value; } else { parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); } return (!isArray && isNaN(parsed) ? value : parsed); } /** * @private */ function getSvgRegex(arr) { return new RegExp('^(' + arr.join('|') + ')\\b', 'i'); } /** * @private * @param {Object} attributes Array of attributes to parse */ function _setStrokeFillOpacity(attributes) { for (var attr in colorAttributes) { if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { continue; } if (typeof attributes[attr] === 'undefined') { if (!fabric.Object.prototype[attr]) { continue; } attributes[attr] = fabric.Object.prototype[attr]; } if (attributes[attr].indexOf('url(') === 0) { continue; } var color = new fabric.Color(attributes[attr]); attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); } return attributes; } /** * @private */ function _getMultipleNodes(doc, nodeNames) { var nodeName, nodeArray = [], nodeList, i, len; for (i = 0, len = nodeNames.length; i < len; i++) { nodeName = nodeNames[i]; nodeList = doc.getElementsByTagName(nodeName); nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); } return nodeArray; } /** * Parses "transform" attribute, returning an array of values * @static * @function * @memberOf fabric * @param {String} attributeValue String containing attribute value * @return {Array} Array of 6 elements representing transformation matrix */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), x = 0, y = 0; if (args.length === 3) { x = args[1]; y = args[2]; } matrix[0] = cos; matrix[1] = sin; matrix[2] = -sin; matrix[3] = cos; matrix[4] = x - (cos * x - sin * y); matrix[5] = y - (sin * x + cos * y); } function scaleMatrix(matrix, args) { var multiplierX = args[0], multiplierY = (args.length === 2) ? args[1] : args[0]; matrix[0] = multiplierX; matrix[3] = multiplierY; } function skewMatrix(matrix, args, pos) { matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); } function translateMatrix(matrix, args) { matrix[4] = args[0]; if (args.length === 2) { matrix[5] = args[1]; } } // identity matrix var iMatrix = fabric.iMatrix, // == begin transform regexp number = fabric.reNum, commaWsp = '(?:\\s+,?\\s*|,\\s*)', skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + '))?\\s*\\))', scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + commaWsp + '(' + number + '))?\\s*\\))', translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + commaWsp + '(' + number + '))?\\s*\\))', matrix = '(?:(matrix)\\s*\\(\\s*' + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + '\\s*\\))', transform = '(?:' + matrix + '|' + translate + '|' + scale + '|' + rotate + '|' + skewX + '|' + skewY + ')', transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', transformList = '^\\s*(?:' + transforms + '?)\\s*$', // http://www.w3.org/TR/SVG/coords.html#TransformAttribute reTransformList = new RegExp(transformList), // == end transform regexp reTransform = new RegExp(transform, 'g'); return function(attributeValue) { // start with identity matrix var matrix = iMatrix.concat(), matrices = []; // 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) { // match !== '' && match != null return (!!match); }), operation = m[1], args = m.slice(2).map(parseFloat); switch (operation) { case 'translate': translateMatrix(matrix, args); break; case 'rotate': args[0] = fabric.util.degreesToRadians(args[0]); rotateMatrix(matrix, args); break; case 'scale': scaleMatrix(matrix, args); break; case 'skewX': skewMatrix(matrix, args, 2); break; case 'skewY': skewMatrix(matrix, args, 1); break; case 'matrix': matrix = args; break; } // snapshot current matrix into matrices array matrices.push(matrix.concat()); // reset matrix = iMatrix.concat(); }); var combinedMatrix = matrices[0]; while (matrices.length > 1) { matrices.shift(); combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); } return combinedMatrix; }; })(); /** * @private */ function parseStyleString(style, oStyle) { var attr, value; style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { var pair = chunk.split(':'); attr = pair[0].trim().toLowerCase(); value = pair[1].trim(); oStyle[attr] = value; }); } /** * @private */ function parseStyleObject(style, oStyle) { var attr, value; for (var prop in style) { if (typeof style[prop] === 'undefined') { continue; } attr = prop.toLowerCase(); value = style[prop]; oStyle[attr] = value; } } /** * @private */ function getGlobalStylesForElement(element, svgUid) { var styles = { }; for (var rule in fabric.cssRules[svgUid]) { if (elementMatchesRule(element, rule.split(' '))) { for (var property in fabric.cssRules[svgUid][rule]) { styles[property] = fabric.cssRules[svgUid][rule][property]; } } } return styles; } /** * @private */ function elementMatchesRule(element, selectors) { var firstMatching, parentMatching = true; //start from rightmost selector. firstMatching = selectorMatches(element, selectors.pop()); if (firstMatching && selectors.length) { parentMatching = doesSomeParentMatch(element, selectors); } return firstMatching && parentMatching && (selectors.length === 0); } function doesSomeParentMatch(element, selectors) { var selector, parentMatching = true; while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { if (parentMatching) { selector = selectors.pop(); } element = element.parentNode; parentMatching = selectorMatches(element, selector); } return selectors.length === 0; } /** * @private */ function selectorMatches(element, selector) { var nodeName = element.nodeName, classNames = element.getAttribute('class'), id = element.getAttribute('id'), matcher, i; // i check if a selector matches slicing away part from it. // if i get empty string i should match matcher = new RegExp('^' + nodeName, 'i'); selector = selector.replace(matcher, ''); if (id && selector.length) { matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); selector = selector.replace(matcher, ''); } if (classNames && selector.length) { classNames = classNames.split(' '); for (i = classNames.length; i--;) { matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); selector = selector.replace(matcher, ''); } } return selector.length === 0; } /** * @private * to support IE8 missing getElementById on SVGdocument and on node xmlDOM */ function elementById(doc, id) { var el; doc.getElementById && (el = doc.getElementById(id)); if (el) { return el; } var node, i, len, nodelist = doc.getElementsByTagName('*'); for (i = 0, len = nodelist.length; i < len; i++) { node = nodelist[i]; if (id === node.getAttribute('id')) { return node; } } } /** * @private */ function parseUseDirectives(doc) { var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; while (nodelist.length && i < nodelist.length) { var el = nodelist[i], xlink = (el.getAttribute('xlink:href') || el.getAttribute('href')).substr(1), x = el.getAttribute('x') || 0, y = el.getAttribute('y') || 0, el2 = elementById(doc, xlink).cloneNode(true), currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', parentNode, oldLength = nodelist.length, attr, j, attrs, len, namespace = fabric.svgNS; applyViewboxTransform(el2); if (/^svg$/i.test(el2.nodeName)) { var el3 = el2.ownerDocument.createElementNS(namespace, 'g'); for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { attr = attrs.item(j); el3.setAttributeNS(namespace, attr.nodeName, attr.nodeValue); } // el2.firstChild != null while (el2.firstChild) { el3.appendChild(el2.firstChild); } el2 = el3; } for (j = 0, attrs = el.attributes, len = attrs.length; j < len; j++) { attr = attrs.item(j); if (attr.nodeName === 'x' || attr.nodeName === 'y' || attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { continue; } if (attr.nodeName === 'transform') { currentTrans = attr.nodeValue + ' ' + currentTrans; } else { el2.setAttribute(attr.nodeName, attr.nodeValue); } } el2.setAttribute('transform', currentTrans); el2.setAttribute('instantiated_by_use', '1'); el2.removeAttribute('id'); parentNode = el.parentNode; parentNode.replaceChild(el2, el); // some browsers do not shorten nodelist after replaceChild (IE8) if (nodelist.length === oldLength) { i++; } } } // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute // matches, e.g.: +14.56e-12, etc. var reViewBoxAttrValue = new RegExp( '^' + '\\s*(' + fabric.reNum + '+)\\s*,?' + '\\s*(' + fabric.reNum + '+)\\s*,?' + '\\s*(' + fabric.reNum + '+)\\s*,?' + '\\s*(' + fabric.reNum + '+)\\s*' + '$' ); /** * Add a <g> element that envelop all child elements and makes the viewbox transformMatrix descend on all elements */ function applyViewboxTransform(element) { var viewBoxAttr = element.getAttribute('viewBox'), scaleX = 1, scaleY = 1, minX = 0, minY = 0, viewBoxWidth, viewBoxHeight, matrix, el, widthAttr = element.getAttribute('width'), heightAttr = element.getAttribute('height'), x = element.getAttribute('x') || 0, y = element.getAttribute('y') || 0, preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', missingViewBox = (!viewBoxAttr || !fabric.svgViewBoxElementsRegEx.test(element.nodeName) || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), toBeParsed = missingViewBox && missingDimAttr, parsedDim = { }, translateMatrix = '', widthDiff = 0, heightDiff = 0; parsedDim.width = 0; parsedDim.height = 0; parsedDim.toBeParsed = toBeParsed; if (toBeParsed) { return parsedDim; } if (missingViewBox) { parsedDim.width = parseUnit(widthAttr); parsedDim.height = parseUnit(heightAttr); return parsedDim; } minX = -parseFloat(viewBoxAttr[1]); minY = -parseFloat(viewBoxAttr[2]); viewBoxWidth = parseFloat(viewBoxAttr[3]); viewBoxHeight = parseFloat(viewBoxAttr[4]); parsedDim.minX = minX; parsedDim.minY = minY; parsedDim.viewBoxWidth = viewBoxWidth; parsedDim.viewBoxHeight = viewBoxHeight; if (!missingDimAttr) { parsedDim.width = parseUnit(widthAttr); parsedDim.height = parseUnit(heightAttr); scaleX = parsedDim.width / viewBoxWidth; scaleY = parsedDim.height / viewBoxHeight; } else { parsedDim.width = viewBoxWidth; parsedDim.height = viewBoxHeight; } // default is to preserve aspect ratio preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); if (preserveAspectRatio.alignX !== 'none') { //translate all container for the effect of Mid, Min, Max if (preserveAspectRatio.meetOrSlice === 'meet') { scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); // calculate additional translation to move the viewbox } if (preserveAspectRatio.meetOrSlice === 'slice') { scaleY = scaleX = (scaleX > scaleY ? scaleX : scaleY); // calculate additional translation to move the viewbox } widthDiff = parsedDim.width - viewBoxWidth * scaleX; heightDiff = parsedDim.height - viewBoxHeight * scaleX; if (preserveAspectRatio.alignX === 'Mid') { widthDiff /= 2; } if (preserveAspectRatio.alignY === 'Mid') { heightDiff /= 2; } if (preserveAspectRatio.alignX === 'Min') { widthDiff = 0; } if (preserveAspectRatio.alignY === 'Min') { heightDiff = 0; } } if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { return parsedDim; } if (x || y) { translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; } matrix = translateMatrix + ' matrix(' + scaleX + ' 0' + ' 0 ' + scaleY + ' ' + (minX * scaleX + widthDiff) + ' ' + (minY * scaleY + heightDiff) + ') '; parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); if (element.nodeName === 'svg') { el = element.ownerDocument.createElementNS(fabric.svgNS, 'g'); // element.firstChild != null while (element.firstChild) { el.appendChild(element.firstChild); } element.appendChild(el); } else { el = element; matrix = el.getAttribute('transform') + matrix; } el.setAttribute('transform', matrix); return parsedDim; } function hasAncestorWithNodeName(element, nodeName) { while (element && (element = element.parentNode)) { if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) && !element.getAttribute('instantiated_by_use')) { return true; } } return false; } /** * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback * @static * @function * @memberOf fabric * @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). * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [parsingOptions] options for parsing document * @param {String} [parsingOptions.crossOrigin] crossOrigin settings */ fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { if (!doc) { return; } parseUseDirectives(doc); var svgUid = fabric.Object.__uid++, i, len, options = applyViewboxTransform(doc), descendants = fabric.util.toArray(doc.getElementsByTagName('*')); options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; options.svgUid = svgUid; if (descendants.length === 0 && fabric.isLikelyNode) { // we're likely in node, where "o3-xml" library fails to gEBTN("*") // https://github.com/ajaxorg/node-o3-xml/issues/21 descendants = doc.selectNodes('//*[name(.)!="svg"]'); var arr = []; for (i = 0, len = descendants.length; i < len; i++) { arr[i] = descendants[i]; } descendants = arr; } var elements = descendants.filter(function(el) { applyViewboxTransform(el); return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')) && !hasAncestorWithNodeName(el, fabric.svgInvalidAncestorsRegEx); // http://www.w3.org/TR/SVG/struct.html#DefsElement }); if (!elements || (elements && !elements.length)) { callback && callback([], {}); return; } var clipPaths = { }; descendants.filter(function(el) { return el.nodeName.replace('svg:', '') === 'clipPath'; }).forEach(function(el) { var id = el.getAttribute('id'); clipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) { return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); }); }); fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); fabric.cssRules[svgUid] = fabric.getCSSRules(doc); fabric.clipPaths[svgUid] = clipPaths; // Precedence of rules: style > class > attribute fabric.parseElements(elements, function(instances, elements) { if (callback) { callback(instances, options, elements, descendants); delete fabric.gradientDefs[svgUid]; delete fabric.cssRules[svgUid]; delete fabric.clipPaths[svgUid]; } }, clone(options), reviver, parsingOptions); }; function recursivelyParseGradientsXlink(doc, gradient) { var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], xlinkAttr = 'xlink:href', xLink = gradient.getAttribute(xlinkAttr).substr(1), referencedGradient = elementById(doc, xLink); if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { recursivelyParseGradientsXlink(doc, referencedGradient); } gradientsAttrs.forEach(function(attr) { if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); } }); if (!gradient.children.length) { var referenceClone = referencedGradient.cloneNode(true); while (referenceClone.firstChild) { gradient.appendChild(referenceClone.firstChild); } } gradient.removeAttribute(xlinkAttr); } var reFontDeclaration = new RegExp( '(normal|italic)?\\s*(normal|small-caps)?\\s*' + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + fabric.reNum + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); extend(fabric, { /** * Parses a short font declaration, building adding its properties to a style object * @static * @function * @memberOf fabric * @param {String} value font declaration * @param {Object} oStyle definition */ parseFontDeclaration: function(value, oStyle) { var match = value.match(reFontDeclaration); if (!match) { return; } var fontStyle = match[1], // font variant is not used // fontVariant = match[2], fontWeight = match[3], fontSize = match[4], lineHeight = match[5], fontFamily = match[6]; if (fontStyle) { oStyle.fontStyle = fontStyle; } if (fontWeight) { oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); } if (fontSize) { oStyle.fontSize = parseUnit(fontSize); } if (fontFamily) { oStyle.fontFamily = fontFamily; } if (lineHeight) { oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; } }, /** * Parses an SVG document, returning all of the gradient declarations found in it * @static * @function * @memberOf fabric * @param {SVGDocument} doc SVG document to parse * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element */ getGradientDefs: function(doc) { var tagArray = [ 'linearGradient', 'radialGradient', 'svg:linearGradient', 'svg:radialGradient'], elList = _getMultipleNodes(doc, tagArray), el, j = 0, gradientDefs = { }; j = elList.length; while (j--) { el = elList[j]; if (el.getAttribute('xlink:href')) { recursivelyParseGradientsXlink(doc, el); } gradientDefs[el.getAttribute('id')] = el; } return gradientDefs; }, /** * Returns an object of attributes' name/value, given element and an array of attribute names; * Parses parent "g" nodes recursively upwards. * @static * @memberOf fabric * @param {DOMElement} element Element to parse * @param {Array} attributes Array of attributes to parse * @return {Object} object containing parsed attributes' names/values */ parseAttributes: function(element, attributes, svgUid) { if (!element) { return; } var value, parentAttributes = { }, fontSize, parentFontSize; if (typeof svgUid === 'undefined') { svgUid = element.getAttribute('svgUid'); } // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); } var ownAttributes = attributes.reduce(function(memo, attr) { value = element.getAttribute(attr); if (value) { // eslint-disable-line memo[attr] = value; } return memo; }, { }); // add values parsed from style, which take precedence over attributes // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) var cssAttrs = extend( getGlobalStylesForElement(element, svgUid), fabric.parseStyleAttribute(element) ); ownAttributes = extend( ownAttributes, cssAttrs ); if (cssAttrs[cPath]) { element.setAttribute(cPath, cssAttrs[cPath]); } fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; if (ownAttributes[fSize]) { // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. ownAttributes[fSize] = fontSize = parseUnit(ownAttributes[fSize], parentFontSize); } var normalizedAttr, normalizedValue, normalizedStyle = {}; for (var attr in ownAttributes) { normalizedAttr = normalizeAttr(attr); normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); normalizedStyle[normalizedAttr] = normalizedValue; } if (normalizedStyle && normalizedStyle.font) { fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); } var mergedAttrs = extend(parentAttributes, normalizedStyle); return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); }, /** * Transforms an array of svg elements to corresponding fabric.* instances * @static * @memberOf fabric * @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 * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. */ parseElements: function(elements, callback, options, reviver, parsingOptions) { new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); }, /** * Parses "style" attribute, retuning an object with values * @static * @memberOf fabric * @param {SVGElement} element Element to parse * @return {Object} Objects with values parsed from style attribute of an element */ parseStyleAttribute: function(element) { var oStyle = { }, style = element.getAttribute('style'); if (!style) { return oStyle; } if (typeof style === 'string') { parseStyleString(style, oStyle); } else { parseStyleObject(style, oStyle); } return oStyle; }, /** * Parses "points" attribute, returning an array of values * @static * @memberOf fabric * @param {String} points points attribute string * @return {Array} array of points */ parsePointsAttribute: function(points) { // points attribute is required and must not be empty if (!points) { return null; } // replace commas with whitespace and remove bookending whitespace points = points.replace(/,/g, ' ').trim(); points = points.split(/\s+/); var parsedPoints = [], i, len; for (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; }, /** * Returns CSS rules for a given SVG document * @static * @function * @memberOf fabric * @param {SVGDocument} doc SVG document to parse * @return {Object} CSS rules of this document */ getCSSRules: function(doc) { var styles = doc.getElementsByTagName('style'), i, len, allRules = { }, rules; // very crude parsing of style contents for (i = 0, len = styles.length; i < len; i++) { // IE9 doesn't support textContent, but provides text instead. var styleContents = styles[i].textContent || styles[i].text; // remove comments styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); if (styleContents.trim() === '') { continue; } rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); rules = rules.map(function(rule) { return rule.trim(); }); // eslint-disable-next-line no-loop-func rules.forEach(function(rule) { var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), ruleObj = { }, declaration = match[2].trim(), propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); for (i = 0, len = propertyValuePairs.length; i < len; i++) { var pair = propertyValuePairs[i].split(/\s*:\s*/), property = pair[0], value = pair[1]; ruleObj[property] = value; } rule = match[1]; rule.split(',').forEach(function(_rule) { _rule = _rule.replace(/^svg/i, '').trim(); if (_rule === '') { return; } if (allRules[_rule]) { fabric.util.object.extend(allRules[_rule], ruleObj); } else { allRules[_rule] = fabric.util.object.clone(ruleObj); } }); }); } return allRules; }, /** * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. * Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) * @memberOf fabric * @param {String} url * @param {Function} callback * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [options] Object containing options for parsing * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources */ loadSVGFromURL: function(url, callback, reviver, options) { url = url.replace(/^\n\s*/, '').trim(); new fabric.util.request(url, { method: 'get', onComplete: onComplete }); function onComplete(r) { var xml = r.responseXML; if (xml && !xml.documentElement && fabric.window.ActiveXObject && r.responseText) { xml = new ActiveXObject('Microsoft.XMLDOM'); xml.async = 'false'; //IE chokes on DOCTYPE xml.loadXML(r.responseText.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i, '')); } if (!xml || !xml.documentElement) { callback && callback(null); return false; } fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { callback && callback(results, _options, elements, allElements); }, reviver, options); } }, /** * Takes string corresponding to an SVG document, and parses it into a set of fabric objects * @memberOf fabric * @param {String} string * @param {Function} callback * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. * @param {Object} [options] Object containing options for parsing * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources */ loadSVGFromString: function(string, callback, reviver, options) { string = string.trim(); var doc; if (typeof fabric.window.DOMParser !== 'undefined') { var parser = new fabric.window.DOMParser(); if (parser && parser.parseFromString) { doc = parser.parseFromString(string, 'text/xml'); } } else if (fabric.window.ActiveXObject) { doc = new ActiveXObject('Microsoft.XMLDOM'); doc.async = 'false'; // IE chokes on DOCTYPE doc.loadXML(string.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i, '')); } fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { callback(results, _options, elements, allElements); }, reviver, options); } }); })(typeof exports !== 'undefined' ? exports : this);