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};
|