package.src.tool.parseSVG.ts Maven / Gradle / Ivy
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 is named,
// `namedFrom` is set to the named item of the ancester .
// 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 can be in front of 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 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 displayable. Do not support
// the full feature of it.
el = parentGroup;
}
else {
// In , elments will not be rendered.
// TODO:
// do not support elements in yet, until requirement come.
// other graphic elements can also be in and referenced by
//
// 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 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 , , , , ,
// 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;
}
// has higher priority than
//
const styleVals = {} as Dictionary;
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,
selfStyleResult: Dictionary
): 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,
selfStyleResult: Dictionary
): 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};
© 2015 - 2025 Weber Informatics LLC | Privacy Policy