UNPKG

197 kBJavaScriptView Raw
1import requestAnimationFrame from 'raf';
2import RGBColor from 'rgbcolor';
3import { SVGPathData } from 'svg-pathdata';
4import { canvasRGBA } from 'stackblur-canvas';
5
6/**
7 * Options preset for `OffscreenCanvas`.
8 * @param config - Preset requirements.
9 * @param config.DOMParser - XML/HTML parser from string into DOM Document.
10 * @returns Preset object.
11 */
12function offscreen({ DOMParser: DOMParserFallback } = {}) {
13 const preset = {
14 window: null,
15 ignoreAnimation: true,
16 ignoreMouse: true,
17 DOMParser: DOMParserFallback,
18 createCanvas(width, height) {
19 return new OffscreenCanvas(width, height);
20 },
21 async createImage(url) {
22 const response = await fetch(url);
23 const blob = await response.blob();
24 const img = await createImageBitmap(blob);
25 return img;
26 }
27 };
28 if (typeof DOMParser !== 'undefined'
29 || typeof DOMParserFallback === 'undefined') {
30 Reflect.deleteProperty(preset, 'DOMParser');
31 }
32 return preset;
33}
34
35/**
36 * Options preset for `node-canvas`.
37 * @param config - Preset requirements.
38 * @param config.DOMParser - XML/HTML parser from string into DOM Document.
39 * @param config.canvas - `node-canvas` exports.
40 * @param config.fetch - WHATWG-compatible `fetch` function.
41 * @returns Preset object.
42 */
43function node({ DOMParser, canvas, fetch }) {
44 return {
45 window: null,
46 ignoreAnimation: true,
47 ignoreMouse: true,
48 DOMParser,
49 fetch,
50 createCanvas: canvas.createCanvas,
51 createImage: canvas.loadImage
52 };
53}
54
55var index = /*#__PURE__*/Object.freeze({
56 __proto__: null,
57 offscreen: offscreen,
58 node: node
59});
60
61/**
62 * HTML-safe compress white-spaces.
63 * @param str - String to compress.
64 * @returns String.
65 */
66function compressSpaces(str) {
67 return str.replace(/(?!\u3000)\s+/gm, ' ');
68}
69/**
70 * HTML-safe left trim.
71 * @param str - String to trim.
72 * @returns String.
73 */
74function trimLeft(str) {
75 return str.replace(/^[\n \t]+/, '');
76}
77/**
78 * HTML-safe right trim.
79 * @param str - String to trim.
80 * @returns String.
81 */
82function trimRight(str) {
83 return str.replace(/[\n \t]+$/, '');
84}
85/**
86 * String to numbers array.
87 * @param str - Numbers string.
88 * @returns Numbers array.
89 */
90function toNumbers(str) {
91 const matches = (str || '').match(/-?(\d+(?:\.\d*(?:[eE][+-]?\d+)?)?|\.\d+)(?=\D|$)/gm) || [];
92 return matches.map(parseFloat);
93}
94// Microsoft Edge fix
95const allUppercase = /^[A-Z-]+$/;
96/**
97 * Normalize attribute name.
98 * @param name - Attribute name.
99 * @returns Normalized attribute name.
100 */
101function normalizeAttributeName(name) {
102 if (allUppercase.test(name)) {
103 return name.toLowerCase();
104 }
105 return name;
106}
107/**
108 * Parse external URL.
109 * @param url - CSS url string.
110 * @returns Parsed URL.
111 */
112function parseExternalUrl(url) {
113 // single quotes [2]
114 // v double quotes [3]
115 // v v no quotes [4]
116 // v v v
117 const urlMatch = /url\(('([^']+)'|"([^"]+)"|([^'")]+))\)/.exec(url) || [];
118 return urlMatch[2] || urlMatch[3] || urlMatch[4];
119}
120/**
121 * Transform floats to integers in rgb colors.
122 * @param color - Color to normalize.
123 * @returns Normalized color.
124 */
125function normalizeColor(color) {
126 if (!color.startsWith('rgb')) {
127 return color;
128 }
129 let rgbParts = 3;
130 const normalizedColor = color.replace(/\d+(\.\d+)?/g, (num, isFloat) => (rgbParts-- && isFloat
131 ? String(Math.round(parseFloat(num)))
132 : num));
133 return normalizedColor;
134}
135
136// slightly modified version of https://github.com/keeganstreet/specificity/blob/master/specificity.js
137const attributeRegex = /(\[[^\]]+\])/g;
138const idRegex = /(#[^\s+>~.[:]+)/g;
139const classRegex = /(\.[^\s+>~.[:]+)/g;
140const pseudoElementRegex = /(::[^\s+>~.[:]+|:first-line|:first-letter|:before|:after)/gi;
141const pseudoClassWithBracketsRegex = /(:[\w-]+\([^)]*\))/gi;
142const pseudoClassRegex = /(:[^\s+>~.[:]+)/g;
143const elementRegex = /([^\s+>~.[:]+)/g;
144function findSelectorMatch(selector, regex) {
145 const matches = regex.exec(selector);
146 if (!matches) {
147 return [
148 selector,
149 0
150 ];
151 }
152 return [
153 selector.replace(regex, ' '),
154 matches.length
155 ];
156}
157/**
158 * Measure selector specificity.
159 * @param selector - Selector to measure.
160 * @returns Specificity.
161 */
162function getSelectorSpecificity(selector) {
163 const specificity = [0, 0, 0];
164 let currentSelector = selector
165 .replace(/:not\(([^)]*)\)/g, ' $1 ')
166 .replace(/{[\s\S]*/gm, ' ');
167 let delta = 0;
168 [currentSelector, delta] = findSelectorMatch(currentSelector, attributeRegex);
169 specificity[1] += delta;
170 [currentSelector, delta] = findSelectorMatch(currentSelector, idRegex);
171 specificity[0] += delta;
172 [currentSelector, delta] = findSelectorMatch(currentSelector, classRegex);
173 specificity[1] += delta;
174 [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoElementRegex);
175 specificity[2] += delta;
176 [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassWithBracketsRegex);
177 specificity[1] += delta;
178 [currentSelector, delta] = findSelectorMatch(currentSelector, pseudoClassRegex);
179 specificity[1] += delta;
180 currentSelector = currentSelector
181 .replace(/[*\s+>~]/g, ' ')
182 .replace(/[#.]/g, ' ');
183 [currentSelector, delta] = findSelectorMatch(currentSelector, elementRegex); // lgtm [js/useless-assignment-to-local]
184 specificity[2] += delta;
185 return specificity.join('');
186}
187
188const PSEUDO_ZERO = .00000001;
189/**
190 * Vector magnitude.
191 * @param v
192 * @returns Number result.
193 */
194function vectorMagnitude(v) {
195 return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2));
196}
197/**
198 * Ratio between two vectors.
199 * @param u
200 * @param v
201 * @returns Number result.
202 */
203function vectorsRatio(u, v) {
204 return (u[0] * v[0] + u[1] * v[1]) / (vectorMagnitude(u) * vectorMagnitude(v));
205}
206/**
207 * Angle between two vectors.
208 * @param u
209 * @param v
210 * @returns Number result.
211 */
212function vectorsAngle(u, v) {
213 return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vectorsRatio(u, v));
214}
215function CB1(t) {
216 return t * t * t;
217}
218function CB2(t) {
219 return 3 * t * t * (1 - t);
220}
221function CB3(t) {
222 return 3 * t * (1 - t) * (1 - t);
223}
224function CB4(t) {
225 return (1 - t) * (1 - t) * (1 - t);
226}
227function QB1(t) {
228 return t * t;
229}
230function QB2(t) {
231 return 2 * t * (1 - t);
232}
233function QB3(t) {
234 return (1 - t) * (1 - t);
235}
236
237/* eslint-disable @typescript-eslint/no-unsafe-assignment */
238class Property {
239 constructor(document, name, value) {
240 this.document = document;
241 this.name = name;
242 this.value = value;
243 this.isNormalizedColor = false;
244 }
245 static empty(document) {
246 return new Property(document, 'EMPTY', '');
247 }
248 split(separator = ' ') {
249 const { document, name } = this;
250 return compressSpaces(this.getString())
251 .trim()
252 .split(separator)
253 .map(value => new Property(document, name, value));
254 }
255 hasValue(zeroIsValue) {
256 const { value } = this;
257 return value !== null
258 && value !== ''
259 && (zeroIsValue || value !== 0)
260 && typeof value !== 'undefined';
261 }
262 isString(regexp) {
263 const { value } = this;
264 const result = typeof value === 'string';
265 if (!result || !regexp) {
266 return result;
267 }
268 return regexp.test(value);
269 }
270 isUrlDefinition() {
271 return this.isString(/^url\(/);
272 }
273 isPixels() {
274 if (!this.hasValue()) {
275 return false;
276 }
277 const asString = this.getString();
278 switch (true) {
279 case asString.endsWith('px'):
280 case /^[0-9]+$/.test(asString):
281 return true;
282 default:
283 return false;
284 }
285 }
286 setValue(value) {
287 this.value = value;
288 return this;
289 }
290 getValue(def) {
291 if (typeof def === 'undefined' || this.hasValue()) {
292 return this.value;
293 }
294 return def;
295 }
296 getNumber(def) {
297 if (!this.hasValue()) {
298 if (typeof def === 'undefined') {
299 return 0;
300 }
301 return parseFloat(def);
302 }
303 const { value } = this;
304 let n = parseFloat(value);
305 if (this.isString(/%$/)) {
306 n /= 100.0;
307 }
308 return n;
309 }
310 getString(def) {
311 if (typeof def === 'undefined' || this.hasValue()) {
312 return typeof this.value === 'undefined'
313 ? ''
314 : String(this.value);
315 }
316 return String(def);
317 }
318 getColor(def) {
319 let color = this.getString(def);
320 if (this.isNormalizedColor) {
321 return color;
322 }
323 this.isNormalizedColor = true;
324 color = normalizeColor(color);
325 this.value = color;
326 return color;
327 }
328 getDpi() {
329 return 96.0; // TODO: compute?
330 }
331 getRem() {
332 return this.document.rootEmSize;
333 }
334 getEm() {
335 return this.document.emSize;
336 }
337 getUnits() {
338 return this.getString().replace(/[0-9.-]/g, '');
339 }
340 getPixels(axisOrIsFontSize, processPercent = false) {
341 if (!this.hasValue()) {
342 return 0;
343 }
344 const [axis, isFontSize] = typeof axisOrIsFontSize === 'boolean'
345 ? [undefined, axisOrIsFontSize]
346 : [axisOrIsFontSize];
347 const { viewPort } = this.document.screen;
348 switch (true) {
349 case this.isString(/vmin$/):
350 return this.getNumber()
351 / 100.0
352 * Math.min(viewPort.computeSize('x'), viewPort.computeSize('y'));
353 case this.isString(/vmax$/):
354 return this.getNumber()
355 / 100.0
356 * Math.max(viewPort.computeSize('x'), viewPort.computeSize('y'));
357 case this.isString(/vw$/):
358 return this.getNumber()
359 / 100.0
360 * viewPort.computeSize('x');
361 case this.isString(/vh$/):
362 return this.getNumber()
363 / 100.0
364 * viewPort.computeSize('y');
365 case this.isString(/rem$/):
366 return this.getNumber() * this.getRem( /* viewPort */);
367 case this.isString(/em$/):
368 return this.getNumber() * this.getEm( /* viewPort */);
369 case this.isString(/ex$/):
370 return this.getNumber() * this.getEm( /* viewPort */) / 2.0;
371 case this.isString(/px$/):
372 return this.getNumber();
373 case this.isString(/pt$/):
374 return this.getNumber() * this.getDpi( /* viewPort */) * (1.0 / 72.0);
375 case this.isString(/pc$/):
376 return this.getNumber() * 15;
377 case this.isString(/cm$/):
378 return this.getNumber() * this.getDpi( /* viewPort */) / 2.54;
379 case this.isString(/mm$/):
380 return this.getNumber() * this.getDpi( /* viewPort */) / 25.4;
381 case this.isString(/in$/):
382 return this.getNumber() * this.getDpi( /* viewPort */);
383 case this.isString(/%$/) && isFontSize:
384 return this.getNumber() * this.getEm( /* viewPort */);
385 case this.isString(/%$/):
386 return this.getNumber() * viewPort.computeSize(axis);
387 default: {
388 const n = this.getNumber();
389 if (processPercent && n < 1.0) {
390 return n * viewPort.computeSize(axis);
391 }
392 return n;
393 }
394 }
395 }
396 getMilliseconds() {
397 if (!this.hasValue()) {
398 return 0;
399 }
400 if (this.isString(/ms$/)) {
401 return this.getNumber();
402 }
403 return this.getNumber() * 1000;
404 }
405 getRadians() {
406 if (!this.hasValue()) {
407 return 0;
408 }
409 switch (true) {
410 case this.isString(/deg$/):
411 return this.getNumber() * (Math.PI / 180.0);
412 case this.isString(/grad$/):
413 return this.getNumber() * (Math.PI / 200.0);
414 case this.isString(/rad$/):
415 return this.getNumber();
416 default:
417 return this.getNumber() * (Math.PI / 180.0);
418 }
419 }
420 getDefinition() {
421 const asString = this.getString();
422 let name = /#([^)'"]+)/.exec(asString);
423 if (name) {
424 name = name[1];
425 }
426 if (!name) {
427 name = asString;
428 }
429 return this.document.definitions[name];
430 }
431 getFillStyleDefinition(element, opacity) {
432 let def = this.getDefinition();
433 if (!def) {
434 return null;
435 }
436 // gradient
437 if (typeof def.createGradient === 'function') {
438 return def.createGradient(this.document.ctx, element, opacity);
439 }
440 // pattern
441 if (typeof def.createPattern === 'function') {
442 if (def.getHrefAttribute().hasValue()) {
443 const patternTransform = def.getAttribute('patternTransform');
444 def = def.getHrefAttribute().getDefinition();
445 if (patternTransform.hasValue()) {
446 def.getAttribute('patternTransform', true).setValue(patternTransform.value);
447 }
448 }
449 return def.createPattern(this.document.ctx, element, opacity);
450 }
451 return null;
452 }
453 getTextBaseline() {
454 if (!this.hasValue()) {
455 return null;
456 }
457 return Property.textBaselineMapping[this.getString()];
458 }
459 addOpacity(opacity) {
460 let value = this.getColor();
461 const len = value.length;
462 let commas = 0;
463 // Simulate old RGBColor version, which can't parse rgba.
464 for (let i = 0; i < len; i++) {
465 if (value[i] === ',') {
466 commas++;
467 }
468 if (commas === 3) {
469 break;
470 }
471 }
472 if (opacity.hasValue() && this.isString() && commas !== 3) {
473 const color = new RGBColor(value);
474 if (color.ok) {
475 color.alpha = opacity.getNumber();
476 value = color.toRGBA();
477 }
478 }
479 return new Property(this.document, this.name, value);
480 }
481}
482Property.textBaselineMapping = {
483 'baseline': 'alphabetic',
484 'before-edge': 'top',
485 'text-before-edge': 'top',
486 'middle': 'middle',
487 'central': 'middle',
488 'after-edge': 'bottom',
489 'text-after-edge': 'bottom',
490 'ideographic': 'ideographic',
491 'alphabetic': 'alphabetic',
492 'hanging': 'hanging',
493 'mathematical': 'alphabetic'
494};
495
496class ViewPort {
497 constructor() {
498 this.viewPorts = [];
499 }
500 clear() {
501 this.viewPorts = [];
502 }
503 setCurrent(width, height) {
504 this.viewPorts.push({
505 width,
506 height
507 });
508 }
509 removeCurrent() {
510 this.viewPorts.pop();
511 }
512 getCurrent() {
513 const { viewPorts } = this;
514 return viewPorts[viewPorts.length - 1];
515 }
516 get width() {
517 return this.getCurrent().width;
518 }
519 get height() {
520 return this.getCurrent().height;
521 }
522 computeSize(d) {
523 if (typeof d === 'number') {
524 return d;
525 }
526 if (d === 'x') {
527 return this.width;
528 }
529 if (d === 'y') {
530 return this.height;
531 }
532 return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2)) / Math.sqrt(2);
533 }
534}
535
536class Point {
537 constructor(x, y) {
538 this.x = x;
539 this.y = y;
540 }
541 static parse(point, defaultValue = 0) {
542 const [x = defaultValue, y = defaultValue] = toNumbers(point);
543 return new Point(x, y);
544 }
545 static parseScale(scale, defaultValue = 1) {
546 const [x = defaultValue, y = x] = toNumbers(scale);
547 return new Point(x, y);
548 }
549 static parsePath(path) {
550 const points = toNumbers(path);
551 const len = points.length;
552 const pathPoints = [];
553 for (let i = 0; i < len; i += 2) {
554 pathPoints.push(new Point(points[i], points[i + 1]));
555 }
556 return pathPoints;
557 }
558 angleTo(point) {
559 return Math.atan2(point.y - this.y, point.x - this.x);
560 }
561 applyTransform(transform) {
562 const { x, y } = this;
563 const xp = x * transform[0] + y * transform[2] + transform[4];
564 const yp = x * transform[1] + y * transform[3] + transform[5];
565 this.x = xp;
566 this.y = yp;
567 }
568}
569
570class Mouse {
571 constructor(screen) {
572 this.screen = screen;
573 this.working = false;
574 this.events = [];
575 this.eventElements = [];
576 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
577 this.onClick = this.onClick.bind(this);
578 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
579 this.onMouseMove = this.onMouseMove.bind(this);
580 }
581 isWorking() {
582 return this.working;
583 }
584 start() {
585 if (this.working) {
586 return;
587 }
588 const { screen, onClick, onMouseMove } = this;
589 const canvas = screen.ctx.canvas;
590 canvas.onclick = onClick;
591 canvas.onmousemove = onMouseMove;
592 this.working = true;
593 }
594 stop() {
595 if (!this.working) {
596 return;
597 }
598 const canvas = this.screen.ctx.canvas;
599 this.working = false;
600 canvas.onclick = null;
601 canvas.onmousemove = null;
602 }
603 hasEvents() {
604 return this.working && this.events.length > 0;
605 }
606 runEvents() {
607 if (!this.working) {
608 return;
609 }
610 const { screen: document, events, eventElements } = this;
611 const { style } = document.ctx.canvas;
612 if (style) {
613 style.cursor = '';
614 }
615 events.forEach(({ run }, i) => {
616 let element = eventElements[i];
617 while (element) {
618 run(element);
619 element = element.parent;
620 }
621 });
622 // done running, clear
623 this.events = [];
624 this.eventElements = [];
625 }
626 checkPath(element, ctx) {
627 if (!this.working || !ctx) {
628 return;
629 }
630 const { events, eventElements } = this;
631 events.forEach(({ x, y }, i) => {
632 if (!eventElements[i] && ctx.isPointInPath && ctx.isPointInPath(x, y)) {
633 eventElements[i] = element;
634 }
635 });
636 }
637 checkBoundingBox(element, boundingBox) {
638 if (!this.working || !boundingBox) {
639 return;
640 }
641 const { events, eventElements } = this;
642 events.forEach(({ x, y }, i) => {
643 if (!eventElements[i] && boundingBox.isPointInBox(x, y)) {
644 eventElements[i] = element;
645 }
646 });
647 }
648 mapXY(x, y) {
649 const { window, ctx } = this.screen;
650 const point = new Point(x, y);
651 let element = ctx.canvas;
652 while (element) {
653 point.x -= element.offsetLeft;
654 point.y -= element.offsetTop;
655 element = element.offsetParent;
656 }
657 if (window.scrollX) {
658 point.x += window.scrollX;
659 }
660 if (window.scrollY) {
661 point.y += window.scrollY;
662 }
663 return point;
664 }
665 onClick(event) {
666 const { x, y } = this.mapXY(event.clientX, event.clientY);
667 this.events.push({
668 type: 'onclick',
669 x,
670 y,
671 run(eventTarget) {
672 if (eventTarget.onClick) {
673 eventTarget.onClick();
674 }
675 }
676 });
677 }
678 onMouseMove(event) {
679 const { x, y } = this.mapXY(event.clientX, event.clientY);
680 this.events.push({
681 type: 'onmousemove',
682 x,
683 y,
684 run(eventTarget) {
685 if (eventTarget.onMouseMove) {
686 eventTarget.onMouseMove();
687 }
688 }
689 });
690 }
691}
692
693const defaultWindow = typeof window !== 'undefined'
694 ? window
695 : null;
696const defaultFetch$1 = typeof fetch !== 'undefined'
697 ? fetch.bind(undefined) // `fetch` depends on context: `someObject.fetch(...)` will throw error.
698 : null;
699class Screen {
700 constructor(ctx, { fetch = defaultFetch$1, window = defaultWindow } = {}) {
701 this.ctx = ctx;
702 this.FRAMERATE = 30;
703 this.MAX_VIRTUAL_PIXELS = 30000;
704 this.CLIENT_WIDTH = 800;
705 this.CLIENT_HEIGHT = 600;
706 this.viewPort = new ViewPort();
707 this.mouse = new Mouse(this);
708 this.animations = [];
709 this.waits = [];
710 this.frameDuration = 0;
711 this.isReadyLock = false;
712 this.isFirstRender = true;
713 this.intervalId = null;
714 this.window = window;
715 this.fetch = fetch;
716 }
717 wait(checker) {
718 this.waits.push(checker);
719 }
720 ready() {
721 // eslint-disable-next-line @typescript-eslint/no-misused-promises
722 if (!this.readyPromise) {
723 return Promise.resolve();
724 }
725 return this.readyPromise;
726 }
727 isReady() {
728 if (this.isReadyLock) {
729 return true;
730 }
731 const isReadyLock = this.waits.every(_ => _());
732 if (isReadyLock) {
733 this.waits = [];
734 if (this.resolveReady) {
735 this.resolveReady();
736 }
737 }
738 this.isReadyLock = isReadyLock;
739 return isReadyLock;
740 }
741 setDefaults(ctx) {
742 // initial values and defaults
743 ctx.strokeStyle = 'rgba(0,0,0,0)';
744 ctx.lineCap = 'butt';
745 ctx.lineJoin = 'miter';
746 ctx.miterLimit = 4;
747 }
748 setViewBox({ document, ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX = 0, minY = 0, refX, refY, clip = false, clipX = 0, clipY = 0 }) {
749 // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
750 const cleanAspectRatio = compressSpaces(aspectRatio).replace(/^defer\s/, ''); // ignore defer
751 const [aspectRatioAlign, aspectRatioMeetOrSlice] = cleanAspectRatio.split(' ');
752 const align = aspectRatioAlign || 'xMidYMid';
753 const meetOrSlice = aspectRatioMeetOrSlice || 'meet';
754 // calculate scale
755 const scaleX = width / desiredWidth;
756 const scaleY = height / desiredHeight;
757 const scaleMin = Math.min(scaleX, scaleY);
758 const scaleMax = Math.max(scaleX, scaleY);
759 let finalDesiredWidth = desiredWidth;
760 let finalDesiredHeight = desiredHeight;
761 if (meetOrSlice === 'meet') {
762 finalDesiredWidth *= scaleMin;
763 finalDesiredHeight *= scaleMin;
764 }
765 if (meetOrSlice === 'slice') {
766 finalDesiredWidth *= scaleMax;
767 finalDesiredHeight *= scaleMax;
768 }
769 const refXProp = new Property(document, 'refX', refX);
770 const refYProp = new Property(document, 'refY', refY);
771 const hasRefs = refXProp.hasValue() && refYProp.hasValue();
772 if (hasRefs) {
773 ctx.translate(-scaleMin * refXProp.getPixels('x'), -scaleMin * refYProp.getPixels('y'));
774 }
775 if (clip) {
776 const scaledClipX = scaleMin * clipX;
777 const scaledClipY = scaleMin * clipY;
778 ctx.beginPath();
779 ctx.moveTo(scaledClipX, scaledClipY);
780 ctx.lineTo(width, scaledClipY);
781 ctx.lineTo(width, height);
782 ctx.lineTo(scaledClipX, height);
783 ctx.closePath();
784 ctx.clip();
785 }
786 if (!hasRefs) {
787 const isMeetMinY = meetOrSlice === 'meet' && scaleMin === scaleY;
788 const isSliceMaxY = meetOrSlice === 'slice' && scaleMax === scaleY;
789 const isMeetMinX = meetOrSlice === 'meet' && scaleMin === scaleX;
790 const isSliceMaxX = meetOrSlice === 'slice' && scaleMax === scaleX;
791 if (align.startsWith('xMid') && (isMeetMinY || isSliceMaxY)) {
792 ctx.translate(width / 2.0 - finalDesiredWidth / 2.0, 0);
793 }
794 if (align.endsWith('YMid') && (isMeetMinX || isSliceMaxX)) {
795 ctx.translate(0, height / 2.0 - finalDesiredHeight / 2.0);
796 }
797 if (align.startsWith('xMax') && (isMeetMinY || isSliceMaxY)) {
798 ctx.translate(width - finalDesiredWidth, 0);
799 }
800 if (align.endsWith('YMax') && (isMeetMinX || isSliceMaxX)) {
801 ctx.translate(0, height - finalDesiredHeight);
802 }
803 }
804 // scale
805 switch (true) {
806 case align === 'none':
807 ctx.scale(scaleX, scaleY);
808 break;
809 case meetOrSlice === 'meet':
810 ctx.scale(scaleMin, scaleMin);
811 break;
812 case meetOrSlice === 'slice':
813 ctx.scale(scaleMax, scaleMax);
814 break;
815 }
816 // translate
817 ctx.translate(-minX, -minY);
818 }
819 start(element, { enableRedraw = false, ignoreMouse = false, ignoreAnimation = false, ignoreDimensions = false, ignoreClear = false, forceRedraw, scaleWidth, scaleHeight, offsetX, offsetY } = {}) {
820 const { FRAMERATE, mouse } = this;
821 const frameDuration = 1000 / FRAMERATE;
822 this.frameDuration = frameDuration;
823 this.readyPromise = new Promise((resolve) => {
824 this.resolveReady = resolve;
825 });
826 if (this.isReady()) {
827 this.render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY);
828 }
829 if (!enableRedraw) {
830 return;
831 }
832 let now = Date.now();
833 let then = now;
834 let delta = 0;
835 const tick = () => {
836 now = Date.now();
837 delta = now - then;
838 if (delta >= frameDuration) {
839 then = now - (delta % frameDuration);
840 if (this.shouldUpdate(ignoreAnimation, forceRedraw)) {
841 this.render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY);
842 mouse.runEvents();
843 }
844 }
845 this.intervalId = requestAnimationFrame(tick);
846 };
847 if (!ignoreMouse) {
848 mouse.start();
849 }
850 this.intervalId = requestAnimationFrame(tick);
851 }
852 stop() {
853 if (this.intervalId) {
854 requestAnimationFrame.cancel(this.intervalId);
855 this.intervalId = null;
856 }
857 this.mouse.stop();
858 }
859 shouldUpdate(ignoreAnimation, forceRedraw) {
860 // need update from animations?
861 if (!ignoreAnimation) {
862 const { frameDuration } = this;
863 const shouldUpdate = this.animations.reduce((shouldUpdate, animation) => animation.update(frameDuration) || shouldUpdate, false);
864 if (shouldUpdate) {
865 return true;
866 }
867 }
868 // need update from redraw?
869 if (typeof forceRedraw === 'function' && forceRedraw()) {
870 return true;
871 }
872 if (!this.isReadyLock && this.isReady()) {
873 return true;
874 }
875 // need update from mouse events?
876 if (this.mouse.hasEvents()) {
877 return true;
878 }
879 return false;
880 }
881 render(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY) {
882 const { CLIENT_WIDTH, CLIENT_HEIGHT, viewPort, ctx, isFirstRender } = this;
883 const canvas = ctx.canvas;
884 viewPort.clear();
885 if (canvas.width && canvas.height) {
886 viewPort.setCurrent(canvas.width, canvas.height);
887 }
888 else {
889 viewPort.setCurrent(CLIENT_WIDTH, CLIENT_HEIGHT);
890 }
891 const widthStyle = element.getStyle('width');
892 const heightStyle = element.getStyle('height');
893 if (!ignoreDimensions && (isFirstRender
894 || typeof scaleWidth !== 'number' && typeof scaleHeight !== 'number')) {
895 // set canvas size
896 if (widthStyle.hasValue()) {
897 canvas.width = widthStyle.getPixels('x');
898 if (canvas.style) {
899 canvas.style.width = `${canvas.width}px`;
900 }
901 }
902 if (heightStyle.hasValue()) {
903 canvas.height = heightStyle.getPixels('y');
904 if (canvas.style) {
905 canvas.style.height = `${canvas.height}px`;
906 }
907 }
908 }
909 let cWidth = canvas.clientWidth || canvas.width;
910 let cHeight = canvas.clientHeight || canvas.height;
911 if (ignoreDimensions && widthStyle.hasValue() && heightStyle.hasValue()) {
912 cWidth = widthStyle.getPixels('x');
913 cHeight = heightStyle.getPixels('y');
914 }
915 viewPort.setCurrent(cWidth, cHeight);
916 if (typeof offsetX === 'number') {
917 element.getAttribute('x', true).setValue(offsetX);
918 }
919 if (typeof offsetY === 'number') {
920 element.getAttribute('y', true).setValue(offsetY);
921 }
922 if (typeof scaleWidth === 'number'
923 || typeof scaleHeight === 'number') {
924 const viewBox = toNumbers(element.getAttribute('viewBox').getString());
925 let xRatio = 0;
926 let yRatio = 0;
927 if (typeof scaleWidth === 'number') {
928 const widthStyle = element.getStyle('width');
929 if (widthStyle.hasValue()) {
930 xRatio = widthStyle.getPixels('x') / scaleWidth;
931 }
932 else if (!isNaN(viewBox[2])) {
933 xRatio = viewBox[2] / scaleWidth;
934 }
935 }
936 if (typeof scaleHeight === 'number') {
937 const heightStyle = element.getStyle('height');
938 if (heightStyle.hasValue()) {
939 yRatio = heightStyle.getPixels('y') / scaleHeight;
940 }
941 else if (!isNaN(viewBox[3])) {
942 yRatio = viewBox[3] / scaleHeight;
943 }
944 }
945 if (!xRatio) {
946 xRatio = yRatio;
947 }
948 if (!yRatio) {
949 yRatio = xRatio;
950 }
951 element.getAttribute('width', true).setValue(scaleWidth);
952 element.getAttribute('height', true).setValue(scaleHeight);
953 const transformStyle = element.getStyle('transform', true, true);
954 transformStyle.setValue(`${transformStyle.getString()} scale(${1.0 / xRatio}, ${1.0 / yRatio})`);
955 }
956 // clear and render
957 if (!ignoreClear) {
958 ctx.clearRect(0, 0, cWidth, cHeight);
959 }
960 element.render(ctx);
961 if (isFirstRender) {
962 this.isFirstRender = false;
963 }
964 }
965}
966Screen.defaultWindow = defaultWindow;
967Screen.defaultFetch = defaultFetch$1;
968
969const { defaultFetch } = Screen;
970const DefaultDOMParser = typeof DOMParser !== 'undefined'
971 ? DOMParser
972 : null;
973class Parser {
974 constructor({ fetch = defaultFetch, DOMParser = DefaultDOMParser } = {}) {
975 this.fetch = fetch;
976 this.DOMParser = DOMParser;
977 }
978 async parse(resource) {
979 if (resource.startsWith('<')) {
980 return this.parseFromString(resource);
981 }
982 return this.load(resource);
983 }
984 parseFromString(xml) {
985 const parser = new this.DOMParser();
986 try {
987 return this.checkDocument(parser.parseFromString(xml, 'image/svg+xml'));
988 }
989 catch (err) {
990 return this.checkDocument(parser.parseFromString(xml, 'text/xml'));
991 }
992 }
993 checkDocument(document) {
994 const parserError = document.getElementsByTagName('parsererror')[0];
995 if (parserError) {
996 throw new Error(parserError.textContent);
997 }
998 return document;
999 }
1000 async load(url) {
1001 const response = await this.fetch(url);
1002 const xml = await response.text();
1003 return this.parseFromString(xml);
1004 }
1005}
1006
1007class Translate {
1008 constructor(_, point) {
1009 this.type = 'translate';
1010 this.point = null;
1011 this.point = Point.parse(point);
1012 }
1013 apply(ctx) {
1014 const { x, y } = this.point;
1015 ctx.translate(x || 0.0, y || 0.0);
1016 }
1017 unapply(ctx) {
1018 const { x, y } = this.point;
1019 ctx.translate(-1.0 * x || 0.0, -1.0 * y || 0.0);
1020 }
1021 applyToPoint(point) {
1022 const { x, y } = this.point;
1023 point.applyTransform([
1024 1,
1025 0,
1026 0,
1027 1,
1028 x || 0.0,
1029 y || 0.0
1030 ]);
1031 }
1032}
1033
1034class Rotate {
1035 constructor(document, rotate, transformOrigin) {
1036 this.type = 'rotate';
1037 this.angle = null;
1038 this.originX = null;
1039 this.originY = null;
1040 this.cx = 0;
1041 this.cy = 0;
1042 const numbers = toNumbers(rotate);
1043 this.angle = new Property(document, 'angle', numbers[0]);
1044 this.originX = transformOrigin[0];
1045 this.originY = transformOrigin[1];
1046 this.cx = numbers[1] || 0;
1047 this.cy = numbers[2] || 0;
1048 }
1049 apply(ctx) {
1050 const { cx, cy, originX, originY, angle } = this;
1051 const tx = cx + originX.getPixels('x');
1052 const ty = cy + originY.getPixels('y');
1053 ctx.translate(tx, ty);
1054 ctx.rotate(angle.getRadians());
1055 ctx.translate(-tx, -ty);
1056 }
1057 unapply(ctx) {
1058 const { cx, cy, originX, originY, angle } = this;
1059 const tx = cx + originX.getPixels('x');
1060 const ty = cy + originY.getPixels('y');
1061 ctx.translate(tx, ty);
1062 ctx.rotate(-1.0 * angle.getRadians());
1063 ctx.translate(-tx, -ty);
1064 }
1065 applyToPoint(point) {
1066 const { cx, cy, angle } = this;
1067 const rad = angle.getRadians();
1068 point.applyTransform([
1069 1,
1070 0,
1071 0,
1072 1,
1073 cx || 0.0,
1074 cy || 0.0 // this.p.y
1075 ]);
1076 point.applyTransform([
1077 Math.cos(rad),
1078 Math.sin(rad),
1079 -Math.sin(rad),
1080 Math.cos(rad),
1081 0,
1082 0
1083 ]);
1084 point.applyTransform([
1085 1,
1086 0,
1087 0,
1088 1,
1089 -cx || 0.0,
1090 -cy || 0.0 // -this.p.y
1091 ]);
1092 }
1093}
1094
1095class Scale {
1096 constructor(_, scale, transformOrigin) {
1097 this.type = 'scale';
1098 this.scale = null;
1099 this.originX = null;
1100 this.originY = null;
1101 const scaleSize = Point.parseScale(scale);
1102 // Workaround for node-canvas
1103 if (scaleSize.x === 0
1104 || scaleSize.y === 0) {
1105 scaleSize.x = PSEUDO_ZERO;
1106 scaleSize.y = PSEUDO_ZERO;
1107 }
1108 this.scale = scaleSize;
1109 this.originX = transformOrigin[0];
1110 this.originY = transformOrigin[1];
1111 }
1112 apply(ctx) {
1113 const { scale: { x, y }, originX, originY } = this;
1114 const tx = originX.getPixels('x');
1115 const ty = originY.getPixels('y');
1116 ctx.translate(tx, ty);
1117 ctx.scale(x, y || x);
1118 ctx.translate(-tx, -ty);
1119 }
1120 unapply(ctx) {
1121 const { scale: { x, y }, originX, originY } = this;
1122 const tx = originX.getPixels('x');
1123 const ty = originY.getPixels('y');
1124 ctx.translate(tx, ty);
1125 ctx.scale(1.0 / x, 1.0 / y || x);
1126 ctx.translate(-tx, -ty);
1127 }
1128 applyToPoint(point) {
1129 const { x, y } = this.scale;
1130 point.applyTransform([
1131 x || 0.0,
1132 0,
1133 0,
1134 y || 0.0,
1135 0,
1136 0
1137 ]);
1138 }
1139}
1140
1141class Matrix {
1142 constructor(_, matrix, transformOrigin) {
1143 this.type = 'matrix';
1144 this.matrix = [];
1145 this.originX = null;
1146 this.originY = null;
1147 this.matrix = toNumbers(matrix);
1148 this.originX = transformOrigin[0];
1149 this.originY = transformOrigin[1];
1150 }
1151 apply(ctx) {
1152 const { originX, originY, matrix } = this;
1153 const tx = originX.getPixels('x');
1154 const ty = originY.getPixels('y');
1155 ctx.translate(tx, ty);
1156 ctx.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
1157 ctx.translate(-tx, -ty);
1158 }
1159 unapply(ctx) {
1160 const { originX, originY, matrix } = this;
1161 const a = matrix[0];
1162 const b = matrix[2];
1163 const c = matrix[4];
1164 const d = matrix[1];
1165 const e = matrix[3];
1166 const f = matrix[5];
1167 const g = 0.0;
1168 const h = 0.0;
1169 const i = 1.0;
1170 const det = 1 / (a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g));
1171 const tx = originX.getPixels('x');
1172 const ty = originY.getPixels('y');
1173 ctx.translate(tx, ty);
1174 ctx.transform(det * (e * i - f * h), det * (f * g - d * i), det * (c * h - b * i), det * (a * i - c * g), det * (b * f - c * e), det * (c * d - a * f));
1175 ctx.translate(-tx, -ty);
1176 }
1177 applyToPoint(point) {
1178 point.applyTransform(this.matrix);
1179 }
1180}
1181
1182class Skew extends Matrix {
1183 constructor(document, skew, transformOrigin) {
1184 super(document, skew, transformOrigin);
1185 this.type = 'skew';
1186 this.angle = null;
1187 this.angle = new Property(document, 'angle', skew);
1188 }
1189}
1190
1191class SkewX extends Skew {
1192 constructor(document, skew, transformOrigin) {
1193 super(document, skew, transformOrigin);
1194 this.type = 'skewX';
1195 this.matrix = [
1196 1,
1197 0,
1198 Math.tan(this.angle.getRadians()),
1199 1,
1200 0,
1201 0
1202 ];
1203 }
1204}
1205
1206class SkewY extends Skew {
1207 constructor(document, skew, transformOrigin) {
1208 super(document, skew, transformOrigin);
1209 this.type = 'skewY';
1210 this.matrix = [
1211 1,
1212 Math.tan(this.angle.getRadians()),
1213 0,
1214 1,
1215 0,
1216 0
1217 ];
1218 }
1219}
1220
1221function parseTransforms(transform) {
1222 return compressSpaces(transform)
1223 .trim()
1224 .replace(/\)([a-zA-Z])/g, ') $1')
1225 .replace(/\)(\s?,\s?)/g, ') ')
1226 .split(/\s(?=[a-z])/);
1227}
1228function parseTransform(transform) {
1229 const [type, value] = transform.split('(');
1230 return [
1231 type.trim(),
1232 value.trim().replace(')', '')
1233 ];
1234}
1235class Transform {
1236 constructor(document, transform, transformOrigin) {
1237 this.document = document;
1238 this.transforms = [];
1239 const data = parseTransforms(transform);
1240 data.forEach((transform) => {
1241 if (transform === 'none') {
1242 return;
1243 }
1244 const [type, value] = parseTransform(transform);
1245 const TransformType = Transform.transformTypes[type];
1246 if (typeof TransformType !== 'undefined') {
1247 this.transforms.push(new TransformType(this.document, value, transformOrigin));
1248 }
1249 });
1250 }
1251 static fromElement(document, element) {
1252 const transformStyle = element.getStyle('transform', false, true);
1253 const [transformOriginXProperty, transformOriginYProperty = transformOriginXProperty] = element.getStyle('transform-origin', false, true).split();
1254 const transformOrigin = [
1255 transformOriginXProperty,
1256 transformOriginYProperty
1257 ];
1258 if (transformStyle.hasValue()) {
1259 return new Transform(document, transformStyle.getString(), transformOrigin);
1260 }
1261 return null;
1262 }
1263 apply(ctx) {
1264 const { transforms } = this;
1265 const len = transforms.length;
1266 for (let i = 0; i < len; i++) {
1267 transforms[i].apply(ctx);
1268 }
1269 }
1270 unapply(ctx) {
1271 const { transforms } = this;
1272 const len = transforms.length;
1273 for (let i = len - 1; i >= 0; i--) {
1274 transforms[i].unapply(ctx);
1275 }
1276 }
1277 // TODO: applyToPoint unused ... remove?
1278 applyToPoint(point) {
1279 const { transforms } = this;
1280 const len = transforms.length;
1281 for (let i = 0; i < len; i++) {
1282 transforms[i].applyToPoint(point);
1283 }
1284 }
1285}
1286Transform.transformTypes = {
1287 translate: Translate,
1288 rotate: Rotate,
1289 scale: Scale,
1290 matrix: Matrix,
1291 skewX: SkewX,
1292 skewY: SkewY
1293};
1294
1295class Element {
1296 constructor(document, node, captureTextNodes = false) {
1297 this.document = document;
1298 this.node = node;
1299 this.captureTextNodes = captureTextNodes;
1300 this.attributes = {};
1301 this.styles = {};
1302 this.stylesSpecificity = {};
1303 this.animationFrozen = false;
1304 this.animationFrozenValue = '';
1305 this.parent = null;
1306 this.children = [];
1307 if (!node || node.nodeType !== 1) { // ELEMENT_NODE
1308 return;
1309 }
1310 // add attributes
1311 Array.from(node.attributes).forEach((attribute) => {
1312 const nodeName = normalizeAttributeName(attribute.nodeName);
1313 this.attributes[nodeName] = new Property(document, nodeName, attribute.value);
1314 });
1315 this.addStylesFromStyleDefinition();
1316 // add inline styles
1317 if (this.getAttribute('style').hasValue()) {
1318 const styles = this.getAttribute('style')
1319 .getString()
1320 .split(';')
1321 .map(_ => _.trim());
1322 styles.forEach((style) => {
1323 if (!style) {
1324 return;
1325 }
1326 const [name, value] = style.split(':').map(_ => _.trim());
1327 this.styles[name] = new Property(document, name, value);
1328 });
1329 }
1330 const { definitions } = document;
1331 const id = this.getAttribute('id');
1332 // add id
1333 if (id.hasValue()) {
1334 if (!definitions[id.getString()]) {
1335 definitions[id.getString()] = this;
1336 }
1337 }
1338 Array.from(node.childNodes).forEach((childNode) => {
1339 if (childNode.nodeType === 1) {
1340 this.addChild(childNode); // ELEMENT_NODE
1341 }
1342 else if (captureTextNodes && (childNode.nodeType === 3
1343 || childNode.nodeType === 4)) {
1344 const textNode = document.createTextNode(childNode);
1345 if (textNode.getText().length > 0) {
1346 this.addChild(textNode); // TEXT_NODE
1347 }
1348 }
1349 });
1350 }
1351 getAttribute(name, createIfNotExists = false) {
1352 const attr = this.attributes[name];
1353 if (!attr && createIfNotExists) {
1354 const attr = new Property(this.document, name, '');
1355 this.attributes[name] = attr;
1356 return attr;
1357 }
1358 return attr || Property.empty(this.document);
1359 }
1360 getHrefAttribute() {
1361 for (const key in this.attributes) {
1362 if (key === 'href' || key.endsWith(':href')) {
1363 return this.attributes[key];
1364 }
1365 }
1366 return Property.empty(this.document);
1367 }
1368 getStyle(name, createIfNotExists = false, skipAncestors = false) {
1369 const style = this.styles[name];
1370 if (style) {
1371 return style;
1372 }
1373 const attr = this.getAttribute(name);
1374 if (attr?.hasValue()) {
1375 this.styles[name] = attr; // move up to me to cache
1376 return attr;
1377 }
1378 if (!skipAncestors) {
1379 const { parent } = this;
1380 if (parent) {
1381 const parentStyle = parent.getStyle(name);
1382 if (parentStyle?.hasValue()) {
1383 return parentStyle;
1384 }
1385 }
1386 }
1387 if (createIfNotExists) {
1388 const style = new Property(this.document, name, '');
1389 this.styles[name] = style;
1390 return style;
1391 }
1392 return style || Property.empty(this.document);
1393 }
1394 render(ctx) {
1395 // don't render display=none
1396 // don't render visibility=hidden
1397 if (this.getStyle('display').getString() === 'none'
1398 || this.getStyle('visibility').getString() === 'hidden') {
1399 return;
1400 }
1401 ctx.save();
1402 if (this.getStyle('mask').hasValue()) { // mask
1403 const mask = this.getStyle('mask').getDefinition();
1404 if (mask) {
1405 this.applyEffects(ctx);
1406 mask.apply(ctx, this);
1407 }
1408 }
1409 else if (this.getStyle('filter').getValue('none') !== 'none') { // filter
1410 const filter = this.getStyle('filter').getDefinition();
1411 if (filter) {
1412 this.applyEffects(ctx);
1413 filter.apply(ctx, this);
1414 }
1415 }
1416 else {
1417 this.setContext(ctx);
1418 this.renderChildren(ctx);
1419 this.clearContext(ctx);
1420 }
1421 ctx.restore();
1422 }
1423 setContext(_) {
1424 // NO RENDER
1425 }
1426 applyEffects(ctx) {
1427 // transform
1428 const transform = Transform.fromElement(this.document, this);
1429 if (transform) {
1430 transform.apply(ctx);
1431 }
1432 // clip
1433 const clipPathStyleProp = this.getStyle('clip-path', false, true);
1434 if (clipPathStyleProp.hasValue()) {
1435 const clip = clipPathStyleProp.getDefinition();
1436 if (clip) {
1437 clip.apply(ctx);
1438 }
1439 }
1440 }
1441 clearContext(_) {
1442 // NO RENDER
1443 }
1444 renderChildren(ctx) {
1445 this.children.forEach((child) => {
1446 child.render(ctx);
1447 });
1448 }
1449 addChild(childNode) {
1450 const child = childNode instanceof Element
1451 ? childNode
1452 : this.document.createElement(childNode);
1453 child.parent = this;
1454 if (!Element.ignoreChildTypes.includes(child.type)) {
1455 this.children.push(child);
1456 }
1457 }
1458 matchesSelector(selector) {
1459 const { node } = this;
1460 if (typeof node.matches === 'function') {
1461 return node.matches(selector);
1462 }
1463 const styleClasses = node.getAttribute('class');
1464 if (!styleClasses || styleClasses === '') {
1465 return false;
1466 }
1467 return styleClasses.split(' ').some(styleClass => `.${styleClass}` === selector);
1468 }
1469 addStylesFromStyleDefinition() {
1470 const { styles, stylesSpecificity } = this.document;
1471 for (const selector in styles) {
1472 if (!selector.startsWith('@') && this.matchesSelector(selector)) {
1473 const style = styles[selector];
1474 const specificity = stylesSpecificity[selector];
1475 if (style) {
1476 for (const name in style) {
1477 let existingSpecificity = this.stylesSpecificity[name];
1478 if (typeof existingSpecificity === 'undefined') {
1479 existingSpecificity = '000';
1480 }
1481 if (specificity >= existingSpecificity) {
1482 this.styles[name] = style[name];
1483 this.stylesSpecificity[name] = specificity;
1484 }
1485 }
1486 }
1487 }
1488 }
1489 }
1490 removeStyles(element, ignoreStyles) {
1491 const toRestore = ignoreStyles.reduce((toRestore, name) => {
1492 const styleProp = element.getStyle(name);
1493 if (!styleProp.hasValue()) {
1494 return toRestore;
1495 }
1496 const value = styleProp.getString();
1497 styleProp.setValue('');
1498 return [
1499 ...toRestore,
1500 [name, value]
1501 ];
1502 }, []);
1503 return toRestore;
1504 }
1505 restoreStyles(element, styles) {
1506 styles.forEach(([name, value]) => {
1507 element.getStyle(name, true).setValue(value);
1508 });
1509 }
1510}
1511Element.ignoreChildTypes = [
1512 'title'
1513];
1514
1515class UnknownElement extends Element {
1516 constructor(document, node, captureTextNodes) {
1517 super(document, node, captureTextNodes);
1518 }
1519}
1520
1521function wrapFontFamily(fontFamily) {
1522 const trimmed = fontFamily.trim();
1523 return /^('|")/.test(trimmed)
1524 ? trimmed
1525 : `"${trimmed}"`;
1526}
1527function prepareFontFamily(fontFamily) {
1528 return typeof process === 'undefined'
1529 ? fontFamily
1530 : fontFamily
1531 .trim()
1532 .split(',')
1533 .map(wrapFontFamily)
1534 .join(',');
1535}
1536/**
1537 * https://developer.mozilla.org/en-US/docs/Web/CSS/font-style
1538 * @param fontStyle
1539 * @returns CSS font style.
1540 */
1541function prepareFontStyle(fontStyle) {
1542 if (!fontStyle) {
1543 return '';
1544 }
1545 const targetFontStyle = fontStyle.trim().toLowerCase();
1546 switch (targetFontStyle) {
1547 case 'normal':
1548 case 'italic':
1549 case 'oblique':
1550 case 'inherit':
1551 case 'initial':
1552 case 'unset':
1553 return targetFontStyle;
1554 default:
1555 if (/^oblique\s+(-|)\d+deg$/.test(targetFontStyle)) {
1556 return targetFontStyle;
1557 }
1558 return '';
1559 }
1560}
1561/**
1562 * https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
1563 * @param fontWeight
1564 * @returns CSS font weight.
1565 */
1566function prepareFontWeight(fontWeight) {
1567 if (!fontWeight) {
1568 return '';
1569 }
1570 const targetFontWeight = fontWeight.trim().toLowerCase();
1571 switch (targetFontWeight) {
1572 case 'normal':
1573 case 'bold':
1574 case 'lighter':
1575 case 'bolder':
1576 case 'inherit':
1577 case 'initial':
1578 case 'unset':
1579 return targetFontWeight;
1580 default:
1581 if (/^[\d.]+$/.test(targetFontWeight)) {
1582 return targetFontWeight;
1583 }
1584 return '';
1585 }
1586}
1587class Font {
1588 constructor(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) {
1589 const inheritFont = inherit
1590 ? typeof inherit === 'string'
1591 ? Font.parse(inherit)
1592 : inherit
1593 : {};
1594 this.fontFamily = fontFamily || inheritFont.fontFamily;
1595 this.fontSize = fontSize || inheritFont.fontSize;
1596 this.fontStyle = fontStyle || inheritFont.fontStyle;
1597 this.fontWeight = fontWeight || inheritFont.fontWeight;
1598 this.fontVariant = fontVariant || inheritFont.fontVariant;
1599 }
1600 static parse(font = '', inherit) {
1601 let fontStyle = '';
1602 let fontVariant = '';
1603 let fontWeight = '';
1604 let fontSize = '';
1605 let fontFamily = '';
1606 const parts = compressSpaces(font).trim().split(' ');
1607 const set = {
1608 fontSize: false,
1609 fontStyle: false,
1610 fontWeight: false,
1611 fontVariant: false
1612 };
1613 parts.forEach((part) => {
1614 switch (true) {
1615 case !set.fontStyle && Font.styles.includes(part):
1616 if (part !== 'inherit') {
1617 fontStyle = part;
1618 }
1619 set.fontStyle = true;
1620 break;
1621 case !set.fontVariant && Font.variants.includes(part):
1622 if (part !== 'inherit') {
1623 fontVariant = part;
1624 }
1625 set.fontStyle = true;
1626 set.fontVariant = true;
1627 break;
1628 case !set.fontWeight && Font.weights.includes(part):
1629 if (part !== 'inherit') {
1630 fontWeight = part;
1631 }
1632 set.fontStyle = true;
1633 set.fontVariant = true;
1634 set.fontWeight = true;
1635 break;
1636 case !set.fontSize:
1637 if (part !== 'inherit') {
1638 [fontSize] = part.split('/');
1639 }
1640 set.fontStyle = true;
1641 set.fontVariant = true;
1642 set.fontWeight = true;
1643 set.fontSize = true;
1644 break;
1645 default:
1646 if (part !== 'inherit') {
1647 fontFamily += part;
1648 }
1649 }
1650 });
1651 return new Font(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit);
1652 }
1653 toString() {
1654 return [
1655 prepareFontStyle(this.fontStyle),
1656 this.fontVariant,
1657 prepareFontWeight(this.fontWeight),
1658 this.fontSize,
1659 // Wrap fontFamily only on nodejs and only for canvas.ctx
1660 prepareFontFamily(this.fontFamily)
1661 ].join(' ').trim();
1662 }
1663}
1664Font.styles = 'normal|italic|oblique|inherit';
1665Font.variants = 'normal|small-caps|inherit';
1666Font.weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit';
1667
1668class BoundingBox {
1669 constructor(x1 = Number.NaN, y1 = Number.NaN, x2 = Number.NaN, y2 = Number.NaN) {
1670 this.x1 = x1;
1671 this.y1 = y1;
1672 this.x2 = x2;
1673 this.y2 = y2;
1674 this.addPoint(x1, y1);
1675 this.addPoint(x2, y2);
1676 }
1677 get x() {
1678 return this.x1;
1679 }
1680 get y() {
1681 return this.y1;
1682 }
1683 get width() {
1684 return this.x2 - this.x1;
1685 }
1686 get height() {
1687 return this.y2 - this.y1;
1688 }
1689 addPoint(x, y) {
1690 if (typeof x !== 'undefined') {
1691 if (isNaN(this.x1) || isNaN(this.x2)) {
1692 this.x1 = x;
1693 this.x2 = x;
1694 }
1695 if (x < this.x1) {
1696 this.x1 = x;
1697 }
1698 if (x > this.x2) {
1699 this.x2 = x;
1700 }
1701 }
1702 if (typeof y !== 'undefined') {
1703 if (isNaN(this.y1) || isNaN(this.y2)) {
1704 this.y1 = y;
1705 this.y2 = y;
1706 }
1707 if (y < this.y1) {
1708 this.y1 = y;
1709 }
1710 if (y > this.y2) {
1711 this.y2 = y;
1712 }
1713 }
1714 }
1715 addX(x) {
1716 this.addPoint(x, null);
1717 }
1718 addY(y) {
1719 this.addPoint(null, y);
1720 }
1721 addBoundingBox(boundingBox) {
1722 if (!boundingBox) {
1723 return;
1724 }
1725 const { x1, y1, x2, y2 } = boundingBox;
1726 this.addPoint(x1, y1);
1727 this.addPoint(x2, y2);
1728 }
1729 sumCubic(t, p0, p1, p2, p3) {
1730 return (Math.pow(1 - t, 3) * p0
1731 + 3 * Math.pow(1 - t, 2) * t * p1
1732 + 3 * (1 - t) * Math.pow(t, 2) * p2
1733 + Math.pow(t, 3) * p3);
1734 }
1735 bezierCurveAdd(forX, p0, p1, p2, p3) {
1736 const b = 6 * p0 - 12 * p1 + 6 * p2;
1737 const a = -3 * p0 + 9 * p1 - 9 * p2 + 3 * p3;
1738 const c = 3 * p1 - 3 * p0;
1739 if (a === 0) {
1740 if (b === 0) {
1741 return;
1742 }
1743 const t = -c / b;
1744 if (0 < t && t < 1) {
1745 if (forX) {
1746 this.addX(this.sumCubic(t, p0, p1, p2, p3));
1747 }
1748 else {
1749 this.addY(this.sumCubic(t, p0, p1, p2, p3));
1750 }
1751 }
1752 return;
1753 }
1754 const b2ac = Math.pow(b, 2) - 4 * c * a;
1755 if (b2ac < 0) {
1756 return;
1757 }
1758 const t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
1759 if (0 < t1 && t1 < 1) {
1760 if (forX) {
1761 this.addX(this.sumCubic(t1, p0, p1, p2, p3));
1762 }
1763 else {
1764 this.addY(this.sumCubic(t1, p0, p1, p2, p3));
1765 }
1766 }
1767 const t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
1768 if (0 < t2 && t2 < 1) {
1769 if (forX) {
1770 this.addX(this.sumCubic(t2, p0, p1, p2, p3));
1771 }
1772 else {
1773 this.addY(this.sumCubic(t2, p0, p1, p2, p3));
1774 }
1775 }
1776 }
1777 // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
1778 addBezierCurve(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
1779 this.addPoint(p0x, p0y);
1780 this.addPoint(p3x, p3y);
1781 this.bezierCurveAdd(true, p0x, p1x, p2x, p3x);
1782 this.bezierCurveAdd(false, p0y, p1y, p2y, p3y);
1783 }
1784 addQuadraticCurve(p0x, p0y, p1x, p1y, p2x, p2y) {
1785 const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0)
1786 const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0)
1787 const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0)
1788 const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0)
1789 this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y);
1790 }
1791 isPointInBox(x, y) {
1792 const { x1, y1, x2, y2 } = this;
1793 return (x1 <= x
1794 && x <= x2
1795 && y1 <= y
1796 && y <= y2);
1797 }
1798}
1799
1800class PathParser extends SVGPathData {
1801 constructor(path) {
1802 super(path
1803 // Fix spaces after signs.
1804 .replace(/([+\-.])\s+/gm, '$1')
1805 // Remove invalid part.
1806 .replace(/[^MmZzLlHhVvCcSsQqTtAae\d\s.,+-].*/g, ''));
1807 this.control = null;
1808 this.start = null;
1809 this.current = null;
1810 this.command = null;
1811 this.commands = this.commands;
1812 this.i = -1;
1813 this.previousCommand = null;
1814 this.points = [];
1815 this.angles = [];
1816 }
1817 reset() {
1818 this.i = -1;
1819 this.command = null;
1820 this.previousCommand = null;
1821 this.start = new Point(0, 0);
1822 this.control = new Point(0, 0);
1823 this.current = new Point(0, 0);
1824 this.points = [];
1825 this.angles = [];
1826 }
1827 isEnd() {
1828 const { i, commands } = this;
1829 return i >= commands.length - 1;
1830 }
1831 next() {
1832 const command = this.commands[++this.i];
1833 this.previousCommand = this.command;
1834 this.command = command;
1835 return command;
1836 }
1837 getPoint(xProp = 'x', yProp = 'y') {
1838 const point = new Point(this.command[xProp], this.command[yProp]);
1839 return this.makeAbsolute(point);
1840 }
1841 getAsControlPoint(xProp, yProp) {
1842 const point = this.getPoint(xProp, yProp);
1843 this.control = point;
1844 return point;
1845 }
1846 getAsCurrentPoint(xProp, yProp) {
1847 const point = this.getPoint(xProp, yProp);
1848 this.current = point;
1849 return point;
1850 }
1851 getReflectedControlPoint() {
1852 const previousCommand = this.previousCommand.type;
1853 if (previousCommand !== SVGPathData.CURVE_TO
1854 && previousCommand !== SVGPathData.SMOOTH_CURVE_TO
1855 && previousCommand !== SVGPathData.QUAD_TO
1856 && previousCommand !== SVGPathData.SMOOTH_QUAD_TO) {
1857 return this.current;
1858 }
1859 // reflect point
1860 const { current: { x: cx, y: cy }, control: { x: ox, y: oy } } = this;
1861 const point = new Point(2 * cx - ox, 2 * cy - oy);
1862 return point;
1863 }
1864 makeAbsolute(point) {
1865 if (this.command.relative) {
1866 const { x, y } = this.current;
1867 point.x += x;
1868 point.y += y;
1869 }
1870 return point;
1871 }
1872 addMarker(point, from, priorTo) {
1873 const { points, angles } = this;
1874 // if the last angle isn't filled in because we didn't have this point yet ...
1875 if (priorTo && angles.length > 0 && !angles[angles.length - 1]) {
1876 angles[angles.length - 1] = points[points.length - 1].angleTo(priorTo);
1877 }
1878 this.addMarkerAngle(point, from ? from.angleTo(point) : null);
1879 }
1880 addMarkerAngle(point, angle) {
1881 this.points.push(point);
1882 this.angles.push(angle);
1883 }
1884 getMarkerPoints() {
1885 return this.points;
1886 }
1887 getMarkerAngles() {
1888 const { angles } = this;
1889 const len = angles.length;
1890 for (let i = 0; i < len; i++) {
1891 if (!angles[i]) {
1892 for (let j = i + 1; j < len; j++) {
1893 if (angles[j]) {
1894 angles[i] = angles[j];
1895 break;
1896 }
1897 }
1898 }
1899 }
1900 return angles;
1901 }
1902}
1903
1904class RenderedElement extends Element {
1905 constructor() {
1906 super(...arguments);
1907 this.modifiedEmSizeStack = false;
1908 }
1909 calculateOpacity() {
1910 let opacity = 1.0;
1911 // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
1912 let element = this;
1913 while (element) {
1914 const opacityStyle = element.getStyle('opacity', false, true); // no ancestors on style call
1915 if (opacityStyle.hasValue(true)) {
1916 opacity *= opacityStyle.getNumber();
1917 }
1918 element = element.parent;
1919 }
1920 return opacity;
1921 }
1922 setContext(ctx, fromMeasure = false) {
1923 if (!fromMeasure) { // causes stack overflow when measuring text with gradients
1924 // fill
1925 const fillStyleProp = this.getStyle('fill');
1926 const fillOpacityStyleProp = this.getStyle('fill-opacity');
1927 const strokeStyleProp = this.getStyle('stroke');
1928 const strokeOpacityProp = this.getStyle('stroke-opacity');
1929 if (fillStyleProp.isUrlDefinition()) {
1930 const fillStyle = fillStyleProp.getFillStyleDefinition(this, fillOpacityStyleProp);
1931 if (fillStyle) {
1932 ctx.fillStyle = fillStyle;
1933 }
1934 }
1935 else if (fillStyleProp.hasValue()) {
1936 if (fillStyleProp.getString() === 'currentColor') {
1937 fillStyleProp.setValue(this.getStyle('color').getColor());
1938 }
1939 const fillStyle = fillStyleProp.getColor();
1940 if (fillStyle !== 'inherit') {
1941 ctx.fillStyle = fillStyle === 'none'
1942 ? 'rgba(0,0,0,0)'
1943 : fillStyle;
1944 }
1945 }
1946 if (fillOpacityStyleProp.hasValue()) {
1947 const fillStyle = new Property(this.document, 'fill', ctx.fillStyle)
1948 .addOpacity(fillOpacityStyleProp)
1949 .getColor();
1950 ctx.fillStyle = fillStyle;
1951 }
1952 // stroke
1953 if (strokeStyleProp.isUrlDefinition()) {
1954 const strokeStyle = strokeStyleProp.getFillStyleDefinition(this, strokeOpacityProp);
1955 if (strokeStyle) {
1956 ctx.strokeStyle = strokeStyle;
1957 }
1958 }
1959 else if (strokeStyleProp.hasValue()) {
1960 if (strokeStyleProp.getString() === 'currentColor') {
1961 strokeStyleProp.setValue(this.getStyle('color').getColor());
1962 }
1963 const strokeStyle = strokeStyleProp.getString();
1964 if (strokeStyle !== 'inherit') {
1965 ctx.strokeStyle = strokeStyle === 'none'
1966 ? 'rgba(0,0,0,0)'
1967 : strokeStyle;
1968 }
1969 }
1970 if (strokeOpacityProp.hasValue()) {
1971 const strokeStyle = new Property(this.document, 'stroke', ctx.strokeStyle)
1972 .addOpacity(strokeOpacityProp)
1973 .getString();
1974 ctx.strokeStyle = strokeStyle;
1975 }
1976 const strokeWidthStyleProp = this.getStyle('stroke-width');
1977 if (strokeWidthStyleProp.hasValue()) {
1978 const newLineWidth = strokeWidthStyleProp.getPixels();
1979 ctx.lineWidth = !newLineWidth
1980 ? PSEUDO_ZERO // browsers don't respect 0 (or node-canvas? :-)
1981 : newLineWidth;
1982 }
1983 const strokeLinecapStyleProp = this.getStyle('stroke-linecap');
1984 const strokeLinejoinStyleProp = this.getStyle('stroke-linejoin');
1985 const strokeMiterlimitProp = this.getStyle('stroke-miterlimit');
1986 // NEED TEST
1987 // const pointOrderStyleProp = this.getStyle('paint-order');
1988 const strokeDasharrayStyleProp = this.getStyle('stroke-dasharray');
1989 const strokeDashoffsetProp = this.getStyle('stroke-dashoffset');
1990 if (strokeLinecapStyleProp.hasValue()) {
1991 ctx.lineCap = strokeLinecapStyleProp.getString();
1992 }
1993 if (strokeLinejoinStyleProp.hasValue()) {
1994 ctx.lineJoin = strokeLinejoinStyleProp.getString();
1995 }
1996 if (strokeMiterlimitProp.hasValue()) {
1997 ctx.miterLimit = strokeMiterlimitProp.getNumber();
1998 }
1999 // NEED TEST
2000 // if (pointOrderStyleProp.hasValue()) {
2001 // // ?
2002 // ctx.paintOrder = pointOrderStyleProp.getValue();
2003 // }
2004 if (strokeDasharrayStyleProp.hasValue() && strokeDasharrayStyleProp.getString() !== 'none') {
2005 const gaps = toNumbers(strokeDasharrayStyleProp.getString());
2006 if (typeof ctx.setLineDash !== 'undefined') {
2007 ctx.setLineDash(gaps);
2008 }
2009 else
2010 // @ts-expect-error Handle browser prefix.
2011 if (typeof ctx.webkitLineDash !== 'undefined') {
2012 // @ts-expect-error Handle browser prefix.
2013 ctx.webkitLineDash = gaps;
2014 }
2015 else
2016 // @ts-expect-error Handle browser prefix.
2017 if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) {
2018 // @ts-expect-error Handle browser prefix.
2019 ctx.mozDash = gaps;
2020 }
2021 const offset = strokeDashoffsetProp.getPixels();
2022 if (typeof ctx.lineDashOffset !== 'undefined') {
2023 ctx.lineDashOffset = offset;
2024 }
2025 else
2026 // @ts-expect-error Handle browser prefix.
2027 if (typeof ctx.webkitLineDashOffset !== 'undefined') {
2028 // @ts-expect-error Handle browser prefix.
2029 ctx.webkitLineDashOffset = offset;
2030 }
2031 else
2032 // @ts-expect-error Handle browser prefix.
2033 if (typeof ctx.mozDashOffset !== 'undefined') {
2034 // @ts-expect-error Handle browser prefix.
2035 ctx.mozDashOffset = offset;
2036 }
2037 }
2038 }
2039 // font
2040 this.modifiedEmSizeStack = false;
2041 if (typeof ctx.font !== 'undefined') {
2042 const fontStyleProp = this.getStyle('font');
2043 const fontStyleStyleProp = this.getStyle('font-style');
2044 const fontVariantStyleProp = this.getStyle('font-variant');
2045 const fontWeightStyleProp = this.getStyle('font-weight');
2046 const fontSizeStyleProp = this.getStyle('font-size');
2047 const fontFamilyStyleProp = this.getStyle('font-family');
2048 const font = new Font(fontStyleStyleProp.getString(), fontVariantStyleProp.getString(), fontWeightStyleProp.getString(), fontSizeStyleProp.hasValue()
2049 ? `${fontSizeStyleProp.getPixels(true)}px`
2050 : '', fontFamilyStyleProp.getString(), Font.parse(fontStyleProp.getString(), ctx.font));
2051 fontStyleStyleProp.setValue(font.fontStyle);
2052 fontVariantStyleProp.setValue(font.fontVariant);
2053 fontWeightStyleProp.setValue(font.fontWeight);
2054 fontSizeStyleProp.setValue(font.fontSize);
2055 fontFamilyStyleProp.setValue(font.fontFamily);
2056 ctx.font = font.toString();
2057 if (fontSizeStyleProp.isPixels()) {
2058 this.document.emSize = fontSizeStyleProp.getPixels();
2059 this.modifiedEmSizeStack = true;
2060 }
2061 }
2062 if (!fromMeasure) {
2063 // effects
2064 this.applyEffects(ctx);
2065 // opacity
2066 ctx.globalAlpha = this.calculateOpacity();
2067 }
2068 }
2069 clearContext(ctx) {
2070 super.clearContext(ctx);
2071 if (this.modifiedEmSizeStack) {
2072 this.document.popEmSize();
2073 }
2074 }
2075}
2076
2077class PathElement extends RenderedElement {
2078 constructor(document, node, captureTextNodes) {
2079 super(document, node, captureTextNodes);
2080 this.type = 'path';
2081 this.pathParser = null;
2082 this.pathParser = new PathParser(this.getAttribute('d').getString());
2083 }
2084 path(ctx) {
2085 const { pathParser } = this;
2086 const boundingBox = new BoundingBox();
2087 pathParser.reset();
2088 if (ctx) {
2089 ctx.beginPath();
2090 }
2091 while (!pathParser.isEnd()) {
2092 switch (pathParser.next().type) {
2093 case PathParser.MOVE_TO:
2094 this.pathM(ctx, boundingBox);
2095 break;
2096 case PathParser.LINE_TO:
2097 this.pathL(ctx, boundingBox);
2098 break;
2099 case PathParser.HORIZ_LINE_TO:
2100 this.pathH(ctx, boundingBox);
2101 break;
2102 case PathParser.VERT_LINE_TO:
2103 this.pathV(ctx, boundingBox);
2104 break;
2105 case PathParser.CURVE_TO:
2106 this.pathC(ctx, boundingBox);
2107 break;
2108 case PathParser.SMOOTH_CURVE_TO:
2109 this.pathS(ctx, boundingBox);
2110 break;
2111 case PathParser.QUAD_TO:
2112 this.pathQ(ctx, boundingBox);
2113 break;
2114 case PathParser.SMOOTH_QUAD_TO:
2115 this.pathT(ctx, boundingBox);
2116 break;
2117 case PathParser.ARC:
2118 this.pathA(ctx, boundingBox);
2119 break;
2120 case PathParser.CLOSE_PATH:
2121 this.pathZ(ctx, boundingBox);
2122 break;
2123 }
2124 }
2125 return boundingBox;
2126 }
2127 getBoundingBox(_) {
2128 return this.path();
2129 }
2130 getMarkers() {
2131 const { pathParser } = this;
2132 const points = pathParser.getMarkerPoints();
2133 const angles = pathParser.getMarkerAngles();
2134 const markers = points.map((point, i) => [
2135 point,
2136 angles[i]
2137 ]);
2138 return markers;
2139 }
2140 renderChildren(ctx) {
2141 this.path(ctx);
2142 this.document.screen.mouse.checkPath(this, ctx);
2143 const fillRuleStyleProp = this.getStyle('fill-rule');
2144 if (ctx.fillStyle !== '') {
2145 if (fillRuleStyleProp.getString('inherit') !== 'inherit') {
2146 ctx.fill(fillRuleStyleProp.getString());
2147 }
2148 else {
2149 ctx.fill();
2150 }
2151 }
2152 if (ctx.strokeStyle !== '') {
2153 if (this.getAttribute('vector-effect').getString() === 'non-scaling-stroke') {
2154 ctx.save();
2155 ctx.setTransform(1, 0, 0, 1, 0, 0);
2156 ctx.stroke();
2157 ctx.restore();
2158 }
2159 else {
2160 ctx.stroke();
2161 }
2162 }
2163 const markers = this.getMarkers();
2164 if (markers) {
2165 const markersLastIndex = markers.length - 1;
2166 const markerStartStyleProp = this.getStyle('marker-start');
2167 const markerMidStyleProp = this.getStyle('marker-mid');
2168 const markerEndStyleProp = this.getStyle('marker-end');
2169 if (markerStartStyleProp.isUrlDefinition()) {
2170 const marker = markerStartStyleProp.getDefinition();
2171 const [point, angle] = markers[0];
2172 marker.render(ctx, point, angle);
2173 }
2174 if (markerMidStyleProp.isUrlDefinition()) {
2175 const marker = markerMidStyleProp.getDefinition();
2176 for (let i = 1; i < markersLastIndex; i++) {
2177 const [point, angle] = markers[i];
2178 marker.render(ctx, point, angle);
2179 }
2180 }
2181 if (markerEndStyleProp.isUrlDefinition()) {
2182 const marker = markerEndStyleProp.getDefinition();
2183 const [point, angle] = markers[markersLastIndex];
2184 marker.render(ctx, point, angle);
2185 }
2186 }
2187 }
2188 static pathM(pathParser) {
2189 const point = pathParser.getAsCurrentPoint();
2190 pathParser.start = pathParser.current;
2191 return {
2192 point
2193 };
2194 }
2195 pathM(ctx, boundingBox) {
2196 const { pathParser } = this;
2197 const { point } = PathElement.pathM(pathParser);
2198 const { x, y } = point;
2199 pathParser.addMarker(point);
2200 boundingBox.addPoint(x, y);
2201 if (ctx) {
2202 ctx.moveTo(x, y);
2203 }
2204 }
2205 static pathL(pathParser) {
2206 const { current } = pathParser;
2207 const point = pathParser.getAsCurrentPoint();
2208 return {
2209 current,
2210 point
2211 };
2212 }
2213 pathL(ctx, boundingBox) {
2214 const { pathParser } = this;
2215 const { current, point } = PathElement.pathL(pathParser);
2216 const { x, y } = point;
2217 pathParser.addMarker(point, current);
2218 boundingBox.addPoint(x, y);
2219 if (ctx) {
2220 ctx.lineTo(x, y);
2221 }
2222 }
2223 static pathH(pathParser) {
2224 const { current, command } = pathParser;
2225 const point = new Point((command.relative ? current.x : 0) + command.x, current.y);
2226 pathParser.current = point;
2227 return {
2228 current,
2229 point
2230 };
2231 }
2232 pathH(ctx, boundingBox) {
2233 const { pathParser } = this;
2234 const { current, point } = PathElement.pathH(pathParser);
2235 const { x, y } = point;
2236 pathParser.addMarker(point, current);
2237 boundingBox.addPoint(x, y);
2238 if (ctx) {
2239 ctx.lineTo(x, y);
2240 }
2241 }
2242 static pathV(pathParser) {
2243 const { current, command } = pathParser;
2244 const point = new Point(current.x, (command.relative ? current.y : 0) + command.y);
2245 pathParser.current = point;
2246 return {
2247 current,
2248 point
2249 };
2250 }
2251 pathV(ctx, boundingBox) {
2252 const { pathParser } = this;
2253 const { current, point } = PathElement.pathV(pathParser);
2254 const { x, y } = point;
2255 pathParser.addMarker(point, current);
2256 boundingBox.addPoint(x, y);
2257 if (ctx) {
2258 ctx.lineTo(x, y);
2259 }
2260 }
2261 static pathC(pathParser) {
2262 const { current } = pathParser;
2263 const point = pathParser.getPoint('x1', 'y1');
2264 const controlPoint = pathParser.getAsControlPoint('x2', 'y2');
2265 const currentPoint = pathParser.getAsCurrentPoint();
2266 return {
2267 current,
2268 point,
2269 controlPoint,
2270 currentPoint
2271 };
2272 }
2273 pathC(ctx, boundingBox) {
2274 const { pathParser } = this;
2275 const { current, point, controlPoint, currentPoint } = PathElement.pathC(pathParser);
2276 pathParser.addMarker(currentPoint, controlPoint, point);
2277 boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2278 if (ctx) {
2279 ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2280 }
2281 }
2282 static pathS(pathParser) {
2283 const { current } = pathParser;
2284 const point = pathParser.getReflectedControlPoint();
2285 const controlPoint = pathParser.getAsControlPoint('x2', 'y2');
2286 const currentPoint = pathParser.getAsCurrentPoint();
2287 return {
2288 current,
2289 point,
2290 controlPoint,
2291 currentPoint
2292 };
2293 }
2294 pathS(ctx, boundingBox) {
2295 const { pathParser } = this;
2296 const { current, point, controlPoint, currentPoint } = PathElement.pathS(pathParser);
2297 pathParser.addMarker(currentPoint, controlPoint, point);
2298 boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2299 if (ctx) {
2300 ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2301 }
2302 }
2303 static pathQ(pathParser) {
2304 const { current } = pathParser;
2305 const controlPoint = pathParser.getAsControlPoint('x1', 'y1');
2306 const currentPoint = pathParser.getAsCurrentPoint();
2307 return {
2308 current,
2309 controlPoint,
2310 currentPoint
2311 };
2312 }
2313 pathQ(ctx, boundingBox) {
2314 const { pathParser } = this;
2315 const { current, controlPoint, currentPoint } = PathElement.pathQ(pathParser);
2316 pathParser.addMarker(currentPoint, controlPoint, controlPoint);
2317 boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2318 if (ctx) {
2319 ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2320 }
2321 }
2322 static pathT(pathParser) {
2323 const { current } = pathParser;
2324 const controlPoint = pathParser.getReflectedControlPoint();
2325 pathParser.control = controlPoint;
2326 const currentPoint = pathParser.getAsCurrentPoint();
2327 return {
2328 current,
2329 controlPoint,
2330 currentPoint
2331 };
2332 }
2333 pathT(ctx, boundingBox) {
2334 const { pathParser } = this;
2335 const { current, controlPoint, currentPoint } = PathElement.pathT(pathParser);
2336 pathParser.addMarker(currentPoint, controlPoint, controlPoint);
2337 boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2338 if (ctx) {
2339 ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2340 }
2341 }
2342 static pathA(pathParser) {
2343 const { current, command } = pathParser;
2344 let { rX, rY, xRot, lArcFlag, sweepFlag } = command;
2345 const xAxisRotation = xRot * (Math.PI / 180.0);
2346 const currentPoint = pathParser.getAsCurrentPoint();
2347 // Conversion from endpoint to center parameterization
2348 // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
2349 // x1', y1'
2350 const currp = new Point(Math.cos(xAxisRotation) * (current.x - currentPoint.x) / 2.0
2351 + Math.sin(xAxisRotation) * (current.y - currentPoint.y) / 2.0, -Math.sin(xAxisRotation) * (current.x - currentPoint.x) / 2.0
2352 + Math.cos(xAxisRotation) * (current.y - currentPoint.y) / 2.0);
2353 // adjust radii
2354 const l = Math.pow(currp.x, 2) / Math.pow(rX, 2)
2355 + Math.pow(currp.y, 2) / Math.pow(rY, 2);
2356 if (l > 1) {
2357 rX *= Math.sqrt(l);
2358 rY *= Math.sqrt(l);
2359 }
2360 // cx', cy'
2361 let s = (lArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(((Math.pow(rX, 2) * Math.pow(rY, 2))
2362 - (Math.pow(rX, 2) * Math.pow(currp.y, 2))
2363 - (Math.pow(rY, 2) * Math.pow(currp.x, 2))) / (Math.pow(rX, 2) * Math.pow(currp.y, 2)
2364 + Math.pow(rY, 2) * Math.pow(currp.x, 2)));
2365 if (isNaN(s)) {
2366 s = 0;
2367 }
2368 const cpp = new Point(s * rX * currp.y / rY, s * -rY * currp.x / rX);
2369 // cx, cy
2370 const centp = new Point((current.x + currentPoint.x) / 2.0
2371 + Math.cos(xAxisRotation) * cpp.x
2372 - Math.sin(xAxisRotation) * cpp.y, (current.y + currentPoint.y) / 2.0
2373 + Math.sin(xAxisRotation) * cpp.x
2374 + Math.cos(xAxisRotation) * cpp.y);
2375 // initial angle
2376 const a1 = vectorsAngle([1, 0], [(currp.x - cpp.x) / rX, (currp.y - cpp.y) / rY]); // θ1
2377 // angle delta
2378 const u = [(currp.x - cpp.x) / rX, (currp.y - cpp.y) / rY];
2379 const v = [(-currp.x - cpp.x) / rX, (-currp.y - cpp.y) / rY];
2380 let ad = vectorsAngle(u, v); // Δθ
2381 if (vectorsRatio(u, v) <= -1) {
2382 ad = Math.PI;
2383 }
2384 if (vectorsRatio(u, v) >= 1) {
2385 ad = 0;
2386 }
2387 return {
2388 currentPoint,
2389 rX,
2390 rY,
2391 sweepFlag,
2392 xAxisRotation,
2393 centp,
2394 a1,
2395 ad
2396 };
2397 }
2398 pathA(ctx, boundingBox) {
2399 const { pathParser } = this;
2400 const { currentPoint, rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser);
2401 // for markers
2402 const dir = 1 - sweepFlag ? 1.0 : -1.0;
2403 const ah = a1 + dir * (ad / 2.0);
2404 const halfWay = new Point(centp.x + rX * Math.cos(ah), centp.y + rY * Math.sin(ah));
2405 pathParser.addMarkerAngle(halfWay, ah - dir * Math.PI / 2);
2406 pathParser.addMarkerAngle(currentPoint, ah - dir * Math.PI);
2407 boundingBox.addPoint(currentPoint.x, currentPoint.y); // TODO: this is too naive, make it better
2408 if (ctx && !isNaN(a1) && !isNaN(ad)) {
2409 const r = rX > rY ? rX : rY;
2410 const sx = rX > rY ? 1 : rX / rY;
2411 const sy = rX > rY ? rY / rX : 1;
2412 ctx.translate(centp.x, centp.y);
2413 ctx.rotate(xAxisRotation);
2414 ctx.scale(sx, sy);
2415 ctx.arc(0, 0, r, a1, a1 + ad, Boolean(1 - sweepFlag));
2416 ctx.scale(1 / sx, 1 / sy);
2417 ctx.rotate(-xAxisRotation);
2418 ctx.translate(-centp.x, -centp.y);
2419 }
2420 }
2421 static pathZ(pathParser) {
2422 pathParser.current = pathParser.start;
2423 }
2424 pathZ(ctx, boundingBox) {
2425 PathElement.pathZ(this.pathParser);
2426 if (ctx) {
2427 // only close path if it is not a straight line
2428 if (boundingBox.x1 !== boundingBox.x2
2429 && boundingBox.y1 !== boundingBox.y2) {
2430 ctx.closePath();
2431 }
2432 }
2433 }
2434}
2435
2436class GlyphElement extends PathElement {
2437 constructor(document, node, captureTextNodes) {
2438 super(document, node, captureTextNodes);
2439 this.type = 'glyph';
2440 this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber();
2441 this.unicode = this.getAttribute('unicode').getString();
2442 this.arabicForm = this.getAttribute('arabic-form').getString();
2443 }
2444}
2445
2446class TextElement extends RenderedElement {
2447 constructor(document, node, captureTextNodes) {
2448 super(document, node, new.target === TextElement
2449 ? true
2450 : captureTextNodes);
2451 this.type = 'text';
2452 this.x = 0;
2453 this.y = 0;
2454 this.measureCache = -1;
2455 }
2456 setContext(ctx, fromMeasure = false) {
2457 super.setContext(ctx, fromMeasure);
2458 const textBaseline = this.getStyle('dominant-baseline').getTextBaseline()
2459 || this.getStyle('alignment-baseline').getTextBaseline();
2460 if (textBaseline) {
2461 ctx.textBaseline = textBaseline;
2462 }
2463 }
2464 initializeCoordinates(ctx) {
2465 this.x = this.getAttribute('x').getPixels('x');
2466 this.y = this.getAttribute('y').getPixels('y');
2467 const dxAttr = this.getAttribute('dx');
2468 const dyAttr = this.getAttribute('dy');
2469 if (dxAttr.hasValue()) {
2470 this.x += dxAttr.getPixels('x');
2471 }
2472 if (dyAttr.hasValue()) {
2473 this.y += dyAttr.getPixels('y');
2474 }
2475 this.x += this.getAnchorDelta(ctx, this, 0);
2476 }
2477 getBoundingBox(ctx) {
2478 if (this.type !== 'text') {
2479 return this.getTElementBoundingBox(ctx);
2480 }
2481 this.initializeCoordinates(ctx);
2482 let boundingBox = null;
2483 this.children.forEach((_, i) => {
2484 const childBoundingBox = this.getChildBoundingBox(ctx, this, this, i);
2485 if (!boundingBox) {
2486 boundingBox = childBoundingBox;
2487 }
2488 else {
2489 boundingBox.addBoundingBox(childBoundingBox);
2490 }
2491 });
2492 return boundingBox;
2493 }
2494 getFontSize() {
2495 const { document, parent } = this;
2496 const inheritFontSize = Font.parse(document.ctx.font).fontSize;
2497 const fontSize = parent.getStyle('font-size').getNumber(inheritFontSize);
2498 return fontSize;
2499 }
2500 getTElementBoundingBox(ctx) {
2501 const fontSize = this.getFontSize();
2502 return new BoundingBox(this.x, this.y - fontSize, this.x + this.measureText(ctx), this.y);
2503 }
2504 getGlyph(font, text, i) {
2505 const char = text[i];
2506 let glyph = null;
2507 if (font.isArabic) {
2508 const len = text.length;
2509 const prevChar = text[i - 1];
2510 const nextChar = text[i + 1];
2511 let arabicForm = 'isolated';
2512 if ((i === 0 || prevChar === ' ') && i < len - 2 && nextChar !== ' ') {
2513 arabicForm = 'terminal';
2514 }
2515 if (i > 0 && prevChar !== ' ' && i < len - 2 && nextChar !== ' ') {
2516 arabicForm = 'medial';
2517 }
2518 if (i > 0 && prevChar !== ' ' && (i === len - 1 || nextChar === ' ')) {
2519 arabicForm = 'initial';
2520 }
2521 if (typeof font.glyphs[char] !== 'undefined') {
2522 // NEED TEST
2523 const maybeGlyph = font.glyphs[char];
2524 glyph = maybeGlyph instanceof GlyphElement
2525 ? maybeGlyph
2526 : maybeGlyph[arabicForm];
2527 }
2528 }
2529 else {
2530 glyph = font.glyphs[char];
2531 }
2532 if (!glyph) {
2533 glyph = font.missingGlyph;
2534 }
2535 return glyph;
2536 }
2537 getText() {
2538 return '';
2539 }
2540 getTextFromNode(node) {
2541 const textNode = node || this.node;
2542 const childNodes = Array.from(textNode.parentNode.childNodes);
2543 const index = childNodes.indexOf(textNode);
2544 const lastIndex = childNodes.length - 1;
2545 let text = compressSpaces(
2546 // textNode.value
2547 // || textNode.text
2548 textNode.textContent
2549 || '');
2550 if (index === 0) {
2551 text = trimLeft(text);
2552 }
2553 if (index === lastIndex) {
2554 text = trimRight(text);
2555 }
2556 return text;
2557 }
2558 renderChildren(ctx) {
2559 if (this.type !== 'text') {
2560 this.renderTElementChildren(ctx);
2561 return;
2562 }
2563 this.initializeCoordinates(ctx);
2564 this.children.forEach((_, i) => {
2565 this.renderChild(ctx, this, this, i);
2566 });
2567 const { mouse } = this.document.screen;
2568 // Do not calc bounding box if mouse is not working.
2569 if (mouse.isWorking()) {
2570 mouse.checkBoundingBox(this, this.getBoundingBox(ctx));
2571 }
2572 }
2573 renderTElementChildren(ctx) {
2574 const { document, parent } = this;
2575 const renderText = this.getText();
2576 const customFont = parent.getStyle('font-family').getDefinition();
2577 if (customFont) {
2578 const { unitsPerEm } = customFont.fontFace;
2579 const ctxFont = Font.parse(document.ctx.font);
2580 const fontSize = parent.getStyle('font-size').getNumber(ctxFont.fontSize);
2581 const fontStyle = parent.getStyle('font-style').getString(ctxFont.fontStyle);
2582 const scale = fontSize / unitsPerEm;
2583 const text = customFont.isRTL
2584 ? renderText.split('').reverse().join('')
2585 : renderText;
2586 const dx = toNumbers(parent.getAttribute('dx').getString());
2587 const len = text.length;
2588 for (let i = 0; i < len; i++) {
2589 const glyph = this.getGlyph(customFont, text, i);
2590 ctx.translate(this.x, this.y);
2591 ctx.scale(scale, -scale);
2592 const lw = ctx.lineWidth;
2593 ctx.lineWidth = ctx.lineWidth * unitsPerEm / fontSize;
2594 if (fontStyle === 'italic') {
2595 ctx.transform(1, 0, .4, 1, 0, 0);
2596 }
2597 glyph.render(ctx);
2598 if (fontStyle === 'italic') {
2599 ctx.transform(1, 0, -.4, 1, 0, 0);
2600 }
2601 ctx.lineWidth = lw;
2602 ctx.scale(1 / scale, -1 / scale);
2603 ctx.translate(-this.x, -this.y);
2604 this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / unitsPerEm;
2605 if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {
2606 this.x += dx[i];
2607 }
2608 }
2609 return;
2610 }
2611 const { x, y } = this;
2612 // NEED TEST
2613 // if (ctx.paintOrder === 'stroke') {
2614 // if (ctx.strokeStyle) {
2615 // ctx.strokeText(renderText, x, y);
2616 // }
2617 // if (ctx.fillStyle) {
2618 // ctx.fillText(renderText, x, y);
2619 // }
2620 // } else {
2621 if (ctx.fillStyle) {
2622 ctx.fillText(renderText, x, y);
2623 }
2624 if (ctx.strokeStyle) {
2625 ctx.strokeText(renderText, x, y);
2626 }
2627 // }
2628 }
2629 getAnchorDelta(ctx, parent, startI) {
2630 const textAnchor = this.getStyle('text-anchor').getString('start');
2631 if (textAnchor !== 'start') {
2632 const { children } = parent;
2633 const len = children.length;
2634 let child = null;
2635 let width = 0;
2636 for (let i = startI; i < len; i++) {
2637 child = children[i];
2638 if (i > startI && child.getAttribute('x').hasValue()
2639 || child.getAttribute('text-anchor').hasValue()) {
2640 break; // new group
2641 }
2642 width += child.measureTextRecursive(ctx);
2643 }
2644 return -1 * (textAnchor === 'end' ? width : width / 2.0);
2645 }
2646 return 0;
2647 }
2648 adjustChildCoordinates(ctx, textParent, parent, i) {
2649 const child = parent.children[i];
2650 if (typeof child.measureText !== 'function') {
2651 return child;
2652 }
2653 ctx.save();
2654 child.setContext(ctx, true);
2655 const xAttr = child.getAttribute('x');
2656 const yAttr = child.getAttribute('y');
2657 const dxAttr = child.getAttribute('dx');
2658 const dyAttr = child.getAttribute('dy');
2659 const textAnchor = child.getAttribute('text-anchor').getString('start');
2660 if (i === 0 && child.type !== 'textNode') {
2661 if (!xAttr.hasValue()) {
2662 xAttr.setValue(textParent.getAttribute('x').getValue('0'));
2663 }
2664 if (!yAttr.hasValue()) {
2665 yAttr.setValue(textParent.getAttribute('y').getValue('0'));
2666 }
2667 if (!dxAttr.hasValue()) {
2668 dxAttr.setValue(textParent.getAttribute('dx').getValue('0'));
2669 }
2670 if (!dyAttr.hasValue()) {
2671 dyAttr.setValue(textParent.getAttribute('dy').getValue('0'));
2672 }
2673 }
2674 if (xAttr.hasValue()) {
2675 child.x = xAttr.getPixels('x') + textParent.getAnchorDelta(ctx, parent, i);
2676 if (textAnchor !== 'start') {
2677 const width = child.measureTextRecursive(ctx);
2678 child.x += -1 * (textAnchor === 'end' ? width : width / 2.0);
2679 }
2680 if (dxAttr.hasValue()) {
2681 child.x += dxAttr.getPixels('x');
2682 }
2683 }
2684 else {
2685 if (textAnchor !== 'start') {
2686 const width = child.measureTextRecursive(ctx);
2687 textParent.x += -1 * (textAnchor === 'end' ? width : width / 2.0);
2688 }
2689 if (dxAttr.hasValue()) {
2690 textParent.x += dxAttr.getPixels('x');
2691 }
2692 child.x = textParent.x;
2693 }
2694 textParent.x = child.x + child.measureText(ctx);
2695 if (yAttr.hasValue()) {
2696 child.y = yAttr.getPixels('y');
2697 if (dyAttr.hasValue()) {
2698 child.y += dyAttr.getPixels('y');
2699 }
2700 }
2701 else {
2702 if (dyAttr.hasValue()) {
2703 textParent.y += dyAttr.getPixels('y');
2704 }
2705 child.y = textParent.y;
2706 }
2707 textParent.y = child.y;
2708 child.clearContext(ctx);
2709 ctx.restore();
2710 return child;
2711 }
2712 getChildBoundingBox(ctx, textParent, parent, i) {
2713 const child = this.adjustChildCoordinates(ctx, textParent, parent, i);
2714 // not a text node?
2715 if (typeof child.getBoundingBox !== 'function') {
2716 return null;
2717 }
2718 const boundingBox = child.getBoundingBox(ctx);
2719 if (!boundingBox) {
2720 return null;
2721 }
2722 child.children.forEach((_, i) => {
2723 const childBoundingBox = textParent.getChildBoundingBox(ctx, textParent, child, i);
2724 boundingBox.addBoundingBox(childBoundingBox);
2725 });
2726 return boundingBox;
2727 }
2728 renderChild(ctx, textParent, parent, i) {
2729 const child = this.adjustChildCoordinates(ctx, textParent, parent, i);
2730 child.render(ctx);
2731 child.children.forEach((_, i) => {
2732 textParent.renderChild(ctx, textParent, child, i);
2733 });
2734 }
2735 measureTextRecursive(ctx) {
2736 const width = this.children.reduce((width, child) => width + child.measureTextRecursive(ctx), this.measureText(ctx));
2737 return width;
2738 }
2739 measureText(ctx) {
2740 const { measureCache } = this;
2741 if (~measureCache) {
2742 return measureCache;
2743 }
2744 const renderText = this.getText();
2745 const measure = this.measureTargetText(ctx, renderText);
2746 this.measureCache = measure;
2747 return measure;
2748 }
2749 measureTargetText(ctx, targetText) {
2750 if (!targetText.length) {
2751 return 0;
2752 }
2753 const { parent } = this;
2754 const customFont = parent.getStyle('font-family').getDefinition();
2755 if (customFont) {
2756 const fontSize = this.getFontSize();
2757 const text = customFont.isRTL
2758 ? targetText.split('').reverse().join('')
2759 : targetText;
2760 const dx = toNumbers(parent.getAttribute('dx').getString());
2761 const len = text.length;
2762 let measure = 0;
2763 for (let i = 0; i < len; i++) {
2764 const glyph = this.getGlyph(customFont, text, i);
2765 measure += (glyph.horizAdvX || customFont.horizAdvX)
2766 * fontSize
2767 / customFont.fontFace.unitsPerEm;
2768 if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {
2769 measure += dx[i];
2770 }
2771 }
2772 return measure;
2773 }
2774 if (!ctx.measureText) {
2775 return targetText.length * 10;
2776 }
2777 ctx.save();
2778 this.setContext(ctx, true);
2779 const { width: measure } = ctx.measureText(targetText);
2780 this.clearContext(ctx);
2781 ctx.restore();
2782 return measure;
2783 }
2784}
2785
2786class TSpanElement extends TextElement {
2787 constructor(document, node, captureTextNodes) {
2788 super(document, node, new.target === TSpanElement
2789 ? true
2790 : captureTextNodes);
2791 this.type = 'tspan';
2792 // if this node has children, then they own the text
2793 this.text = this.children.length > 0
2794 ? ''
2795 : this.getTextFromNode();
2796 }
2797 getText() {
2798 return this.text;
2799 }
2800}
2801
2802class TextNode extends TSpanElement {
2803 constructor() {
2804 super(...arguments);
2805 this.type = 'textNode';
2806 }
2807}
2808
2809class SVGElement extends RenderedElement {
2810 constructor() {
2811 super(...arguments);
2812 this.type = 'svg';
2813 this.root = false;
2814 }
2815 setContext(ctx) {
2816 const { document } = this;
2817 const { screen, window } = document;
2818 const canvas = ctx.canvas;
2819 screen.setDefaults(ctx);
2820 if (canvas.style
2821 && typeof ctx.font !== 'undefined'
2822 && window
2823 && typeof window.getComputedStyle !== 'undefined') {
2824 ctx.font = window.getComputedStyle(canvas).getPropertyValue('font');
2825 const fontSizeProp = new Property(document, 'fontSize', Font.parse(ctx.font).fontSize);
2826 if (fontSizeProp.hasValue()) {
2827 document.rootEmSize = fontSizeProp.getPixels('y');
2828 document.emSize = document.rootEmSize;
2829 }
2830 }
2831 // create new view port
2832 if (!this.getAttribute('x').hasValue()) {
2833 this.getAttribute('x', true).setValue(0);
2834 }
2835 if (!this.getAttribute('y').hasValue()) {
2836 this.getAttribute('y', true).setValue(0);
2837 }
2838 let { width, height } = screen.viewPort;
2839 if (!this.getStyle('width').hasValue()) {
2840 this.getStyle('width', true).setValue('100%');
2841 }
2842 if (!this.getStyle('height').hasValue()) {
2843 this.getStyle('height', true).setValue('100%');
2844 }
2845 if (!this.getStyle('color').hasValue()) {
2846 this.getStyle('color', true).setValue('black');
2847 }
2848 const refXAttr = this.getAttribute('refX');
2849 const refYAttr = this.getAttribute('refY');
2850 const viewBoxAttr = this.getAttribute('viewBox');
2851 const viewBox = viewBoxAttr.hasValue()
2852 ? toNumbers(viewBoxAttr.getString())
2853 : null;
2854 const clip = !this.root
2855 && this.getStyle('overflow').getValue('hidden') !== 'visible';
2856 let minX = 0;
2857 let minY = 0;
2858 let clipX = 0;
2859 let clipY = 0;
2860 if (viewBox) {
2861 minX = viewBox[0];
2862 minY = viewBox[1];
2863 }
2864 if (!this.root) {
2865 width = this.getStyle('width').getPixels('x');
2866 height = this.getStyle('height').getPixels('y');
2867 if (this.type === 'marker') {
2868 clipX = minX;
2869 clipY = minY;
2870 minX = 0;
2871 minY = 0;
2872 }
2873 }
2874 screen.viewPort.setCurrent(width, height);
2875 // Default value of transform-origin is center only for root SVG elements
2876 // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform-origin
2877 if (this.node // is not temporary SVGElement
2878 && (!this.parent || this.node.parentNode?.nodeName === 'foreignObject')
2879 && this.getStyle('transform', false, true).hasValue()
2880 && !this.getStyle('transform-origin', false, true).hasValue()) {
2881 this.getStyle('transform-origin', true, true).setValue('50% 50%');
2882 }
2883 super.setContext(ctx);
2884 ctx.translate(this.getAttribute('x').getPixels('x'), this.getAttribute('y').getPixels('y'));
2885 if (viewBox) {
2886 width = viewBox[2];
2887 height = viewBox[3];
2888 }
2889 document.setViewBox({
2890 ctx,
2891 aspectRatio: this.getAttribute('preserveAspectRatio').getString(),
2892 width: screen.viewPort.width,
2893 desiredWidth: width,
2894 height: screen.viewPort.height,
2895 desiredHeight: height,
2896 minX,
2897 minY,
2898 refX: refXAttr.getValue(),
2899 refY: refYAttr.getValue(),
2900 clip,
2901 clipX,
2902 clipY
2903 });
2904 if (viewBox) {
2905 screen.viewPort.removeCurrent();
2906 screen.viewPort.setCurrent(width, height);
2907 }
2908 }
2909 clearContext(ctx) {
2910 super.clearContext(ctx);
2911 this.document.screen.viewPort.removeCurrent();
2912 }
2913 /**
2914 * Resize SVG to fit in given size.
2915 * @param width
2916 * @param height
2917 * @param preserveAspectRatio
2918 */
2919 resize(width, height = width, preserveAspectRatio = false) {
2920 const widthAttr = this.getAttribute('width', true);
2921 const heightAttr = this.getAttribute('height', true);
2922 const viewBoxAttr = this.getAttribute('viewBox');
2923 const styleAttr = this.getAttribute('style');
2924 const originWidth = widthAttr.getNumber(0);
2925 const originHeight = heightAttr.getNumber(0);
2926 if (preserveAspectRatio) {
2927 if (typeof preserveAspectRatio === 'string') {
2928 this.getAttribute('preserveAspectRatio', true).setValue(preserveAspectRatio);
2929 }
2930 else {
2931 const preserveAspectRatioAttr = this.getAttribute('preserveAspectRatio');
2932 if (preserveAspectRatioAttr.hasValue()) {
2933 preserveAspectRatioAttr.setValue(preserveAspectRatioAttr.getString().replace(/^\s*(\S.*\S)\s*$/, '$1'));
2934 }
2935 }
2936 }
2937 widthAttr.setValue(width);
2938 heightAttr.setValue(height);
2939 if (!viewBoxAttr.hasValue()) {
2940 viewBoxAttr.setValue(`0 0 ${originWidth || width} ${originHeight || height}`);
2941 }
2942 if (styleAttr.hasValue()) {
2943 const widthStyle = this.getStyle('width');
2944 const heightStyle = this.getStyle('height');
2945 if (widthStyle.hasValue()) {
2946 widthStyle.setValue(`${width}px`);
2947 }
2948 if (heightStyle.hasValue()) {
2949 heightStyle.setValue(`${height}px`);
2950 }
2951 }
2952 }
2953}
2954
2955class RectElement extends PathElement {
2956 constructor() {
2957 super(...arguments);
2958 this.type = 'rect';
2959 }
2960 path(ctx) {
2961 const x = this.getAttribute('x').getPixels('x');
2962 const y = this.getAttribute('y').getPixels('y');
2963 const width = this.getStyle('width', false, true).getPixels('x');
2964 const height = this.getStyle('height', false, true).getPixels('y');
2965 const rxAttr = this.getAttribute('rx');
2966 const ryAttr = this.getAttribute('ry');
2967 let rx = rxAttr.getPixels('x');
2968 let ry = ryAttr.getPixels('y');
2969 if (rxAttr.hasValue() && !ryAttr.hasValue()) {
2970 ry = rx;
2971 }
2972 if (ryAttr.hasValue() && !rxAttr.hasValue()) {
2973 rx = ry;
2974 }
2975 rx = Math.min(rx, width / 2.0);
2976 ry = Math.min(ry, height / 2.0);
2977 if (ctx) {
2978 const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
2979 ctx.beginPath(); // always start the path so we don't fill prior paths
2980 if (height > 0 && width > 0) {
2981 ctx.moveTo(x + rx, y);
2982 ctx.lineTo(x + width - rx, y);
2983 ctx.bezierCurveTo(x + width - rx + (KAPPA * rx), y, x + width, y + ry - (KAPPA * ry), x + width, y + ry);
2984 ctx.lineTo(x + width, y + height - ry);
2985 ctx.bezierCurveTo(x + width, y + height - ry + (KAPPA * ry), x + width - rx + (KAPPA * rx), y + height, x + width - rx, y + height);
2986 ctx.lineTo(x + rx, y + height);
2987 ctx.bezierCurveTo(x + rx - (KAPPA * rx), y + height, x, y + height - ry + (KAPPA * ry), x, y + height - ry);
2988 ctx.lineTo(x, y + ry);
2989 ctx.bezierCurveTo(x, y + ry - (KAPPA * ry), x + rx - (KAPPA * rx), y, x + rx, y);
2990 ctx.closePath();
2991 }
2992 }
2993 return new BoundingBox(x, y, x + width, y + height);
2994 }
2995 getMarkers() {
2996 return null;
2997 }
2998}
2999
3000class CircleElement extends PathElement {
3001 constructor() {
3002 super(...arguments);
3003 this.type = 'circle';
3004 }
3005 path(ctx) {
3006 const cx = this.getAttribute('cx').getPixels('x');
3007 const cy = this.getAttribute('cy').getPixels('y');
3008 const r = this.getAttribute('r').getPixels();
3009 if (ctx && r > 0) {
3010 ctx.beginPath();
3011 ctx.arc(cx, cy, r, 0, Math.PI * 2, false);
3012 ctx.closePath();
3013 }
3014 return new BoundingBox(cx - r, cy - r, cx + r, cy + r);
3015 }
3016 getMarkers() {
3017 return null;
3018 }
3019}
3020
3021class EllipseElement extends PathElement {
3022 constructor() {
3023 super(...arguments);
3024 this.type = 'ellipse';
3025 }
3026 path(ctx) {
3027 const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
3028 const rx = this.getAttribute('rx').getPixels('x');
3029 const ry = this.getAttribute('ry').getPixels('y');
3030 const cx = this.getAttribute('cx').getPixels('x');
3031 const cy = this.getAttribute('cy').getPixels('y');
3032 if (ctx && rx > 0 && ry > 0) {
3033 ctx.beginPath();
3034 ctx.moveTo(cx + rx, cy);
3035 ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry);
3036 ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy);
3037 ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry);
3038 ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy);
3039 ctx.closePath();
3040 }
3041 return new BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry);
3042 }
3043 getMarkers() {
3044 return null;
3045 }
3046}
3047
3048class LineElement extends PathElement {
3049 constructor() {
3050 super(...arguments);
3051 this.type = 'line';
3052 }
3053 getPoints() {
3054 return [
3055 new Point(this.getAttribute('x1').getPixels('x'), this.getAttribute('y1').getPixels('y')),
3056 new Point(this.getAttribute('x2').getPixels('x'), this.getAttribute('y2').getPixels('y'))
3057 ];
3058 }
3059 path(ctx) {
3060 const [{ x: x0, y: y0 }, { x: x1, y: y1 }] = this.getPoints();
3061 if (ctx) {
3062 ctx.beginPath();
3063 ctx.moveTo(x0, y0);
3064 ctx.lineTo(x1, y1);
3065 }
3066 return new BoundingBox(x0, y0, x1, y1);
3067 }
3068 getMarkers() {
3069 const [p0, p1] = this.getPoints();
3070 const a = p0.angleTo(p1);
3071 return [
3072 [p0, a],
3073 [p1, a]
3074 ];
3075 }
3076}
3077
3078class PolylineElement extends PathElement {
3079 constructor(document, node, captureTextNodes) {
3080 super(document, node, captureTextNodes);
3081 this.type = 'polyline';
3082 this.points = [];
3083 this.points = Point.parsePath(this.getAttribute('points').getString());
3084 }
3085 path(ctx) {
3086 const { points } = this;
3087 const [{ x: x0, y: y0 }] = points;
3088 const boundingBox = new BoundingBox(x0, y0);
3089 if (ctx) {
3090 ctx.beginPath();
3091 ctx.moveTo(x0, y0);
3092 }
3093 points.forEach(({ x, y }) => {
3094 boundingBox.addPoint(x, y);
3095 if (ctx) {
3096 ctx.lineTo(x, y);
3097 }
3098 });
3099 return boundingBox;
3100 }
3101 getMarkers() {
3102 const { points } = this;
3103 const lastIndex = points.length - 1;
3104 const markers = [];
3105 points.forEach((point, i) => {
3106 if (i === lastIndex) {
3107 return;
3108 }
3109 markers.push([
3110 point,
3111 point.angleTo(points[i + 1])
3112 ]);
3113 });
3114 if (markers.length > 0) {
3115 markers.push([
3116 points[points.length - 1],
3117 markers[markers.length - 1][1]
3118 ]);
3119 }
3120 return markers;
3121 }
3122}
3123
3124class PolygonElement extends PolylineElement {
3125 constructor() {
3126 super(...arguments);
3127 this.type = 'polygon';
3128 }
3129 path(ctx) {
3130 const boundingBox = super.path(ctx);
3131 const [{ x, y }] = this.points;
3132 if (ctx) {
3133 ctx.lineTo(x, y);
3134 ctx.closePath();
3135 }
3136 return boundingBox;
3137 }
3138}
3139
3140class PatternElement extends Element {
3141 constructor() {
3142 super(...arguments);
3143 this.type = 'pattern';
3144 }
3145 createPattern(ctx, _, parentOpacityProp) {
3146 const width = this.getStyle('width').getPixels('x', true);
3147 const height = this.getStyle('height').getPixels('y', true);
3148 // render me using a temporary svg element
3149 const patternSvg = new SVGElement(this.document, null);
3150 patternSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue());
3151 patternSvg.attributes.width = new Property(this.document, 'width', `${width}px`);
3152 patternSvg.attributes.height = new Property(this.document, 'height', `${height}px`);
3153 patternSvg.attributes.transform = new Property(this.document, 'transform', this.getAttribute('patternTransform').getValue());
3154 patternSvg.children = this.children;
3155 const patternCanvas = this.document.createCanvas(width, height);
3156 const patternCtx = patternCanvas.getContext('2d');
3157 const xAttr = this.getAttribute('x');
3158 const yAttr = this.getAttribute('y');
3159 if (xAttr.hasValue() && yAttr.hasValue()) {
3160 patternCtx.translate(xAttr.getPixels('x', true), yAttr.getPixels('y', true));
3161 }
3162 if (parentOpacityProp.hasValue()) {
3163 this.styles['fill-opacity'] = parentOpacityProp;
3164 }
3165 else {
3166 Reflect.deleteProperty(this.styles, 'fill-opacity');
3167 }
3168 // render 3x3 grid so when we transform there's no white space on edges
3169 for (let x = -1; x <= 1; x++) {
3170 for (let y = -1; y <= 1; y++) {
3171 patternCtx.save();
3172 patternSvg.attributes.x = new Property(this.document, 'x', x * patternCanvas.width);
3173 patternSvg.attributes.y = new Property(this.document, 'y', y * patternCanvas.height);
3174 patternSvg.render(patternCtx);
3175 patternCtx.restore();
3176 }
3177 }
3178 const pattern = ctx.createPattern(patternCanvas, 'repeat');
3179 return pattern;
3180 }
3181}
3182
3183class MarkerElement extends Element {
3184 constructor() {
3185 super(...arguments);
3186 this.type = 'marker';
3187 }
3188 render(ctx, point, angle) {
3189 if (!point) {
3190 return;
3191 }
3192 const { x, y } = point;
3193 const orient = this.getAttribute('orient').getString('auto');
3194 const markerUnits = this.getAttribute('markerUnits').getString('strokeWidth');
3195 ctx.translate(x, y);
3196 if (orient === 'auto') {
3197 ctx.rotate(angle);
3198 }
3199 if (markerUnits === 'strokeWidth') {
3200 ctx.scale(ctx.lineWidth, ctx.lineWidth);
3201 }
3202 ctx.save();
3203 // render me using a temporary svg element
3204 const markerSvg = new SVGElement(this.document, null);
3205 markerSvg.type = this.type;
3206 markerSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue());
3207 markerSvg.attributes.refX = new Property(this.document, 'refX', this.getAttribute('refX').getValue());
3208 markerSvg.attributes.refY = new Property(this.document, 'refY', this.getAttribute('refY').getValue());
3209 markerSvg.attributes.width = new Property(this.document, 'width', this.getAttribute('markerWidth').getValue());
3210 markerSvg.attributes.height = new Property(this.document, 'height', this.getAttribute('markerHeight').getValue());
3211 markerSvg.attributes.overflow = new Property(this.document, 'overflow', this.getAttribute('overflow').getValue());
3212 markerSvg.attributes.fill = new Property(this.document, 'fill', this.getAttribute('fill').getColor('black'));
3213 markerSvg.attributes.stroke = new Property(this.document, 'stroke', this.getAttribute('stroke').getValue('none'));
3214 markerSvg.children = this.children;
3215 markerSvg.render(ctx);
3216 ctx.restore();
3217 if (markerUnits === 'strokeWidth') {
3218 ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth);
3219 }
3220 if (orient === 'auto') {
3221 ctx.rotate(-angle);
3222 }
3223 ctx.translate(-x, -y);
3224 }
3225}
3226
3227class DefsElement extends Element {
3228 constructor() {
3229 super(...arguments);
3230 this.type = 'defs';
3231 }
3232 render() {
3233 // NOOP
3234 }
3235}
3236
3237class GElement extends RenderedElement {
3238 constructor() {
3239 super(...arguments);
3240 this.type = 'g';
3241 }
3242 getBoundingBox(ctx) {
3243 const boundingBox = new BoundingBox();
3244 this.children.forEach((child) => {
3245 boundingBox.addBoundingBox(child.getBoundingBox(ctx));
3246 });
3247 return boundingBox;
3248 }
3249}
3250
3251class GradientElement extends Element {
3252 constructor(document, node, captureTextNodes) {
3253 super(document, node, captureTextNodes);
3254 this.attributesToInherit = [
3255 'gradientUnits'
3256 ];
3257 this.stops = [];
3258 const { stops, children } = this;
3259 children.forEach((child) => {
3260 if (child.type === 'stop') {
3261 stops.push(child);
3262 }
3263 });
3264 }
3265 getGradientUnits() {
3266 return this.getAttribute('gradientUnits').getString('objectBoundingBox');
3267 }
3268 createGradient(ctx, element, parentOpacityProp) {
3269 // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
3270 let stopsContainer = this;
3271 if (this.getHrefAttribute().hasValue()) {
3272 stopsContainer = this.getHrefAttribute().getDefinition();
3273 this.inheritStopContainer(stopsContainer);
3274 }
3275 const { stops } = stopsContainer;
3276 const gradient = this.getGradient(ctx, element);
3277 if (!gradient) {
3278 return this.addParentOpacity(parentOpacityProp, stops[stops.length - 1].color);
3279 }
3280 stops.forEach((stop) => {
3281 gradient.addColorStop(stop.offset, this.addParentOpacity(parentOpacityProp, stop.color));
3282 });
3283 if (this.getAttribute('gradientTransform').hasValue()) {
3284 // render as transformed pattern on temporary canvas
3285 const { document } = this;
3286 const { MAX_VIRTUAL_PIXELS, viewPort } = document.screen;
3287 const [rootView] = viewPort.viewPorts;
3288 const rect = new RectElement(document, null);
3289 rect.attributes.x = new Property(document, 'x', -MAX_VIRTUAL_PIXELS / 3.0);
3290 rect.attributes.y = new Property(document, 'y', -MAX_VIRTUAL_PIXELS / 3.0);
3291 rect.attributes.width = new Property(document, 'width', MAX_VIRTUAL_PIXELS);
3292 rect.attributes.height = new Property(document, 'height', MAX_VIRTUAL_PIXELS);
3293 const group = new GElement(document, null);
3294 group.attributes.transform = new Property(document, 'transform', this.getAttribute('gradientTransform').getValue());
3295 group.children = [rect];
3296 const patternSvg = new SVGElement(document, null);
3297 patternSvg.attributes.x = new Property(document, 'x', 0);
3298 patternSvg.attributes.y = new Property(document, 'y', 0);
3299 patternSvg.attributes.width = new Property(document, 'width', rootView.width);
3300 patternSvg.attributes.height = new Property(document, 'height', rootView.height);
3301 patternSvg.children = [group];
3302 const patternCanvas = document.createCanvas(rootView.width, rootView.height);
3303 const patternCtx = patternCanvas.getContext('2d');
3304 patternCtx.fillStyle = gradient;
3305 patternSvg.render(patternCtx);
3306 return patternCtx.createPattern(patternCanvas, 'no-repeat');
3307 }
3308 return gradient;
3309 }
3310 inheritStopContainer(stopsContainer) {
3311 this.attributesToInherit.forEach((attributeToInherit) => {
3312 if (!this.getAttribute(attributeToInherit).hasValue()
3313 && stopsContainer.getAttribute(attributeToInherit).hasValue()) {
3314 this.getAttribute(attributeToInherit, true)
3315 .setValue(stopsContainer.getAttribute(attributeToInherit).getValue());
3316 }
3317 });
3318 }
3319 addParentOpacity(parentOpacityProp, color) {
3320 if (parentOpacityProp.hasValue()) {
3321 const colorProp = new Property(this.document, 'color', color);
3322 return colorProp.addOpacity(parentOpacityProp).getColor();
3323 }
3324 return color;
3325 }
3326}
3327
3328class LinearGradientElement extends GradientElement {
3329 constructor(document, node, captureTextNodes) {
3330 super(document, node, captureTextNodes);
3331 this.type = 'linearGradient';
3332 this.attributesToInherit.push('x1', 'y1', 'x2', 'y2');
3333 }
3334 getGradient(ctx, element) {
3335 const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox';
3336 const boundingBox = isBoundingBoxUnits
3337 ? element.getBoundingBox(ctx)
3338 : null;
3339 if (isBoundingBoxUnits && !boundingBox) {
3340 return null;
3341 }
3342 if (!this.getAttribute('x1').hasValue()
3343 && !this.getAttribute('y1').hasValue()
3344 && !this.getAttribute('x2').hasValue()
3345 && !this.getAttribute('y2').hasValue()) {
3346 this.getAttribute('x1', true).setValue(0);
3347 this.getAttribute('y1', true).setValue(0);
3348 this.getAttribute('x2', true).setValue(1);
3349 this.getAttribute('y2', true).setValue(0);
3350 }
3351 const x1 = isBoundingBoxUnits
3352 ? boundingBox.x + boundingBox.width * this.getAttribute('x1').getNumber()
3353 : this.getAttribute('x1').getPixels('x');
3354 const y1 = isBoundingBoxUnits
3355 ? boundingBox.y + boundingBox.height * this.getAttribute('y1').getNumber()
3356 : this.getAttribute('y1').getPixels('y');
3357 const x2 = isBoundingBoxUnits
3358 ? boundingBox.x + boundingBox.width * this.getAttribute('x2').getNumber()
3359 : this.getAttribute('x2').getPixels('x');
3360 const y2 = isBoundingBoxUnits
3361 ? boundingBox.y + boundingBox.height * this.getAttribute('y2').getNumber()
3362 : this.getAttribute('y2').getPixels('y');
3363 if (x1 === x2 && y1 === y2) {
3364 return null;
3365 }
3366 return ctx.createLinearGradient(x1, y1, x2, y2);
3367 }
3368}
3369
3370class RadialGradientElement extends GradientElement {
3371 constructor(document, node, captureTextNodes) {
3372 super(document, node, captureTextNodes);
3373 this.type = 'radialGradient';
3374 this.attributesToInherit.push('cx', 'cy', 'r', 'fx', 'fy', 'fr');
3375 }
3376 getGradient(ctx, element) {
3377 const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox';
3378 const boundingBox = element.getBoundingBox(ctx);
3379 if (isBoundingBoxUnits && !boundingBox) {
3380 return null;
3381 }
3382 if (!this.getAttribute('cx').hasValue()) {
3383 this.getAttribute('cx', true).setValue('50%');
3384 }
3385 if (!this.getAttribute('cy').hasValue()) {
3386 this.getAttribute('cy', true).setValue('50%');
3387 }
3388 if (!this.getAttribute('r').hasValue()) {
3389 this.getAttribute('r', true).setValue('50%');
3390 }
3391 const cx = isBoundingBoxUnits
3392 ? boundingBox.x + boundingBox.width * this.getAttribute('cx').getNumber()
3393 : this.getAttribute('cx').getPixels('x');
3394 const cy = isBoundingBoxUnits
3395 ? boundingBox.y + boundingBox.height * this.getAttribute('cy').getNumber()
3396 : this.getAttribute('cy').getPixels('y');
3397 let fx = cx;
3398 let fy = cy;
3399 if (this.getAttribute('fx').hasValue()) {
3400 fx = isBoundingBoxUnits
3401 ? boundingBox.x + boundingBox.width * this.getAttribute('fx').getNumber()
3402 : this.getAttribute('fx').getPixels('x');
3403 }
3404 if (this.getAttribute('fy').hasValue()) {
3405 fy = isBoundingBoxUnits
3406 ? boundingBox.y + boundingBox.height * this.getAttribute('fy').getNumber()
3407 : this.getAttribute('fy').getPixels('y');
3408 }
3409 const r = isBoundingBoxUnits
3410 ? (boundingBox.width + boundingBox.height) / 2.0 * this.getAttribute('r').getNumber()
3411 : this.getAttribute('r').getPixels();
3412 const fr = this.getAttribute('fr').getPixels();
3413 return ctx.createRadialGradient(fx, fy, fr, cx, cy, r);
3414 }
3415}
3416
3417class StopElement extends Element {
3418 constructor(document, node, captureTextNodes) {
3419 super(document, node, captureTextNodes);
3420 this.type = 'stop';
3421 const offset = Math.max(0, Math.min(1, this.getAttribute('offset').getNumber()));
3422 const stopOpacity = this.getStyle('stop-opacity');
3423 let stopColor = this.getStyle('stop-color', true);
3424 if (stopColor.getString() === '') {
3425 stopColor.setValue('#000');
3426 }
3427 if (stopOpacity.hasValue()) {
3428 stopColor = stopColor.addOpacity(stopOpacity);
3429 }
3430 this.offset = offset;
3431 this.color = stopColor.getColor();
3432 }
3433}
3434
3435class AnimateElement extends Element {
3436 constructor(document, node, captureTextNodes) {
3437 super(document, node, captureTextNodes);
3438 this.type = 'animate';
3439 this.duration = 0;
3440 this.initialValue = null;
3441 this.initialUnits = '';
3442 this.removed = false;
3443 this.frozen = false;
3444 document.screen.animations.push(this);
3445 this.begin = this.getAttribute('begin').getMilliseconds();
3446 this.maxDuration = this.begin + this.getAttribute('dur').getMilliseconds();
3447 this.from = this.getAttribute('from');
3448 this.to = this.getAttribute('to');
3449 this.values = new Property(document, 'values', null);
3450 const valuesAttr = this.getAttribute('values');
3451 if (valuesAttr.hasValue()) {
3452 this.values.setValue(valuesAttr.getString().split(';'));
3453 }
3454 }
3455 getProperty() {
3456 const attributeType = this.getAttribute('attributeType').getString();
3457 const attributeName = this.getAttribute('attributeName').getString();
3458 if (attributeType === 'CSS') {
3459 return this.parent.getStyle(attributeName, true);
3460 }
3461 return this.parent.getAttribute(attributeName, true);
3462 }
3463 calcValue() {
3464 const { initialUnits } = this;
3465 const { progress, from, to } = this.getProgress();
3466 // tween value linearly
3467 let newValue = from.getNumber() + (to.getNumber() - from.getNumber()) * progress;
3468 if (initialUnits === '%') {
3469 newValue *= 100.0; // numValue() returns 0-1 whereas properties are 0-100
3470 }
3471 return `${newValue}${initialUnits}`;
3472 }
3473 update(delta) {
3474 const { parent } = this;
3475 const prop = this.getProperty();
3476 // set initial value
3477 if (!this.initialValue) {
3478 this.initialValue = prop.getString();
3479 this.initialUnits = prop.getUnits();
3480 }
3481 // if we're past the end time
3482 if (this.duration > this.maxDuration) {
3483 const fill = this.getAttribute('fill').getString('remove');
3484 // loop for indefinitely repeating animations
3485 if (this.getAttribute('repeatCount').getString() === 'indefinite'
3486 || this.getAttribute('repeatDur').getString() === 'indefinite') {
3487 this.duration = 0;
3488 }
3489 else if (fill === 'freeze' && !this.frozen) {
3490 this.frozen = true;
3491 parent.animationFrozen = true;
3492 parent.animationFrozenValue = prop.getString();
3493 }
3494 else if (fill === 'remove' && !this.removed) {
3495 this.removed = true;
3496 prop.setValue(parent.animationFrozen
3497 ? parent.animationFrozenValue
3498 : this.initialValue);
3499 return true;
3500 }
3501 return false;
3502 }
3503 this.duration += delta;
3504 // if we're past the begin time
3505 let updated = false;
3506 if (this.begin < this.duration) {
3507 let newValue = this.calcValue(); // tween
3508 const typeAttr = this.getAttribute('type');
3509 if (typeAttr.hasValue()) {
3510 // for transform, etc.
3511 const type = typeAttr.getString();
3512 newValue = `${type}(${newValue})`;
3513 }
3514 prop.setValue(newValue);
3515 updated = true;
3516 }
3517 return updated;
3518 }
3519 getProgress() {
3520 const { document, values } = this;
3521 const result = {
3522 progress: (this.duration - this.begin) / (this.maxDuration - this.begin)
3523 };
3524 if (values.hasValue()) {
3525 const p = result.progress * (values.getValue().length - 1);
3526 const lb = Math.floor(p);
3527 const ub = Math.ceil(p);
3528 result.from = new Property(document, 'from', parseFloat(values.getValue()[lb]));
3529 result.to = new Property(document, 'to', parseFloat(values.getValue()[ub]));
3530 result.progress = (p - lb) / (ub - lb);
3531 }
3532 else {
3533 result.from = this.from;
3534 result.to = this.to;
3535 }
3536 return result;
3537 }
3538}
3539
3540class AnimateColorElement extends AnimateElement {
3541 constructor() {
3542 super(...arguments);
3543 this.type = 'animateColor';
3544 }
3545 calcValue() {
3546 const { progress, from, to } = this.getProgress();
3547 const colorFrom = new RGBColor(from.getColor());
3548 const colorTo = new RGBColor(to.getColor());
3549 if (colorFrom.ok && colorTo.ok) {
3550 // tween color linearly
3551 const r = colorFrom.r + (colorTo.r - colorFrom.r) * progress;
3552 const g = colorFrom.g + (colorTo.g - colorFrom.g) * progress;
3553 const b = colorFrom.b + (colorTo.b - colorFrom.b) * progress;
3554 // ? alpha
3555 return `rgb(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)})`;
3556 }
3557 return this.getAttribute('from').getColor();
3558 }
3559}
3560
3561class AnimateTransformElement extends AnimateElement {
3562 constructor() {
3563 super(...arguments);
3564 this.type = 'animateTransform';
3565 }
3566 calcValue() {
3567 const { progress, from, to } = this.getProgress();
3568 // tween value linearly
3569 const transformFrom = toNumbers(from.getString());
3570 const transformTo = toNumbers(to.getString());
3571 const newValue = transformFrom.map((from, i) => {
3572 const to = transformTo[i];
3573 return from + (to - from) * progress;
3574 }).join(' ');
3575 return newValue;
3576 }
3577}
3578
3579class FontElement extends Element {
3580 constructor(document, node, captureTextNodes) {
3581 super(document, node, captureTextNodes);
3582 this.type = 'font';
3583 this.glyphs = {};
3584 this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber();
3585 const { definitions } = document;
3586 const { children } = this;
3587 for (const child of children) {
3588 switch (child.type) {
3589 case 'font-face': {
3590 this.fontFace = child;
3591 const fontFamilyStyle = child.getStyle('font-family');
3592 if (fontFamilyStyle.hasValue()) {
3593 definitions[fontFamilyStyle.getString()] = this;
3594 }
3595 break;
3596 }
3597 case 'missing-glyph':
3598 this.missingGlyph = child;
3599 break;
3600 case 'glyph': {
3601 const glyph = child;
3602 if (glyph.arabicForm) {
3603 this.isRTL = true;
3604 this.isArabic = true;
3605 if (typeof this.glyphs[glyph.unicode] === 'undefined') {
3606 this.glyphs[glyph.unicode] = {};
3607 }
3608 this.glyphs[glyph.unicode][glyph.arabicForm] = glyph;
3609 }
3610 else {
3611 this.glyphs[glyph.unicode] = glyph;
3612 }
3613 break;
3614 }
3615 }
3616 }
3617 }
3618 render() {
3619 // NO RENDER
3620 }
3621}
3622
3623class FontFaceElement extends Element {
3624 constructor(document, node, captureTextNodes) {
3625 super(document, node, captureTextNodes);
3626 this.type = 'font-face';
3627 this.ascent = this.getAttribute('ascent').getNumber();
3628 this.descent = this.getAttribute('descent').getNumber();
3629 this.unitsPerEm = this.getAttribute('units-per-em').getNumber();
3630 }
3631}
3632
3633class MissingGlyphElement extends PathElement {
3634 constructor() {
3635 super(...arguments);
3636 this.type = 'missing-glyph';
3637 this.horizAdvX = 0;
3638 }
3639}
3640
3641class TRefElement extends TextElement {
3642 constructor() {
3643 super(...arguments);
3644 this.type = 'tref';
3645 }
3646 getText() {
3647 const element = this.getHrefAttribute().getDefinition();
3648 if (element) {
3649 const firstChild = element.children[0];
3650 if (firstChild) {
3651 return firstChild.getText();
3652 }
3653 }
3654 return '';
3655 }
3656}
3657
3658class AElement extends TextElement {
3659 constructor(document, node, captureTextNodes) {
3660 super(document, node, captureTextNodes);
3661 this.type = 'a';
3662 const { childNodes } = node;
3663 const firstChild = childNodes[0];
3664 const hasText = childNodes.length > 0
3665 && Array.from(childNodes).every(node => node.nodeType === 3);
3666 this.hasText = hasText;
3667 this.text = hasText
3668 ? this.getTextFromNode(firstChild)
3669 : '';
3670 }
3671 getText() {
3672 return this.text;
3673 }
3674 renderChildren(ctx) {
3675 if (this.hasText) {
3676 // render as text element
3677 super.renderChildren(ctx);
3678 const { document, x, y } = this;
3679 const { mouse } = document.screen;
3680 const fontSize = new Property(document, 'fontSize', Font.parse(document.ctx.font).fontSize);
3681 // Do not calc bounding box if mouse is not working.
3682 if (mouse.isWorking()) {
3683 mouse.checkBoundingBox(this, new BoundingBox(x, y - fontSize.getPixels('y'), x + this.measureText(ctx), y));
3684 }
3685 }
3686 else if (this.children.length > 0) {
3687 // render as temporary group
3688 const g = new GElement(this.document, null);
3689 g.children = this.children;
3690 g.parent = this;
3691 g.render(ctx);
3692 }
3693 }
3694 onClick() {
3695 const { window } = this.document;
3696 if (window) {
3697 window.open(this.getHrefAttribute().getString());
3698 }
3699 }
3700 onMouseMove() {
3701 const ctx = this.document.ctx;
3702 ctx.canvas.style.cursor = 'pointer';
3703 }
3704}
3705
3706class TextPathElement extends TextElement {
3707 constructor(document, node, captureTextNodes) {
3708 super(document, node, captureTextNodes);
3709 this.type = 'textPath';
3710 this.textWidth = 0;
3711 this.textHeight = 0;
3712 this.pathLength = -1;
3713 this.glyphInfo = null;
3714 this.letterSpacingCache = [];
3715 this.measuresCache = new Map([['', 0]]);
3716 const pathElement = this.getHrefAttribute().getDefinition();
3717 this.text = this.getTextFromNode();
3718 this.dataArray = this.parsePathData(pathElement);
3719 }
3720 getText() {
3721 return this.text;
3722 }
3723 path(ctx) {
3724 const { dataArray } = this;
3725 if (ctx) {
3726 ctx.beginPath();
3727 }
3728 dataArray.forEach(({ type, points }) => {
3729 switch (type) {
3730 case PathParser.LINE_TO:
3731 if (ctx) {
3732 ctx.lineTo(points[0], points[1]);
3733 }
3734 break;
3735 case PathParser.MOVE_TO:
3736 if (ctx) {
3737 ctx.moveTo(points[0], points[1]);
3738 }
3739 break;
3740 case PathParser.CURVE_TO:
3741 if (ctx) {
3742 ctx.bezierCurveTo(points[0], points[1], points[2], points[3], points[4], points[5]);
3743 }
3744 break;
3745 case PathParser.QUAD_TO:
3746 if (ctx) {
3747 ctx.quadraticCurveTo(points[0], points[1], points[2], points[3]);
3748 }
3749 break;
3750 case PathParser.ARC: {
3751 const [cx, cy, rx, ry, theta, dTheta, psi, fs] = points;
3752 const r = rx > ry ? rx : ry;
3753 const scaleX = rx > ry ? 1 : rx / ry;
3754 const scaleY = rx > ry ? ry / rx : 1;
3755 if (ctx) {
3756 ctx.translate(cx, cy);
3757 ctx.rotate(psi);
3758 ctx.scale(scaleX, scaleY);
3759 ctx.arc(0, 0, r, theta, theta + dTheta, Boolean(1 - fs));
3760 ctx.scale(1 / scaleX, 1 / scaleY);
3761 ctx.rotate(-psi);
3762 ctx.translate(-cx, -cy);
3763 }
3764 break;
3765 }
3766 case PathParser.CLOSE_PATH:
3767 if (ctx) {
3768 ctx.closePath();
3769 }
3770 break;
3771 }
3772 });
3773 }
3774 renderChildren(ctx) {
3775 this.setTextData(ctx);
3776 ctx.save();
3777 const textDecoration = this.parent.getStyle('text-decoration').getString();
3778 const fontSize = this.getFontSize();
3779 const { glyphInfo } = this;
3780 const fill = ctx.fillStyle;
3781 if (textDecoration === 'underline') {
3782 ctx.beginPath();
3783 }
3784 glyphInfo.forEach((glyph, i) => {
3785 const { p0, p1, rotation, text: partialText } = glyph;
3786 ctx.save();
3787 ctx.translate(p0.x, p0.y);
3788 ctx.rotate(rotation);
3789 if (ctx.fillStyle) {
3790 ctx.fillText(partialText, 0, 0);
3791 }
3792 if (ctx.strokeStyle) {
3793 ctx.strokeText(partialText, 0, 0);
3794 }
3795 ctx.restore();
3796 if (textDecoration === 'underline') {
3797 if (i === 0) {
3798 ctx.moveTo(p0.x, p0.y + fontSize / 8);
3799 }
3800 ctx.lineTo(p1.x, p1.y + fontSize / 5);
3801 }
3802 // // To assist with debugging visually, uncomment following
3803 //
3804 // ctx.beginPath();
3805 // if (i % 2)
3806 // ctx.strokeStyle = 'red';
3807 // else
3808 // ctx.strokeStyle = 'green';
3809 // ctx.moveTo(p0.x, p0.y);
3810 // ctx.lineTo(p1.x, p1.y);
3811 // ctx.stroke();
3812 // ctx.closePath();
3813 });
3814 if (textDecoration === 'underline') {
3815 ctx.lineWidth = fontSize / 20;
3816 ctx.strokeStyle = fill;
3817 ctx.stroke();
3818 ctx.closePath();
3819 }
3820 ctx.restore();
3821 }
3822 getLetterSpacingAt(idx = 0) {
3823 return this.letterSpacingCache[idx] || 0;
3824 }
3825 findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, inputOffset, dy, c, charI) {
3826 let offset = inputOffset;
3827 let glyphWidth = this.measureText(ctx, c);
3828 if (c === ' '
3829 && anchor === 'justify'
3830 && textFullWidth < fullPathWidth) {
3831 glyphWidth += (fullPathWidth - textFullWidth) / spacesNumber;
3832 }
3833 if (charI > -1) {
3834 offset += this.getLetterSpacingAt(charI);
3835 }
3836 const splineStep = this.textHeight / 20;
3837 const p0 = this.getEquidistantPointOnPath(offset, splineStep, 0);
3838 const p1 = this.getEquidistantPointOnPath(offset + glyphWidth, splineStep, 0);
3839 const segment = {
3840 p0,
3841 p1
3842 };
3843 const rotation = p0 && p1
3844 ? Math.atan2(p1.y - p0.y, p1.x - p0.x)
3845 : 0;
3846 if (dy) {
3847 const dyX = Math.cos(Math.PI / 2 + rotation) * dy;
3848 const dyY = Math.cos(-rotation) * dy;
3849 segment.p0 = {
3850 ...p0,
3851 x: p0.x + dyX,
3852 y: p0.y + dyY
3853 };
3854 segment.p1 = {
3855 ...p1,
3856 x: p1.x + dyX,
3857 y: p1.y + dyY
3858 };
3859 }
3860 offset += glyphWidth;
3861 return {
3862 offset,
3863 segment,
3864 rotation
3865 };
3866 }
3867 measureText(ctx, text) {
3868 const { measuresCache } = this;
3869 const targetText = text || this.getText();
3870 if (measuresCache.has(targetText)) {
3871 return measuresCache.get(targetText);
3872 }
3873 const measure = this.measureTargetText(ctx, targetText);
3874 measuresCache.set(targetText, measure);
3875 return measure;
3876 }
3877 // This method supposes what all custom fonts already loaded.
3878 // If some font will be loaded after this method call, <textPath> will not be rendered correctly.
3879 // You need to call this method manually to update glyphs cache.
3880 setTextData(ctx) {
3881 if (this.glyphInfo) {
3882 return;
3883 }
3884 const renderText = this.getText();
3885 const chars = renderText.split('');
3886 const spacesNumber = renderText.split(' ').length - 1;
3887 const dx = this.parent.getAttribute('dx').split().map(_ => _.getPixels('x'));
3888 const dy = this.parent.getAttribute('dy').getPixels('y');
3889 const anchor = this.parent.getStyle('text-anchor').getString('start');
3890 const thisSpacing = this.getStyle('letter-spacing');
3891 const parentSpacing = this.parent.getStyle('letter-spacing');
3892 let letterSpacing = 0;
3893 if (!thisSpacing.hasValue()
3894 || thisSpacing.getValue() === 'inherit') {
3895 letterSpacing = parentSpacing.getPixels();
3896 }
3897 else if (thisSpacing.hasValue()) {
3898 if (thisSpacing.getValue() !== 'initial'
3899 && thisSpacing.getValue() !== 'unset') {
3900 letterSpacing = thisSpacing.getPixels();
3901 }
3902 }
3903 // fill letter-spacing cache
3904 const letterSpacingCache = [];
3905 const textLen = renderText.length;
3906 this.letterSpacingCache = letterSpacingCache;
3907 for (let i = 0; i < textLen; i++) {
3908 letterSpacingCache.push(typeof dx[i] !== 'undefined'
3909 ? dx[i]
3910 : letterSpacing);
3911 }
3912 const dxSum = letterSpacingCache.reduce((acc, cur, i) => (i === 0
3913 ? 0
3914 : acc + cur || 0), 0);
3915 const textWidth = this.measureText(ctx);
3916 const textFullWidth = Math.max(textWidth + dxSum, 0);
3917 this.textWidth = textWidth;
3918 this.textHeight = this.getFontSize();
3919 this.glyphInfo = [];
3920 const fullPathWidth = this.getPathLength();
3921 const startOffset = this.getStyle('startOffset').getNumber(0) * fullPathWidth;
3922 let offset = 0;
3923 if (anchor === 'middle'
3924 || anchor === 'center') {
3925 offset = -textFullWidth / 2;
3926 }
3927 if (anchor === 'end'
3928 || anchor === 'right') {
3929 offset = -textFullWidth;
3930 }
3931 offset += startOffset;
3932 chars.forEach((char, i) => {
3933 // Find such segment what distance between p0 and p1 is approx. width of glyph
3934 const { offset: nextOffset, segment, rotation } = this.findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, offset, dy, char, i);
3935 offset = nextOffset;
3936 if (!segment.p0 || !segment.p1) {
3937 return;
3938 }
3939 // const width = this.getLineLength(
3940 // segment.p0.x,
3941 // segment.p0.y,
3942 // segment.p1.x,
3943 // segment.p1.y
3944 // );
3945 // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used.
3946 // Can foresee having a rough pair table built in that the developer can override as needed.
3947 // Or use "dx" attribute of the <text> node as a naive replacement
3948 // const kern = 0;
3949 // placeholder for future implementation
3950 // const midpoint = this.getPointOnLine(
3951 // kern + width / 2.0,
3952 // segment.p0.x, segment.p0.y, segment.p1.x, segment.p1.y
3953 // );
3954 this.glyphInfo.push({
3955 // transposeX: midpoint.x,
3956 // transposeY: midpoint.y,
3957 text: chars[i],
3958 p0: segment.p0,
3959 p1: segment.p1,
3960 rotation
3961 });
3962 });
3963 }
3964 parsePathData(path) {
3965 this.pathLength = -1; // reset path length
3966 if (!path) {
3967 return [];
3968 }
3969 const pathCommands = [];
3970 const { pathParser } = path;
3971 pathParser.reset();
3972 // convert l, H, h, V, and v to L
3973 while (!pathParser.isEnd()) {
3974 const { current } = pathParser;
3975 const startX = current ? current.x : 0;
3976 const startY = current ? current.y : 0;
3977 const command = pathParser.next();
3978 let nextCommandType = command.type;
3979 let points = [];
3980 switch (command.type) {
3981 case PathParser.MOVE_TO:
3982 this.pathM(pathParser, points);
3983 break;
3984 case PathParser.LINE_TO:
3985 nextCommandType = this.pathL(pathParser, points);
3986 break;
3987 case PathParser.HORIZ_LINE_TO:
3988 nextCommandType = this.pathH(pathParser, points);
3989 break;
3990 case PathParser.VERT_LINE_TO:
3991 nextCommandType = this.pathV(pathParser, points);
3992 break;
3993 case PathParser.CURVE_TO:
3994 this.pathC(pathParser, points);
3995 break;
3996 case PathParser.SMOOTH_CURVE_TO:
3997 nextCommandType = this.pathS(pathParser, points);
3998 break;
3999 case PathParser.QUAD_TO:
4000 this.pathQ(pathParser, points);
4001 break;
4002 case PathParser.SMOOTH_QUAD_TO:
4003 nextCommandType = this.pathT(pathParser, points);
4004 break;
4005 case PathParser.ARC:
4006 points = this.pathA(pathParser);
4007 break;
4008 case PathParser.CLOSE_PATH:
4009 PathElement.pathZ(pathParser);
4010 break;
4011 }
4012 if (command.type !== PathParser.CLOSE_PATH) {
4013 pathCommands.push({
4014 type: nextCommandType,
4015 points,
4016 start: {
4017 x: startX,
4018 y: startY
4019 },
4020 pathLength: this.calcLength(startX, startY, nextCommandType, points)
4021 });
4022 }
4023 else {
4024 pathCommands.push({
4025 type: PathParser.CLOSE_PATH,
4026 points: [],
4027 pathLength: 0
4028 });
4029 }
4030 }
4031 return pathCommands;
4032 }
4033 pathM(pathParser, points) {
4034 const { x, y } = PathElement.pathM(pathParser).point;
4035 points.push(x, y);
4036 }
4037 pathL(pathParser, points) {
4038 const { x, y } = PathElement.pathL(pathParser).point;
4039 points.push(x, y);
4040 return PathParser.LINE_TO;
4041 }
4042 pathH(pathParser, points) {
4043 const { x, y } = PathElement.pathH(pathParser).point;
4044 points.push(x, y);
4045 return PathParser.LINE_TO;
4046 }
4047 pathV(pathParser, points) {
4048 const { x, y } = PathElement.pathV(pathParser).point;
4049 points.push(x, y);
4050 return PathParser.LINE_TO;
4051 }
4052 pathC(pathParser, points) {
4053 const { point, controlPoint, currentPoint } = PathElement.pathC(pathParser);
4054 points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4055 }
4056 pathS(pathParser, points) {
4057 const { point, controlPoint, currentPoint } = PathElement.pathS(pathParser);
4058 points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4059 return PathParser.CURVE_TO;
4060 }
4061 pathQ(pathParser, points) {
4062 const { controlPoint, currentPoint } = PathElement.pathQ(pathParser);
4063 points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4064 }
4065 pathT(pathParser, points) {
4066 const { controlPoint, currentPoint } = PathElement.pathT(pathParser);
4067 points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4068 return PathParser.QUAD_TO;
4069 }
4070 pathA(pathParser) {
4071 let { rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser);
4072 if (sweepFlag === 0 && ad > 0) {
4073 ad -= 2 * Math.PI;
4074 }
4075 if (sweepFlag === 1 && ad < 0) {
4076 ad += 2 * Math.PI;
4077 }
4078 return [
4079 centp.x,
4080 centp.y,
4081 rX,
4082 rY,
4083 a1,
4084 ad,
4085 xAxisRotation,
4086 sweepFlag
4087 ];
4088 }
4089 calcLength(x, y, commandType, points) {
4090 let len = 0;
4091 let p1 = null;
4092 let p2 = null;
4093 let t = 0;
4094 switch (commandType) {
4095 case PathParser.LINE_TO:
4096 return this.getLineLength(x, y, points[0], points[1]);
4097 case PathParser.CURVE_TO:
4098 // Approximates by breaking curve into 100 line segments
4099 len = 0.0;
4100 p1 = this.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
4101 for (t = 0.01; t <= 1; t += 0.01) {
4102 p2 = this.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
4103 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4104 p1 = p2;
4105 }
4106 return len;
4107 case PathParser.QUAD_TO:
4108 // Approximates by breaking curve into 100 line segments
4109 len = 0.0;
4110 p1 = this.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]);
4111 for (t = 0.01; t <= 1; t += 0.01) {
4112 p2 = this.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]);
4113 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4114 p1 = p2;
4115 }
4116 return len;
4117 case PathParser.ARC: {
4118 // Approximates by breaking curve into line segments
4119 len = 0.0;
4120 const start = points[4];
4121 // 4 = theta
4122 const dTheta = points[5];
4123 // 5 = dTheta
4124 const end = points[4] + dTheta;
4125 let inc = Math.PI / 180.0;
4126 // 1 degree resolution
4127 if (Math.abs(start - end) < inc) {
4128 inc = Math.abs(start - end);
4129 }
4130 // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi
4131 p1 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0);
4132 if (dTheta < 0) { // clockwise
4133 for (t = start - inc; t > end; t -= inc) {
4134 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
4135 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4136 p1 = p2;
4137 }
4138 }
4139 else { // counter-clockwise
4140 for (t = start + inc; t < end; t += inc) {
4141 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
4142 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4143 p1 = p2;
4144 }
4145 }
4146 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0);
4147 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4148 return len;
4149 }
4150 }
4151 return 0;
4152 }
4153 getPointOnLine(dist, p1x, p1y, p2x, p2y, fromX = p1x, fromY = p1y) {
4154 const m = (p2y - p1y) / ((p2x - p1x) + PSEUDO_ZERO);
4155 let run = Math.sqrt(dist * dist / (1 + m * m));
4156 if (p2x < p1x) {
4157 run *= -1;
4158 }
4159 let rise = m * run;
4160 let pt = null;
4161 if (p2x === p1x) { // vertical line
4162 pt = {
4163 x: fromX,
4164 y: fromY + rise
4165 };
4166 }
4167 else if ((fromY - p1y) / ((fromX - p1x) + PSEUDO_ZERO) === m) {
4168 pt = {
4169 x: fromX + run,
4170 y: fromY + rise
4171 };
4172 }
4173 else {
4174 let ix = 0;
4175 let iy = 0;
4176 const len = this.getLineLength(p1x, p1y, p2x, p2y);
4177 if (len < PSEUDO_ZERO) {
4178 return null;
4179 }
4180 let u = ((fromX - p1x) * (p2x - p1x))
4181 + ((fromY - p1y) * (p2y - p1y));
4182 u /= len * len;
4183 ix = p1x + u * (p2x - p1x);
4184 iy = p1y + u * (p2y - p1y);
4185 const pRise = this.getLineLength(fromX, fromY, ix, iy);
4186 const pRun = Math.sqrt(dist * dist - pRise * pRise);
4187 run = Math.sqrt(pRun * pRun / (1 + m * m));
4188 if (p2x < p1x) {
4189 run *= -1;
4190 }
4191 rise = m * run;
4192 pt = {
4193 x: ix + run,
4194 y: iy + rise
4195 };
4196 }
4197 return pt;
4198 }
4199 getPointOnPath(distance) {
4200 const fullLen = this.getPathLength();
4201 let cumulativePathLength = 0;
4202 let p = null;
4203 if (distance < -0.00005
4204 || distance - 0.00005 > fullLen) {
4205 return null;
4206 }
4207 const { dataArray } = this;
4208 for (const command of dataArray) {
4209 if (command
4210 && (command.pathLength < 0.00005
4211 || cumulativePathLength + command.pathLength + 0.00005 < distance)) {
4212 cumulativePathLength += command.pathLength;
4213 continue;
4214 }
4215 const delta = distance - cumulativePathLength;
4216 let currentT = 0;
4217 switch (command.type) {
4218 case PathParser.LINE_TO:
4219 p = this.getPointOnLine(delta, command.start.x, command.start.y, command.points[0], command.points[1], command.start.x, command.start.y);
4220 break;
4221 case PathParser.ARC: {
4222 const start = command.points[4];
4223 // 4 = theta
4224 const dTheta = command.points[5];
4225 // 5 = dTheta
4226 const end = command.points[4] + dTheta;
4227 currentT = start + delta / command.pathLength * dTheta;
4228 if (dTheta < 0 && currentT < end
4229 || dTheta >= 0 && currentT > end) {
4230 break;
4231 }
4232 p = this.getPointOnEllipticalArc(command.points[0], command.points[1], command.points[2], command.points[3], currentT, command.points[6]);
4233 break;
4234 }
4235 case PathParser.CURVE_TO:
4236 currentT = delta / command.pathLength;
4237 if (currentT > 1) {
4238 currentT = 1;
4239 }
4240 p = this.getPointOnCubicBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3], command.points[4], command.points[5]);
4241 break;
4242 case PathParser.QUAD_TO:
4243 currentT = delta / command.pathLength;
4244 if (currentT > 1) {
4245 currentT = 1;
4246 }
4247 p = this.getPointOnQuadraticBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3]);
4248 break;
4249 }
4250 if (p) {
4251 return p;
4252 }
4253 break;
4254 }
4255 return null;
4256 }
4257 getLineLength(x1, y1, x2, y2) {
4258 return Math.sqrt((x2 - x1) * (x2 - x1)
4259 + (y2 - y1) * (y2 - y1));
4260 }
4261 getPathLength() {
4262 if (this.pathLength === -1) {
4263 this.pathLength = this.dataArray.reduce((length, command) => (command.pathLength > 0
4264 ? length + command.pathLength
4265 : length), 0);
4266 }
4267 return this.pathLength;
4268 }
4269 getPointOnCubicBezier(pct, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) {
4270 const x = p4x * CB1(pct) + p3x * CB2(pct) + p2x * CB3(pct) + p1x * CB4(pct);
4271 const y = p4y * CB1(pct) + p3y * CB2(pct) + p2y * CB3(pct) + p1y * CB4(pct);
4272 return {
4273 x,
4274 y
4275 };
4276 }
4277 getPointOnQuadraticBezier(pct, p1x, p1y, p2x, p2y, p3x, p3y) {
4278 const x = p3x * QB1(pct) + p2x * QB2(pct) + p1x * QB3(pct);
4279 const y = p3y * QB1(pct) + p2y * QB2(pct) + p1y * QB3(pct);
4280 return {
4281 x,
4282 y
4283 };
4284 }
4285 getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) {
4286 const cosPsi = Math.cos(psi);
4287 const sinPsi = Math.sin(psi);
4288 const pt = {
4289 x: rx * Math.cos(theta),
4290 y: ry * Math.sin(theta)
4291 };
4292 return {
4293 x: cx + (pt.x * cosPsi - pt.y * sinPsi),
4294 y: cy + (pt.x * sinPsi + pt.y * cosPsi)
4295 };
4296 }
4297 // TODO need some optimisations. possibly build cache only for curved segments?
4298 buildEquidistantCache(inputStep, inputPrecision) {
4299 const fullLen = this.getPathLength();
4300 const precision = inputPrecision || 0.25; // accuracy vs performance
4301 const step = inputStep || fullLen / 100;
4302 if (!this.equidistantCache
4303 || this.equidistantCache.step !== step
4304 || this.equidistantCache.precision !== precision) {
4305 // Prepare cache
4306 this.equidistantCache = {
4307 step,
4308 precision,
4309 points: []
4310 };
4311 // Calculate points
4312 let s = 0;
4313 for (let l = 0; l <= fullLen; l += precision) {
4314 const p0 = this.getPointOnPath(l);
4315 const p1 = this.getPointOnPath(l + precision);
4316 if (!p0 || !p1) {
4317 continue;
4318 }
4319 s += this.getLineLength(p0.x, p0.y, p1.x, p1.y);
4320 if (s >= step) {
4321 this.equidistantCache.points.push({
4322 x: p0.x,
4323 y: p0.y,
4324 distance: l
4325 });
4326 s -= step;
4327 }
4328 }
4329 }
4330 }
4331 getEquidistantPointOnPath(targetDistance, step, precision) {
4332 this.buildEquidistantCache(step, precision);
4333 if (targetDistance < 0
4334 || targetDistance - this.getPathLength() > 0.00005) {
4335 return null;
4336 }
4337 const idx = Math.round(targetDistance
4338 / this.getPathLength()
4339 * (this.equidistantCache.points.length - 1));
4340 return this.equidistantCache.points[idx] || null;
4341 }
4342}
4343
4344// groups: 1: mime-type (+ charset), 2: mime-type (w/o charset), 3: charset, 4: base64?, 5: body
4345const dataUriRegex = /^\s*data:(([^/,;]+\/[^/,;]+)(?:;([^,;=]+=[^,;=]+))?)?(?:;(base64))?,(.*)$/i;
4346class ImageElement extends RenderedElement {
4347 constructor(document, node, captureTextNodes) {
4348 super(document, node, captureTextNodes);
4349 this.type = 'image';
4350 this.loaded = false;
4351 const href = this.getHrefAttribute().getString();
4352 if (!href) {
4353 return;
4354 }
4355 const isSvg = href.endsWith('.svg') || /^\s*data:image\/svg\+xml/i.test(href);
4356 document.images.push(this);
4357 if (!isSvg) {
4358 void this.loadImage(href);
4359 }
4360 else {
4361 void this.loadSvg(href);
4362 }
4363 this.isSvg = isSvg;
4364 }
4365 async loadImage(href) {
4366 try {
4367 const image = await this.document.createImage(href);
4368 this.image = image;
4369 }
4370 catch (err) {
4371 console.error(`Error while loading image "${href}":`, err);
4372 }
4373 this.loaded = true;
4374 }
4375 async loadSvg(href) {
4376 const match = dataUriRegex.exec(href);
4377 if (match) {
4378 const data = match[5];
4379 if (match[4] === 'base64') {
4380 this.image = atob(data);
4381 }
4382 else {
4383 this.image = decodeURIComponent(data);
4384 }
4385 }
4386 else {
4387 try {
4388 const response = await this.document.fetch(href);
4389 const svg = await response.text();
4390 this.image = svg;
4391 }
4392 catch (err) {
4393 console.error(`Error while loading image "${href}":`, err);
4394 }
4395 }
4396 this.loaded = true;
4397 }
4398 renderChildren(ctx) {
4399 const { document, image, loaded } = this;
4400 const x = this.getAttribute('x').getPixels('x');
4401 const y = this.getAttribute('y').getPixels('y');
4402 const width = this.getStyle('width').getPixels('x');
4403 const height = this.getStyle('height').getPixels('y');
4404 if (!loaded || !image
4405 || !width || !height) {
4406 return;
4407 }
4408 ctx.save();
4409 ctx.translate(x, y);
4410 if (this.isSvg) {
4411 const subDocument = document.canvg.forkString(ctx, this.image, {
4412 ignoreMouse: true,
4413 ignoreAnimation: true,
4414 ignoreDimensions: true,
4415 ignoreClear: true,
4416 offsetX: 0,
4417 offsetY: 0,
4418 scaleWidth: width,
4419 scaleHeight: height
4420 });
4421 subDocument.document.documentElement.parent = this;
4422 void subDocument.render();
4423 }
4424 else {
4425 const image = this.image;
4426 document.setViewBox({
4427 ctx,
4428 aspectRatio: this.getAttribute('preserveAspectRatio').getString(),
4429 width,
4430 desiredWidth: image.width,
4431 height,
4432 desiredHeight: image.height
4433 });
4434 if (this.loaded) {
4435 if (typeof image.complete === 'undefined' || image.complete) {
4436 ctx.drawImage(image, 0, 0);
4437 }
4438 }
4439 }
4440 ctx.restore();
4441 }
4442 getBoundingBox() {
4443 const x = this.getAttribute('x').getPixels('x');
4444 const y = this.getAttribute('y').getPixels('y');
4445 const width = this.getStyle('width').getPixels('x');
4446 const height = this.getStyle('height').getPixels('y');
4447 return new BoundingBox(x, y, x + width, y + height);
4448 }
4449}
4450
4451class SymbolElement extends RenderedElement {
4452 constructor() {
4453 super(...arguments);
4454 this.type = 'symbol';
4455 }
4456 render(_) {
4457 // NO RENDER
4458 }
4459}
4460
4461class SVGFontLoader {
4462 constructor(document) {
4463 this.document = document;
4464 this.loaded = false;
4465 document.fonts.push(this);
4466 }
4467 async load(fontFamily, url) {
4468 try {
4469 const { document } = this;
4470 const svgDocument = await document.canvg.parser.load(url);
4471 const fonts = svgDocument.getElementsByTagName('font');
4472 Array.from(fonts).forEach((fontNode) => {
4473 const font = document.createElement(fontNode);
4474 document.definitions[fontFamily] = font;
4475 });
4476 }
4477 catch (err) {
4478 console.error(`Error while loading font "${url}":`, err);
4479 }
4480 this.loaded = true;
4481 }
4482}
4483
4484class StyleElement extends Element {
4485 constructor(document, node, captureTextNodes) {
4486 super(document, node, captureTextNodes);
4487 this.type = 'style';
4488 const css = compressSpaces(Array.from(node.childNodes)
4489 // NEED TEST
4490 .map(_ => _.textContent)
4491 .join('')
4492 .replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, '') // remove comments
4493 .replace(/@import.*;/g, '') // remove imports
4494 );
4495 const cssDefs = css.split('}');
4496 cssDefs.forEach((_) => {
4497 const def = _.trim();
4498 if (!def) {
4499 return;
4500 }
4501 const cssParts = def.split('{');
4502 const cssClasses = cssParts[0].split(',');
4503 const cssProps = cssParts[1].split(';');
4504 cssClasses.forEach((_) => {
4505 const cssClass = _.trim();
4506 if (!cssClass) {
4507 return;
4508 }
4509 const props = document.styles[cssClass] || {};
4510 cssProps.forEach((cssProp) => {
4511 const prop = cssProp.indexOf(':');
4512 const name = cssProp.substr(0, prop).trim();
4513 const value = cssProp.substr(prop + 1, cssProp.length - prop).trim();
4514 if (name && value) {
4515 props[name] = new Property(document, name, value);
4516 }
4517 });
4518 document.styles[cssClass] = props;
4519 document.stylesSpecificity[cssClass] = getSelectorSpecificity(cssClass);
4520 if (cssClass === '@font-face') { // && !nodeEnv
4521 const fontFamily = props['font-family'].getString().replace(/"|'/g, '');
4522 const srcs = props.src.getString().split(',');
4523 srcs.forEach((src) => {
4524 if (src.indexOf('format("svg")') > 0) {
4525 const url = parseExternalUrl(src);
4526 if (url) {
4527 void new SVGFontLoader(document).load(fontFamily, url);
4528 }
4529 }
4530 });
4531 }
4532 });
4533 });
4534 }
4535}
4536StyleElement.parseExternalUrl = parseExternalUrl;
4537
4538class UseElement extends RenderedElement {
4539 constructor() {
4540 super(...arguments);
4541 this.type = 'use';
4542 }
4543 setContext(ctx) {
4544 super.setContext(ctx);
4545 const xAttr = this.getAttribute('x');
4546 const yAttr = this.getAttribute('y');
4547 if (xAttr.hasValue()) {
4548 ctx.translate(xAttr.getPixels('x'), 0);
4549 }
4550 if (yAttr.hasValue()) {
4551 ctx.translate(0, yAttr.getPixels('y'));
4552 }
4553 }
4554 path(ctx) {
4555 const { element } = this;
4556 if (element) {
4557 element.path(ctx);
4558 }
4559 }
4560 renderChildren(ctx) {
4561 const { document, element } = this;
4562 if (element) {
4563 let tempSvg = element;
4564 if (element.type === 'symbol') {
4565 // render me using a temporary svg element in symbol cases (http://www.w3.org/TR/SVG/struct.html#UseElement)
4566 tempSvg = new SVGElement(document, null);
4567 tempSvg.attributes.viewBox = new Property(document, 'viewBox', element.getAttribute('viewBox').getString());
4568 tempSvg.attributes.preserveAspectRatio = new Property(document, 'preserveAspectRatio', element.getAttribute('preserveAspectRatio').getString());
4569 tempSvg.attributes.overflow = new Property(document, 'overflow', element.getAttribute('overflow').getString());
4570 tempSvg.children = element.children;
4571 // element is still the parent of the children
4572 element.styles.opacity = new Property(document, 'opacity', this.calculateOpacity());
4573 }
4574 if (tempSvg.type === 'svg') {
4575 const widthStyle = this.getStyle('width', false, true);
4576 const heightStyle = this.getStyle('height', false, true);
4577 // if symbol or svg, inherit width/height from me
4578 if (widthStyle.hasValue()) {
4579 tempSvg.attributes.width = new Property(document, 'width', widthStyle.getString());
4580 }
4581 if (heightStyle.hasValue()) {
4582 tempSvg.attributes.height = new Property(document, 'height', heightStyle.getString());
4583 }
4584 }
4585 const oldParent = tempSvg.parent;
4586 tempSvg.parent = this;
4587 tempSvg.render(ctx);
4588 tempSvg.parent = oldParent;
4589 }
4590 }
4591 getBoundingBox(ctx) {
4592 const { element } = this;
4593 if (element) {
4594 return element.getBoundingBox(ctx);
4595 }
4596 return null;
4597 }
4598 elementTransform() {
4599 const { document, element } = this;
4600 return Transform.fromElement(document, element);
4601 }
4602 get element() {
4603 if (!this.cachedElement) {
4604 this.cachedElement = this.getHrefAttribute().getDefinition();
4605 }
4606 return this.cachedElement;
4607 }
4608}
4609
4610function imGet(img, x, y, width, _height, rgba) {
4611 return img[y * width * 4 + x * 4 + rgba];
4612}
4613function imSet(img, x, y, width, _height, rgba, val) {
4614 img[y * width * 4 + x * 4 + rgba] = val;
4615}
4616function m(matrix, i, v) {
4617 const mi = matrix[i];
4618 return mi * v;
4619}
4620function c(a, m1, m2, m3) {
4621 return m1 + Math.cos(a) * m2 + Math.sin(a) * m3;
4622}
4623class FeColorMatrixElement extends Element {
4624 constructor(document, node, captureTextNodes) {
4625 super(document, node, captureTextNodes);
4626 this.type = 'feColorMatrix';
4627 let matrix = toNumbers(this.getAttribute('values').getString());
4628 switch (this.getAttribute('type').getString('matrix')) { // http://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
4629 case 'saturate': {
4630 const s = matrix[0];
4631 /* eslint-disable array-element-newline */
4632 matrix = [
4633 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0,
4634 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0,
4635 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0,
4636 0, 0, 0, 1, 0,
4637 0, 0, 0, 0, 1
4638 ];
4639 /* eslint-enable array-element-newline */
4640 break;
4641 }
4642 case 'hueRotate': {
4643 const a = matrix[0] * Math.PI / 180.0;
4644 /* eslint-disable array-element-newline */
4645 matrix = [
4646 c(a, 0.213, 0.787, -0.213), c(a, 0.715, -0.715, -0.715), c(a, 0.072, -0.072, 0.928), 0, 0,
4647 c(a, 0.213, -0.213, 0.143), c(a, 0.715, 0.285, 0.140), c(a, 0.072, -0.072, -0.283), 0, 0,
4648 c(a, 0.213, -0.213, -0.787), c(a, 0.715, -0.715, 0.715), c(a, 0.072, 0.928, 0.072), 0, 0,
4649 0, 0, 0, 1, 0,
4650 0, 0, 0, 0, 1
4651 ];
4652 /* eslint-enable array-element-newline */
4653 break;
4654 }
4655 case 'luminanceToAlpha':
4656 /* eslint-disable array-element-newline */
4657 matrix = [
4658 0, 0, 0, 0, 0,
4659 0, 0, 0, 0, 0,
4660 0, 0, 0, 0, 0,
4661 0.2125, 0.7154, 0.0721, 0, 0,
4662 0, 0, 0, 0, 1
4663 ];
4664 /* eslint-enable array-element-newline */
4665 break;
4666 }
4667 this.matrix = matrix;
4668 this.includeOpacity = this.getAttribute('includeOpacity').hasValue();
4669 }
4670 apply(ctx, _x, _y, width, height) {
4671 // assuming x==0 && y==0 for now
4672 const { includeOpacity, matrix } = this;
4673 const srcData = ctx.getImageData(0, 0, width, height);
4674 for (let y = 0; y < height; y++) {
4675 for (let x = 0; x < width; x++) {
4676 const r = imGet(srcData.data, x, y, width, height, 0);
4677 const g = imGet(srcData.data, x, y, width, height, 1);
4678 const b = imGet(srcData.data, x, y, width, height, 2);
4679 const a = imGet(srcData.data, x, y, width, height, 3);
4680 let nr = m(matrix, 0, r) + m(matrix, 1, g) + m(matrix, 2, b) + m(matrix, 3, a) + m(matrix, 4, 1);
4681 let ng = m(matrix, 5, r) + m(matrix, 6, g) + m(matrix, 7, b) + m(matrix, 8, a) + m(matrix, 9, 1);
4682 let nb = m(matrix, 10, r) + m(matrix, 11, g) + m(matrix, 12, b) + m(matrix, 13, a) + m(matrix, 14, 1);
4683 let na = m(matrix, 15, r) + m(matrix, 16, g) + m(matrix, 17, b) + m(matrix, 18, a) + m(matrix, 19, 1);
4684 if (includeOpacity) {
4685 nr = 0;
4686 ng = 0;
4687 nb = 0;
4688 na *= a / 255;
4689 }
4690 imSet(srcData.data, x, y, width, height, 0, nr);
4691 imSet(srcData.data, x, y, width, height, 1, ng);
4692 imSet(srcData.data, x, y, width, height, 2, nb);
4693 imSet(srcData.data, x, y, width, height, 3, na);
4694 }
4695 }
4696 ctx.clearRect(0, 0, width, height);
4697 ctx.putImageData(srcData, 0, 0);
4698 }
4699}
4700
4701class MaskElement extends Element {
4702 constructor() {
4703 super(...arguments);
4704 this.type = 'mask';
4705 }
4706 apply(ctx, element) {
4707 const { document } = this;
4708 // render as temp svg
4709 let x = this.getAttribute('x').getPixels('x');
4710 let y = this.getAttribute('y').getPixels('y');
4711 let width = this.getStyle('width').getPixels('x');
4712 let height = this.getStyle('height').getPixels('y');
4713 if (!width && !height) {
4714 const boundingBox = new BoundingBox();
4715 this.children.forEach((child) => {
4716 boundingBox.addBoundingBox(child.getBoundingBox(ctx));
4717 });
4718 x = Math.floor(boundingBox.x1);
4719 y = Math.floor(boundingBox.y1);
4720 width = Math.floor(boundingBox.width);
4721 height = Math.floor(boundingBox.height);
4722 }
4723 const ignoredStyles = this.removeStyles(element, MaskElement.ignoreStyles);
4724 const maskCanvas = document.createCanvas(x + width, y + height);
4725 const maskCtx = maskCanvas.getContext('2d');
4726 document.screen.setDefaults(maskCtx);
4727 this.renderChildren(maskCtx);
4728 // convert mask to alpha with a fake node
4729 // TODO: refactor out apply from feColorMatrix
4730 new FeColorMatrixElement(document, ({
4731 nodeType: 1,
4732 childNodes: [],
4733 attributes: [
4734 {
4735 nodeName: 'type',
4736 value: 'luminanceToAlpha'
4737 },
4738 {
4739 nodeName: 'includeOpacity',
4740 value: 'true'
4741 }
4742 ]
4743 })).apply(maskCtx, 0, 0, x + width, y + height);
4744 const tmpCanvas = document.createCanvas(x + width, y + height);
4745 const tmpCtx = tmpCanvas.getContext('2d');
4746 document.screen.setDefaults(tmpCtx);
4747 element.render(tmpCtx);
4748 tmpCtx.globalCompositeOperation = 'destination-in';
4749 tmpCtx.fillStyle = maskCtx.createPattern(maskCanvas, 'no-repeat');
4750 tmpCtx.fillRect(0, 0, x + width, y + height);
4751 ctx.fillStyle = tmpCtx.createPattern(tmpCanvas, 'no-repeat');
4752 ctx.fillRect(0, 0, x + width, y + height);
4753 // reassign mask
4754 this.restoreStyles(element, ignoredStyles);
4755 }
4756 render(_) {
4757 // NO RENDER
4758 }
4759}
4760MaskElement.ignoreStyles = [
4761 'mask',
4762 'transform',
4763 'clip-path'
4764];
4765
4766const noop = () => {
4767 // NOOP
4768};
4769class ClipPathElement extends Element {
4770 constructor() {
4771 super(...arguments);
4772 this.type = 'clipPath';
4773 }
4774 apply(ctx) {
4775 const { document } = this;
4776 const contextProto = Reflect.getPrototypeOf(ctx);
4777 const { beginPath, closePath } = ctx;
4778 if (contextProto) {
4779 contextProto.beginPath = noop;
4780 contextProto.closePath = noop;
4781 }
4782 Reflect.apply(beginPath, ctx, []);
4783 this.children.forEach((child) => {
4784 if (typeof child.path === 'undefined') {
4785 return;
4786 }
4787 let transform = typeof child.elementTransform !== 'undefined'
4788 ? child.elementTransform()
4789 : null; // handle <use />
4790 if (!transform) {
4791 transform = Transform.fromElement(document, child);
4792 }
4793 if (transform) {
4794 transform.apply(ctx);
4795 }
4796 child.path(ctx);
4797 if (contextProto) {
4798 contextProto.closePath = closePath;
4799 }
4800 if (transform) {
4801 transform.unapply(ctx);
4802 }
4803 });
4804 Reflect.apply(closePath, ctx, []);
4805 ctx.clip();
4806 if (contextProto) {
4807 contextProto.beginPath = beginPath;
4808 contextProto.closePath = closePath;
4809 }
4810 }
4811 render(_) {
4812 // NO RENDER
4813 }
4814}
4815
4816class FilterElement extends Element {
4817 constructor() {
4818 super(...arguments);
4819 this.type = 'filter';
4820 }
4821 apply(ctx, element) {
4822 // render as temp svg
4823 const { document, children } = this;
4824 const boundingBox = element.getBoundingBox(ctx);
4825 if (!boundingBox) {
4826 return;
4827 }
4828 let px = 0;
4829 let py = 0;
4830 children.forEach((child) => {
4831 const efd = child.extraFilterDistance || 0;
4832 px = Math.max(px, efd);
4833 py = Math.max(py, efd);
4834 });
4835 const width = Math.floor(boundingBox.width);
4836 const height = Math.floor(boundingBox.height);
4837 const tmpCanvasWidth = width + 2 * px;
4838 const tmpCanvasHeight = height + 2 * py;
4839 if (tmpCanvasWidth < 1 || tmpCanvasHeight < 1) {
4840 return;
4841 }
4842 const x = Math.floor(boundingBox.x);
4843 const y = Math.floor(boundingBox.y);
4844 const ignoredStyles = this.removeStyles(element, FilterElement.ignoreStyles);
4845 const tmpCanvas = document.createCanvas(tmpCanvasWidth, tmpCanvasHeight);
4846 const tmpCtx = tmpCanvas.getContext('2d');
4847 document.screen.setDefaults(tmpCtx);
4848 tmpCtx.translate(-x + px, -y + py);
4849 element.render(tmpCtx);
4850 // apply filters
4851 children.forEach((child) => {
4852 if (typeof child.apply === 'function') {
4853 child.apply(tmpCtx, 0, 0, tmpCanvasWidth, tmpCanvasHeight);
4854 }
4855 });
4856 // render on me
4857 ctx.drawImage(tmpCanvas, 0, 0, tmpCanvasWidth, tmpCanvasHeight, x - px, y - py, tmpCanvasWidth, tmpCanvasHeight);
4858 this.restoreStyles(element, ignoredStyles);
4859 }
4860 render(_) {
4861 // NO RENDER
4862 }
4863}
4864FilterElement.ignoreStyles = [
4865 'filter',
4866 'transform',
4867 'clip-path'
4868];
4869
4870class FeDropShadowElement extends Element {
4871 constructor(document, node, captureTextNodes) {
4872 super(document, node, captureTextNodes);
4873 this.type = 'feDropShadow';
4874 this.addStylesFromStyleDefinition();
4875 }
4876 apply(_, _x, _y, _width, _height) {
4877 // TODO: implement
4878 }
4879}
4880
4881class FeMorphologyElement extends Element {
4882 constructor() {
4883 super(...arguments);
4884 this.type = 'feMorphology';
4885 }
4886 apply(_, _x, _y, _width, _height) {
4887 // TODO: implement
4888 }
4889}
4890
4891class FeCompositeElement extends Element {
4892 constructor() {
4893 super(...arguments);
4894 this.type = 'feComposite';
4895 }
4896 apply(_, _x, _y, _width, _height) {
4897 // TODO: implement
4898 }
4899}
4900
4901class FeGaussianBlurElement extends Element {
4902 constructor(document, node, captureTextNodes) {
4903 super(document, node, captureTextNodes);
4904 this.type = 'feGaussianBlur';
4905 this.blurRadius = Math.floor(this.getAttribute('stdDeviation').getNumber());
4906 this.extraFilterDistance = this.blurRadius;
4907 }
4908 apply(ctx, x, y, width, height) {
4909 const { document, blurRadius } = this;
4910 const body = document.window
4911 ? document.window.document.body
4912 : null;
4913 const canvas = ctx.canvas;
4914 // StackBlur requires canvas be on document
4915 canvas.id = document.getUniqueId();
4916 if (body) {
4917 canvas.style.display = 'none';
4918 body.appendChild(canvas);
4919 }
4920 canvasRGBA(canvas, x, y, width, height, blurRadius);
4921 if (body) {
4922 body.removeChild(canvas);
4923 }
4924 }
4925}
4926
4927class TitleElement extends Element {
4928 constructor() {
4929 super(...arguments);
4930 this.type = 'title';
4931 }
4932}
4933
4934class DescElement extends Element {
4935 constructor() {
4936 super(...arguments);
4937 this.type = 'desc';
4938 }
4939}
4940
4941const elements = {
4942 'svg': SVGElement,
4943 'rect': RectElement,
4944 'circle': CircleElement,
4945 'ellipse': EllipseElement,
4946 'line': LineElement,
4947 'polyline': PolylineElement,
4948 'polygon': PolygonElement,
4949 'path': PathElement,
4950 'pattern': PatternElement,
4951 'marker': MarkerElement,
4952 'defs': DefsElement,
4953 'linearGradient': LinearGradientElement,
4954 'radialGradient': RadialGradientElement,
4955 'stop': StopElement,
4956 'animate': AnimateElement,
4957 'animateColor': AnimateColorElement,
4958 'animateTransform': AnimateTransformElement,
4959 'font': FontElement,
4960 'font-face': FontFaceElement,
4961 'missing-glyph': MissingGlyphElement,
4962 'glyph': GlyphElement,
4963 'text': TextElement,
4964 'tspan': TSpanElement,
4965 'tref': TRefElement,
4966 'a': AElement,
4967 'textPath': TextPathElement,
4968 'image': ImageElement,
4969 'g': GElement,
4970 'symbol': SymbolElement,
4971 'style': StyleElement,
4972 'use': UseElement,
4973 'mask': MaskElement,
4974 'clipPath': ClipPathElement,
4975 'filter': FilterElement,
4976 'feDropShadow': FeDropShadowElement,
4977 'feMorphology': FeMorphologyElement,
4978 'feComposite': FeCompositeElement,
4979 'feColorMatrix': FeColorMatrixElement,
4980 'feGaussianBlur': FeGaussianBlurElement,
4981 'title': TitleElement,
4982 'desc': DescElement
4983};
4984
4985function createCanvas(width, height) {
4986 const canvas = document.createElement('canvas');
4987 canvas.width = width;
4988 canvas.height = height;
4989 return canvas;
4990}
4991async function createImage(src, anonymousCrossOrigin = false) {
4992 const image = document.createElement('img');
4993 if (anonymousCrossOrigin) {
4994 image.crossOrigin = 'Anonymous';
4995 }
4996 return new Promise((resolve, reject) => {
4997 image.onload = () => {
4998 resolve(image);
4999 };
5000 image.onerror = (_event, _source, _lineno, _colno, error) => {
5001 reject(error);
5002 };
5003 image.src = src;
5004 });
5005}
5006class Document {
5007 constructor(canvg, { rootEmSize = 12, emSize = 12, createCanvas = Document.createCanvas, createImage = Document.createImage, anonymousCrossOrigin } = {}) {
5008 this.canvg = canvg;
5009 this.definitions = {};
5010 this.styles = {};
5011 this.stylesSpecificity = {};
5012 this.images = [];
5013 this.fonts = [];
5014 this.emSizeStack = [];
5015 this.uniqueId = 0;
5016 this.screen = canvg.screen;
5017 this.rootEmSize = rootEmSize;
5018 this.emSize = emSize;
5019 this.createCanvas = createCanvas;
5020 this.createImage = this.bindCreateImage(createImage, anonymousCrossOrigin);
5021 this.screen.wait(this.isImagesLoaded.bind(this));
5022 this.screen.wait(this.isFontsLoaded.bind(this));
5023 }
5024 bindCreateImage(createImage, anonymousCrossOrigin) {
5025 if (typeof anonymousCrossOrigin === 'boolean') {
5026 return (source, forceAnonymousCrossOrigin) => createImage(source, typeof forceAnonymousCrossOrigin === 'boolean'
5027 ? forceAnonymousCrossOrigin
5028 : anonymousCrossOrigin);
5029 }
5030 return createImage;
5031 }
5032 get window() {
5033 return this.screen.window;
5034 }
5035 get fetch() {
5036 return this.screen.fetch;
5037 }
5038 get ctx() {
5039 return this.screen.ctx;
5040 }
5041 get emSize() {
5042 const { emSizeStack } = this;
5043 return emSizeStack[emSizeStack.length - 1];
5044 }
5045 set emSize(value) {
5046 const { emSizeStack } = this;
5047 emSizeStack.push(value);
5048 }
5049 popEmSize() {
5050 const { emSizeStack } = this;
5051 emSizeStack.pop();
5052 }
5053 getUniqueId() {
5054 return `canvg${++this.uniqueId}`;
5055 }
5056 isImagesLoaded() {
5057 return this.images.every(_ => _.loaded);
5058 }
5059 isFontsLoaded() {
5060 return this.fonts.every(_ => _.loaded);
5061 }
5062 createDocumentElement(document) {
5063 const documentElement = this.createElement(document.documentElement);
5064 documentElement.root = true;
5065 documentElement.addStylesFromStyleDefinition();
5066 this.documentElement = documentElement;
5067 return documentElement;
5068 }
5069 createElement(node) {
5070 const elementType = node.nodeName.replace(/^[^:]+:/, '');
5071 const ElementType = Document.elementTypes[elementType];
5072 if (typeof ElementType !== 'undefined') {
5073 return new ElementType(this, node);
5074 }
5075 return new UnknownElement(this, node);
5076 }
5077 createTextNode(node) {
5078 return new TextNode(this, node);
5079 }
5080 setViewBox(config) {
5081 this.screen.setViewBox({
5082 document: this,
5083 ...config
5084 });
5085 }
5086}
5087Document.createCanvas = createCanvas;
5088Document.createImage = createImage;
5089Document.elementTypes = elements;
5090
5091/**
5092 * SVG renderer on canvas.
5093 */
5094class Canvg {
5095 /**
5096 * Main constructor.
5097 * @param ctx - Rendering context.
5098 * @param svg - SVG Document.
5099 * @param options - Rendering options.
5100 */
5101 constructor(ctx, svg, options = {}) {
5102 this.parser = new Parser(options);
5103 this.screen = new Screen(ctx, options);
5104 this.options = options;
5105 const document = new Document(this, options);
5106 const documentElement = document.createDocumentElement(svg);
5107 this.document = document;
5108 this.documentElement = documentElement;
5109 }
5110 /**
5111 * Create Canvg instance from SVG source string or URL.
5112 * @param ctx - Rendering context.
5113 * @param svg - SVG source string or URL.
5114 * @param options - Rendering options.
5115 * @returns Canvg instance.
5116 */
5117 static async from(ctx, svg, options = {}) {
5118 const parser = new Parser(options);
5119 const svgDocument = await parser.parse(svg);
5120 return new Canvg(ctx, svgDocument, options);
5121 }
5122 /**
5123 * Create Canvg instance from SVG source string.
5124 * @param ctx - Rendering context.
5125 * @param svg - SVG source string.
5126 * @param options - Rendering options.
5127 * @returns Canvg instance.
5128 */
5129 static fromString(ctx, svg, options = {}) {
5130 const parser = new Parser(options);
5131 const svgDocument = parser.parseFromString(svg);
5132 return new Canvg(ctx, svgDocument, options);
5133 }
5134 /**
5135 * Create new Canvg instance with inherited options.
5136 * @param ctx - Rendering context.
5137 * @param svg - SVG source string or URL.
5138 * @param options - Rendering options.
5139 * @returns Canvg instance.
5140 */
5141 fork(ctx, svg, options = {}) {
5142 return Canvg.from(ctx, svg, {
5143 ...this.options,
5144 ...options
5145 });
5146 }
5147 /**
5148 * Create new Canvg instance with inherited options.
5149 * @param ctx - Rendering context.
5150 * @param svg - SVG source string.
5151 * @param options - Rendering options.
5152 * @returns Canvg instance.
5153 */
5154 forkString(ctx, svg, options = {}) {
5155 return Canvg.fromString(ctx, svg, {
5156 ...this.options,
5157 ...options
5158 });
5159 }
5160 /**
5161 * Document is ready promise.
5162 * @returns Ready promise.
5163 */
5164 ready() {
5165 return this.screen.ready();
5166 }
5167 /**
5168 * Document is ready value.
5169 * @returns Is ready or not.
5170 */
5171 isReady() {
5172 return this.screen.isReady();
5173 }
5174 /**
5175 * Render only first frame, ignoring animations and mouse.
5176 * @param options - Rendering options.
5177 */
5178 async render(options = {}) {
5179 this.start({
5180 enableRedraw: true,
5181 ignoreAnimation: true,
5182 ignoreMouse: true,
5183 ...options
5184 });
5185 await this.ready();
5186 this.stop();
5187 }
5188 /**
5189 * Start rendering.
5190 * @param options - Render options.
5191 */
5192 start(options = {}) {
5193 const { documentElement, screen, options: baseOptions } = this;
5194 screen.start(documentElement, {
5195 enableRedraw: true,
5196 ...baseOptions,
5197 ...options
5198 });
5199 }
5200 /**
5201 * Stop rendering.
5202 */
5203 stop() {
5204 this.screen.stop();
5205 }
5206 /**
5207 * Resize SVG to fit in given size.
5208 * @param width
5209 * @param height
5210 * @param preserveAspectRatio
5211 */
5212 resize(width, height = width, preserveAspectRatio = false) {
5213 this.documentElement.resize(width, height, preserveAspectRatio);
5214 }
5215}
5216
5217export default Canvg;
5218export { AElement, AnimateColorElement, AnimateElement, AnimateTransformElement, BoundingBox, CB1, CB2, CB3, CB4, Canvg, CircleElement, ClipPathElement, DefsElement, DescElement, Document, Element, EllipseElement, FeColorMatrixElement, FeCompositeElement, FeDropShadowElement, FeGaussianBlurElement, FeMorphologyElement, FilterElement, Font, FontElement, FontFaceElement, GElement, GlyphElement, GradientElement, ImageElement, LineElement, LinearGradientElement, MarkerElement, MaskElement, Matrix, MissingGlyphElement, Mouse, PSEUDO_ZERO, Parser, PathElement, PathParser, PatternElement, Point, PolygonElement, PolylineElement, Property, QB1, QB2, QB3, RadialGradientElement, RectElement, RenderedElement, Rotate, SVGElement, SVGFontLoader, Scale, Screen, Skew, SkewX, SkewY, StopElement, StyleElement, SymbolElement, TRefElement, TSpanElement, TextElement, TextPathElement, TitleElement, Transform, Translate, UnknownElement, UseElement, ViewPort, compressSpaces, getSelectorSpecificity, normalizeAttributeName, normalizeColor, parseExternalUrl, index as presets, toNumbers, trimLeft, trimRight, vectorMagnitude, vectorsAngle, vectorsRatio };
5219//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguYmFiZWwuanMiLCJzb3VyY2VzIjpbXSwic291cmNlc0NvbnRlbnQiOltdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OzsifQ==