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;