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