var Group = require("../graphic/Group");
var QImage = require("../graphic/Image");
var Text = require("../graphic/Text");
var Circle = require("../graphic/shape/Circle");
var Rect = require("../graphic/shape/Rect");
var Ellipse = require("../graphic/shape/Ellipse");
var Line = require("../graphic/line/Line");
var Polyline = require("../graphic/line/Polyline");
var Path = require("../graphic/Path");
var Polygon = require("../graphic/shape/Polygon");
var LinearGradient = require("../graphic/gradient/LinearGradient");
var Style = require("../graphic/Style");
var matrixUtil = require("../utils/affine_matrix_util");
var _path_util = require("../utils/path_util");
var createFromString = _path_util.createFromString;
var _data_structure_util = require("../utils/data_structure_util");
var isString = _data_structure_util.isString;
var extend = _data_structure_util.extend;
var trim = _data_structure_util.trim;
var each = _data_structure_util.each;
var _class_util = require("../utils/class_util");
var defaults = _class_util.defaults;
var _constants = require("../utils/constants");
var mathMin = _constants.mathMin;
/* eslint-disable no-prototype-builtins */
/* eslint-disable no-unused-vars */
/* eslint-disable no-useless-escape */
// Most of the values can be separated by comma and/or white space.
var DILIMITER_REG = /[\s,]+/;
/**
* For big svg string, this method might be time consuming.
* //TODO:try to move this into webworker.
* @param {String} svg xml string
* @return {Object} xml root.
*/
function parseXML(svg) {
if (isString(svg)) {
var parser = new DOMParser();
svg = parser.parseFromString(svg, 'text/xml');
} // Document node. If using $.get, doc node may be input.
if (svg.nodeType === 9) {
svg = svg.firstChild;
} // nodeName of <!DOCTYPE svg> is also 'svg'.
while (svg.nodeName.toLowerCase() !== 'svg' || svg.nodeType !== 1) {
svg = svg.nextSibling;
}
return svg;
}
/**
* @class qrenderer.svg.SVGParser
*
* This is a tool class for parsing SVG xml string to standard shape classes.
*
* 这是一个工具类,用来把 SVG 格式的 xml 解析成 graphic 包中定义的标准类。
*
* @docauthor 大漠穷秋 damoqiongqiu@126.com
*/
function SVGParser() {
this._defs = {};
this._root = null;
this._isDefine = false;
this._isText = false;
}
SVGParser.prototype = {
constructor: SVGParser,
parse: function parse(xml, opt) {
opt = opt || {};
var svg = parseXML(xml);
if (!svg) {
throw new Error('Illegal svg');
}
var root = new Group();
this._root = root; // parse view port
var viewBox = svg.getAttribute('viewBox') || ''; // If width/height not specified, means "100%" of `opt.width/height`.
// TODO: Other percent value not supported yet.
var width = parseFloat(svg.getAttribute('width') || opt.width);
var height = parseFloat(svg.getAttribute('height') || opt.height); // If width/height not specified, set as null for output.
isNaN(width) && (width = null);
isNaN(height) && (height = null); // Apply inline style on svg element.
parseAttributes(svg, root, null, true);
var child = svg.firstChild;
while (child) {
this._parseNode(child, root);
child = child.nextSibling;
}
var viewBoxRect;
var viewBoxTransform;
if (viewBox) {
var viewBoxArr = trim(viewBox).split(DILIMITER_REG); // Some invalid case like viewBox: 'none'.
if (viewBoxArr.length >= 4) {
viewBoxRect = {
x: parseFloat(viewBoxArr[0] || 0),
y: parseFloat(viewBoxArr[1] || 0),
width: parseFloat(viewBoxArr[2]),
height: parseFloat(viewBoxArr[3])
};
}
}
if (viewBoxRect && width != null && height != null) {
viewBoxTransform = makeViewBoxTransform(viewBoxRect, width, height);
if (!opt.ignoreViewBox) {
// If set transform on the output group, it probably bring trouble when
// some users only intend to show the clipped content inside the viewBox,
// but not intend to transform the output group. So we keep the output
// group no transform. If the user intend to use the viewBox as a
// camera, just set `opt.ignoreViewBox` as `true` and set transfrom
// manually according to the viewBox info in the output of this method.
var elRoot = root;
root = new Group();
root.add(elRoot);
elRoot.scale = viewBoxTransform.scale.slice();
elRoot.position = viewBoxTransform.position.slice();
}
} // Some shapes might be overflow the viewport, which should be
// clipped despite whether the viewBox is used, as the SVG does.
if (!opt.ignoreRootClip && width != null && height != null) {
root.setClipPath(new Rect({
shape: {
x: 0,
y: 0,
width: width,
height: height
}
}));
} // Set width/height on group just for output the viewport size.
return {
root: root,
width: width,
height: height,
viewBoxRect: viewBoxRect,
viewBoxTransform: viewBoxTransform
};
},
_parseNode: function _parseNode(xmlNode, parentGroup) {
var nodeName = xmlNode.nodeName.toLowerCase(); // TODO
// support <style>...</style> in svg, where nodeName is 'style',
// CSS classes is defined globally wherever the style tags are declared.
if (nodeName === 'defs') {
// define flag
this._isDefine = true;
} else if (nodeName === 'text') {
this._isText = true;
}
var el;
if (this._isDefine) {
var parser = defineParsers[nodeName];
if (parser) {
var def = parser.call(this, xmlNode);
var id = xmlNode.getAttribute('id');
if (id) {
this._defs[id] = def;
}
}
} else {
var _parser = nodeParsers[nodeName];
if (_parser) {
el = _parser.call(this, xmlNode, parentGroup);
parentGroup.add(el);
}
}
var child = xmlNode.firstChild;
while (child) {
if (child.nodeType === 1) {
this._parseNode(child, el);
} // Is text
if (child.nodeType === 3 && this._isText) {
this._parseText(child, el);
}
child = child.nextSibling;
} // Quit define
if (nodeName === 'defs') {
this._isDefine = false;
} else if (nodeName === 'text') {
this._isText = false;
}
},
_parseText: function _parseText(xmlNode, parentGroup) {
if (xmlNode.nodeType === 1) {
var dx = xmlNode.getAttribute('dx') || 0;
var dy = xmlNode.getAttribute('dy') || 0;
this._textX += parseFloat(dx);
this._textY += parseFloat(dy);
}
var text = new Text({
style: {
text: xmlNode.textContent,
transformText: true
},
position: [this._textX || 0, this._textY || 0]
});
inheritStyle(parentGroup, text);
parseAttributes(xmlNode, text, this._defs);
var fontSize = text.style.fontSize;
if (fontSize && fontSize < 9) {
// PENDING
text.style.fontSize = 9;
text.scale = text.scale || [1, 1];
text.scale[0] *= fontSize / 9;
text.scale[1] *= fontSize / 9;
}
var rect = text.getBoundingRect();
this._textX += rect.width;
parentGroup.add(text);
return text;
}
};
var nodeParsers = {
'g': function g(xmlNode, parentGroup) {
var g = new Group();
inheritStyle(parentGroup, g);
parseAttributes(xmlNode, g, this._defs);
return g;
},
'rect': function rect(xmlNode, parentGroup) {
var rect = new Rect();
inheritStyle(parentGroup, rect);
parseAttributes(xmlNode, rect, this._defs);
rect.setShape({
x: parseFloat(xmlNode.getAttribute('x') || 0),
y: parseFloat(xmlNode.getAttribute('y') || 0),
width: parseFloat(xmlNode.getAttribute('width') || 0),
height: parseFloat(xmlNode.getAttribute('height') || 0)
});
return rect;
},
'circle': function circle(xmlNode, parentGroup) {
var circle = new Circle();
inheritStyle(parentGroup, circle);
parseAttributes(xmlNode, circle, this._defs);
circle.setShape({
cx: parseFloat(xmlNode.getAttribute('cx') || 0),
cy: parseFloat(xmlNode.getAttribute('cy') || 0),
r: parseFloat(xmlNode.getAttribute('r') || 0)
});
return circle;
},
'line': function line(xmlNode, parentGroup) {
var line = new Line();
inheritStyle(parentGroup, line);
parseAttributes(xmlNode, line, this._defs);
line.setShape({
x1: parseFloat(xmlNode.getAttribute('x1') || 0),
y1: parseFloat(xmlNode.getAttribute('y1') || 0),
x2: parseFloat(xmlNode.getAttribute('x2') || 0),
y2: parseFloat(xmlNode.getAttribute('y2') || 0)
});
return line;
},
'ellipse': function ellipse(xmlNode, parentGroup) {
var ellipse = new Ellipse();
inheritStyle(parentGroup, ellipse);
parseAttributes(xmlNode, ellipse, this._defs);
ellipse.setShape({
cx: parseFloat(xmlNode.getAttribute('cx') || 0),
cy: parseFloat(xmlNode.getAttribute('cy') || 0),
rx: parseFloat(xmlNode.getAttribute('rx') || 0),
ry: parseFloat(xmlNode.getAttribute('ry') || 0)
});
return ellipse;
},
'polygon': function polygon(xmlNode, parentGroup) {
var points = xmlNode.getAttribute('points');
if (points) {
points = parsePoints(points);
}
var polygon = new Polygon({
shape: {
points: points || []
}
});
inheritStyle(parentGroup, polygon);
parseAttributes(xmlNode, polygon, this._defs);
return polygon;
},
'polyline': function polyline(xmlNode, parentGroup) {
var path = new Path();
inheritStyle(parentGroup, path);
parseAttributes(xmlNode, path, this._defs);
var points = xmlNode.getAttribute('points');
if (points) {
points = parsePoints(points);
}
var polyline = new Polyline({
shape: {
points: points || []
}
});
return polyline;
},
'image': function image(xmlNode, parentGroup) {
var img = new QImage();
inheritStyle(parentGroup, img);
parseAttributes(xmlNode, img, this._defs);
img.attr({
style: {
image: xmlNode.getAttribute('xlink:href'),
x: xmlNode.getAttribute('x'),
y: xmlNode.getAttribute('y'),
width: xmlNode.getAttribute('width'),
height: xmlNode.getAttribute('height')
}
});
return img;
},
'text': function text(xmlNode, parentGroup) {
var x = xmlNode.getAttribute('x') || 0;
var y = xmlNode.getAttribute('y') || 0;
var dx = xmlNode.getAttribute('dx') || 0;
var dy = xmlNode.getAttribute('dy') || 0;
this._textX = parseFloat(x) + parseFloat(dx);
this._textY = parseFloat(y) + parseFloat(dy);
var g = new Group();
inheritStyle(parentGroup, g);
parseAttributes(xmlNode, g, this._defs);
return g;
},
'tspan': function tspan(xmlNode, parentGroup) {
var x = xmlNode.getAttribute('x');
var y = xmlNode.getAttribute('y');
if (x != null) {
// new offset x
this._textX = parseFloat(x);
}
if (y != null) {
// new offset y
this._textY = parseFloat(y);
}
var dx = xmlNode.getAttribute('dx') || 0;
var dy = xmlNode.getAttribute('dy') || 0;
var g = new Group();
inheritStyle(parentGroup, g);
parseAttributes(xmlNode, g, this._defs);
this._textX += dx;
this._textY += dy;
return g;
},
'path': function path(xmlNode, parentGroup) {
// TODO svg fill rule
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule
// path.style.globalCompositeOperation = 'xor';
var d = xmlNode.getAttribute('d') || ''; // Performance sensitive.
var path = createFromString(d);
inheritStyle(parentGroup, path);
parseAttributes(xmlNode, path, this._defs);
return path;
}
};
var defineParsers = {
'lineargradient': function lineargradient(xmlNode) {
var x1 = parseInt(xmlNode.getAttribute('x1') || 0, 10);
var y1 = parseInt(xmlNode.getAttribute('y1') || 0, 10);
var x2 = parseInt(xmlNode.getAttribute('x2') || 10, 10);
var y2 = parseInt(xmlNode.getAttribute('y2') || 0, 10);
var gradient = new LinearGradient(x1, y1, x2, y2);
_parseGradientColorStops(xmlNode, gradient);
return gradient;
},
'radialgradient': function radialgradient(xmlNode) {}
};
function _parseGradientColorStops(xmlNode, gradient) {
var stop = xmlNode.firstChild;
while (stop) {
if (stop.nodeType === 1) {
var offset = stop.getAttribute('offset');
if (offset.indexOf('%') > 0) {
// percentage
offset = parseInt(offset, 10) / 100;
} else if (offset) {
// number from 0 to 1
offset = parseFloat(offset);
} else {
offset = 0;
}
var stopColor = stop.getAttribute('stop-color') || '#000000';
gradient.addColorStop(offset, stopColor);
}
stop = stop.nextSibling;
}
}
function inheritStyle(parent, child) {
if (parent && parent.__inheritedStyle) {
if (!child.__inheritedStyle) {
child.__inheritedStyle = {};
}
defaults(child.__inheritedStyle, parent.__inheritedStyle);
}
}
function parsePoints(pointsString) {
var list = trim(pointsString).split(DILIMITER_REG);
var points = [];
for (var i = 0; i < list.length; i += 2) {
var x = parseFloat(list[i]);
var y = parseFloat(list[i + 1]);
points.push([x, y]);
}
return points;
}
var attributesMap = {
'fill': 'fill',
'stroke': 'stroke',
'stroke-width': 'lineWidth',
'opacity': 'opacity',
'fill-opacity': 'fillOpacity',
'stroke-opacity': 'strokeOpacity',
'stroke-dasharray': 'lineDash',
'stroke-dashoffset': 'lineDashOffset',
'stroke-linecap': 'lineCap',
'stroke-linejoin': 'lineJoin',
'stroke-miterlimit': 'miterLimit',
'font-family': 'fontFamily',
'font-size': 'fontSize',
'font-style': 'fontStyle',
'font-weight': 'fontWeight',
'text-align': 'textAlign',
'alignment-baseline': 'textBaseline'
};
function parseAttributes(xmlNode, el, defs, onlyInlineStyle) {
var qrStyle = el.__inheritedStyle || {};
var isTextEl = el.type === 'text'; // TODO Shadow
if (xmlNode.nodeType === 1) {
parseTransformAttribute(xmlNode, el);
extend(qrStyle, parseStyleAttribute(xmlNode));
if (!onlyInlineStyle) {
for (var svgAttrName in attributesMap) {
if (attributesMap.hasOwnProperty(svgAttrName)) {
var attrValue = xmlNode.getAttribute(svgAttrName);
if (attrValue != null) {
qrStyle[attributesMap[svgAttrName]] = attrValue;
}
}
}
}
}
var elFillProp = isTextEl ? 'textFill' : 'fill';
var elStrokeProp = isTextEl ? 'textStroke' : 'stroke';
el.style = el.style || new Style();
var elStyle = el.style;
qrStyle.fill != null && elStyle.set(elFillProp, getPaint(qrStyle.fill, defs));
qrStyle.stroke != null && elStyle.set(elStrokeProp, getPaint(qrStyle.stroke, defs));
each(['lineWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'miterLimit', 'fontSize'], function (propName) {
var elPropName = propName === 'lineWidth' && isTextEl ? 'textStrokeWidth' : propName;
qrStyle[propName] != null && elStyle.set(elPropName, parseFloat(qrStyle[propName]));
});
if (!qrStyle.textBaseline || qrStyle.textBaseline === 'auto') {
qrStyle.textBaseline = 'alphabetic';
}
if (qrStyle.textBaseline === 'alphabetic') {
qrStyle.textBaseline = 'bottom';
}
if (qrStyle.textAlign === 'start') {
qrStyle.textAlign = 'left';
}
if (qrStyle.textAlign === 'end') {
qrStyle.textAlign = 'right';
}
each(['lineDashOffset', 'lineCap', 'lineJoin', 'fontWeight', 'fontFamily', 'fontStyle', 'textAlign', 'textBaseline'], function (propName) {
qrStyle[propName] != null && elStyle.set(propName, qrStyle[propName]);
});
if (qrStyle.lineDash) {
el.style.lineDash = trim(qrStyle.lineDash).split(DILIMITER_REG);
}
if (elStyle[elStrokeProp] && elStyle[elStrokeProp] !== 'none') {
// enable stroke
el[elStrokeProp] = true;
}
el.__inheritedStyle = qrStyle;
}
var urlRegex = /url\(\s*#(.*?)\)/;
function getPaint(str, defs) {
// if (str === 'none') {
// return;
// }
var urlMatch = defs && str && str.match(urlRegex);
if (urlMatch) {
var url = trim(urlMatch[1]);
var def = defs[url];
return def;
}
return str;
}
var transformRegex = /(translate|scale|rotate|skewX|skewY|matrix)\(([\-\s0-9\.e,]*)\)/g;
function parseTransformAttribute(xmlNode, node) {
var transform = xmlNode.getAttribute('transform');
if (transform) {
transform = transform.replace(/,/g, ' ');
var m = null;
var transformOps = [];
transform.replace(transformRegex, function (str, type, value) {
transformOps.push(type, value);
});
var px = 0;
var py = 0;
var sx = 0;
var sy = 0;
var rotation = 0;
var skewX = 0;
var skewY = 0;
for (var i = transformOps.length - 1; i > 0; i -= 2) {
var value = transformOps[i];
var type = transformOps[i - 1];
m = m || matrixUtil.create();
switch (type) {
case 'translate':
value = trim(value).split(DILIMITER_REG);
px = value[0] + parseFloat(value[0]);
py = value[1] + parseFloat(value[1] || 0);
m = matrixUtil.translate(m, [px, py]);
break;
case 'scale':
value = trim(value).split(DILIMITER_REG);
sx = parseFloat(value[0]);
sy = parseFloat(value[1] || value[0]);
m = matrixUtil.scale(m, [sx, sy]);
break;
case 'rotate':
value = trim(value).split(DILIMITER_REG);
rotation = parseFloat(value[0]);
m = matrixUtil.rotate(m, rotation);
break;
case 'skew':
value = trim(value).split(DILIMITER_REG);
skewX = parseFloat(value[0]);
skewY = parseFloat(value[1] || value[0]);
m = matrixUtil.scale(m, [skewX, skewY]);
break;
case 'matrix':
value = trim(value).split(DILIMITER_REG);
m[0] = parseFloat(value[0]);
m[1] = parseFloat(value[1]);
m[2] = parseFloat(value[2]);
m[3] = parseFloat(value[3]);
m[4] = parseFloat(value[4]);
m[5] = parseFloat(value[5]);
break;
}
node.transform = m;
}
}
} // Value may contain space.
var styleRegex = /([^\s:;]+)\s*:\s*([^:;]+)/g;
function parseStyleAttribute(xmlNode) {
var style = xmlNode.getAttribute('style');
var result = {};
if (!style) {
return result;
}
var styleList = {};
styleRegex.lastIndex = 0;
var styleRegResult;
while ((styleRegResult = styleRegex.exec(style)) != null) {
styleList[styleRegResult[1]] = styleRegResult[2];
}
for (var svgAttrName in attributesMap) {
if (attributesMap.hasOwnProperty(svgAttrName) && styleList[svgAttrName] != null) {
result[attributesMap[svgAttrName]] = styleList[svgAttrName];
}
}
return result;
}
/**
* @param {Array<Number>} viewBoxRect
* @param {Number} width
* @param {Number} height
* @return {Object} {scale, position}
*/
function makeViewBoxTransform(viewBoxRect, width, height) {
var scaleX = width / viewBoxRect.width;
var scaleY = height / viewBoxRect.height;
var scale = mathMin(scaleX, scaleY); // preserveAspectRatio 'xMidYMid'
var viewBoxScale = [scale, scale];
var viewBoxPosition = [-(viewBoxRect.x + viewBoxRect.width / 2) * scale + width / 2, -(viewBoxRect.y + viewBoxRect.height / 2) * scale + height / 2];
return {
scale: viewBoxScale,
position: viewBoxPosition
};
}
/**
* @static
* @method parseSVG
*
* Parse SVG DOM to QuarkRenderer specific interfaces.
*
* 把 SVG DOM 标签解析成 QuarkRenderer 所定义的接口。
*
* @param {String|XMLElement} xml
* @param {Object} [opt]
* @param {Number} [opt.width] Default width if svg width not specified or is a percent value.
* @param {Number} [opt.height] Default height if svg height not specified or is a percent value.
* @param {Boolean} [opt.ignoreViewBox]
* @param {Boolean} [opt.ignoreRootClip]
* @return {Object} result:
* {
* root: Group, The root of the the result tree of qrenderer shapes,
* width: number, the viewport width of the SVG,
* height: number, the viewport height of the SVG,
* viewBoxRect: {x, y, width, height}, the declared viewBox rect of the SVG, if exists,
* viewBoxTransform: the {scale, position} calculated by viewBox and viewport, is exists.
* }
*/
function parseSVG(xml, opt) {
var parser = new SVGParser();
return parser.parse(xml, opt);
}
exports.parseXML = parseXML;
exports.makeViewBoxTransform = makeViewBoxTransform;
exports.parseSVG = parseSVG;