mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-16 22:10:32 +00:00
1067 lines
35 KiB
JavaScript
1067 lines
35 KiB
JavaScript
(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'
|
|
};
|
|
|
|
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;
|
|
|
|
applyViewboxTransform(el2);
|
|
if (/^svg$/i.test(el2.nodeName)) {
|
|
var el3 = el2.ownerDocument.createElement('g');
|
|
for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) {
|
|
attr = attrs.item(j);
|
|
el3.setAttribute(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]);
|
|
|
|
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.createElement('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 (!gradient.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)
|
|
ownAttributes = extend(ownAttributes,
|
|
extend(getGlobalStylesForElement(element, svgUid), fabric.parseStyleAttribute(element)));
|
|
|
|
fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE;
|
|
if (ownAttributes['font-size']) {
|
|
// looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers.
|
|
ownAttributes['font-size'] = fontSize = parseUnit(ownAttributes['font-size'], 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 DOMParser !== 'undefined') {
|
|
var parser = new 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);
|