UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

955 lines (817 loc) 34 kB
import Group from '../graphic/Group'; import ZRImage from '../graphic/Image'; import Circle from '../graphic/shape/Circle'; import Rect from '../graphic/shape/Rect'; import Ellipse from '../graphic/shape/Ellipse'; import Line from '../graphic/shape/Line'; import Polygon from '../graphic/shape/Polygon'; import Polyline from '../graphic/shape/Polyline'; import * as matrix from '../core/matrix'; import { createFromString } from './path'; import { defaults, trim, each, map, keys, hasOwn } from '../core/util'; import Displayable from '../graphic/Displayable'; import Element from '../Element'; import { RectLike } from '../core/BoundingRect'; import { Dictionary } from '../core/types'; import { PatternObject } from '../graphic/Pattern'; import LinearGradient, { LinearGradientObject } from '../graphic/LinearGradient'; import RadialGradient, { RadialGradientObject } from '../graphic/RadialGradient'; import Gradient, { GradientObject } from '../graphic/Gradient'; import TSpan, { TSpanStyleProps } from '../graphic/TSpan'; import { parseXML } from './parseXML'; interface SVGParserOption { // Default width if svg width not specified or is a percent value. width?: number; // Default height if svg height not specified or is a percent value. height?: number; ignoreViewBox?: boolean; ignoreRootClip?: boolean; } export interface SVGParserResult { // Group, The root of the the result tree of zrender shapes root: Group; // number, the viewport width of the SVG width: number; // number, the viewport height of the SVG height: number; // {x, y, width, height}, the declared viewBox rect of the SVG, if exists viewBoxRect: RectLike; // the {scale, position} calculated by viewBox and viewport, is exists viewBoxTransform: { x: number; y: number; scale: number; }; named: SVGParserResultNamedItem[]; } export interface SVGParserResultNamedItem { name: string; // If a tag has no name attribute but its ancester <g> is named, // `namedFrom` is set to the named item of the ancester <g>. // Otherwise null/undefined namedFrom: SVGParserResultNamedItem; svgNodeTagLower: SVGNodeTagLower; el: Element; }; export type SVGNodeTagLower = 'g' | 'rect' | 'circle' | 'line' | 'ellipse' | 'polygon' | 'polyline' | 'image' | 'text' | 'tspan' | 'path' | 'defs' | 'switch'; type DefsId = string; type DefsMap = { [id in DefsId]: LinearGradientObject | RadialGradientObject | PatternObject }; type DefsUsePending = [Displayable, 'fill' | 'stroke', DefsId][]; type ElementExtended = Element & { __inheritedStyle?: InheritedStyleByZRKey; __selfStyle?: SelfStyleByZRKey; } type DisplayableExtended = Displayable & { __inheritedStyle?: InheritedStyleByZRKey; __selfStyle?: SelfStyleByZRKey; } type TextStyleOptionExtended = TSpanStyleProps & { fontSize: number; fontFamily: string; fontWeight: string; fontStyle: string; } let nodeParsers: {[name in SVGNodeTagLower]?: ( this: SVGParser, xmlNode: SVGElement, parentGroup: Group ) => Element}; type InheritedStyleByZRKey = {[name in InheritableStyleZRKey]?: string}; type InheritableStyleZRKey = typeof INHERITABLE_STYLE_ATTRIBUTES_MAP[keyof typeof INHERITABLE_STYLE_ATTRIBUTES_MAP]; const INHERITABLE_STYLE_ATTRIBUTES_MAP = { '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-anchor': 'textAlign', 'visibility': 'visibility', 'display': 'display' } as const; const INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS = keys(INHERITABLE_STYLE_ATTRIBUTES_MAP); type SelfStyleByZRKey = {[name in SelfStyleZRKey]?: string}; type SelfStyleZRKey = typeof SELF_STYLE_ATTRIBUTES_MAP[keyof typeof SELF_STYLE_ATTRIBUTES_MAP]; const SELF_STYLE_ATTRIBUTES_MAP = { 'alignment-baseline': 'textBaseline', 'stop-color': 'stopColor' }; const SELF_STYLE_ATTRIBUTES_MAP_KEYS = keys(SELF_STYLE_ATTRIBUTES_MAP); class SVGParser { private _defs: DefsMap = {}; // The use of <defs> can be in front of <defs> declared. // So save them temporarily in `_defsUsePending`. private _defsUsePending: DefsUsePending; private _root: Group = null; private _textX: number; private _textY: number; parse(xml: string | Document | SVGElement, opt: SVGParserOption): SVGParserResult { opt = opt || {}; const svg = parseXML(xml); if (process.env.NODE_ENV !== 'production') { if (!svg) { throw new Error('Illegal svg'); } } this._defsUsePending = []; let root = new Group(); this._root = root; const named: SVGParserResult['named'] = []; // parse view port const viewBox = svg.getAttribute('viewBox') || ''; // If width/height not specified, means "100%" of `opt.width/height`. // TODO: Other percent value not supported yet. let width = parseFloat((svg.getAttribute('width') || opt.width) as string); let height = parseFloat((svg.getAttribute('height') || opt.height) as string); // 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, false); let child = svg.firstChild as SVGElement; while (child) { this._parseNode(child, root, named, null, false, false); child = child.nextSibling as SVGElement; } applyDefs(this._defs, this._defsUsePending); this._defsUsePending = []; let viewBoxRect; let viewBoxTransform; if (viewBox) { const viewBoxArr = splitNumberSequence(viewBox); // Some invalid case like viewBox: 'none'. if (viewBoxArr.length >= 4) { viewBoxRect = { x: parseFloat((viewBoxArr[0] || 0) as string), y: parseFloat((viewBoxArr[1] || 0) as string), width: parseFloat(viewBoxArr[2]), height: parseFloat(viewBoxArr[3]) }; } } if (viewBoxRect && width != null && height != null) { viewBoxTransform = makeViewBoxTransform(viewBoxRect, { x: 0, y: 0, width: width, height: 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. const elRoot = root; root = new Group(); root.add(elRoot); elRoot.scaleX = elRoot.scaleY = viewBoxTransform.scale; elRoot.x = viewBoxTransform.x; elRoot.y = viewBoxTransform.y; } } // 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, named: named }; } private _parseNode( xmlNode: SVGElement, parentGroup: Group, named: SVGParserResultNamedItem[], namedFrom: SVGParserResultNamedItem['namedFrom'], isInDefs: boolean, isInText: boolean ): void { const nodeName = xmlNode.nodeName.toLowerCase() as SVGNodeTagLower; // TODO: // support <style>...</style> in svg, where nodeName is 'style', // CSS classes is defined globally wherever the style tags are declared. let el; let namedFromForSub = namedFrom; if (nodeName === 'defs') { isInDefs = true; } if (nodeName === 'text') { isInText = true; } if (nodeName === 'defs' || nodeName === 'switch') { // Just make <switch> displayable. Do not support // the full feature of it. el = parentGroup; } else { // In <defs>, elments will not be rendered. // TODO: // do not support elements in <defs> yet, until requirement come. // other graphic elements can also be in <defs> and referenced by // <use x="5" y="5" xlink:href="#myCircle" /> // multiple times if (!isInDefs) { const parser = nodeParsers[nodeName]; if (parser && hasOwn(nodeParsers, nodeName)) { el = parser.call(this, xmlNode, parentGroup); // Do not support empty string; const nameAttr = xmlNode.getAttribute('name'); if (nameAttr) { const newNamed: SVGParserResultNamedItem = { name: nameAttr, namedFrom: null, svgNodeTagLower: nodeName, el: el }; named.push(newNamed); if (nodeName === 'g') { namedFromForSub = newNamed; } } else if (namedFrom) { named.push({ name: namedFrom.name, namedFrom: namedFrom, svgNodeTagLower: nodeName, el: el }); } parentGroup.add(el); } } // Whether gradients/patterns are declared in <defs> or not, // they all work. const parser = paintServerParsers[nodeName]; if (parser && hasOwn(paintServerParsers, nodeName)) { const def = parser.call(this, xmlNode); const id = xmlNode.getAttribute('id'); if (id) { this._defs[id] = def; } } } // If xmlNode is <g>, <text>, <tspan>, <defs>, <switch>, // el will be a group, and traverse the children. if (el && el.isGroup) { let child = xmlNode.firstChild as SVGElement; while (child) { if (child.nodeType === 1) { this._parseNode(child, el as Group, named, namedFromForSub, isInDefs, isInText); } // Is plain text rather than a tagged node. else if (child.nodeType === 3 && isInText) { this._parseText(child, el as Group); } child = child.nextSibling as SVGElement; } } } private _parseText(xmlNode: SVGElement, parentGroup: Group): TSpan { const text = new TSpan({ style: { text: xmlNode.textContent }, silent: true, x: this._textX || 0, y: this._textY || 0 }); inheritStyle(parentGroup, text); parseAttributes(xmlNode, text, this._defsUsePending, false, false); applyTextAlignment(text, parentGroup); const textStyle = text.style as TextStyleOptionExtended; const fontSize = textStyle.fontSize; if (fontSize && fontSize < 9) { // PENDING textStyle.fontSize = 9; text.scaleX *= fontSize / 9; text.scaleY *= fontSize / 9; } const font = (textStyle.fontSize || textStyle.fontFamily) && [ textStyle.fontStyle, textStyle.fontWeight, (textStyle.fontSize || 12) + 'px', // If font properties are defined, `fontFamily` should not be ignored. textStyle.fontFamily || 'sans-serif' ].join(' '); // Make font textStyle.font = font; const rect = text.getBoundingRect(); this._textX += rect.width; parentGroup.add(text); return text; } static internalField = (function () { nodeParsers = { 'g': function (xmlNode, parentGroup) { const g = new Group(); inheritStyle(parentGroup, g); parseAttributes(xmlNode, g, this._defsUsePending, false, false); return g; }, 'rect': function (xmlNode, parentGroup) { const rect = new Rect(); inheritStyle(parentGroup, rect); parseAttributes(xmlNode, rect, this._defsUsePending, false, false); 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') }); rect.silent = true; return rect; }, 'circle': function (xmlNode, parentGroup) { const circle = new Circle(); inheritStyle(parentGroup, circle); parseAttributes(xmlNode, circle, this._defsUsePending, false, false); circle.setShape({ cx: parseFloat(xmlNode.getAttribute('cx') || '0'), cy: parseFloat(xmlNode.getAttribute('cy') || '0'), r: parseFloat(xmlNode.getAttribute('r') || '0') }); circle.silent = true; return circle; }, 'line': function (xmlNode, parentGroup) { const line = new Line(); inheritStyle(parentGroup, line); parseAttributes(xmlNode, line, this._defsUsePending, false, false); 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') }); line.silent = true; return line; }, 'ellipse': function (xmlNode, parentGroup) { const ellipse = new Ellipse(); inheritStyle(parentGroup, ellipse); parseAttributes(xmlNode, ellipse, this._defsUsePending, false, false); 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') }); ellipse.silent = true; return ellipse; }, 'polygon': function (xmlNode, parentGroup) { const pointsStr = xmlNode.getAttribute('points'); let pointsArr; if (pointsStr) { pointsArr = parsePoints(pointsStr); } const polygon = new Polygon({ shape: { points: pointsArr || [] }, silent: true }); inheritStyle(parentGroup, polygon); parseAttributes(xmlNode, polygon, this._defsUsePending, false, false); return polygon; }, 'polyline': function (xmlNode, parentGroup) { const pointsStr = xmlNode.getAttribute('points'); let pointsArr; if (pointsStr) { pointsArr = parsePoints(pointsStr); } const polyline = new Polyline({ shape: { points: pointsArr || [] }, silent: true }); inheritStyle(parentGroup, polyline); parseAttributes(xmlNode, polyline, this._defsUsePending, false, false); return polyline; }, 'image': function (xmlNode, parentGroup) { const img = new ZRImage(); inheritStyle(parentGroup, img); parseAttributes(xmlNode, img, this._defsUsePending, false, false); img.setStyle({ image: xmlNode.getAttribute('xlink:href') || xmlNode.getAttribute('href'), x: +xmlNode.getAttribute('x'), y: +xmlNode.getAttribute('y'), width: +xmlNode.getAttribute('width'), height: +xmlNode.getAttribute('height') }); img.silent = true; return img; }, 'text': function (xmlNode, parentGroup) { const x = xmlNode.getAttribute('x') || '0'; const y = xmlNode.getAttribute('y') || '0'; const dx = xmlNode.getAttribute('dx') || '0'; const dy = xmlNode.getAttribute('dy') || '0'; this._textX = parseFloat(x) + parseFloat(dx); this._textY = parseFloat(y) + parseFloat(dy); const g = new Group(); inheritStyle(parentGroup, g); parseAttributes(xmlNode, g, this._defsUsePending, false, true); return g; }, 'tspan': function (xmlNode, parentGroup) { const x = xmlNode.getAttribute('x'); const y = xmlNode.getAttribute('y'); if (x != null) { // new offset x this._textX = parseFloat(x); } if (y != null) { // new offset y this._textY = parseFloat(y); } const dx = xmlNode.getAttribute('dx') || '0'; const dy = xmlNode.getAttribute('dy') || '0'; const g = new Group(); inheritStyle(parentGroup, g); parseAttributes(xmlNode, g, this._defsUsePending, false, true); this._textX += parseFloat(dx); this._textY += parseFloat(dy); return g; }, 'path': function (xmlNode, parentGroup) { // TODO svg fill rule // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule // path.style.globalCompositeOperation = 'xor'; const d = xmlNode.getAttribute('d') || ''; // Performance sensitive. const path = createFromString(d); inheritStyle(parentGroup, path); parseAttributes(xmlNode, path, this._defsUsePending, false, false); path.silent = true; return path; } }; })(); } const paintServerParsers: Dictionary<(xmlNode: SVGElement) => any> = { 'lineargradient': function (xmlNode: SVGElement) { // TODO: // Support that x1,y1,x2,y2 are not declared lineargradient but in node. const x1 = parseInt(xmlNode.getAttribute('x1') || '0', 10); const y1 = parseInt(xmlNode.getAttribute('y1') || '0', 10); const x2 = parseInt(xmlNode.getAttribute('x2') || '10', 10); const y2 = parseInt(xmlNode.getAttribute('y2') || '0', 10); const gradient = new LinearGradient(x1, y1, x2, y2); parsePaintServerUnit(xmlNode, gradient); parseGradientColorStops(xmlNode, gradient); return gradient; }, 'radialgradient': function (xmlNode) { // TODO: // Support that x1,y1,x2,y2 are not declared radialgradient but in node. // TODO: // Support fx, fy, fr. const cx = parseInt(xmlNode.getAttribute('cx') || '0', 10); const cy = parseInt(xmlNode.getAttribute('cy') || '0', 10); const r = parseInt(xmlNode.getAttribute('r') || '0', 10); const gradient = new RadialGradient(cx, cy, r); parsePaintServerUnit(xmlNode, gradient); parseGradientColorStops(xmlNode, gradient); return gradient; } // TODO // 'pattern': function (xmlNode: SVGElement) { // } }; function parsePaintServerUnit(xmlNode: SVGElement, gradient: Gradient) { const gradientUnits = xmlNode.getAttribute('gradientUnits'); if (gradientUnits === 'userSpaceOnUse') { gradient.global = true; } } function parseGradientColorStops(xmlNode: SVGElement, gradient: GradientObject): void { let stop = xmlNode.firstChild as SVGStopElement; while (stop) { if (stop.nodeType === 1 // there might be some other irrelevant tags used by editor. && stop.nodeName.toLocaleLowerCase() === 'stop' ) { const offsetStr = stop.getAttribute('offset'); let offset: number; if (offsetStr && offsetStr.indexOf('%') > 0) { // percentage offset = parseInt(offsetStr, 10) / 100; } else if (offsetStr) { // number from 0 to 1 offset = parseFloat(offsetStr); } else { offset = 0; } // <stop style="stop-color:red"/> has higher priority than // <stop stop-color="red"/> const styleVals = {} as Dictionary<string>; parseInlineStyle(stop, styleVals, styleVals); const stopColor = styleVals.stopColor || stop.getAttribute('stop-color') || '#000000'; gradient.colorStops.push({ offset: offset, color: stopColor }); } stop = stop.nextSibling as SVGStopElement; } } function inheritStyle(parent: Element, child: Element): void { if (parent && (parent as ElementExtended).__inheritedStyle) { if (!(child as ElementExtended).__inheritedStyle) { (child as ElementExtended).__inheritedStyle = {}; } defaults((child as ElementExtended).__inheritedStyle, (parent as ElementExtended).__inheritedStyle); } } function parsePoints(pointsString: string): number[][] { const list = splitNumberSequence(pointsString); const points = []; for (let i = 0; i < list.length; i += 2) { const x = parseFloat(list[i]); const y = parseFloat(list[i + 1]); points.push([x, y]); } return points; } function parseAttributes( xmlNode: SVGElement, el: Element, defsUsePending: DefsUsePending, onlyInlineStyle: boolean, isTextGroup: boolean ): void { const disp = el as DisplayableExtended; const inheritedStyle = disp.__inheritedStyle = disp.__inheritedStyle || {}; const selfStyle: SelfStyleByZRKey = {}; // TODO Shadow if (xmlNode.nodeType === 1) { parseTransformAttribute(xmlNode, el); parseInlineStyle(xmlNode, inheritedStyle, selfStyle); if (!onlyInlineStyle) { parseAttributeStyle(xmlNode, inheritedStyle, selfStyle); } } disp.style = disp.style || {}; if (inheritedStyle.fill != null) { disp.style.fill = getFillStrokeStyle(disp, 'fill', inheritedStyle.fill, defsUsePending); } if (inheritedStyle.stroke != null) { disp.style.stroke = getFillStrokeStyle(disp, 'stroke', inheritedStyle.stroke, defsUsePending); } each([ 'lineWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'miterLimit', 'fontSize' ] as const, function (propName) { if (inheritedStyle[propName] != null) { disp.style[propName] = parseFloat(inheritedStyle[propName]); } }); each([ 'lineDashOffset', 'lineCap', 'lineJoin', 'fontWeight', 'fontFamily', 'fontStyle', 'textAlign' ] as const, function (propName) { if (inheritedStyle[propName] != null) { disp.style[propName] = inheritedStyle[propName]; } }); // Because selfStyle only support textBaseline, so only text group need it. // in other cases selfStyle can be released. if (isTextGroup) { disp.__selfStyle = selfStyle; } if (inheritedStyle.lineDash) { disp.style.lineDash = map(splitNumberSequence(inheritedStyle.lineDash), function (str) { return parseFloat(str); }); } if (inheritedStyle.visibility === 'hidden' || inheritedStyle.visibility === 'collapse') { disp.invisible = true; } if (inheritedStyle.display === 'none') { disp.ignore = true; } } function applyTextAlignment( text: TSpan, parentGroup: Group ): void { const parentSelfStyle = (parentGroup as ElementExtended).__selfStyle; if (parentSelfStyle) { const textBaseline = parentSelfStyle.textBaseline; let zrTextBaseline = textBaseline as CanvasTextBaseline; if (!textBaseline || textBaseline === 'auto') { // FIXME: 'auto' means the value is the dominant-baseline of the script to // which the character belongs - i.e., use the dominant-baseline of the parent. zrTextBaseline = 'alphabetic'; } else if (textBaseline === 'baseline') { zrTextBaseline = 'alphabetic'; } else if (textBaseline === 'before-edge' || textBaseline === 'text-before-edge') { zrTextBaseline = 'top'; } else if (textBaseline === 'after-edge' || textBaseline === 'text-after-edge') { zrTextBaseline = 'bottom'; } else if (textBaseline === 'central' || textBaseline === 'mathematical') { zrTextBaseline = 'middle'; } text.style.textBaseline = zrTextBaseline; } const parentInheritedStyle = (parentGroup as ElementExtended).__inheritedStyle; if (parentInheritedStyle) { // PENDING: // canvas `direction` is an experimental attribute. // so we do not support SVG direction "rtl" for text-anchor yet. const textAlign = parentInheritedStyle.textAlign; let zrTextAlign = textAlign as CanvasTextAlign; if (textAlign) { if (textAlign === 'middle') { zrTextAlign = 'center'; } text.style.textAlign = zrTextAlign; } } } // Support `fill:url(#someId)`. const urlRegex = /^url\(\s*#(.*?)\)/; function getFillStrokeStyle( el: Displayable, method: 'fill' | 'stroke', str: string, defsUsePending: DefsUsePending ): string { const urlMatch = str && str.match(urlRegex); if (urlMatch) { const url = trim(urlMatch[1]); defsUsePending.push([el, method, url]); return; } // SVG fill and stroke can be 'none'. if (str === 'none') { str = null; } return str; } function applyDefs( defs: DefsMap, defsUsePending: DefsUsePending ): void { for (let i = 0; i < defsUsePending.length; i++) { const item = defsUsePending[i]; item[0].style[item[1]] = defs[item[2]]; } } // value can be like: // '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983', // 'l-.5E1,54', '121-23-44-11' (no delimiter) // PENDING: here continuous commas are treat as one comma, but the // browser SVG parser treats this by printing error. const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g; function splitNumberSequence(rawStr: string): string[] { return rawStr.match(numberReg) || []; } // Most of the values can be separated by comma and/or white space. // const DILIMITER_REG = /[\s,]+/; const transformRegex = /(translate|scale|rotate|skewX|skewY|matrix)\(([\-\s0-9\.eE,]*)\)/g; const DEGREE_TO_ANGLE = Math.PI / 180; function parseTransformAttribute(xmlNode: SVGElement, node: Element): void { let transform = xmlNode.getAttribute('transform'); if (transform) { transform = transform.replace(/,/g, ' '); const transformOps: string[] = []; let mt = null; transform.replace(transformRegex, function (str: string, type: string, value: string) { transformOps.push(type, value); return ''; }); for (let i = transformOps.length - 1; i > 0; i -= 2) { const value = transformOps[i]; const type = transformOps[i - 1]; const valueArr: string[] = splitNumberSequence(value); mt = mt || matrix.create(); switch (type) { case 'translate': matrix.translate(mt, mt, [parseFloat(valueArr[0]), parseFloat(valueArr[1] || '0')]); break; case 'scale': matrix.scale(mt, mt, [parseFloat(valueArr[0]), parseFloat(valueArr[1] || valueArr[0])]); break; case 'rotate': // TODO: zrender use different hand in coordinate system. matrix.rotate(mt, mt, -parseFloat(valueArr[0]) * DEGREE_TO_ANGLE, [ parseFloat(valueArr[1] || '0'), parseFloat(valueArr[2] || '0') ]); break; case 'skewX': const sx = Math.tan(parseFloat(valueArr[0]) * DEGREE_TO_ANGLE); matrix.mul(mt, [1, 0, sx, 1, 0, 0], mt); break; case 'skewY': const sy = Math.tan(parseFloat(valueArr[0]) * DEGREE_TO_ANGLE); matrix.mul(mt, [1, sy, 0, 1, 0, 0], mt); break; case 'matrix': mt[0] = parseFloat(valueArr[0]); mt[1] = parseFloat(valueArr[1]); mt[2] = parseFloat(valueArr[2]); mt[3] = parseFloat(valueArr[3]); mt[4] = parseFloat(valueArr[4]); mt[5] = parseFloat(valueArr[5]); break; } } node.setLocalTransform(mt); } } // Value may contain space. const styleRegex = /([^\s:;]+)\s*:\s*([^:;]+)/g; function parseInlineStyle( xmlNode: SVGElement, inheritableStyleResult: Dictionary<string>, selfStyleResult: Dictionary<string> ): void { const style = xmlNode.getAttribute('style'); if (!style) { return; } styleRegex.lastIndex = 0; let styleRegResult; while ((styleRegResult = styleRegex.exec(style)) != null) { const svgStlAttr = styleRegResult[1]; const zrInheritableStlAttr = hasOwn(INHERITABLE_STYLE_ATTRIBUTES_MAP, svgStlAttr) ? INHERITABLE_STYLE_ATTRIBUTES_MAP[svgStlAttr as keyof typeof INHERITABLE_STYLE_ATTRIBUTES_MAP] : null; if (zrInheritableStlAttr) { inheritableStyleResult[zrInheritableStlAttr] = styleRegResult[2]; } const zrSelfStlAttr = hasOwn(SELF_STYLE_ATTRIBUTES_MAP, svgStlAttr) ? SELF_STYLE_ATTRIBUTES_MAP[svgStlAttr as keyof typeof SELF_STYLE_ATTRIBUTES_MAP] : null; if (zrSelfStlAttr) { selfStyleResult[zrSelfStlAttr] = styleRegResult[2]; } } } function parseAttributeStyle( xmlNode: SVGElement, inheritableStyleResult: Dictionary<string>, selfStyleResult: Dictionary<string> ): void { for (let i = 0; i < INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS.length; i++) { const svgAttrName = INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS[i]; const attrValue = xmlNode.getAttribute(svgAttrName); if (attrValue != null) { inheritableStyleResult[INHERITABLE_STYLE_ATTRIBUTES_MAP[svgAttrName]] = attrValue; } } for (let i = 0; i < SELF_STYLE_ATTRIBUTES_MAP_KEYS.length; i++) { const svgAttrName = SELF_STYLE_ATTRIBUTES_MAP_KEYS[i]; const attrValue = xmlNode.getAttribute(svgAttrName); if (attrValue != null) { selfStyleResult[SELF_STYLE_ATTRIBUTES_MAP[svgAttrName]] = attrValue; } } } export function makeViewBoxTransform(viewBoxRect: RectLike, boundingRect: RectLike): { scale: number; x: number; y: number; } { const scaleX = boundingRect.width / viewBoxRect.width; const scaleY = boundingRect.height / viewBoxRect.height; const scale = Math.min(scaleX, scaleY); // preserveAspectRatio 'xMidYMid' return { scale, x: -(viewBoxRect.x + viewBoxRect.width / 2) * scale + (boundingRect.x + boundingRect.width / 2), y: -(viewBoxRect.y + viewBoxRect.height / 2) * scale + (boundingRect.y + boundingRect.height / 2) }; } export function parseSVG(xml: string | Document | SVGElement, opt: SVGParserOption): SVGParserResult { const parser = new SVGParser(); return parser.parse(xml, opt); } // Also export parseXML to avoid breaking change. export {parseXML};