fbf492cbf631ab5e12a0dfadba606d68fe80d94fd749984db081e7e28e96c3061cbeeba8711c5f444c63c620f9ddb2184c3272978e04093dbabe3151b746be 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. import Group from '../graphic/Group';
  2. import ZRImage from '../graphic/Image';
  3. import Circle from '../graphic/shape/Circle';
  4. import Rect from '../graphic/shape/Rect';
  5. import Ellipse from '../graphic/shape/Ellipse';
  6. import Line from '../graphic/shape/Line';
  7. import Polygon from '../graphic/shape/Polygon';
  8. import Polyline from '../graphic/shape/Polyline';
  9. import * as matrix from '../core/matrix';
  10. import { createFromString } from './path';
  11. import { defaults, trim, each, map, keys, hasOwn } from '../core/util';
  12. import Displayable from '../graphic/Displayable';
  13. import Element from '../Element';
  14. import { RectLike } from '../core/BoundingRect';
  15. import { Dictionary } from '../core/types';
  16. import { PatternObject } from '../graphic/Pattern';
  17. import LinearGradient, { LinearGradientObject } from '../graphic/LinearGradient';
  18. import RadialGradient, { RadialGradientObject } from '../graphic/RadialGradient';
  19. import Gradient, { GradientObject } from '../graphic/Gradient';
  20. import TSpan, { TSpanStyleProps } from '../graphic/TSpan';
  21. import { parseXML } from './parseXML';
  22. interface SVGParserOption {
  23. // Default width if svg width not specified or is a percent value.
  24. width?: number;
  25. // Default height if svg height not specified or is a percent value.
  26. height?: number;
  27. ignoreViewBox?: boolean;
  28. ignoreRootClip?: boolean;
  29. }
  30. export interface SVGParserResult {
  31. // Group, The root of the the result tree of zrender shapes
  32. root: Group;
  33. // number, the viewport width of the SVG
  34. width: number;
  35. // number, the viewport height of the SVG
  36. height: number;
  37. // {x, y, width, height}, the declared viewBox rect of the SVG, if exists
  38. viewBoxRect: RectLike;
  39. // the {scale, position} calculated by viewBox and viewport, is exists
  40. viewBoxTransform: {
  41. x: number;
  42. y: number;
  43. scale: number;
  44. };
  45. named: SVGParserResultNamedItem[];
  46. }
  47. export interface SVGParserResultNamedItem {
  48. name: string;
  49. // If a tag has no name attribute but its ancester <g> is named,
  50. // `namedFrom` is set to the named item of the ancester <g>.
  51. // Otherwise null/undefined
  52. namedFrom: SVGParserResultNamedItem;
  53. svgNodeTagLower: SVGNodeTagLower;
  54. el: Element;
  55. };
  56. export type SVGNodeTagLower =
  57. 'g' | 'rect' | 'circle' | 'line' | 'ellipse' | 'polygon'
  58. | 'polyline' | 'image' | 'text' | 'tspan' | 'path' | 'defs' | 'switch';
  59. type DefsId = string;
  60. type DefsMap = { [id in DefsId]: LinearGradientObject | RadialGradientObject | PatternObject };
  61. type DefsUsePending = [Displayable, 'fill' | 'stroke', DefsId][];
  62. type ElementExtended = Element & {
  63. __inheritedStyle?: InheritedStyleByZRKey;
  64. __selfStyle?: SelfStyleByZRKey;
  65. }
  66. type DisplayableExtended = Displayable & {
  67. __inheritedStyle?: InheritedStyleByZRKey;
  68. __selfStyle?: SelfStyleByZRKey;
  69. }
  70. type TextStyleOptionExtended = TSpanStyleProps & {
  71. fontSize: number;
  72. fontFamily: string;
  73. fontWeight: string;
  74. fontStyle: string;
  75. }
  76. let nodeParsers: {[name in SVGNodeTagLower]?: (
  77. this: SVGParser, xmlNode: SVGElement, parentGroup: Group
  78. ) => Element};
  79. type InheritedStyleByZRKey = {[name in InheritableStyleZRKey]?: string};
  80. type InheritableStyleZRKey =
  81. typeof INHERITABLE_STYLE_ATTRIBUTES_MAP[keyof typeof INHERITABLE_STYLE_ATTRIBUTES_MAP];
  82. const INHERITABLE_STYLE_ATTRIBUTES_MAP = {
  83. 'fill': 'fill',
  84. 'stroke': 'stroke',
  85. 'stroke-width': 'lineWidth',
  86. 'opacity': 'opacity',
  87. 'fill-opacity': 'fillOpacity',
  88. 'stroke-opacity': 'strokeOpacity',
  89. 'stroke-dasharray': 'lineDash',
  90. 'stroke-dashoffset': 'lineDashOffset',
  91. 'stroke-linecap': 'lineCap',
  92. 'stroke-linejoin': 'lineJoin',
  93. 'stroke-miterlimit': 'miterLimit',
  94. 'font-family': 'fontFamily',
  95. 'font-size': 'fontSize',
  96. 'font-style': 'fontStyle',
  97. 'font-weight': 'fontWeight',
  98. 'text-anchor': 'textAlign',
  99. 'visibility': 'visibility',
  100. 'display': 'display'
  101. } as const;
  102. const INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS = keys(INHERITABLE_STYLE_ATTRIBUTES_MAP);
  103. type SelfStyleByZRKey = {[name in SelfStyleZRKey]?: string};
  104. type SelfStyleZRKey =
  105. typeof SELF_STYLE_ATTRIBUTES_MAP[keyof typeof SELF_STYLE_ATTRIBUTES_MAP];
  106. const SELF_STYLE_ATTRIBUTES_MAP = {
  107. 'alignment-baseline': 'textBaseline',
  108. 'stop-color': 'stopColor'
  109. };
  110. const SELF_STYLE_ATTRIBUTES_MAP_KEYS = keys(SELF_STYLE_ATTRIBUTES_MAP);
  111. class SVGParser {
  112. private _defs: DefsMap = {};
  113. // The use of <defs> can be in front of <defs> declared.
  114. // So save them temporarily in `_defsUsePending`.
  115. private _defsUsePending: DefsUsePending;
  116. private _root: Group = null;
  117. private _textX: number;
  118. private _textY: number;
  119. parse(xml: string | Document | SVGElement, opt: SVGParserOption): SVGParserResult {
  120. opt = opt || {};
  121. const svg = parseXML(xml);
  122. if (process.env.NODE_ENV !== 'production') {
  123. if (!svg) {
  124. throw new Error('Illegal svg');
  125. }
  126. }
  127. this._defsUsePending = [];
  128. let root = new Group();
  129. this._root = root;
  130. const named: SVGParserResult['named'] = [];
  131. // parse view port
  132. const viewBox = svg.getAttribute('viewBox') || '';
  133. // If width/height not specified, means "100%" of `opt.width/height`.
  134. // TODO: Other percent value not supported yet.
  135. let width = parseFloat((svg.getAttribute('width') || opt.width) as string);
  136. let height = parseFloat((svg.getAttribute('height') || opt.height) as string);
  137. // If width/height not specified, set as null for output.
  138. isNaN(width) && (width = null);
  139. isNaN(height) && (height = null);
  140. // Apply inline style on svg element.
  141. parseAttributes(svg, root, null, true, false);
  142. let child = svg.firstChild as SVGElement;
  143. while (child) {
  144. this._parseNode(child, root, named, null, false, false);
  145. child = child.nextSibling as SVGElement;
  146. }
  147. applyDefs(this._defs, this._defsUsePending);
  148. this._defsUsePending = [];
  149. let viewBoxRect;
  150. let viewBoxTransform;
  151. if (viewBox) {
  152. const viewBoxArr = splitNumberSequence(viewBox);
  153. // Some invalid case like viewBox: 'none'.
  154. if (viewBoxArr.length >= 4) {
  155. viewBoxRect = {
  156. x: parseFloat((viewBoxArr[0] || 0) as string),
  157. y: parseFloat((viewBoxArr[1] || 0) as string),
  158. width: parseFloat(viewBoxArr[2]),
  159. height: parseFloat(viewBoxArr[3])
  160. };
  161. }
  162. }
  163. if (viewBoxRect && width != null && height != null) {
  164. viewBoxTransform = makeViewBoxTransform(viewBoxRect, { x: 0, y: 0, width: width, height: height });
  165. if (!opt.ignoreViewBox) {
  166. // If set transform on the output group, it probably bring trouble when
  167. // some users only intend to show the clipped content inside the viewBox,
  168. // but not intend to transform the output group. So we keep the output
  169. // group no transform. If the user intend to use the viewBox as a
  170. // camera, just set `opt.ignoreViewBox` as `true` and set transfrom
  171. // manually according to the viewBox info in the output of this method.
  172. const elRoot = root;
  173. root = new Group();
  174. root.add(elRoot);
  175. elRoot.scaleX = elRoot.scaleY = viewBoxTransform.scale;
  176. elRoot.x = viewBoxTransform.x;
  177. elRoot.y = viewBoxTransform.y;
  178. }
  179. }
  180. // Some shapes might be overflow the viewport, which should be
  181. // clipped despite whether the viewBox is used, as the SVG does.
  182. if (!opt.ignoreRootClip && width != null && height != null) {
  183. root.setClipPath(new Rect({
  184. shape: {x: 0, y: 0, width: width, height: height}
  185. }));
  186. }
  187. // Set width/height on group just for output the viewport size.
  188. return {
  189. root: root,
  190. width: width,
  191. height: height,
  192. viewBoxRect: viewBoxRect,
  193. viewBoxTransform: viewBoxTransform,
  194. named: named
  195. };
  196. }
  197. private _parseNode(
  198. xmlNode: SVGElement,
  199. parentGroup: Group,
  200. named: SVGParserResultNamedItem[],
  201. namedFrom: SVGParserResultNamedItem['namedFrom'],
  202. isInDefs: boolean,
  203. isInText: boolean
  204. ): void {
  205. const nodeName = xmlNode.nodeName.toLowerCase() as SVGNodeTagLower;
  206. // TODO:
  207. // support <style>...</style> in svg, where nodeName is 'style',
  208. // CSS classes is defined globally wherever the style tags are declared.
  209. let el;
  210. let namedFromForSub = namedFrom;
  211. if (nodeName === 'defs') {
  212. isInDefs = true;
  213. }
  214. if (nodeName === 'text') {
  215. isInText = true;
  216. }
  217. if (nodeName === 'defs' || nodeName === 'switch') {
  218. // Just make <switch> displayable. Do not support
  219. // the full feature of it.
  220. el = parentGroup;
  221. }
  222. else {
  223. // In <defs>, elments will not be rendered.
  224. // TODO:
  225. // do not support elements in <defs> yet, until requirement come.
  226. // other graphic elements can also be in <defs> and referenced by
  227. // <use x="5" y="5" xlink:href="#myCircle" />
  228. // multiple times
  229. if (!isInDefs) {
  230. const parser = nodeParsers[nodeName];
  231. if (parser && hasOwn(nodeParsers, nodeName)) {
  232. el = parser.call(this, xmlNode, parentGroup);
  233. // Do not support empty string;
  234. const nameAttr = xmlNode.getAttribute('name');
  235. if (nameAttr) {
  236. const newNamed: SVGParserResultNamedItem = {
  237. name: nameAttr,
  238. namedFrom: null,
  239. svgNodeTagLower: nodeName,
  240. el: el
  241. };
  242. named.push(newNamed);
  243. if (nodeName === 'g') {
  244. namedFromForSub = newNamed;
  245. }
  246. }
  247. else if (namedFrom) {
  248. named.push({
  249. name: namedFrom.name,
  250. namedFrom: namedFrom,
  251. svgNodeTagLower: nodeName,
  252. el: el
  253. });
  254. }
  255. parentGroup.add(el);
  256. }
  257. }
  258. // Whether gradients/patterns are declared in <defs> or not,
  259. // they all work.
  260. const parser = paintServerParsers[nodeName];
  261. if (parser && hasOwn(paintServerParsers, nodeName)) {
  262. const def = parser.call(this, xmlNode);
  263. const id = xmlNode.getAttribute('id');
  264. if (id) {
  265. this._defs[id] = def;
  266. }
  267. }
  268. }
  269. // If xmlNode is <g>, <text>, <tspan>, <defs>, <switch>,
  270. // el will be a group, and traverse the children.
  271. if (el && el.isGroup) {
  272. let child = xmlNode.firstChild as SVGElement;
  273. while (child) {
  274. if (child.nodeType === 1) {
  275. this._parseNode(child, el as Group, named, namedFromForSub, isInDefs, isInText);
  276. }
  277. // Is plain text rather than a tagged node.
  278. else if (child.nodeType === 3 && isInText) {
  279. this._parseText(child, el as Group);
  280. }
  281. child = child.nextSibling as SVGElement;
  282. }
  283. }
  284. }
  285. private _parseText(xmlNode: SVGElement, parentGroup: Group): TSpan {
  286. const text = new TSpan({
  287. style: {
  288. text: xmlNode.textContent
  289. },
  290. silent: true,
  291. x: this._textX || 0,
  292. y: this._textY || 0
  293. });
  294. inheritStyle(parentGroup, text);
  295. parseAttributes(xmlNode, text, this._defsUsePending, false, false);
  296. applyTextAlignment(text, parentGroup);
  297. const textStyle = text.style as TextStyleOptionExtended;
  298. const fontSize = textStyle.fontSize;
  299. if (fontSize && fontSize < 9) {
  300. // PENDING
  301. textStyle.fontSize = 9;
  302. text.scaleX *= fontSize / 9;
  303. text.scaleY *= fontSize / 9;
  304. }
  305. const font = (textStyle.fontSize || textStyle.fontFamily) && [
  306. textStyle.fontStyle,
  307. textStyle.fontWeight,
  308. (textStyle.fontSize || 12) + 'px',
  309. // If font properties are defined, `fontFamily` should not be ignored.
  310. textStyle.fontFamily || 'sans-serif'
  311. ].join(' ');
  312. // Make font
  313. textStyle.font = font;
  314. const rect = text.getBoundingRect();
  315. this._textX += rect.width;
  316. parentGroup.add(text);
  317. return text;
  318. }
  319. static internalField = (function () {
  320. nodeParsers = {
  321. 'g': function (xmlNode, parentGroup) {
  322. const g = new Group();
  323. inheritStyle(parentGroup, g);
  324. parseAttributes(xmlNode, g, this._defsUsePending, false, false);
  325. return g;
  326. },
  327. 'rect': function (xmlNode, parentGroup) {
  328. const rect = new Rect();
  329. inheritStyle(parentGroup, rect);
  330. parseAttributes(xmlNode, rect, this._defsUsePending, false, false);
  331. rect.setShape({
  332. x: parseFloat(xmlNode.getAttribute('x') || '0'),
  333. y: parseFloat(xmlNode.getAttribute('y') || '0'),
  334. width: parseFloat(xmlNode.getAttribute('width') || '0'),
  335. height: parseFloat(xmlNode.getAttribute('height') || '0')
  336. });
  337. rect.silent = true;
  338. return rect;
  339. },
  340. 'circle': function (xmlNode, parentGroup) {
  341. const circle = new Circle();
  342. inheritStyle(parentGroup, circle);
  343. parseAttributes(xmlNode, circle, this._defsUsePending, false, false);
  344. circle.setShape({
  345. cx: parseFloat(xmlNode.getAttribute('cx') || '0'),
  346. cy: parseFloat(xmlNode.getAttribute('cy') || '0'),
  347. r: parseFloat(xmlNode.getAttribute('r') || '0')
  348. });
  349. circle.silent = true;
  350. return circle;
  351. },
  352. 'line': function (xmlNode, parentGroup) {
  353. const line = new Line();
  354. inheritStyle(parentGroup, line);
  355. parseAttributes(xmlNode, line, this._defsUsePending, false, false);
  356. line.setShape({
  357. x1: parseFloat(xmlNode.getAttribute('x1') || '0'),
  358. y1: parseFloat(xmlNode.getAttribute('y1') || '0'),
  359. x2: parseFloat(xmlNode.getAttribute('x2') || '0'),
  360. y2: parseFloat(xmlNode.getAttribute('y2') || '0')
  361. });
  362. line.silent = true;
  363. return line;
  364. },
  365. 'ellipse': function (xmlNode, parentGroup) {
  366. const ellipse = new Ellipse();
  367. inheritStyle(parentGroup, ellipse);
  368. parseAttributes(xmlNode, ellipse, this._defsUsePending, false, false);
  369. ellipse.setShape({
  370. cx: parseFloat(xmlNode.getAttribute('cx') || '0'),
  371. cy: parseFloat(xmlNode.getAttribute('cy') || '0'),
  372. rx: parseFloat(xmlNode.getAttribute('rx') || '0'),
  373. ry: parseFloat(xmlNode.getAttribute('ry') || '0')
  374. });
  375. ellipse.silent = true;
  376. return ellipse;
  377. },
  378. 'polygon': function (xmlNode, parentGroup) {
  379. const pointsStr = xmlNode.getAttribute('points');
  380. let pointsArr;
  381. if (pointsStr) {
  382. pointsArr = parsePoints(pointsStr);
  383. }
  384. const polygon = new Polygon({
  385. shape: {
  386. points: pointsArr || []
  387. },
  388. silent: true
  389. });
  390. inheritStyle(parentGroup, polygon);
  391. parseAttributes(xmlNode, polygon, this._defsUsePending, false, false);
  392. return polygon;
  393. },
  394. 'polyline': function (xmlNode, parentGroup) {
  395. const pointsStr = xmlNode.getAttribute('points');
  396. let pointsArr;
  397. if (pointsStr) {
  398. pointsArr = parsePoints(pointsStr);
  399. }
  400. const polyline = new Polyline({
  401. shape: {
  402. points: pointsArr || []
  403. },
  404. silent: true
  405. });
  406. inheritStyle(parentGroup, polyline);
  407. parseAttributes(xmlNode, polyline, this._defsUsePending, false, false);
  408. return polyline;
  409. },
  410. 'image': function (xmlNode, parentGroup) {
  411. const img = new ZRImage();
  412. inheritStyle(parentGroup, img);
  413. parseAttributes(xmlNode, img, this._defsUsePending, false, false);
  414. img.setStyle({
  415. image: xmlNode.getAttribute('xlink:href') || xmlNode.getAttribute('href'),
  416. x: +xmlNode.getAttribute('x'),
  417. y: +xmlNode.getAttribute('y'),
  418. width: +xmlNode.getAttribute('width'),
  419. height: +xmlNode.getAttribute('height')
  420. });
  421. img.silent = true;
  422. return img;
  423. },
  424. 'text': function (xmlNode, parentGroup) {
  425. const x = xmlNode.getAttribute('x') || '0';
  426. const y = xmlNode.getAttribute('y') || '0';
  427. const dx = xmlNode.getAttribute('dx') || '0';
  428. const dy = xmlNode.getAttribute('dy') || '0';
  429. this._textX = parseFloat(x) + parseFloat(dx);
  430. this._textY = parseFloat(y) + parseFloat(dy);
  431. const g = new Group();
  432. inheritStyle(parentGroup, g);
  433. parseAttributes(xmlNode, g, this._defsUsePending, false, true);
  434. return g;
  435. },
  436. 'tspan': function (xmlNode, parentGroup) {
  437. const x = xmlNode.getAttribute('x');
  438. const y = xmlNode.getAttribute('y');
  439. if (x != null) {
  440. // new offset x
  441. this._textX = parseFloat(x);
  442. }
  443. if (y != null) {
  444. // new offset y
  445. this._textY = parseFloat(y);
  446. }
  447. const dx = xmlNode.getAttribute('dx') || '0';
  448. const dy = xmlNode.getAttribute('dy') || '0';
  449. const g = new Group();
  450. inheritStyle(parentGroup, g);
  451. parseAttributes(xmlNode, g, this._defsUsePending, false, true);
  452. this._textX += parseFloat(dx);
  453. this._textY += parseFloat(dy);
  454. return g;
  455. },
  456. 'path': function (xmlNode, parentGroup) {
  457. // TODO svg fill rule
  458. // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule
  459. // path.style.globalCompositeOperation = 'xor';
  460. const d = xmlNode.getAttribute('d') || '';
  461. // Performance sensitive.
  462. const path = createFromString(d);
  463. inheritStyle(parentGroup, path);
  464. parseAttributes(xmlNode, path, this._defsUsePending, false, false);
  465. path.silent = true;
  466. return path;
  467. }
  468. };
  469. })();
  470. }
  471. const paintServerParsers: Dictionary<(xmlNode: SVGElement) => any> = {
  472. 'lineargradient': function (xmlNode: SVGElement) {
  473. // TODO:
  474. // Support that x1,y1,x2,y2 are not declared lineargradient but in node.
  475. const x1 = parseInt(xmlNode.getAttribute('x1') || '0', 10);
  476. const y1 = parseInt(xmlNode.getAttribute('y1') || '0', 10);
  477. const x2 = parseInt(xmlNode.getAttribute('x2') || '10', 10);
  478. const y2 = parseInt(xmlNode.getAttribute('y2') || '0', 10);
  479. const gradient = new LinearGradient(x1, y1, x2, y2);
  480. parsePaintServerUnit(xmlNode, gradient);
  481. parseGradientColorStops(xmlNode, gradient);
  482. return gradient;
  483. },
  484. 'radialgradient': function (xmlNode) {
  485. // TODO:
  486. // Support that x1,y1,x2,y2 are not declared radialgradient but in node.
  487. // TODO:
  488. // Support fx, fy, fr.
  489. const cx = parseInt(xmlNode.getAttribute('cx') || '0', 10);
  490. const cy = parseInt(xmlNode.getAttribute('cy') || '0', 10);
  491. const r = parseInt(xmlNode.getAttribute('r') || '0', 10);
  492. const gradient = new RadialGradient(cx, cy, r);
  493. parsePaintServerUnit(xmlNode, gradient);
  494. parseGradientColorStops(xmlNode, gradient);
  495. return gradient;
  496. }
  497. // TODO
  498. // 'pattern': function (xmlNode: SVGElement) {
  499. // }
  500. };
  501. function parsePaintServerUnit(xmlNode: SVGElement, gradient: Gradient) {
  502. const gradientUnits = xmlNode.getAttribute('gradientUnits');
  503. if (gradientUnits === 'userSpaceOnUse') {
  504. gradient.global = true;
  505. }
  506. }
  507. function parseGradientColorStops(xmlNode: SVGElement, gradient: GradientObject): void {
  508. let stop = xmlNode.firstChild as SVGStopElement;
  509. while (stop) {
  510. if (stop.nodeType === 1
  511. // there might be some other irrelevant tags used by editor.
  512. && stop.nodeName.toLocaleLowerCase() === 'stop'
  513. ) {
  514. const offsetStr = stop.getAttribute('offset');
  515. let offset: number;
  516. if (offsetStr && offsetStr.indexOf('%') > 0) { // percentage
  517. offset = parseInt(offsetStr, 10) / 100;
  518. }
  519. else if (offsetStr) { // number from 0 to 1
  520. offset = parseFloat(offsetStr);
  521. }
  522. else {
  523. offset = 0;
  524. }
  525. // <stop style="stop-color:red"/> has higher priority than
  526. // <stop stop-color="red"/>
  527. const styleVals = {} as Dictionary<string>;
  528. parseInlineStyle(stop, styleVals, styleVals);
  529. const stopColor = styleVals.stopColor
  530. || stop.getAttribute('stop-color')
  531. || '#000000';
  532. gradient.colorStops.push({
  533. offset: offset,
  534. color: stopColor
  535. });
  536. }
  537. stop = stop.nextSibling as SVGStopElement;
  538. }
  539. }
  540. function inheritStyle(parent: Element, child: Element): void {
  541. if (parent && (parent as ElementExtended).__inheritedStyle) {
  542. if (!(child as ElementExtended).__inheritedStyle) {
  543. (child as ElementExtended).__inheritedStyle = {};
  544. }
  545. defaults((child as ElementExtended).__inheritedStyle, (parent as ElementExtended).__inheritedStyle);
  546. }
  547. }
  548. function parsePoints(pointsString: string): number[][] {
  549. const list = splitNumberSequence(pointsString);
  550. const points = [];
  551. for (let i = 0; i < list.length; i += 2) {
  552. const x = parseFloat(list[i]);
  553. const y = parseFloat(list[i + 1]);
  554. points.push([x, y]);
  555. }
  556. return points;
  557. }
  558. function parseAttributes(
  559. xmlNode: SVGElement,
  560. el: Element,
  561. defsUsePending: DefsUsePending,
  562. onlyInlineStyle: boolean,
  563. isTextGroup: boolean
  564. ): void {
  565. const disp = el as DisplayableExtended;
  566. const inheritedStyle = disp.__inheritedStyle = disp.__inheritedStyle || {};
  567. const selfStyle: SelfStyleByZRKey = {};
  568. // TODO Shadow
  569. if (xmlNode.nodeType === 1) {
  570. parseTransformAttribute(xmlNode, el);
  571. parseInlineStyle(xmlNode, inheritedStyle, selfStyle);
  572. if (!onlyInlineStyle) {
  573. parseAttributeStyle(xmlNode, inheritedStyle, selfStyle);
  574. }
  575. }
  576. disp.style = disp.style || {};
  577. if (inheritedStyle.fill != null) {
  578. disp.style.fill = getFillStrokeStyle(disp, 'fill', inheritedStyle.fill, defsUsePending);
  579. }
  580. if (inheritedStyle.stroke != null) {
  581. disp.style.stroke = getFillStrokeStyle(disp, 'stroke', inheritedStyle.stroke, defsUsePending);
  582. }
  583. each([
  584. 'lineWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'miterLimit', 'fontSize'
  585. ] as const, function (propName) {
  586. if (inheritedStyle[propName] != null) {
  587. disp.style[propName] = parseFloat(inheritedStyle[propName]);
  588. }
  589. });
  590. each([
  591. 'lineDashOffset', 'lineCap', 'lineJoin', 'fontWeight', 'fontFamily', 'fontStyle', 'textAlign'
  592. ] as const, function (propName) {
  593. if (inheritedStyle[propName] != null) {
  594. disp.style[propName] = inheritedStyle[propName];
  595. }
  596. });
  597. // Because selfStyle only support textBaseline, so only text group need it.
  598. // in other cases selfStyle can be released.
  599. if (isTextGroup) {
  600. disp.__selfStyle = selfStyle;
  601. }
  602. if (inheritedStyle.lineDash) {
  603. disp.style.lineDash = map(splitNumberSequence(inheritedStyle.lineDash), function (str) {
  604. return parseFloat(str);
  605. });
  606. }
  607. if (inheritedStyle.visibility === 'hidden' || inheritedStyle.visibility === 'collapse') {
  608. disp.invisible = true;
  609. }
  610. if (inheritedStyle.display === 'none') {
  611. disp.ignore = true;
  612. }
  613. }
  614. function applyTextAlignment(
  615. text: TSpan,
  616. parentGroup: Group
  617. ): void {
  618. const parentSelfStyle = (parentGroup as ElementExtended).__selfStyle;
  619. if (parentSelfStyle) {
  620. const textBaseline = parentSelfStyle.textBaseline;
  621. let zrTextBaseline = textBaseline as CanvasTextBaseline;
  622. if (!textBaseline || textBaseline === 'auto') {
  623. // FIXME: 'auto' means the value is the dominant-baseline of the script to
  624. // which the character belongs - i.e., use the dominant-baseline of the parent.
  625. zrTextBaseline = 'alphabetic';
  626. }
  627. else if (textBaseline === 'baseline') {
  628. zrTextBaseline = 'alphabetic';
  629. }
  630. else if (textBaseline === 'before-edge' || textBaseline === 'text-before-edge') {
  631. zrTextBaseline = 'top';
  632. }
  633. else if (textBaseline === 'after-edge' || textBaseline === 'text-after-edge') {
  634. zrTextBaseline = 'bottom';
  635. }
  636. else if (textBaseline === 'central' || textBaseline === 'mathematical') {
  637. zrTextBaseline = 'middle';
  638. }
  639. text.style.textBaseline = zrTextBaseline;
  640. }
  641. const parentInheritedStyle = (parentGroup as ElementExtended).__inheritedStyle;
  642. if (parentInheritedStyle) {
  643. // PENDING:
  644. // canvas `direction` is an experimental attribute.
  645. // so we do not support SVG direction "rtl" for text-anchor yet.
  646. const textAlign = parentInheritedStyle.textAlign;
  647. let zrTextAlign = textAlign as CanvasTextAlign;
  648. if (textAlign) {
  649. if (textAlign === 'middle') {
  650. zrTextAlign = 'center';
  651. }
  652. text.style.textAlign = zrTextAlign;
  653. }
  654. }
  655. }
  656. // Support `fill:url(#someId)`.
  657. const urlRegex = /^url\(\s*#(.*?)\)/;
  658. function getFillStrokeStyle(
  659. el: Displayable,
  660. method: 'fill' | 'stroke',
  661. str: string,
  662. defsUsePending: DefsUsePending
  663. ): string {
  664. const urlMatch = str && str.match(urlRegex);
  665. if (urlMatch) {
  666. const url = trim(urlMatch[1]);
  667. defsUsePending.push([el, method, url]);
  668. return;
  669. }
  670. // SVG fill and stroke can be 'none'.
  671. if (str === 'none') {
  672. str = null;
  673. }
  674. return str;
  675. }
  676. function applyDefs(
  677. defs: DefsMap,
  678. defsUsePending: DefsUsePending
  679. ): void {
  680. for (let i = 0; i < defsUsePending.length; i++) {
  681. const item = defsUsePending[i];
  682. item[0].style[item[1]] = defs[item[2]];
  683. }
  684. }
  685. // value can be like:
  686. // '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983',
  687. // 'l-.5E1,54', '121-23-44-11' (no delimiter)
  688. // PENDING: here continuous commas are treat as one comma, but the
  689. // browser SVG parser treats this by printing error.
  690. const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;
  691. function splitNumberSequence(rawStr: string): string[] {
  692. return rawStr.match(numberReg) || [];
  693. }
  694. // Most of the values can be separated by comma and/or white space.
  695. // const DILIMITER_REG = /[\s,]+/;
  696. const transformRegex = /(translate|scale|rotate|skewX|skewY|matrix)\(([\-\s0-9\.eE,]*)\)/g;
  697. const DEGREE_TO_ANGLE = Math.PI / 180;
  698. function parseTransformAttribute(xmlNode: SVGElement, node: Element): void {
  699. let transform = xmlNode.getAttribute('transform');
  700. if (transform) {
  701. transform = transform.replace(/,/g, ' ');
  702. const transformOps: string[] = [];
  703. let mt = null;
  704. transform.replace(transformRegex, function (str: string, type: string, value: string) {
  705. transformOps.push(type, value);
  706. return '';
  707. });
  708. for (let i = transformOps.length - 1; i > 0; i -= 2) {
  709. const value = transformOps[i];
  710. const type = transformOps[i - 1];
  711. const valueArr: string[] = splitNumberSequence(value);
  712. mt = mt || matrix.create();
  713. switch (type) {
  714. case 'translate':
  715. matrix.translate(mt, mt, [parseFloat(valueArr[0]), parseFloat(valueArr[1] || '0')]);
  716. break;
  717. case 'scale':
  718. matrix.scale(mt, mt, [parseFloat(valueArr[0]), parseFloat(valueArr[1] || valueArr[0])]);
  719. break;
  720. case 'rotate':
  721. // TODO: zrender use different hand in coordinate system.
  722. matrix.rotate(mt, mt, -parseFloat(valueArr[0]) * DEGREE_TO_ANGLE);
  723. break;
  724. case 'skewX':
  725. const sx = Math.tan(parseFloat(valueArr[0]) * DEGREE_TO_ANGLE);
  726. matrix.mul(mt, [1, 0, sx, 1, 0, 0], mt);
  727. break;
  728. case 'skewY':
  729. const sy = Math.tan(parseFloat(valueArr[0]) * DEGREE_TO_ANGLE);
  730. matrix.mul(mt, [1, sy, 0, 1, 0, 0], mt);
  731. break;
  732. case 'matrix':
  733. mt[0] = parseFloat(valueArr[0]);
  734. mt[1] = parseFloat(valueArr[1]);
  735. mt[2] = parseFloat(valueArr[2]);
  736. mt[3] = parseFloat(valueArr[3]);
  737. mt[4] = parseFloat(valueArr[4]);
  738. mt[5] = parseFloat(valueArr[5]);
  739. break;
  740. }
  741. }
  742. node.setLocalTransform(mt);
  743. }
  744. }
  745. // Value may contain space.
  746. const styleRegex = /([^\s:;]+)\s*:\s*([^:;]+)/g;
  747. function parseInlineStyle(
  748. xmlNode: SVGElement,
  749. inheritableStyleResult: Dictionary<string>,
  750. selfStyleResult: Dictionary<string>
  751. ): void {
  752. const style = xmlNode.getAttribute('style');
  753. if (!style) {
  754. return;
  755. }
  756. styleRegex.lastIndex = 0;
  757. let styleRegResult;
  758. while ((styleRegResult = styleRegex.exec(style)) != null) {
  759. const svgStlAttr = styleRegResult[1];
  760. const zrInheritableStlAttr = hasOwn(INHERITABLE_STYLE_ATTRIBUTES_MAP, svgStlAttr)
  761. ? INHERITABLE_STYLE_ATTRIBUTES_MAP[svgStlAttr as keyof typeof INHERITABLE_STYLE_ATTRIBUTES_MAP]
  762. : null;
  763. if (zrInheritableStlAttr) {
  764. inheritableStyleResult[zrInheritableStlAttr] = styleRegResult[2];
  765. }
  766. const zrSelfStlAttr = hasOwn(SELF_STYLE_ATTRIBUTES_MAP, svgStlAttr)
  767. ? SELF_STYLE_ATTRIBUTES_MAP[svgStlAttr as keyof typeof SELF_STYLE_ATTRIBUTES_MAP]
  768. : null;
  769. if (zrSelfStlAttr) {
  770. selfStyleResult[zrSelfStlAttr] = styleRegResult[2];
  771. }
  772. }
  773. }
  774. function parseAttributeStyle(
  775. xmlNode: SVGElement,
  776. inheritableStyleResult: Dictionary<string>,
  777. selfStyleResult: Dictionary<string>
  778. ): void {
  779. for (let i = 0; i < INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS.length; i++) {
  780. const svgAttrName = INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS[i];
  781. const attrValue = xmlNode.getAttribute(svgAttrName);
  782. if (attrValue != null) {
  783. inheritableStyleResult[INHERITABLE_STYLE_ATTRIBUTES_MAP[svgAttrName]] = attrValue;
  784. }
  785. }
  786. for (let i = 0; i < SELF_STYLE_ATTRIBUTES_MAP_KEYS.length; i++) {
  787. const svgAttrName = SELF_STYLE_ATTRIBUTES_MAP_KEYS[i];
  788. const attrValue = xmlNode.getAttribute(svgAttrName);
  789. if (attrValue != null) {
  790. selfStyleResult[SELF_STYLE_ATTRIBUTES_MAP[svgAttrName]] = attrValue;
  791. }
  792. }
  793. }
  794. export function makeViewBoxTransform(viewBoxRect: RectLike, boundingRect: RectLike): {
  795. scale: number;
  796. x: number;
  797. y: number;
  798. } {
  799. const scaleX = boundingRect.width / viewBoxRect.width;
  800. const scaleY = boundingRect.height / viewBoxRect.height;
  801. const scale = Math.min(scaleX, scaleY);
  802. // preserveAspectRatio 'xMidYMid'
  803. return {
  804. scale,
  805. x: -(viewBoxRect.x + viewBoxRect.width / 2) * scale + (boundingRect.x + boundingRect.width / 2),
  806. y: -(viewBoxRect.y + viewBoxRect.height / 2) * scale + (boundingRect.y + boundingRect.height / 2)
  807. };
  808. }
  809. export function parseSVG(xml: string | Document | SVGElement, opt: SVGParserOption): SVGParserResult {
  810. const parser = new SVGParser();
  811. return parser.parse(xml, opt);
  812. }
  813. // Also export parseXML to avoid breaking change.
  814. export {parseXML};