source: imaps-frontend/node_modules/canvg/lib/index.babel.js@ 79a0317

main
Last change on this file since 79a0317 was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 3 days ago

F4 Finalna Verzija

  • Property mode set to 100644
File size: 194.7 KB
Line 
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 isFirstChild() {
1511 return this.parent?.children.indexOf(this) === 0;
1512 }
1513}
1514Element.ignoreChildTypes = [
1515 'title'
1516];
1517
1518class UnknownElement extends Element {
1519 constructor(document, node, captureTextNodes) {
1520 super(document, node, captureTextNodes);
1521 }
1522}
1523
1524function wrapFontFamily(fontFamily) {
1525 const trimmed = fontFamily.trim();
1526 return /^('|")/.test(trimmed)
1527 ? trimmed
1528 : `"${trimmed}"`;
1529}
1530function prepareFontFamily(fontFamily) {
1531 return typeof process === 'undefined'
1532 ? fontFamily
1533 : fontFamily
1534 .trim()
1535 .split(',')
1536 .map(wrapFontFamily)
1537 .join(',');
1538}
1539/**
1540 * https://developer.mozilla.org/en-US/docs/Web/CSS/font-style
1541 * @param fontStyle
1542 * @returns CSS font style.
1543 */
1544function prepareFontStyle(fontStyle) {
1545 if (!fontStyle) {
1546 return '';
1547 }
1548 const targetFontStyle = fontStyle.trim().toLowerCase();
1549 switch (targetFontStyle) {
1550 case 'normal':
1551 case 'italic':
1552 case 'oblique':
1553 case 'inherit':
1554 case 'initial':
1555 case 'unset':
1556 return targetFontStyle;
1557 default:
1558 if (/^oblique\s+(-|)\d+deg$/.test(targetFontStyle)) {
1559 return targetFontStyle;
1560 }
1561 return '';
1562 }
1563}
1564/**
1565 * https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
1566 * @param fontWeight
1567 * @returns CSS font weight.
1568 */
1569function prepareFontWeight(fontWeight) {
1570 if (!fontWeight) {
1571 return '';
1572 }
1573 const targetFontWeight = fontWeight.trim().toLowerCase();
1574 switch (targetFontWeight) {
1575 case 'normal':
1576 case 'bold':
1577 case 'lighter':
1578 case 'bolder':
1579 case 'inherit':
1580 case 'initial':
1581 case 'unset':
1582 return targetFontWeight;
1583 default:
1584 if (/^[\d.]+$/.test(targetFontWeight)) {
1585 return targetFontWeight;
1586 }
1587 return '';
1588 }
1589}
1590class Font {
1591 constructor(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) {
1592 const inheritFont = inherit
1593 ? typeof inherit === 'string'
1594 ? Font.parse(inherit)
1595 : inherit
1596 : {};
1597 this.fontFamily = fontFamily || inheritFont.fontFamily;
1598 this.fontSize = fontSize || inheritFont.fontSize;
1599 this.fontStyle = fontStyle || inheritFont.fontStyle;
1600 this.fontWeight = fontWeight || inheritFont.fontWeight;
1601 this.fontVariant = fontVariant || inheritFont.fontVariant;
1602 }
1603 static parse(font = '', inherit) {
1604 let fontStyle = '';
1605 let fontVariant = '';
1606 let fontWeight = '';
1607 let fontSize = '';
1608 let fontFamily = '';
1609 const parts = compressSpaces(font).trim().split(' ');
1610 const set = {
1611 fontSize: false,
1612 fontStyle: false,
1613 fontWeight: false,
1614 fontVariant: false
1615 };
1616 parts.forEach((part) => {
1617 switch (true) {
1618 case !set.fontStyle && Font.styles.includes(part):
1619 if (part !== 'inherit') {
1620 fontStyle = part;
1621 }
1622 set.fontStyle = true;
1623 break;
1624 case !set.fontVariant && Font.variants.includes(part):
1625 if (part !== 'inherit') {
1626 fontVariant = part;
1627 }
1628 set.fontStyle = true;
1629 set.fontVariant = true;
1630 break;
1631 case !set.fontWeight && Font.weights.includes(part):
1632 if (part !== 'inherit') {
1633 fontWeight = part;
1634 }
1635 set.fontStyle = true;
1636 set.fontVariant = true;
1637 set.fontWeight = true;
1638 break;
1639 case !set.fontSize:
1640 if (part !== 'inherit') {
1641 [fontSize] = part.split('/');
1642 }
1643 set.fontStyle = true;
1644 set.fontVariant = true;
1645 set.fontWeight = true;
1646 set.fontSize = true;
1647 break;
1648 default:
1649 if (part !== 'inherit') {
1650 fontFamily += part;
1651 }
1652 }
1653 });
1654 return new Font(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit);
1655 }
1656 toString() {
1657 return [
1658 prepareFontStyle(this.fontStyle),
1659 this.fontVariant,
1660 prepareFontWeight(this.fontWeight),
1661 this.fontSize,
1662 // Wrap fontFamily only on nodejs and only for canvas.ctx
1663 prepareFontFamily(this.fontFamily)
1664 ].join(' ').trim();
1665 }
1666}
1667Font.styles = 'normal|italic|oblique|inherit';
1668Font.variants = 'normal|small-caps|inherit';
1669Font.weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit';
1670
1671class BoundingBox {
1672 constructor(x1 = Number.NaN, y1 = Number.NaN, x2 = Number.NaN, y2 = Number.NaN) {
1673 this.x1 = x1;
1674 this.y1 = y1;
1675 this.x2 = x2;
1676 this.y2 = y2;
1677 this.addPoint(x1, y1);
1678 this.addPoint(x2, y2);
1679 }
1680 get x() {
1681 return this.x1;
1682 }
1683 get y() {
1684 return this.y1;
1685 }
1686 get width() {
1687 return this.x2 - this.x1;
1688 }
1689 get height() {
1690 return this.y2 - this.y1;
1691 }
1692 addPoint(x, y) {
1693 if (typeof x !== 'undefined') {
1694 if (isNaN(this.x1) || isNaN(this.x2)) {
1695 this.x1 = x;
1696 this.x2 = x;
1697 }
1698 if (x < this.x1) {
1699 this.x1 = x;
1700 }
1701 if (x > this.x2) {
1702 this.x2 = x;
1703 }
1704 }
1705 if (typeof y !== 'undefined') {
1706 if (isNaN(this.y1) || isNaN(this.y2)) {
1707 this.y1 = y;
1708 this.y2 = y;
1709 }
1710 if (y < this.y1) {
1711 this.y1 = y;
1712 }
1713 if (y > this.y2) {
1714 this.y2 = y;
1715 }
1716 }
1717 }
1718 addX(x) {
1719 this.addPoint(x, null);
1720 }
1721 addY(y) {
1722 this.addPoint(null, y);
1723 }
1724 addBoundingBox(boundingBox) {
1725 if (!boundingBox) {
1726 return;
1727 }
1728 const { x1, y1, x2, y2 } = boundingBox;
1729 this.addPoint(x1, y1);
1730 this.addPoint(x2, y2);
1731 }
1732 sumCubic(t, p0, p1, p2, p3) {
1733 return (Math.pow(1 - t, 3) * p0
1734 + 3 * Math.pow(1 - t, 2) * t * p1
1735 + 3 * (1 - t) * Math.pow(t, 2) * p2
1736 + Math.pow(t, 3) * p3);
1737 }
1738 bezierCurveAdd(forX, p0, p1, p2, p3) {
1739 const b = 6 * p0 - 12 * p1 + 6 * p2;
1740 const a = -3 * p0 + 9 * p1 - 9 * p2 + 3 * p3;
1741 const c = 3 * p1 - 3 * p0;
1742 if (a === 0) {
1743 if (b === 0) {
1744 return;
1745 }
1746 const t = -c / b;
1747 if (0 < t && t < 1) {
1748 if (forX) {
1749 this.addX(this.sumCubic(t, p0, p1, p2, p3));
1750 }
1751 else {
1752 this.addY(this.sumCubic(t, p0, p1, p2, p3));
1753 }
1754 }
1755 return;
1756 }
1757 const b2ac = Math.pow(b, 2) - 4 * c * a;
1758 if (b2ac < 0) {
1759 return;
1760 }
1761 const t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
1762 if (0 < t1 && t1 < 1) {
1763 if (forX) {
1764 this.addX(this.sumCubic(t1, p0, p1, p2, p3));
1765 }
1766 else {
1767 this.addY(this.sumCubic(t1, p0, p1, p2, p3));
1768 }
1769 }
1770 const t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
1771 if (0 < t2 && t2 < 1) {
1772 if (forX) {
1773 this.addX(this.sumCubic(t2, p0, p1, p2, p3));
1774 }
1775 else {
1776 this.addY(this.sumCubic(t2, p0, p1, p2, p3));
1777 }
1778 }
1779 }
1780 // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
1781 addBezierCurve(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
1782 this.addPoint(p0x, p0y);
1783 this.addPoint(p3x, p3y);
1784 this.bezierCurveAdd(true, p0x, p1x, p2x, p3x);
1785 this.bezierCurveAdd(false, p0y, p1y, p2y, p3y);
1786 }
1787 addQuadraticCurve(p0x, p0y, p1x, p1y, p2x, p2y) {
1788 const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0)
1789 const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0)
1790 const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0)
1791 const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0)
1792 this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y);
1793 }
1794 isPointInBox(x, y) {
1795 const { x1, y1, x2, y2 } = this;
1796 return (x1 <= x
1797 && x <= x2
1798 && y1 <= y
1799 && y <= y2);
1800 }
1801}
1802
1803class PathParser extends SVGPathData {
1804 constructor(path) {
1805 super(path
1806 // Fix spaces after signs.
1807 .replace(/([+\-.])\s+/gm, '$1')
1808 // Remove invalid part.
1809 .replace(/[^MmZzLlHhVvCcSsQqTtAae\d\s.,+-].*/g, ''));
1810 this.control = null;
1811 this.start = null;
1812 this.current = null;
1813 this.command = null;
1814 this.commands = this.commands;
1815 this.i = -1;
1816 this.previousCommand = null;
1817 this.points = [];
1818 this.angles = [];
1819 }
1820 reset() {
1821 this.i = -1;
1822 this.command = null;
1823 this.previousCommand = null;
1824 this.start = new Point(0, 0);
1825 this.control = new Point(0, 0);
1826 this.current = new Point(0, 0);
1827 this.points = [];
1828 this.angles = [];
1829 }
1830 isEnd() {
1831 const { i, commands } = this;
1832 return i >= commands.length - 1;
1833 }
1834 next() {
1835 const command = this.commands[++this.i];
1836 this.previousCommand = this.command;
1837 this.command = command;
1838 return command;
1839 }
1840 getPoint(xProp = 'x', yProp = 'y') {
1841 const point = new Point(this.command[xProp], this.command[yProp]);
1842 return this.makeAbsolute(point);
1843 }
1844 getAsControlPoint(xProp, yProp) {
1845 const point = this.getPoint(xProp, yProp);
1846 this.control = point;
1847 return point;
1848 }
1849 getAsCurrentPoint(xProp, yProp) {
1850 const point = this.getPoint(xProp, yProp);
1851 this.current = point;
1852 return point;
1853 }
1854 getReflectedControlPoint() {
1855 const previousCommand = this.previousCommand.type;
1856 if (previousCommand !== SVGPathData.CURVE_TO
1857 && previousCommand !== SVGPathData.SMOOTH_CURVE_TO
1858 && previousCommand !== SVGPathData.QUAD_TO
1859 && previousCommand !== SVGPathData.SMOOTH_QUAD_TO) {
1860 return this.current;
1861 }
1862 // reflect point
1863 const { current: { x: cx, y: cy }, control: { x: ox, y: oy } } = this;
1864 const point = new Point(2 * cx - ox, 2 * cy - oy);
1865 return point;
1866 }
1867 makeAbsolute(point) {
1868 if (this.command.relative) {
1869 const { x, y } = this.current;
1870 point.x += x;
1871 point.y += y;
1872 }
1873 return point;
1874 }
1875 addMarker(point, from, priorTo) {
1876 const { points, angles } = this;
1877 // if the last angle isn't filled in because we didn't have this point yet ...
1878 if (priorTo && angles.length > 0 && !angles[angles.length - 1]) {
1879 angles[angles.length - 1] = points[points.length - 1].angleTo(priorTo);
1880 }
1881 this.addMarkerAngle(point, from ? from.angleTo(point) : null);
1882 }
1883 addMarkerAngle(point, angle) {
1884 this.points.push(point);
1885 this.angles.push(angle);
1886 }
1887 getMarkerPoints() {
1888 return this.points;
1889 }
1890 getMarkerAngles() {
1891 const { angles } = this;
1892 const len = angles.length;
1893 for (let i = 0; i < len; i++) {
1894 if (!angles[i]) {
1895 for (let j = i + 1; j < len; j++) {
1896 if (angles[j]) {
1897 angles[i] = angles[j];
1898 break;
1899 }
1900 }
1901 }
1902 }
1903 return angles;
1904 }
1905}
1906
1907class RenderedElement extends Element {
1908 constructor() {
1909 super(...arguments);
1910 this.modifiedEmSizeStack = false;
1911 }
1912 calculateOpacity() {
1913 let opacity = 1.0;
1914 // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
1915 let element = this;
1916 while (element) {
1917 const opacityStyle = element.getStyle('opacity', false, true); // no ancestors on style call
1918 if (opacityStyle.hasValue(true)) {
1919 opacity *= opacityStyle.getNumber();
1920 }
1921 element = element.parent;
1922 }
1923 return opacity;
1924 }
1925 setContext(ctx, fromMeasure = false) {
1926 if (!fromMeasure) { // causes stack overflow when measuring text with gradients
1927 // fill
1928 const fillStyleProp = this.getStyle('fill');
1929 const fillOpacityStyleProp = this.getStyle('fill-opacity');
1930 const strokeStyleProp = this.getStyle('stroke');
1931 const strokeOpacityProp = this.getStyle('stroke-opacity');
1932 if (fillStyleProp.isUrlDefinition()) {
1933 const fillStyle = fillStyleProp.getFillStyleDefinition(this, fillOpacityStyleProp);
1934 if (fillStyle) {
1935 ctx.fillStyle = fillStyle;
1936 }
1937 }
1938 else if (fillStyleProp.hasValue()) {
1939 if (fillStyleProp.getString() === 'currentColor') {
1940 fillStyleProp.setValue(this.getStyle('color').getColor());
1941 }
1942 const fillStyle = fillStyleProp.getColor();
1943 if (fillStyle !== 'inherit') {
1944 ctx.fillStyle = fillStyle === 'none'
1945 ? 'rgba(0,0,0,0)'
1946 : fillStyle;
1947 }
1948 }
1949 if (fillOpacityStyleProp.hasValue()) {
1950 const fillStyle = new Property(this.document, 'fill', ctx.fillStyle)
1951 .addOpacity(fillOpacityStyleProp)
1952 .getColor();
1953 ctx.fillStyle = fillStyle;
1954 }
1955 // stroke
1956 if (strokeStyleProp.isUrlDefinition()) {
1957 const strokeStyle = strokeStyleProp.getFillStyleDefinition(this, strokeOpacityProp);
1958 if (strokeStyle) {
1959 ctx.strokeStyle = strokeStyle;
1960 }
1961 }
1962 else if (strokeStyleProp.hasValue()) {
1963 if (strokeStyleProp.getString() === 'currentColor') {
1964 strokeStyleProp.setValue(this.getStyle('color').getColor());
1965 }
1966 const strokeStyle = strokeStyleProp.getString();
1967 if (strokeStyle !== 'inherit') {
1968 ctx.strokeStyle = strokeStyle === 'none'
1969 ? 'rgba(0,0,0,0)'
1970 : strokeStyle;
1971 }
1972 }
1973 if (strokeOpacityProp.hasValue()) {
1974 const strokeStyle = new Property(this.document, 'stroke', ctx.strokeStyle)
1975 .addOpacity(strokeOpacityProp)
1976 .getString();
1977 ctx.strokeStyle = strokeStyle;
1978 }
1979 const strokeWidthStyleProp = this.getStyle('stroke-width');
1980 if (strokeWidthStyleProp.hasValue()) {
1981 const newLineWidth = strokeWidthStyleProp.getPixels();
1982 ctx.lineWidth = !newLineWidth
1983 ? PSEUDO_ZERO // browsers don't respect 0 (or node-canvas? :-)
1984 : newLineWidth;
1985 }
1986 const strokeLinecapStyleProp = this.getStyle('stroke-linecap');
1987 const strokeLinejoinStyleProp = this.getStyle('stroke-linejoin');
1988 const strokeMiterlimitProp = this.getStyle('stroke-miterlimit');
1989 // NEED TEST
1990 // const pointOrderStyleProp = this.getStyle('paint-order');
1991 const strokeDasharrayStyleProp = this.getStyle('stroke-dasharray');
1992 const strokeDashoffsetProp = this.getStyle('stroke-dashoffset');
1993 if (strokeLinecapStyleProp.hasValue()) {
1994 ctx.lineCap = strokeLinecapStyleProp.getString();
1995 }
1996 if (strokeLinejoinStyleProp.hasValue()) {
1997 ctx.lineJoin = strokeLinejoinStyleProp.getString();
1998 }
1999 if (strokeMiterlimitProp.hasValue()) {
2000 ctx.miterLimit = strokeMiterlimitProp.getNumber();
2001 }
2002 // NEED TEST
2003 // if (pointOrderStyleProp.hasValue()) {
2004 // // ?
2005 // ctx.paintOrder = pointOrderStyleProp.getValue();
2006 // }
2007 if (strokeDasharrayStyleProp.hasValue() && strokeDasharrayStyleProp.getString() !== 'none') {
2008 const gaps = toNumbers(strokeDasharrayStyleProp.getString());
2009 if (typeof ctx.setLineDash !== 'undefined') {
2010 ctx.setLineDash(gaps);
2011 }
2012 else
2013 // @ts-expect-error Handle browser prefix.
2014 if (typeof ctx.webkitLineDash !== 'undefined') {
2015 // @ts-expect-error Handle browser prefix.
2016 ctx.webkitLineDash = gaps;
2017 }
2018 else
2019 // @ts-expect-error Handle browser prefix.
2020 if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) {
2021 // @ts-expect-error Handle browser prefix.
2022 ctx.mozDash = gaps;
2023 }
2024 const offset = strokeDashoffsetProp.getPixels();
2025 if (typeof ctx.lineDashOffset !== 'undefined') {
2026 ctx.lineDashOffset = offset;
2027 }
2028 else
2029 // @ts-expect-error Handle browser prefix.
2030 if (typeof ctx.webkitLineDashOffset !== 'undefined') {
2031 // @ts-expect-error Handle browser prefix.
2032 ctx.webkitLineDashOffset = offset;
2033 }
2034 else
2035 // @ts-expect-error Handle browser prefix.
2036 if (typeof ctx.mozDashOffset !== 'undefined') {
2037 // @ts-expect-error Handle browser prefix.
2038 ctx.mozDashOffset = offset;
2039 }
2040 }
2041 }
2042 // font
2043 this.modifiedEmSizeStack = false;
2044 if (typeof ctx.font !== 'undefined') {
2045 const fontStyleProp = this.getStyle('font');
2046 const fontStyleStyleProp = this.getStyle('font-style');
2047 const fontVariantStyleProp = this.getStyle('font-variant');
2048 const fontWeightStyleProp = this.getStyle('font-weight');
2049 const fontSizeStyleProp = this.getStyle('font-size');
2050 const fontFamilyStyleProp = this.getStyle('font-family');
2051 const font = new Font(fontStyleStyleProp.getString(), fontVariantStyleProp.getString(), fontWeightStyleProp.getString(), fontSizeStyleProp.hasValue()
2052 ? `${fontSizeStyleProp.getPixels(true)}px`
2053 : '', fontFamilyStyleProp.getString(), Font.parse(fontStyleProp.getString(), ctx.font));
2054 fontStyleStyleProp.setValue(font.fontStyle);
2055 fontVariantStyleProp.setValue(font.fontVariant);
2056 fontWeightStyleProp.setValue(font.fontWeight);
2057 fontSizeStyleProp.setValue(font.fontSize);
2058 fontFamilyStyleProp.setValue(font.fontFamily);
2059 ctx.font = font.toString();
2060 if (fontSizeStyleProp.isPixels()) {
2061 this.document.emSize = fontSizeStyleProp.getPixels();
2062 this.modifiedEmSizeStack = true;
2063 }
2064 }
2065 if (!fromMeasure) {
2066 // effects
2067 this.applyEffects(ctx);
2068 // opacity
2069 ctx.globalAlpha = this.calculateOpacity();
2070 }
2071 }
2072 clearContext(ctx) {
2073 super.clearContext(ctx);
2074 if (this.modifiedEmSizeStack) {
2075 this.document.popEmSize();
2076 }
2077 }
2078}
2079
2080class PathElement extends RenderedElement {
2081 constructor(document, node, captureTextNodes) {
2082 super(document, node, captureTextNodes);
2083 this.type = 'path';
2084 this.pathParser = null;
2085 this.pathParser = new PathParser(this.getAttribute('d').getString());
2086 }
2087 path(ctx) {
2088 const { pathParser } = this;
2089 const boundingBox = new BoundingBox();
2090 pathParser.reset();
2091 if (ctx) {
2092 ctx.beginPath();
2093 }
2094 while (!pathParser.isEnd()) {
2095 switch (pathParser.next().type) {
2096 case PathParser.MOVE_TO:
2097 this.pathM(ctx, boundingBox);
2098 break;
2099 case PathParser.LINE_TO:
2100 this.pathL(ctx, boundingBox);
2101 break;
2102 case PathParser.HORIZ_LINE_TO:
2103 this.pathH(ctx, boundingBox);
2104 break;
2105 case PathParser.VERT_LINE_TO:
2106 this.pathV(ctx, boundingBox);
2107 break;
2108 case PathParser.CURVE_TO:
2109 this.pathC(ctx, boundingBox);
2110 break;
2111 case PathParser.SMOOTH_CURVE_TO:
2112 this.pathS(ctx, boundingBox);
2113 break;
2114 case PathParser.QUAD_TO:
2115 this.pathQ(ctx, boundingBox);
2116 break;
2117 case PathParser.SMOOTH_QUAD_TO:
2118 this.pathT(ctx, boundingBox);
2119 break;
2120 case PathParser.ARC:
2121 this.pathA(ctx, boundingBox);
2122 break;
2123 case PathParser.CLOSE_PATH:
2124 this.pathZ(ctx, boundingBox);
2125 break;
2126 }
2127 }
2128 return boundingBox;
2129 }
2130 getBoundingBox(_) {
2131 return this.path();
2132 }
2133 getMarkers() {
2134 const { pathParser } = this;
2135 const points = pathParser.getMarkerPoints();
2136 const angles = pathParser.getMarkerAngles();
2137 const markers = points.map((point, i) => [
2138 point,
2139 angles[i]
2140 ]);
2141 return markers;
2142 }
2143 renderChildren(ctx) {
2144 this.path(ctx);
2145 this.document.screen.mouse.checkPath(this, ctx);
2146 const fillRuleStyleProp = this.getStyle('fill-rule');
2147 if (ctx.fillStyle !== '') {
2148 if (fillRuleStyleProp.getString('inherit') !== 'inherit') {
2149 ctx.fill(fillRuleStyleProp.getString());
2150 }
2151 else {
2152 ctx.fill();
2153 }
2154 }
2155 if (ctx.strokeStyle !== '') {
2156 if (this.getAttribute('vector-effect').getString() === 'non-scaling-stroke') {
2157 ctx.save();
2158 ctx.setTransform(1, 0, 0, 1, 0, 0);
2159 ctx.stroke();
2160 ctx.restore();
2161 }
2162 else {
2163 ctx.stroke();
2164 }
2165 }
2166 const markers = this.getMarkers();
2167 if (markers) {
2168 const markersLastIndex = markers.length - 1;
2169 const markerStartStyleProp = this.getStyle('marker-start');
2170 const markerMidStyleProp = this.getStyle('marker-mid');
2171 const markerEndStyleProp = this.getStyle('marker-end');
2172 if (markerStartStyleProp.isUrlDefinition()) {
2173 const marker = markerStartStyleProp.getDefinition();
2174 const [point, angle] = markers[0];
2175 marker.render(ctx, point, angle);
2176 }
2177 if (markerMidStyleProp.isUrlDefinition()) {
2178 const marker = markerMidStyleProp.getDefinition();
2179 for (let i = 1; i < markersLastIndex; i++) {
2180 const [point, angle] = markers[i];
2181 marker.render(ctx, point, angle);
2182 }
2183 }
2184 if (markerEndStyleProp.isUrlDefinition()) {
2185 const marker = markerEndStyleProp.getDefinition();
2186 const [point, angle] = markers[markersLastIndex];
2187 marker.render(ctx, point, angle);
2188 }
2189 }
2190 }
2191 static pathM(pathParser) {
2192 const point = pathParser.getAsCurrentPoint();
2193 pathParser.start = pathParser.current;
2194 return {
2195 point
2196 };
2197 }
2198 pathM(ctx, boundingBox) {
2199 const { pathParser } = this;
2200 const { point } = PathElement.pathM(pathParser);
2201 const { x, y } = point;
2202 pathParser.addMarker(point);
2203 boundingBox.addPoint(x, y);
2204 if (ctx) {
2205 ctx.moveTo(x, y);
2206 }
2207 }
2208 static pathL(pathParser) {
2209 const { current } = pathParser;
2210 const point = pathParser.getAsCurrentPoint();
2211 return {
2212 current,
2213 point
2214 };
2215 }
2216 pathL(ctx, boundingBox) {
2217 const { pathParser } = this;
2218 const { current, point } = PathElement.pathL(pathParser);
2219 const { x, y } = point;
2220 pathParser.addMarker(point, current);
2221 boundingBox.addPoint(x, y);
2222 if (ctx) {
2223 ctx.lineTo(x, y);
2224 }
2225 }
2226 static pathH(pathParser) {
2227 const { current, command } = pathParser;
2228 const point = new Point((command.relative ? current.x : 0) + command.x, current.y);
2229 pathParser.current = point;
2230 return {
2231 current,
2232 point
2233 };
2234 }
2235 pathH(ctx, boundingBox) {
2236 const { pathParser } = this;
2237 const { current, point } = PathElement.pathH(pathParser);
2238 const { x, y } = point;
2239 pathParser.addMarker(point, current);
2240 boundingBox.addPoint(x, y);
2241 if (ctx) {
2242 ctx.lineTo(x, y);
2243 }
2244 }
2245 static pathV(pathParser) {
2246 const { current, command } = pathParser;
2247 const point = new Point(current.x, (command.relative ? current.y : 0) + command.y);
2248 pathParser.current = point;
2249 return {
2250 current,
2251 point
2252 };
2253 }
2254 pathV(ctx, boundingBox) {
2255 const { pathParser } = this;
2256 const { current, point } = PathElement.pathV(pathParser);
2257 const { x, y } = point;
2258 pathParser.addMarker(point, current);
2259 boundingBox.addPoint(x, y);
2260 if (ctx) {
2261 ctx.lineTo(x, y);
2262 }
2263 }
2264 static pathC(pathParser) {
2265 const { current } = pathParser;
2266 const point = pathParser.getPoint('x1', 'y1');
2267 const controlPoint = pathParser.getAsControlPoint('x2', 'y2');
2268 const currentPoint = pathParser.getAsCurrentPoint();
2269 return {
2270 current,
2271 point,
2272 controlPoint,
2273 currentPoint
2274 };
2275 }
2276 pathC(ctx, boundingBox) {
2277 const { pathParser } = this;
2278 const { current, point, controlPoint, currentPoint } = PathElement.pathC(pathParser);
2279 pathParser.addMarker(currentPoint, controlPoint, point);
2280 boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2281 if (ctx) {
2282 ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2283 }
2284 }
2285 static pathS(pathParser) {
2286 const { current } = pathParser;
2287 const point = pathParser.getReflectedControlPoint();
2288 const controlPoint = pathParser.getAsControlPoint('x2', 'y2');
2289 const currentPoint = pathParser.getAsCurrentPoint();
2290 return {
2291 current,
2292 point,
2293 controlPoint,
2294 currentPoint
2295 };
2296 }
2297 pathS(ctx, boundingBox) {
2298 const { pathParser } = this;
2299 const { current, point, controlPoint, currentPoint } = PathElement.pathS(pathParser);
2300 pathParser.addMarker(currentPoint, controlPoint, point);
2301 boundingBox.addBezierCurve(current.x, current.y, point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2302 if (ctx) {
2303 ctx.bezierCurveTo(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2304 }
2305 }
2306 static pathQ(pathParser) {
2307 const { current } = pathParser;
2308 const controlPoint = pathParser.getAsControlPoint('x1', 'y1');
2309 const currentPoint = pathParser.getAsCurrentPoint();
2310 return {
2311 current,
2312 controlPoint,
2313 currentPoint
2314 };
2315 }
2316 pathQ(ctx, boundingBox) {
2317 const { pathParser } = this;
2318 const { current, controlPoint, currentPoint } = PathElement.pathQ(pathParser);
2319 pathParser.addMarker(currentPoint, controlPoint, controlPoint);
2320 boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2321 if (ctx) {
2322 ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2323 }
2324 }
2325 static pathT(pathParser) {
2326 const { current } = pathParser;
2327 const controlPoint = pathParser.getReflectedControlPoint();
2328 pathParser.control = controlPoint;
2329 const currentPoint = pathParser.getAsCurrentPoint();
2330 return {
2331 current,
2332 controlPoint,
2333 currentPoint
2334 };
2335 }
2336 pathT(ctx, boundingBox) {
2337 const { pathParser } = this;
2338 const { current, controlPoint, currentPoint } = PathElement.pathT(pathParser);
2339 pathParser.addMarker(currentPoint, controlPoint, controlPoint);
2340 boundingBox.addQuadraticCurve(current.x, current.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2341 if (ctx) {
2342 ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
2343 }
2344 }
2345 static pathA(pathParser) {
2346 const { current, command } = pathParser;
2347 let { rX, rY, xRot, lArcFlag, sweepFlag } = command;
2348 const xAxisRotation = xRot * (Math.PI / 180.0);
2349 const currentPoint = pathParser.getAsCurrentPoint();
2350 // Conversion from endpoint to center parameterization
2351 // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
2352 // x1', y1'
2353 const currp = new Point(Math.cos(xAxisRotation) * (current.x - currentPoint.x) / 2.0
2354 + Math.sin(xAxisRotation) * (current.y - currentPoint.y) / 2.0, -Math.sin(xAxisRotation) * (current.x - currentPoint.x) / 2.0
2355 + Math.cos(xAxisRotation) * (current.y - currentPoint.y) / 2.0);
2356 // adjust radii
2357 const l = Math.pow(currp.x, 2) / Math.pow(rX, 2)
2358 + Math.pow(currp.y, 2) / Math.pow(rY, 2);
2359 if (l > 1) {
2360 rX *= Math.sqrt(l);
2361 rY *= Math.sqrt(l);
2362 }
2363 // cx', cy'
2364 let s = (lArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(((Math.pow(rX, 2) * Math.pow(rY, 2))
2365 - (Math.pow(rX, 2) * Math.pow(currp.y, 2))
2366 - (Math.pow(rY, 2) * Math.pow(currp.x, 2))) / (Math.pow(rX, 2) * Math.pow(currp.y, 2)
2367 + Math.pow(rY, 2) * Math.pow(currp.x, 2)));
2368 if (isNaN(s)) {
2369 s = 0;
2370 }
2371 const cpp = new Point(s * rX * currp.y / rY, s * -rY * currp.x / rX);
2372 // cx, cy
2373 const centp = new Point((current.x + currentPoint.x) / 2.0
2374 + Math.cos(xAxisRotation) * cpp.x
2375 - Math.sin(xAxisRotation) * cpp.y, (current.y + currentPoint.y) / 2.0
2376 + Math.sin(xAxisRotation) * cpp.x
2377 + Math.cos(xAxisRotation) * cpp.y);
2378 // initial angle
2379 const a1 = vectorsAngle([1, 0], [(currp.x - cpp.x) / rX, (currp.y - cpp.y) / rY]); // θ1
2380 // angle delta
2381 const u = [(currp.x - cpp.x) / rX, (currp.y - cpp.y) / rY];
2382 const v = [(-currp.x - cpp.x) / rX, (-currp.y - cpp.y) / rY];
2383 let ad = vectorsAngle(u, v); // Δθ
2384 if (vectorsRatio(u, v) <= -1) {
2385 ad = Math.PI;
2386 }
2387 if (vectorsRatio(u, v) >= 1) {
2388 ad = 0;
2389 }
2390 return {
2391 currentPoint,
2392 rX,
2393 rY,
2394 sweepFlag,
2395 xAxisRotation,
2396 centp,
2397 a1,
2398 ad
2399 };
2400 }
2401 pathA(ctx, boundingBox) {
2402 const { pathParser } = this;
2403 const { currentPoint, rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser);
2404 // for markers
2405 const dir = 1 - sweepFlag ? 1.0 : -1.0;
2406 const ah = a1 + dir * (ad / 2.0);
2407 const halfWay = new Point(centp.x + rX * Math.cos(ah), centp.y + rY * Math.sin(ah));
2408 pathParser.addMarkerAngle(halfWay, ah - dir * Math.PI / 2);
2409 pathParser.addMarkerAngle(currentPoint, ah - dir * Math.PI);
2410 boundingBox.addPoint(currentPoint.x, currentPoint.y); // TODO: this is too naive, make it better
2411 if (ctx && !isNaN(a1) && !isNaN(ad)) {
2412 const r = rX > rY ? rX : rY;
2413 const sx = rX > rY ? 1 : rX / rY;
2414 const sy = rX > rY ? rY / rX : 1;
2415 ctx.translate(centp.x, centp.y);
2416 ctx.rotate(xAxisRotation);
2417 ctx.scale(sx, sy);
2418 ctx.arc(0, 0, r, a1, a1 + ad, Boolean(1 - sweepFlag));
2419 ctx.scale(1 / sx, 1 / sy);
2420 ctx.rotate(-xAxisRotation);
2421 ctx.translate(-centp.x, -centp.y);
2422 }
2423 }
2424 static pathZ(pathParser) {
2425 pathParser.current = pathParser.start;
2426 }
2427 pathZ(ctx, boundingBox) {
2428 PathElement.pathZ(this.pathParser);
2429 if (ctx) {
2430 // only close path if it is not a straight line
2431 if (boundingBox.x1 !== boundingBox.x2
2432 && boundingBox.y1 !== boundingBox.y2) {
2433 ctx.closePath();
2434 }
2435 }
2436 }
2437}
2438
2439class GlyphElement extends PathElement {
2440 constructor(document, node, captureTextNodes) {
2441 super(document, node, captureTextNodes);
2442 this.type = 'glyph';
2443 this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber();
2444 this.unicode = this.getAttribute('unicode').getString();
2445 this.arabicForm = this.getAttribute('arabic-form').getString();
2446 }
2447}
2448
2449class TextElement extends RenderedElement {
2450 constructor(document, node, captureTextNodes) {
2451 super(document, node, new.target === TextElement
2452 ? true
2453 : captureTextNodes);
2454 this.type = 'text';
2455 this.x = 0;
2456 this.y = 0;
2457 this.measureCache = -1;
2458 }
2459 setContext(ctx, fromMeasure = false) {
2460 super.setContext(ctx, fromMeasure);
2461 const textBaseline = this.getStyle('dominant-baseline').getTextBaseline()
2462 || this.getStyle('alignment-baseline').getTextBaseline();
2463 if (textBaseline) {
2464 ctx.textBaseline = textBaseline;
2465 }
2466 }
2467 initializeCoordinates() {
2468 this.x = 0;
2469 this.y = 0;
2470 this.leafTexts = [];
2471 this.textChunkStart = 0;
2472 this.minX = Number.POSITIVE_INFINITY;
2473 this.maxX = Number.NEGATIVE_INFINITY;
2474 }
2475 getBoundingBox(ctx) {
2476 if (this.type !== 'text') {
2477 return this.getTElementBoundingBox(ctx);
2478 }
2479 // first, calculate child positions
2480 this.initializeCoordinates();
2481 this.adjustChildCoordinatesRecursive(ctx);
2482 let boundingBox = null;
2483 // then calculate bounding box
2484 this.children.forEach((_, i) => {
2485 const childBoundingBox = this.getChildBoundingBox(ctx, this, this, i);
2486 if (!boundingBox) {
2487 boundingBox = childBoundingBox;
2488 }
2489 else {
2490 boundingBox.addBoundingBox(childBoundingBox);
2491 }
2492 });
2493 return boundingBox;
2494 }
2495 getFontSize() {
2496 const { document, parent } = this;
2497 const inheritFontSize = Font.parse(document.ctx.font).fontSize;
2498 const fontSize = parent.getStyle('font-size').getNumber(inheritFontSize);
2499 return fontSize;
2500 }
2501 getTElementBoundingBox(ctx) {
2502 const fontSize = this.getFontSize();
2503 return new BoundingBox(this.x, this.y - fontSize, this.x + this.measureText(ctx), this.y);
2504 }
2505 getGlyph(font, text, i) {
2506 const char = text[i];
2507 let glyph = null;
2508 if (font.isArabic) {
2509 const len = text.length;
2510 const prevChar = text[i - 1];
2511 const nextChar = text[i + 1];
2512 let arabicForm = 'isolated';
2513 if ((i === 0 || prevChar === ' ') && i < len - 1 && nextChar !== ' ') {
2514 arabicForm = 'terminal';
2515 }
2516 if (i > 0 && prevChar !== ' ' && i < len - 1 && nextChar !== ' ') {
2517 arabicForm = 'medial';
2518 }
2519 if (i > 0 && prevChar !== ' ' && (i === len - 1 || nextChar === ' ')) {
2520 arabicForm = 'initial';
2521 }
2522 if (typeof font.glyphs[char] !== 'undefined') {
2523 // NEED TEST
2524 const maybeGlyph = font.glyphs[char];
2525 glyph = maybeGlyph instanceof GlyphElement
2526 ? maybeGlyph
2527 : maybeGlyph[arabicForm];
2528 }
2529 }
2530 else {
2531 glyph = font.glyphs[char];
2532 }
2533 if (!glyph) {
2534 glyph = font.missingGlyph;
2535 }
2536 return glyph;
2537 }
2538 getText() {
2539 return '';
2540 }
2541 getTextFromNode(node) {
2542 const textNode = node || this.node;
2543 const childNodes = Array.from(textNode.parentNode.childNodes);
2544 const index = childNodes.indexOf(textNode);
2545 const lastIndex = childNodes.length - 1;
2546 let text = compressSpaces(
2547 // textNode.value
2548 // || textNode.text
2549 textNode.textContent
2550 || '');
2551 if (index === 0) {
2552 text = trimLeft(text);
2553 }
2554 if (index === lastIndex) {
2555 text = trimRight(text);
2556 }
2557 return text;
2558 }
2559 renderChildren(ctx) {
2560 if (this.type !== 'text') {
2561 this.renderTElementChildren(ctx);
2562 return;
2563 }
2564 // first, calculate child positions
2565 this.initializeCoordinates();
2566 this.adjustChildCoordinatesRecursive(ctx);
2567 // then render
2568 this.children.forEach((_, i) => {
2569 this.renderChild(ctx, this, this, i);
2570 });
2571 const { mouse } = this.document.screen;
2572 // Do not calc bounding box if mouse is not working.
2573 if (mouse.isWorking()) {
2574 mouse.checkBoundingBox(this, this.getBoundingBox(ctx));
2575 }
2576 }
2577 renderTElementChildren(ctx) {
2578 const { document, parent } = this;
2579 const renderText = this.getText();
2580 const customFont = parent.getStyle('font-family').getDefinition();
2581 if (customFont) {
2582 const { unitsPerEm } = customFont.fontFace;
2583 const ctxFont = Font.parse(document.ctx.font);
2584 const fontSize = parent.getStyle('font-size').getNumber(ctxFont.fontSize);
2585 const fontStyle = parent.getStyle('font-style').getString(ctxFont.fontStyle);
2586 const scale = fontSize / unitsPerEm;
2587 const text = customFont.isRTL
2588 ? renderText.split('').reverse().join('')
2589 : renderText;
2590 const dx = toNumbers(parent.getAttribute('dx').getString());
2591 const len = text.length;
2592 for (let i = 0; i < len; i++) {
2593 const glyph = this.getGlyph(customFont, text, i);
2594 ctx.translate(this.x, this.y);
2595 ctx.scale(scale, -scale);
2596 const lw = ctx.lineWidth;
2597 ctx.lineWidth = ctx.lineWidth * unitsPerEm / fontSize;
2598 if (fontStyle === 'italic') {
2599 ctx.transform(1, 0, .4, 1, 0, 0);
2600 }
2601 glyph.render(ctx);
2602 if (fontStyle === 'italic') {
2603 ctx.transform(1, 0, -.4, 1, 0, 0);
2604 }
2605 ctx.lineWidth = lw;
2606 ctx.scale(1 / scale, -1 / scale);
2607 ctx.translate(-this.x, -this.y);
2608 this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / unitsPerEm;
2609 if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {
2610 this.x += dx[i];
2611 }
2612 }
2613 return;
2614 }
2615 const { x, y } = this;
2616 // NEED TEST
2617 // if (ctx.paintOrder === 'stroke') {
2618 // if (ctx.strokeStyle) {
2619 // ctx.strokeText(renderText, x, y);
2620 // }
2621 // if (ctx.fillStyle) {
2622 // ctx.fillText(renderText, x, y);
2623 // }
2624 // } else {
2625 if (ctx.fillStyle) {
2626 ctx.fillText(renderText, x, y);
2627 }
2628 if (ctx.strokeStyle) {
2629 ctx.strokeText(renderText, x, y);
2630 }
2631 // }
2632 }
2633 applyAnchoring() {
2634 if (this.textChunkStart >= this.leafTexts.length) {
2635 return;
2636 }
2637 // This is basically the "Apply anchoring" part of https://www.w3.org/TR/SVG2/text.html#TextLayoutAlgorithm.
2638 // The difference is that we apply the anchoring as soon as a chunk is finished. This saves some extra looping.
2639 // Vertical text is not supported.
2640 const firstElement = this.leafTexts[this.textChunkStart];
2641 const textAnchor = firstElement.getStyle('text-anchor').getString('start');
2642 const isRTL = false; // we treat RTL like LTR
2643 let shift = 0;
2644 if (textAnchor === 'start' && !isRTL || textAnchor === 'end' && isRTL) {
2645 shift = firstElement.x - this.minX;
2646 }
2647 else if (textAnchor === 'end' && !isRTL || textAnchor === 'start' && isRTL) {
2648 shift = firstElement.x - this.maxX;
2649 }
2650 else {
2651 shift = firstElement.x - (this.minX + this.maxX) / 2;
2652 }
2653 for (let i = this.textChunkStart; i < this.leafTexts.length; i++) {
2654 this.leafTexts[i].x += shift;
2655 }
2656 // start new chunk
2657 this.minX = Number.POSITIVE_INFINITY;
2658 this.maxX = Number.NEGATIVE_INFINITY;
2659 this.textChunkStart = this.leafTexts.length;
2660 }
2661 adjustChildCoordinatesRecursive(ctx) {
2662 this.children.forEach((_, i) => {
2663 this.adjustChildCoordinatesRecursiveCore(ctx, this, this, i);
2664 });
2665 this.applyAnchoring();
2666 }
2667 adjustChildCoordinatesRecursiveCore(ctx, textParent, parent, i) {
2668 const child = parent.children[i];
2669 if (child.children.length > 0) {
2670 child.children.forEach((_, i) => {
2671 textParent.adjustChildCoordinatesRecursiveCore(ctx, textParent, child, i);
2672 });
2673 }
2674 else {
2675 // only leafs are relevant
2676 this.adjustChildCoordinates(ctx, textParent, parent, i);
2677 }
2678 }
2679 adjustChildCoordinates(ctx, textParent, parent, i) {
2680 const child = parent.children[i];
2681 if (typeof child.measureText !== 'function') {
2682 return child;
2683 }
2684 ctx.save();
2685 child.setContext(ctx, true);
2686 const xAttr = child.getAttribute('x');
2687 const yAttr = child.getAttribute('y');
2688 const dxAttr = child.getAttribute('dx');
2689 const dyAttr = child.getAttribute('dy');
2690 const customFont = child.getStyle('font-family').getDefinition();
2691 const isRTL = Boolean(customFont) && customFont.isRTL;
2692 if (i === 0) {
2693 // First children inherit attributes from parent(s). Positional attributes
2694 // are only inherited from a parent to it's first child.
2695 if (!xAttr.hasValue()) {
2696 xAttr.setValue(child.getInheritedAttribute('x'));
2697 }
2698 if (!yAttr.hasValue()) {
2699 yAttr.setValue(child.getInheritedAttribute('y'));
2700 }
2701 if (!dxAttr.hasValue()) {
2702 dxAttr.setValue(child.getInheritedAttribute('dx'));
2703 }
2704 if (!dyAttr.hasValue()) {
2705 dyAttr.setValue(child.getInheritedAttribute('dy'));
2706 }
2707 }
2708 const width = child.measureText(ctx);
2709 if (isRTL) {
2710 textParent.x -= width;
2711 }
2712 if (xAttr.hasValue()) {
2713 // an "x" attribute marks the start of a new chunk
2714 textParent.applyAnchoring();
2715 child.x = xAttr.getPixels('x');
2716 if (dxAttr.hasValue()) {
2717 child.x += dxAttr.getPixels('x');
2718 }
2719 }
2720 else {
2721 if (dxAttr.hasValue()) {
2722 textParent.x += dxAttr.getPixels('x');
2723 }
2724 child.x = textParent.x;
2725 }
2726 textParent.x = child.x;
2727 if (!isRTL) {
2728 textParent.x += width;
2729 }
2730 if (yAttr.hasValue()) {
2731 child.y = yAttr.getPixels('y');
2732 if (dyAttr.hasValue()) {
2733 child.y += dyAttr.getPixels('y');
2734 }
2735 }
2736 else {
2737 if (dyAttr.hasValue()) {
2738 textParent.y += dyAttr.getPixels('y');
2739 }
2740 child.y = textParent.y;
2741 }
2742 textParent.y = child.y;
2743 // update the current chunk and it's bounds
2744 textParent.leafTexts.push(child);
2745 textParent.minX = Math.min(textParent.minX, child.x, child.x + width);
2746 textParent.maxX = Math.max(textParent.maxX, child.x, child.x + width);
2747 child.clearContext(ctx);
2748 ctx.restore();
2749 return child;
2750 }
2751 getChildBoundingBox(ctx, textParent, parent, i) {
2752 const child = parent.children[i];
2753 // not a text node?
2754 if (typeof child.getBoundingBox !== 'function') {
2755 return null;
2756 }
2757 const boundingBox = child.getBoundingBox(ctx);
2758 if (!boundingBox) {
2759 return null;
2760 }
2761 child.children.forEach((_, i) => {
2762 const childBoundingBox = textParent.getChildBoundingBox(ctx, textParent, child, i);
2763 boundingBox.addBoundingBox(childBoundingBox);
2764 });
2765 return boundingBox;
2766 }
2767 renderChild(ctx, textParent, parent, i) {
2768 const child = parent.children[i];
2769 child.render(ctx);
2770 child.children.forEach((_, i) => {
2771 textParent.renderChild(ctx, textParent, child, i);
2772 });
2773 }
2774 measureText(ctx) {
2775 const { measureCache } = this;
2776 if (~measureCache) {
2777 return measureCache;
2778 }
2779 const renderText = this.getText();
2780 const measure = this.measureTargetText(ctx, renderText);
2781 this.measureCache = measure;
2782 return measure;
2783 }
2784 measureTargetText(ctx, targetText) {
2785 if (!targetText.length) {
2786 return 0;
2787 }
2788 const { parent } = this;
2789 const customFont = parent.getStyle('font-family').getDefinition();
2790 if (customFont) {
2791 const fontSize = this.getFontSize();
2792 const text = customFont.isRTL
2793 ? targetText.split('').reverse().join('')
2794 : targetText;
2795 const dx = toNumbers(parent.getAttribute('dx').getString());
2796 const len = text.length;
2797 let measure = 0;
2798 for (let i = 0; i < len; i++) {
2799 const glyph = this.getGlyph(customFont, text, i);
2800 measure += (glyph.horizAdvX || customFont.horizAdvX)
2801 * fontSize
2802 / customFont.fontFace.unitsPerEm;
2803 if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {
2804 measure += dx[i];
2805 }
2806 }
2807 return measure;
2808 }
2809 if (!ctx.measureText) {
2810 return targetText.length * 10;
2811 }
2812 ctx.save();
2813 this.setContext(ctx, true);
2814 const { width: measure } = ctx.measureText(targetText);
2815 this.clearContext(ctx);
2816 ctx.restore();
2817 return measure;
2818 }
2819 /**
2820 * Inherits positional attributes from {@link TextElement} parent(s). Attributes
2821 * are only inherited from a parent to its first child.
2822 * @param name - The attribute name.
2823 * @returns The attribute value or null.
2824 */
2825 getInheritedAttribute(name) {
2826 // eslint-disable-next-line @typescript-eslint/no-this-alias,consistent-this
2827 let current = this;
2828 while (current instanceof TextElement && current.isFirstChild()) {
2829 const parentAttr = current.parent.getAttribute(name);
2830 if (parentAttr.hasValue(true)) {
2831 return parentAttr.getValue('0');
2832 }
2833 current = current.parent;
2834 }
2835 return null;
2836 }
2837}
2838
2839class TSpanElement extends TextElement {
2840 constructor(document, node, captureTextNodes) {
2841 super(document, node, new.target === TSpanElement
2842 ? true
2843 : captureTextNodes);
2844 this.type = 'tspan';
2845 // if this node has children, then they own the text
2846 this.text = this.children.length > 0
2847 ? ''
2848 : this.getTextFromNode();
2849 }
2850 getText() {
2851 return this.text;
2852 }
2853}
2854
2855class TextNode extends TSpanElement {
2856 constructor() {
2857 super(...arguments);
2858 this.type = 'textNode';
2859 }
2860}
2861
2862class SVGElement extends RenderedElement {
2863 constructor() {
2864 super(...arguments);
2865 this.type = 'svg';
2866 this.root = false;
2867 }
2868 setContext(ctx) {
2869 const { document } = this;
2870 const { screen, window } = document;
2871 const canvas = ctx.canvas;
2872 screen.setDefaults(ctx);
2873 if (canvas.style
2874 && typeof ctx.font !== 'undefined'
2875 && window
2876 && typeof window.getComputedStyle !== 'undefined') {
2877 ctx.font = window.getComputedStyle(canvas).getPropertyValue('font');
2878 const fontSizeProp = new Property(document, 'fontSize', Font.parse(ctx.font).fontSize);
2879 if (fontSizeProp.hasValue()) {
2880 document.rootEmSize = fontSizeProp.getPixels('y');
2881 document.emSize = document.rootEmSize;
2882 }
2883 }
2884 // create new view port
2885 if (!this.getAttribute('x').hasValue()) {
2886 this.getAttribute('x', true).setValue(0);
2887 }
2888 if (!this.getAttribute('y').hasValue()) {
2889 this.getAttribute('y', true).setValue(0);
2890 }
2891 let { width, height } = screen.viewPort;
2892 if (!this.getStyle('width').hasValue()) {
2893 this.getStyle('width', true).setValue('100%');
2894 }
2895 if (!this.getStyle('height').hasValue()) {
2896 this.getStyle('height', true).setValue('100%');
2897 }
2898 if (!this.getStyle('color').hasValue()) {
2899 this.getStyle('color', true).setValue('black');
2900 }
2901 const refXAttr = this.getAttribute('refX');
2902 const refYAttr = this.getAttribute('refY');
2903 const viewBoxAttr = this.getAttribute('viewBox');
2904 const viewBox = viewBoxAttr.hasValue()
2905 ? toNumbers(viewBoxAttr.getString())
2906 : null;
2907 const clip = !this.root
2908 && this.getStyle('overflow').getValue('hidden') !== 'visible';
2909 let minX = 0;
2910 let minY = 0;
2911 let clipX = 0;
2912 let clipY = 0;
2913 if (viewBox) {
2914 minX = viewBox[0];
2915 minY = viewBox[1];
2916 }
2917 if (!this.root) {
2918 width = this.getStyle('width').getPixels('x');
2919 height = this.getStyle('height').getPixels('y');
2920 if (this.type === 'marker') {
2921 clipX = minX;
2922 clipY = minY;
2923 minX = 0;
2924 minY = 0;
2925 }
2926 }
2927 screen.viewPort.setCurrent(width, height);
2928 // Default value of transform-origin is center only for root SVG elements
2929 // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform-origin
2930 if (this.node // is not temporary SVGElement
2931 && (!this.parent || this.node.parentNode?.nodeName === 'foreignObject')
2932 && this.getStyle('transform', false, true).hasValue()
2933 && !this.getStyle('transform-origin', false, true).hasValue()) {
2934 this.getStyle('transform-origin', true, true).setValue('50% 50%');
2935 }
2936 super.setContext(ctx);
2937 ctx.translate(this.getAttribute('x').getPixels('x'), this.getAttribute('y').getPixels('y'));
2938 if (viewBox) {
2939 width = viewBox[2];
2940 height = viewBox[3];
2941 }
2942 document.setViewBox({
2943 ctx,
2944 aspectRatio: this.getAttribute('preserveAspectRatio').getString(),
2945 width: screen.viewPort.width,
2946 desiredWidth: width,
2947 height: screen.viewPort.height,
2948 desiredHeight: height,
2949 minX,
2950 minY,
2951 refX: refXAttr.getValue(),
2952 refY: refYAttr.getValue(),
2953 clip,
2954 clipX,
2955 clipY
2956 });
2957 if (viewBox) {
2958 screen.viewPort.removeCurrent();
2959 screen.viewPort.setCurrent(width, height);
2960 }
2961 }
2962 clearContext(ctx) {
2963 super.clearContext(ctx);
2964 this.document.screen.viewPort.removeCurrent();
2965 }
2966 /**
2967 * Resize SVG to fit in given size.
2968 * @param width
2969 * @param height
2970 * @param preserveAspectRatio
2971 */
2972 resize(width, height = width, preserveAspectRatio = false) {
2973 const widthAttr = this.getAttribute('width', true);
2974 const heightAttr = this.getAttribute('height', true);
2975 const viewBoxAttr = this.getAttribute('viewBox');
2976 const styleAttr = this.getAttribute('style');
2977 const originWidth = widthAttr.getNumber(0);
2978 const originHeight = heightAttr.getNumber(0);
2979 if (preserveAspectRatio) {
2980 if (typeof preserveAspectRatio === 'string') {
2981 this.getAttribute('preserveAspectRatio', true).setValue(preserveAspectRatio);
2982 }
2983 else {
2984 const preserveAspectRatioAttr = this.getAttribute('preserveAspectRatio');
2985 if (preserveAspectRatioAttr.hasValue()) {
2986 preserveAspectRatioAttr.setValue(preserveAspectRatioAttr.getString().replace(/^\s*(\S.*\S)\s*$/, '$1'));
2987 }
2988 }
2989 }
2990 widthAttr.setValue(width);
2991 heightAttr.setValue(height);
2992 if (!viewBoxAttr.hasValue()) {
2993 viewBoxAttr.setValue(`0 0 ${originWidth || width} ${originHeight || height}`);
2994 }
2995 if (styleAttr.hasValue()) {
2996 const widthStyle = this.getStyle('width');
2997 const heightStyle = this.getStyle('height');
2998 if (widthStyle.hasValue()) {
2999 widthStyle.setValue(`${width}px`);
3000 }
3001 if (heightStyle.hasValue()) {
3002 heightStyle.setValue(`${height}px`);
3003 }
3004 }
3005 }
3006}
3007
3008class RectElement extends PathElement {
3009 constructor() {
3010 super(...arguments);
3011 this.type = 'rect';
3012 }
3013 path(ctx) {
3014 const x = this.getAttribute('x').getPixels('x');
3015 const y = this.getAttribute('y').getPixels('y');
3016 const width = this.getStyle('width', false, true).getPixels('x');
3017 const height = this.getStyle('height', false, true).getPixels('y');
3018 const rxAttr = this.getAttribute('rx');
3019 const ryAttr = this.getAttribute('ry');
3020 let rx = rxAttr.getPixels('x');
3021 let ry = ryAttr.getPixels('y');
3022 if (rxAttr.hasValue() && !ryAttr.hasValue()) {
3023 ry = rx;
3024 }
3025 if (ryAttr.hasValue() && !rxAttr.hasValue()) {
3026 rx = ry;
3027 }
3028 rx = Math.min(rx, width / 2.0);
3029 ry = Math.min(ry, height / 2.0);
3030 if (ctx) {
3031 const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
3032 ctx.beginPath(); // always start the path so we don't fill prior paths
3033 if (height > 0 && width > 0) {
3034 ctx.moveTo(x + rx, y);
3035 ctx.lineTo(x + width - rx, y);
3036 ctx.bezierCurveTo(x + width - rx + (KAPPA * rx), y, x + width, y + ry - (KAPPA * ry), x + width, y + ry);
3037 ctx.lineTo(x + width, y + height - ry);
3038 ctx.bezierCurveTo(x + width, y + height - ry + (KAPPA * ry), x + width - rx + (KAPPA * rx), y + height, x + width - rx, y + height);
3039 ctx.lineTo(x + rx, y + height);
3040 ctx.bezierCurveTo(x + rx - (KAPPA * rx), y + height, x, y + height - ry + (KAPPA * ry), x, y + height - ry);
3041 ctx.lineTo(x, y + ry);
3042 ctx.bezierCurveTo(x, y + ry - (KAPPA * ry), x + rx - (KAPPA * rx), y, x + rx, y);
3043 ctx.closePath();
3044 }
3045 }
3046 return new BoundingBox(x, y, x + width, y + height);
3047 }
3048 getMarkers() {
3049 return null;
3050 }
3051}
3052
3053class CircleElement extends PathElement {
3054 constructor() {
3055 super(...arguments);
3056 this.type = 'circle';
3057 }
3058 path(ctx) {
3059 const cx = this.getAttribute('cx').getPixels('x');
3060 const cy = this.getAttribute('cy').getPixels('y');
3061 const r = this.getAttribute('r').getPixels();
3062 if (ctx && r > 0) {
3063 ctx.beginPath();
3064 ctx.arc(cx, cy, r, 0, Math.PI * 2, false);
3065 ctx.closePath();
3066 }
3067 return new BoundingBox(cx - r, cy - r, cx + r, cy + r);
3068 }
3069 getMarkers() {
3070 return null;
3071 }
3072}
3073
3074class EllipseElement extends PathElement {
3075 constructor() {
3076 super(...arguments);
3077 this.type = 'ellipse';
3078 }
3079 path(ctx) {
3080 const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
3081 const rx = this.getAttribute('rx').getPixels('x');
3082 const ry = this.getAttribute('ry').getPixels('y');
3083 const cx = this.getAttribute('cx').getPixels('x');
3084 const cy = this.getAttribute('cy').getPixels('y');
3085 if (ctx && rx > 0 && ry > 0) {
3086 ctx.beginPath();
3087 ctx.moveTo(cx + rx, cy);
3088 ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry);
3089 ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy);
3090 ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry);
3091 ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy);
3092 ctx.closePath();
3093 }
3094 return new BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry);
3095 }
3096 getMarkers() {
3097 return null;
3098 }
3099}
3100
3101class LineElement extends PathElement {
3102 constructor() {
3103 super(...arguments);
3104 this.type = 'line';
3105 }
3106 getPoints() {
3107 return [
3108 new Point(this.getAttribute('x1').getPixels('x'), this.getAttribute('y1').getPixels('y')),
3109 new Point(this.getAttribute('x2').getPixels('x'), this.getAttribute('y2').getPixels('y'))
3110 ];
3111 }
3112 path(ctx) {
3113 const [{ x: x0, y: y0 }, { x: x1, y: y1 }] = this.getPoints();
3114 if (ctx) {
3115 ctx.beginPath();
3116 ctx.moveTo(x0, y0);
3117 ctx.lineTo(x1, y1);
3118 }
3119 return new BoundingBox(x0, y0, x1, y1);
3120 }
3121 getMarkers() {
3122 const [p0, p1] = this.getPoints();
3123 const a = p0.angleTo(p1);
3124 return [
3125 [p0, a],
3126 [p1, a]
3127 ];
3128 }
3129}
3130
3131class PolylineElement extends PathElement {
3132 constructor(document, node, captureTextNodes) {
3133 super(document, node, captureTextNodes);
3134 this.type = 'polyline';
3135 this.points = [];
3136 this.points = Point.parsePath(this.getAttribute('points').getString());
3137 }
3138 path(ctx) {
3139 const { points } = this;
3140 const [{ x: x0, y: y0 }] = points;
3141 const boundingBox = new BoundingBox(x0, y0);
3142 if (ctx) {
3143 ctx.beginPath();
3144 ctx.moveTo(x0, y0);
3145 }
3146 points.forEach(({ x, y }) => {
3147 boundingBox.addPoint(x, y);
3148 if (ctx) {
3149 ctx.lineTo(x, y);
3150 }
3151 });
3152 return boundingBox;
3153 }
3154 getMarkers() {
3155 const { points } = this;
3156 const lastIndex = points.length - 1;
3157 const markers = [];
3158 points.forEach((point, i) => {
3159 if (i === lastIndex) {
3160 return;
3161 }
3162 markers.push([
3163 point,
3164 point.angleTo(points[i + 1])
3165 ]);
3166 });
3167 if (markers.length > 0) {
3168 markers.push([
3169 points[points.length - 1],
3170 markers[markers.length - 1][1]
3171 ]);
3172 }
3173 return markers;
3174 }
3175}
3176
3177class PolygonElement extends PolylineElement {
3178 constructor() {
3179 super(...arguments);
3180 this.type = 'polygon';
3181 }
3182 path(ctx) {
3183 const boundingBox = super.path(ctx);
3184 const [{ x, y }] = this.points;
3185 if (ctx) {
3186 ctx.lineTo(x, y);
3187 ctx.closePath();
3188 }
3189 return boundingBox;
3190 }
3191}
3192
3193class PatternElement extends Element {
3194 constructor() {
3195 super(...arguments);
3196 this.type = 'pattern';
3197 }
3198 createPattern(ctx, _, parentOpacityProp) {
3199 const width = this.getStyle('width').getPixels('x', true);
3200 const height = this.getStyle('height').getPixels('y', true);
3201 // render me using a temporary svg element
3202 const patternSvg = new SVGElement(this.document, null);
3203 patternSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue());
3204 patternSvg.attributes.width = new Property(this.document, 'width', `${width}px`);
3205 patternSvg.attributes.height = new Property(this.document, 'height', `${height}px`);
3206 patternSvg.attributes.transform = new Property(this.document, 'transform', this.getAttribute('patternTransform').getValue());
3207 patternSvg.children = this.children;
3208 const patternCanvas = this.document.createCanvas(width, height);
3209 const patternCtx = patternCanvas.getContext('2d');
3210 const xAttr = this.getAttribute('x');
3211 const yAttr = this.getAttribute('y');
3212 if (xAttr.hasValue() && yAttr.hasValue()) {
3213 patternCtx.translate(xAttr.getPixels('x', true), yAttr.getPixels('y', true));
3214 }
3215 if (parentOpacityProp.hasValue()) {
3216 this.styles['fill-opacity'] = parentOpacityProp;
3217 }
3218 else {
3219 Reflect.deleteProperty(this.styles, 'fill-opacity');
3220 }
3221 // render 3x3 grid so when we transform there's no white space on edges
3222 for (let x = -1; x <= 1; x++) {
3223 for (let y = -1; y <= 1; y++) {
3224 patternCtx.save();
3225 patternSvg.attributes.x = new Property(this.document, 'x', x * patternCanvas.width);
3226 patternSvg.attributes.y = new Property(this.document, 'y', y * patternCanvas.height);
3227 patternSvg.render(patternCtx);
3228 patternCtx.restore();
3229 }
3230 }
3231 const pattern = ctx.createPattern(patternCanvas, 'repeat');
3232 return pattern;
3233 }
3234}
3235
3236class MarkerElement extends Element {
3237 constructor() {
3238 super(...arguments);
3239 this.type = 'marker';
3240 }
3241 render(ctx, point, angle) {
3242 if (!point) {
3243 return;
3244 }
3245 const { x, y } = point;
3246 const orient = this.getAttribute('orient').getString('auto');
3247 const markerUnits = this.getAttribute('markerUnits').getString('strokeWidth');
3248 ctx.translate(x, y);
3249 if (orient === 'auto') {
3250 ctx.rotate(angle);
3251 }
3252 if (markerUnits === 'strokeWidth') {
3253 ctx.scale(ctx.lineWidth, ctx.lineWidth);
3254 }
3255 ctx.save();
3256 // render me using a temporary svg element
3257 const markerSvg = new SVGElement(this.document, null);
3258 markerSvg.type = this.type;
3259 markerSvg.attributes.viewBox = new Property(this.document, 'viewBox', this.getAttribute('viewBox').getValue());
3260 markerSvg.attributes.refX = new Property(this.document, 'refX', this.getAttribute('refX').getValue());
3261 markerSvg.attributes.refY = new Property(this.document, 'refY', this.getAttribute('refY').getValue());
3262 markerSvg.attributes.width = new Property(this.document, 'width', this.getAttribute('markerWidth').getValue());
3263 markerSvg.attributes.height = new Property(this.document, 'height', this.getAttribute('markerHeight').getValue());
3264 markerSvg.attributes.overflow = new Property(this.document, 'overflow', this.getAttribute('overflow').getValue());
3265 markerSvg.attributes.fill = new Property(this.document, 'fill', this.getAttribute('fill').getColor('black'));
3266 markerSvg.attributes.stroke = new Property(this.document, 'stroke', this.getAttribute('stroke').getValue('none'));
3267 markerSvg.children = this.children;
3268 markerSvg.render(ctx);
3269 ctx.restore();
3270 if (markerUnits === 'strokeWidth') {
3271 ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth);
3272 }
3273 if (orient === 'auto') {
3274 ctx.rotate(-angle);
3275 }
3276 ctx.translate(-x, -y);
3277 }
3278}
3279
3280class DefsElement extends Element {
3281 constructor() {
3282 super(...arguments);
3283 this.type = 'defs';
3284 }
3285 render() {
3286 // NOOP
3287 }
3288}
3289
3290class GElement extends RenderedElement {
3291 constructor() {
3292 super(...arguments);
3293 this.type = 'g';
3294 }
3295 getBoundingBox(ctx) {
3296 const boundingBox = new BoundingBox();
3297 this.children.forEach((child) => {
3298 boundingBox.addBoundingBox(child.getBoundingBox(ctx));
3299 });
3300 return boundingBox;
3301 }
3302}
3303
3304class GradientElement extends Element {
3305 constructor(document, node, captureTextNodes) {
3306 super(document, node, captureTextNodes);
3307 this.attributesToInherit = [
3308 'gradientUnits'
3309 ];
3310 this.stops = [];
3311 const { stops, children } = this;
3312 children.forEach((child) => {
3313 if (child.type === 'stop') {
3314 stops.push(child);
3315 }
3316 });
3317 }
3318 getGradientUnits() {
3319 return this.getAttribute('gradientUnits').getString('objectBoundingBox');
3320 }
3321 createGradient(ctx, element, parentOpacityProp) {
3322 // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
3323 let stopsContainer = this;
3324 if (this.getHrefAttribute().hasValue()) {
3325 stopsContainer = this.getHrefAttribute().getDefinition();
3326 this.inheritStopContainer(stopsContainer);
3327 }
3328 const { stops } = stopsContainer;
3329 const gradient = this.getGradient(ctx, element);
3330 if (!gradient) {
3331 return this.addParentOpacity(parentOpacityProp, stops[stops.length - 1].color);
3332 }
3333 stops.forEach((stop) => {
3334 gradient.addColorStop(stop.offset, this.addParentOpacity(parentOpacityProp, stop.color));
3335 });
3336 if (this.getAttribute('gradientTransform').hasValue()) {
3337 // render as transformed pattern on temporary canvas
3338 const { document } = this;
3339 const { MAX_VIRTUAL_PIXELS, viewPort } = document.screen;
3340 const [rootView] = viewPort.viewPorts;
3341 const rect = new RectElement(document, null);
3342 rect.attributes.x = new Property(document, 'x', -MAX_VIRTUAL_PIXELS / 3.0);
3343 rect.attributes.y = new Property(document, 'y', -MAX_VIRTUAL_PIXELS / 3.0);
3344 rect.attributes.width = new Property(document, 'width', MAX_VIRTUAL_PIXELS);
3345 rect.attributes.height = new Property(document, 'height', MAX_VIRTUAL_PIXELS);
3346 const group = new GElement(document, null);
3347 group.attributes.transform = new Property(document, 'transform', this.getAttribute('gradientTransform').getValue());
3348 group.children = [rect];
3349 const patternSvg = new SVGElement(document, null);
3350 patternSvg.attributes.x = new Property(document, 'x', 0);
3351 patternSvg.attributes.y = new Property(document, 'y', 0);
3352 patternSvg.attributes.width = new Property(document, 'width', rootView.width);
3353 patternSvg.attributes.height = new Property(document, 'height', rootView.height);
3354 patternSvg.children = [group];
3355 const patternCanvas = document.createCanvas(rootView.width, rootView.height);
3356 const patternCtx = patternCanvas.getContext('2d');
3357 patternCtx.fillStyle = gradient;
3358 patternSvg.render(patternCtx);
3359 return patternCtx.createPattern(patternCanvas, 'no-repeat');
3360 }
3361 return gradient;
3362 }
3363 inheritStopContainer(stopsContainer) {
3364 this.attributesToInherit.forEach((attributeToInherit) => {
3365 if (!this.getAttribute(attributeToInherit).hasValue()
3366 && stopsContainer.getAttribute(attributeToInherit).hasValue()) {
3367 this.getAttribute(attributeToInherit, true)
3368 .setValue(stopsContainer.getAttribute(attributeToInherit).getValue());
3369 }
3370 });
3371 }
3372 addParentOpacity(parentOpacityProp, color) {
3373 if (parentOpacityProp.hasValue()) {
3374 const colorProp = new Property(this.document, 'color', color);
3375 return colorProp.addOpacity(parentOpacityProp).getColor();
3376 }
3377 return color;
3378 }
3379}
3380
3381class LinearGradientElement extends GradientElement {
3382 constructor(document, node, captureTextNodes) {
3383 super(document, node, captureTextNodes);
3384 this.type = 'linearGradient';
3385 this.attributesToInherit.push('x1', 'y1', 'x2', 'y2');
3386 }
3387 getGradient(ctx, element) {
3388 const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox';
3389 const boundingBox = isBoundingBoxUnits
3390 ? element.getBoundingBox(ctx)
3391 : null;
3392 if (isBoundingBoxUnits && !boundingBox) {
3393 return null;
3394 }
3395 if (!this.getAttribute('x1').hasValue()
3396 && !this.getAttribute('y1').hasValue()
3397 && !this.getAttribute('x2').hasValue()
3398 && !this.getAttribute('y2').hasValue()) {
3399 this.getAttribute('x1', true).setValue(0);
3400 this.getAttribute('y1', true).setValue(0);
3401 this.getAttribute('x2', true).setValue(1);
3402 this.getAttribute('y2', true).setValue(0);
3403 }
3404 const x1 = isBoundingBoxUnits
3405 ? boundingBox.x + boundingBox.width * this.getAttribute('x1').getNumber()
3406 : this.getAttribute('x1').getPixels('x');
3407 const y1 = isBoundingBoxUnits
3408 ? boundingBox.y + boundingBox.height * this.getAttribute('y1').getNumber()
3409 : this.getAttribute('y1').getPixels('y');
3410 const x2 = isBoundingBoxUnits
3411 ? boundingBox.x + boundingBox.width * this.getAttribute('x2').getNumber()
3412 : this.getAttribute('x2').getPixels('x');
3413 const y2 = isBoundingBoxUnits
3414 ? boundingBox.y + boundingBox.height * this.getAttribute('y2').getNumber()
3415 : this.getAttribute('y2').getPixels('y');
3416 if (x1 === x2 && y1 === y2) {
3417 return null;
3418 }
3419 return ctx.createLinearGradient(x1, y1, x2, y2);
3420 }
3421}
3422
3423class RadialGradientElement extends GradientElement {
3424 constructor(document, node, captureTextNodes) {
3425 super(document, node, captureTextNodes);
3426 this.type = 'radialGradient';
3427 this.attributesToInherit.push('cx', 'cy', 'r', 'fx', 'fy', 'fr');
3428 }
3429 getGradient(ctx, element) {
3430 const isBoundingBoxUnits = this.getGradientUnits() === 'objectBoundingBox';
3431 const boundingBox = element.getBoundingBox(ctx);
3432 if (isBoundingBoxUnits && !boundingBox) {
3433 return null;
3434 }
3435 if (!this.getAttribute('cx').hasValue()) {
3436 this.getAttribute('cx', true).setValue('50%');
3437 }
3438 if (!this.getAttribute('cy').hasValue()) {
3439 this.getAttribute('cy', true).setValue('50%');
3440 }
3441 if (!this.getAttribute('r').hasValue()) {
3442 this.getAttribute('r', true).setValue('50%');
3443 }
3444 const cx = isBoundingBoxUnits
3445 ? boundingBox.x + boundingBox.width * this.getAttribute('cx').getNumber()
3446 : this.getAttribute('cx').getPixels('x');
3447 const cy = isBoundingBoxUnits
3448 ? boundingBox.y + boundingBox.height * this.getAttribute('cy').getNumber()
3449 : this.getAttribute('cy').getPixels('y');
3450 let fx = cx;
3451 let fy = cy;
3452 if (this.getAttribute('fx').hasValue()) {
3453 fx = isBoundingBoxUnits
3454 ? boundingBox.x + boundingBox.width * this.getAttribute('fx').getNumber()
3455 : this.getAttribute('fx').getPixels('x');
3456 }
3457 if (this.getAttribute('fy').hasValue()) {
3458 fy = isBoundingBoxUnits
3459 ? boundingBox.y + boundingBox.height * this.getAttribute('fy').getNumber()
3460 : this.getAttribute('fy').getPixels('y');
3461 }
3462 const r = isBoundingBoxUnits
3463 ? (boundingBox.width + boundingBox.height) / 2.0 * this.getAttribute('r').getNumber()
3464 : this.getAttribute('r').getPixels();
3465 const fr = this.getAttribute('fr').getPixels();
3466 return ctx.createRadialGradient(fx, fy, fr, cx, cy, r);
3467 }
3468}
3469
3470class StopElement extends Element {
3471 constructor(document, node, captureTextNodes) {
3472 super(document, node, captureTextNodes);
3473 this.type = 'stop';
3474 const offset = Math.max(0, Math.min(1, this.getAttribute('offset').getNumber()));
3475 const stopOpacity = this.getStyle('stop-opacity');
3476 let stopColor = this.getStyle('stop-color', true);
3477 if (stopColor.getString() === '') {
3478 stopColor.setValue('#000');
3479 }
3480 if (stopOpacity.hasValue()) {
3481 stopColor = stopColor.addOpacity(stopOpacity);
3482 }
3483 this.offset = offset;
3484 this.color = stopColor.getColor();
3485 }
3486}
3487
3488class AnimateElement extends Element {
3489 constructor(document, node, captureTextNodes) {
3490 super(document, node, captureTextNodes);
3491 this.type = 'animate';
3492 this.duration = 0;
3493 this.initialValue = null;
3494 this.initialUnits = '';
3495 this.removed = false;
3496 this.frozen = false;
3497 document.screen.animations.push(this);
3498 this.begin = this.getAttribute('begin').getMilliseconds();
3499 this.maxDuration = this.begin + this.getAttribute('dur').getMilliseconds();
3500 this.from = this.getAttribute('from');
3501 this.to = this.getAttribute('to');
3502 this.values = new Property(document, 'values', null);
3503 const valuesAttr = this.getAttribute('values');
3504 if (valuesAttr.hasValue()) {
3505 this.values.setValue(valuesAttr.getString().split(';'));
3506 }
3507 }
3508 getProperty() {
3509 const attributeType = this.getAttribute('attributeType').getString();
3510 const attributeName = this.getAttribute('attributeName').getString();
3511 if (attributeType === 'CSS') {
3512 return this.parent.getStyle(attributeName, true);
3513 }
3514 return this.parent.getAttribute(attributeName, true);
3515 }
3516 calcValue() {
3517 const { initialUnits } = this;
3518 const { progress, from, to } = this.getProgress();
3519 // tween value linearly
3520 let newValue = from.getNumber() + (to.getNumber() - from.getNumber()) * progress;
3521 if (initialUnits === '%') {
3522 newValue *= 100.0; // numValue() returns 0-1 whereas properties are 0-100
3523 }
3524 return `${newValue}${initialUnits}`;
3525 }
3526 update(delta) {
3527 const { parent } = this;
3528 const prop = this.getProperty();
3529 // set initial value
3530 if (!this.initialValue) {
3531 this.initialValue = prop.getString();
3532 this.initialUnits = prop.getUnits();
3533 }
3534 // if we're past the end time
3535 if (this.duration > this.maxDuration) {
3536 const fill = this.getAttribute('fill').getString('remove');
3537 // loop for indefinitely repeating animations
3538 if (this.getAttribute('repeatCount').getString() === 'indefinite'
3539 || this.getAttribute('repeatDur').getString() === 'indefinite') {
3540 this.duration = 0;
3541 }
3542 else if (fill === 'freeze' && !this.frozen) {
3543 this.frozen = true;
3544 parent.animationFrozen = true;
3545 parent.animationFrozenValue = prop.getString();
3546 }
3547 else if (fill === 'remove' && !this.removed) {
3548 this.removed = true;
3549 prop.setValue(parent.animationFrozen
3550 ? parent.animationFrozenValue
3551 : this.initialValue);
3552 return true;
3553 }
3554 return false;
3555 }
3556 this.duration += delta;
3557 // if we're past the begin time
3558 let updated = false;
3559 if (this.begin < this.duration) {
3560 let newValue = this.calcValue(); // tween
3561 const typeAttr = this.getAttribute('type');
3562 if (typeAttr.hasValue()) {
3563 // for transform, etc.
3564 const type = typeAttr.getString();
3565 newValue = `${type}(${newValue})`;
3566 }
3567 prop.setValue(newValue);
3568 updated = true;
3569 }
3570 return updated;
3571 }
3572 getProgress() {
3573 const { document, values } = this;
3574 const result = {
3575 progress: (this.duration - this.begin) / (this.maxDuration - this.begin)
3576 };
3577 if (values.hasValue()) {
3578 const p = result.progress * (values.getValue().length - 1);
3579 const lb = Math.floor(p);
3580 const ub = Math.ceil(p);
3581 result.from = new Property(document, 'from', parseFloat(values.getValue()[lb]));
3582 result.to = new Property(document, 'to', parseFloat(values.getValue()[ub]));
3583 result.progress = (p - lb) / (ub - lb);
3584 }
3585 else {
3586 result.from = this.from;
3587 result.to = this.to;
3588 }
3589 return result;
3590 }
3591}
3592
3593class AnimateColorElement extends AnimateElement {
3594 constructor() {
3595 super(...arguments);
3596 this.type = 'animateColor';
3597 }
3598 calcValue() {
3599 const { progress, from, to } = this.getProgress();
3600 const colorFrom = new RGBColor(from.getColor());
3601 const colorTo = new RGBColor(to.getColor());
3602 if (colorFrom.ok && colorTo.ok) {
3603 // tween color linearly
3604 const r = colorFrom.r + (colorTo.r - colorFrom.r) * progress;
3605 const g = colorFrom.g + (colorTo.g - colorFrom.g) * progress;
3606 const b = colorFrom.b + (colorTo.b - colorFrom.b) * progress;
3607 // ? alpha
3608 return `rgb(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)})`;
3609 }
3610 return this.getAttribute('from').getColor();
3611 }
3612}
3613
3614class AnimateTransformElement extends AnimateElement {
3615 constructor() {
3616 super(...arguments);
3617 this.type = 'animateTransform';
3618 }
3619 calcValue() {
3620 const { progress, from, to } = this.getProgress();
3621 // tween value linearly
3622 const transformFrom = toNumbers(from.getString());
3623 const transformTo = toNumbers(to.getString());
3624 const newValue = transformFrom.map((from, i) => {
3625 const to = transformTo[i];
3626 return from + (to - from) * progress;
3627 }).join(' ');
3628 return newValue;
3629 }
3630}
3631
3632class FontElement extends Element {
3633 constructor(document, node, captureTextNodes) {
3634 super(document, node, captureTextNodes);
3635 this.type = 'font';
3636 this.glyphs = {};
3637 this.horizAdvX = this.getAttribute('horiz-adv-x').getNumber();
3638 const { definitions } = document;
3639 const { children } = this;
3640 for (const child of children) {
3641 switch (child.type) {
3642 case 'font-face': {
3643 this.fontFace = child;
3644 const fontFamilyStyle = child.getStyle('font-family');
3645 if (fontFamilyStyle.hasValue()) {
3646 definitions[fontFamilyStyle.getString()] = this;
3647 }
3648 break;
3649 }
3650 case 'missing-glyph':
3651 this.missingGlyph = child;
3652 break;
3653 case 'glyph': {
3654 const glyph = child;
3655 if (glyph.arabicForm) {
3656 this.isRTL = true;
3657 this.isArabic = true;
3658 if (typeof this.glyphs[glyph.unicode] === 'undefined') {
3659 this.glyphs[glyph.unicode] = {};
3660 }
3661 this.glyphs[glyph.unicode][glyph.arabicForm] = glyph;
3662 }
3663 else {
3664 this.glyphs[glyph.unicode] = glyph;
3665 }
3666 break;
3667 }
3668 }
3669 }
3670 }
3671 render() {
3672 // NO RENDER
3673 }
3674}
3675
3676class FontFaceElement extends Element {
3677 constructor(document, node, captureTextNodes) {
3678 super(document, node, captureTextNodes);
3679 this.type = 'font-face';
3680 this.ascent = this.getAttribute('ascent').getNumber();
3681 this.descent = this.getAttribute('descent').getNumber();
3682 this.unitsPerEm = this.getAttribute('units-per-em').getNumber();
3683 }
3684}
3685
3686class MissingGlyphElement extends PathElement {
3687 constructor() {
3688 super(...arguments);
3689 this.type = 'missing-glyph';
3690 this.horizAdvX = 0;
3691 }
3692}
3693
3694class TRefElement extends TextElement {
3695 constructor() {
3696 super(...arguments);
3697 this.type = 'tref';
3698 }
3699 getText() {
3700 const element = this.getHrefAttribute().getDefinition();
3701 if (element) {
3702 const firstChild = element.children[0];
3703 if (firstChild) {
3704 return firstChild.getText();
3705 }
3706 }
3707 return '';
3708 }
3709}
3710
3711class AElement extends TextElement {
3712 constructor(document, node, captureTextNodes) {
3713 super(document, node, captureTextNodes);
3714 this.type = 'a';
3715 const { childNodes } = node;
3716 const firstChild = childNodes[0];
3717 const hasText = childNodes.length > 0
3718 && Array.from(childNodes).every(node => node.nodeType === 3);
3719 this.hasText = hasText;
3720 this.text = hasText
3721 ? this.getTextFromNode(firstChild)
3722 : '';
3723 }
3724 getText() {
3725 return this.text;
3726 }
3727 renderChildren(ctx) {
3728 if (this.hasText) {
3729 // render as text element
3730 super.renderChildren(ctx);
3731 const { document, x, y } = this;
3732 const { mouse } = document.screen;
3733 const fontSize = new Property(document, 'fontSize', Font.parse(document.ctx.font).fontSize);
3734 // Do not calc bounding box if mouse is not working.
3735 if (mouse.isWorking()) {
3736 mouse.checkBoundingBox(this, new BoundingBox(x, y - fontSize.getPixels('y'), x + this.measureText(ctx), y));
3737 }
3738 }
3739 else if (this.children.length > 0) {
3740 // render as temporary group
3741 const g = new GElement(this.document, null);
3742 g.children = this.children;
3743 g.parent = this;
3744 g.render(ctx);
3745 }
3746 }
3747 onClick() {
3748 const { window } = this.document;
3749 if (window) {
3750 window.open(this.getHrefAttribute().getString());
3751 }
3752 }
3753 onMouseMove() {
3754 const ctx = this.document.ctx;
3755 ctx.canvas.style.cursor = 'pointer';
3756 }
3757}
3758
3759class TextPathElement extends TextElement {
3760 constructor(document, node, captureTextNodes) {
3761 super(document, node, captureTextNodes);
3762 this.type = 'textPath';
3763 this.textWidth = 0;
3764 this.textHeight = 0;
3765 this.pathLength = -1;
3766 this.glyphInfo = null;
3767 this.letterSpacingCache = [];
3768 this.measuresCache = new Map([['', 0]]);
3769 const pathElement = this.getHrefAttribute().getDefinition();
3770 this.text = this.getTextFromNode();
3771 this.dataArray = this.parsePathData(pathElement);
3772 }
3773 getText() {
3774 return this.text;
3775 }
3776 path(ctx) {
3777 const { dataArray } = this;
3778 if (ctx) {
3779 ctx.beginPath();
3780 }
3781 dataArray.forEach(({ type, points }) => {
3782 switch (type) {
3783 case PathParser.LINE_TO:
3784 if (ctx) {
3785 ctx.lineTo(points[0], points[1]);
3786 }
3787 break;
3788 case PathParser.MOVE_TO:
3789 if (ctx) {
3790 ctx.moveTo(points[0], points[1]);
3791 }
3792 break;
3793 case PathParser.CURVE_TO:
3794 if (ctx) {
3795 ctx.bezierCurveTo(points[0], points[1], points[2], points[3], points[4], points[5]);
3796 }
3797 break;
3798 case PathParser.QUAD_TO:
3799 if (ctx) {
3800 ctx.quadraticCurveTo(points[0], points[1], points[2], points[3]);
3801 }
3802 break;
3803 case PathParser.ARC: {
3804 const [cx, cy, rx, ry, theta, dTheta, psi, fs] = points;
3805 const r = rx > ry ? rx : ry;
3806 const scaleX = rx > ry ? 1 : rx / ry;
3807 const scaleY = rx > ry ? ry / rx : 1;
3808 if (ctx) {
3809 ctx.translate(cx, cy);
3810 ctx.rotate(psi);
3811 ctx.scale(scaleX, scaleY);
3812 ctx.arc(0, 0, r, theta, theta + dTheta, Boolean(1 - fs));
3813 ctx.scale(1 / scaleX, 1 / scaleY);
3814 ctx.rotate(-psi);
3815 ctx.translate(-cx, -cy);
3816 }
3817 break;
3818 }
3819 case PathParser.CLOSE_PATH:
3820 if (ctx) {
3821 ctx.closePath();
3822 }
3823 break;
3824 }
3825 });
3826 }
3827 renderChildren(ctx) {
3828 this.setTextData(ctx);
3829 ctx.save();
3830 const textDecoration = this.parent.getStyle('text-decoration').getString();
3831 const fontSize = this.getFontSize();
3832 const { glyphInfo } = this;
3833 const fill = ctx.fillStyle;
3834 if (textDecoration === 'underline') {
3835 ctx.beginPath();
3836 }
3837 glyphInfo.forEach((glyph, i) => {
3838 const { p0, p1, rotation, text: partialText } = glyph;
3839 ctx.save();
3840 ctx.translate(p0.x, p0.y);
3841 ctx.rotate(rotation);
3842 if (ctx.fillStyle) {
3843 ctx.fillText(partialText, 0, 0);
3844 }
3845 if (ctx.strokeStyle) {
3846 ctx.strokeText(partialText, 0, 0);
3847 }
3848 ctx.restore();
3849 if (textDecoration === 'underline') {
3850 if (i === 0) {
3851 ctx.moveTo(p0.x, p0.y + fontSize / 8);
3852 }
3853 ctx.lineTo(p1.x, p1.y + fontSize / 5);
3854 }
3855 // // To assist with debugging visually, uncomment following
3856 //
3857 // ctx.beginPath();
3858 // if (i % 2)
3859 // ctx.strokeStyle = 'red';
3860 // else
3861 // ctx.strokeStyle = 'green';
3862 // ctx.moveTo(p0.x, p0.y);
3863 // ctx.lineTo(p1.x, p1.y);
3864 // ctx.stroke();
3865 // ctx.closePath();
3866 });
3867 if (textDecoration === 'underline') {
3868 ctx.lineWidth = fontSize / 20;
3869 ctx.strokeStyle = fill;
3870 ctx.stroke();
3871 ctx.closePath();
3872 }
3873 ctx.restore();
3874 }
3875 getLetterSpacingAt(idx = 0) {
3876 return this.letterSpacingCache[idx] || 0;
3877 }
3878 findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, inputOffset, dy, c, charI) {
3879 let offset = inputOffset;
3880 let glyphWidth = this.measureText(ctx, c);
3881 if (c === ' '
3882 && anchor === 'justify'
3883 && textFullWidth < fullPathWidth) {
3884 glyphWidth += (fullPathWidth - textFullWidth) / spacesNumber;
3885 }
3886 if (charI > -1) {
3887 offset += this.getLetterSpacingAt(charI);
3888 }
3889 const splineStep = this.textHeight / 20;
3890 const p0 = this.getEquidistantPointOnPath(offset, splineStep, 0);
3891 const p1 = this.getEquidistantPointOnPath(offset + glyphWidth, splineStep, 0);
3892 const segment = {
3893 p0,
3894 p1
3895 };
3896 const rotation = p0 && p1
3897 ? Math.atan2(p1.y - p0.y, p1.x - p0.x)
3898 : 0;
3899 if (dy) {
3900 const dyX = Math.cos(Math.PI / 2 + rotation) * dy;
3901 const dyY = Math.cos(-rotation) * dy;
3902 segment.p0 = {
3903 ...p0,
3904 x: p0.x + dyX,
3905 y: p0.y + dyY
3906 };
3907 segment.p1 = {
3908 ...p1,
3909 x: p1.x + dyX,
3910 y: p1.y + dyY
3911 };
3912 }
3913 offset += glyphWidth;
3914 return {
3915 offset,
3916 segment,
3917 rotation
3918 };
3919 }
3920 measureText(ctx, text) {
3921 const { measuresCache } = this;
3922 const targetText = text || this.getText();
3923 if (measuresCache.has(targetText)) {
3924 return measuresCache.get(targetText);
3925 }
3926 const measure = this.measureTargetText(ctx, targetText);
3927 measuresCache.set(targetText, measure);
3928 return measure;
3929 }
3930 // This method supposes what all custom fonts already loaded.
3931 // If some font will be loaded after this method call, <textPath> will not be rendered correctly.
3932 // You need to call this method manually to update glyphs cache.
3933 setTextData(ctx) {
3934 if (this.glyphInfo) {
3935 return;
3936 }
3937 const renderText = this.getText();
3938 const chars = renderText.split('');
3939 const spacesNumber = renderText.split(' ').length - 1;
3940 const dx = this.parent.getAttribute('dx').split().map(_ => _.getPixels('x'));
3941 const dy = this.parent.getAttribute('dy').getPixels('y');
3942 const anchor = this.parent.getStyle('text-anchor').getString('start');
3943 const thisSpacing = this.getStyle('letter-spacing');
3944 const parentSpacing = this.parent.getStyle('letter-spacing');
3945 let letterSpacing = 0;
3946 if (!thisSpacing.hasValue()
3947 || thisSpacing.getValue() === 'inherit') {
3948 letterSpacing = parentSpacing.getPixels();
3949 }
3950 else if (thisSpacing.hasValue()) {
3951 if (thisSpacing.getValue() !== 'initial'
3952 && thisSpacing.getValue() !== 'unset') {
3953 letterSpacing = thisSpacing.getPixels();
3954 }
3955 }
3956 // fill letter-spacing cache
3957 const letterSpacingCache = [];
3958 const textLen = renderText.length;
3959 this.letterSpacingCache = letterSpacingCache;
3960 for (let i = 0; i < textLen; i++) {
3961 letterSpacingCache.push(typeof dx[i] !== 'undefined'
3962 ? dx[i]
3963 : letterSpacing);
3964 }
3965 const dxSum = letterSpacingCache.reduce((acc, cur, i) => (i === 0
3966 ? 0
3967 : acc + cur || 0), 0);
3968 const textWidth = this.measureText(ctx);
3969 const textFullWidth = Math.max(textWidth + dxSum, 0);
3970 this.textWidth = textWidth;
3971 this.textHeight = this.getFontSize();
3972 this.glyphInfo = [];
3973 const fullPathWidth = this.getPathLength();
3974 const startOffset = this.getStyle('startOffset').getNumber(0) * fullPathWidth;
3975 let offset = 0;
3976 if (anchor === 'middle'
3977 || anchor === 'center') {
3978 offset = -textFullWidth / 2;
3979 }
3980 if (anchor === 'end'
3981 || anchor === 'right') {
3982 offset = -textFullWidth;
3983 }
3984 offset += startOffset;
3985 chars.forEach((char, i) => {
3986 // Find such segment what distance between p0 and p1 is approx. width of glyph
3987 const { offset: nextOffset, segment, rotation } = this.findSegmentToFitChar(ctx, anchor, textFullWidth, fullPathWidth, spacesNumber, offset, dy, char, i);
3988 offset = nextOffset;
3989 if (!segment.p0 || !segment.p1) {
3990 return;
3991 }
3992 // const width = this.getLineLength(
3993 // segment.p0.x,
3994 // segment.p0.y,
3995 // segment.p1.x,
3996 // segment.p1.y
3997 // );
3998 // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used.
3999 // Can foresee having a rough pair table built in that the developer can override as needed.
4000 // Or use "dx" attribute of the <text> node as a naive replacement
4001 // const kern = 0;
4002 // placeholder for future implementation
4003 // const midpoint = this.getPointOnLine(
4004 // kern + width / 2.0,
4005 // segment.p0.x, segment.p0.y, segment.p1.x, segment.p1.y
4006 // );
4007 this.glyphInfo.push({
4008 // transposeX: midpoint.x,
4009 // transposeY: midpoint.y,
4010 text: chars[i],
4011 p0: segment.p0,
4012 p1: segment.p1,
4013 rotation
4014 });
4015 });
4016 }
4017 parsePathData(path) {
4018 this.pathLength = -1; // reset path length
4019 if (!path) {
4020 return [];
4021 }
4022 const pathCommands = [];
4023 const { pathParser } = path;
4024 pathParser.reset();
4025 // convert l, H, h, V, and v to L
4026 while (!pathParser.isEnd()) {
4027 const { current } = pathParser;
4028 const startX = current ? current.x : 0;
4029 const startY = current ? current.y : 0;
4030 const command = pathParser.next();
4031 let nextCommandType = command.type;
4032 let points = [];
4033 switch (command.type) {
4034 case PathParser.MOVE_TO:
4035 this.pathM(pathParser, points);
4036 break;
4037 case PathParser.LINE_TO:
4038 nextCommandType = this.pathL(pathParser, points);
4039 break;
4040 case PathParser.HORIZ_LINE_TO:
4041 nextCommandType = this.pathH(pathParser, points);
4042 break;
4043 case PathParser.VERT_LINE_TO:
4044 nextCommandType = this.pathV(pathParser, points);
4045 break;
4046 case PathParser.CURVE_TO:
4047 this.pathC(pathParser, points);
4048 break;
4049 case PathParser.SMOOTH_CURVE_TO:
4050 nextCommandType = this.pathS(pathParser, points);
4051 break;
4052 case PathParser.QUAD_TO:
4053 this.pathQ(pathParser, points);
4054 break;
4055 case PathParser.SMOOTH_QUAD_TO:
4056 nextCommandType = this.pathT(pathParser, points);
4057 break;
4058 case PathParser.ARC:
4059 points = this.pathA(pathParser);
4060 break;
4061 case PathParser.CLOSE_PATH:
4062 PathElement.pathZ(pathParser);
4063 break;
4064 }
4065 if (command.type !== PathParser.CLOSE_PATH) {
4066 pathCommands.push({
4067 type: nextCommandType,
4068 points,
4069 start: {
4070 x: startX,
4071 y: startY
4072 },
4073 pathLength: this.calcLength(startX, startY, nextCommandType, points)
4074 });
4075 }
4076 else {
4077 pathCommands.push({
4078 type: PathParser.CLOSE_PATH,
4079 points: [],
4080 pathLength: 0
4081 });
4082 }
4083 }
4084 return pathCommands;
4085 }
4086 pathM(pathParser, points) {
4087 const { x, y } = PathElement.pathM(pathParser).point;
4088 points.push(x, y);
4089 }
4090 pathL(pathParser, points) {
4091 const { x, y } = PathElement.pathL(pathParser).point;
4092 points.push(x, y);
4093 return PathParser.LINE_TO;
4094 }
4095 pathH(pathParser, points) {
4096 const { x, y } = PathElement.pathH(pathParser).point;
4097 points.push(x, y);
4098 return PathParser.LINE_TO;
4099 }
4100 pathV(pathParser, points) {
4101 const { x, y } = PathElement.pathV(pathParser).point;
4102 points.push(x, y);
4103 return PathParser.LINE_TO;
4104 }
4105 pathC(pathParser, points) {
4106 const { point, controlPoint, currentPoint } = PathElement.pathC(pathParser);
4107 points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4108 }
4109 pathS(pathParser, points) {
4110 const { point, controlPoint, currentPoint } = PathElement.pathS(pathParser);
4111 points.push(point.x, point.y, controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4112 return PathParser.CURVE_TO;
4113 }
4114 pathQ(pathParser, points) {
4115 const { controlPoint, currentPoint } = PathElement.pathQ(pathParser);
4116 points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4117 }
4118 pathT(pathParser, points) {
4119 const { controlPoint, currentPoint } = PathElement.pathT(pathParser);
4120 points.push(controlPoint.x, controlPoint.y, currentPoint.x, currentPoint.y);
4121 return PathParser.QUAD_TO;
4122 }
4123 pathA(pathParser) {
4124 let { rX, rY, sweepFlag, xAxisRotation, centp, a1, ad } = PathElement.pathA(pathParser);
4125 if (sweepFlag === 0 && ad > 0) {
4126 ad -= 2 * Math.PI;
4127 }
4128 if (sweepFlag === 1 && ad < 0) {
4129 ad += 2 * Math.PI;
4130 }
4131 return [
4132 centp.x,
4133 centp.y,
4134 rX,
4135 rY,
4136 a1,
4137 ad,
4138 xAxisRotation,
4139 sweepFlag
4140 ];
4141 }
4142 calcLength(x, y, commandType, points) {
4143 let len = 0;
4144 let p1 = null;
4145 let p2 = null;
4146 let t = 0;
4147 switch (commandType) {
4148 case PathParser.LINE_TO:
4149 return this.getLineLength(x, y, points[0], points[1]);
4150 case PathParser.CURVE_TO:
4151 // Approximates by breaking curve into 100 line segments
4152 len = 0.0;
4153 p1 = this.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
4154 for (t = 0.01; t <= 1; t += 0.01) {
4155 p2 = this.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
4156 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4157 p1 = p2;
4158 }
4159 return len;
4160 case PathParser.QUAD_TO:
4161 // Approximates by breaking curve into 100 line segments
4162 len = 0.0;
4163 p1 = this.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]);
4164 for (t = 0.01; t <= 1; t += 0.01) {
4165 p2 = this.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]);
4166 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4167 p1 = p2;
4168 }
4169 return len;
4170 case PathParser.ARC: {
4171 // Approximates by breaking curve into line segments
4172 len = 0.0;
4173 const start = points[4];
4174 // 4 = theta
4175 const dTheta = points[5];
4176 // 5 = dTheta
4177 const end = points[4] + dTheta;
4178 let inc = Math.PI / 180.0;
4179 // 1 degree resolution
4180 if (Math.abs(start - end) < inc) {
4181 inc = Math.abs(start - end);
4182 }
4183 // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi
4184 p1 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0);
4185 if (dTheta < 0) { // clockwise
4186 for (t = start - inc; t > end; t -= inc) {
4187 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
4188 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4189 p1 = p2;
4190 }
4191 }
4192 else { // counter-clockwise
4193 for (t = start + inc; t < end; t += inc) {
4194 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
4195 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4196 p1 = p2;
4197 }
4198 }
4199 p2 = this.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0);
4200 len += this.getLineLength(p1.x, p1.y, p2.x, p2.y);
4201 return len;
4202 }
4203 }
4204 return 0;
4205 }
4206 getPointOnLine(dist, p1x, p1y, p2x, p2y, fromX = p1x, fromY = p1y) {
4207 const m = (p2y - p1y) / ((p2x - p1x) + PSEUDO_ZERO);
4208 let run = Math.sqrt(dist * dist / (1 + m * m));
4209 if (p2x < p1x) {
4210 run *= -1;
4211 }
4212 let rise = m * run;
4213 let pt = null;
4214 if (p2x === p1x) { // vertical line
4215 pt = {
4216 x: fromX,
4217 y: fromY + rise
4218 };
4219 }
4220 else if ((fromY - p1y) / ((fromX - p1x) + PSEUDO_ZERO) === m) {
4221 pt = {
4222 x: fromX + run,
4223 y: fromY + rise
4224 };
4225 }
4226 else {
4227 let ix = 0;
4228 let iy = 0;
4229 const len = this.getLineLength(p1x, p1y, p2x, p2y);
4230 if (len < PSEUDO_ZERO) {
4231 return null;
4232 }
4233 let u = ((fromX - p1x) * (p2x - p1x))
4234 + ((fromY - p1y) * (p2y - p1y));
4235 u /= len * len;
4236 ix = p1x + u * (p2x - p1x);
4237 iy = p1y + u * (p2y - p1y);
4238 const pRise = this.getLineLength(fromX, fromY, ix, iy);
4239 const pRun = Math.sqrt(dist * dist - pRise * pRise);
4240 run = Math.sqrt(pRun * pRun / (1 + m * m));
4241 if (p2x < p1x) {
4242 run *= -1;
4243 }
4244 rise = m * run;
4245 pt = {
4246 x: ix + run,
4247 y: iy + rise
4248 };
4249 }
4250 return pt;
4251 }
4252 getPointOnPath(distance) {
4253 const fullLen = this.getPathLength();
4254 let cumulativePathLength = 0;
4255 let p = null;
4256 if (distance < -0.00005
4257 || distance - 0.00005 > fullLen) {
4258 return null;
4259 }
4260 const { dataArray } = this;
4261 for (const command of dataArray) {
4262 if (command
4263 && (command.pathLength < 0.00005
4264 || cumulativePathLength + command.pathLength + 0.00005 < distance)) {
4265 cumulativePathLength += command.pathLength;
4266 continue;
4267 }
4268 const delta = distance - cumulativePathLength;
4269 let currentT = 0;
4270 switch (command.type) {
4271 case PathParser.LINE_TO:
4272 p = this.getPointOnLine(delta, command.start.x, command.start.y, command.points[0], command.points[1], command.start.x, command.start.y);
4273 break;
4274 case PathParser.ARC: {
4275 const start = command.points[4];
4276 // 4 = theta
4277 const dTheta = command.points[5];
4278 // 5 = dTheta
4279 const end = command.points[4] + dTheta;
4280 currentT = start + delta / command.pathLength * dTheta;
4281 if (dTheta < 0 && currentT < end
4282 || dTheta >= 0 && currentT > end) {
4283 break;
4284 }
4285 p = this.getPointOnEllipticalArc(command.points[0], command.points[1], command.points[2], command.points[3], currentT, command.points[6]);
4286 break;
4287 }
4288 case PathParser.CURVE_TO:
4289 currentT = delta / command.pathLength;
4290 if (currentT > 1) {
4291 currentT = 1;
4292 }
4293 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]);
4294 break;
4295 case PathParser.QUAD_TO:
4296 currentT = delta / command.pathLength;
4297 if (currentT > 1) {
4298 currentT = 1;
4299 }
4300 p = this.getPointOnQuadraticBezier(currentT, command.start.x, command.start.y, command.points[0], command.points[1], command.points[2], command.points[3]);
4301 break;
4302 }
4303 if (p) {
4304 return p;
4305 }
4306 break;
4307 }
4308 return null;
4309 }
4310 getLineLength(x1, y1, x2, y2) {
4311 return Math.sqrt((x2 - x1) * (x2 - x1)
4312 + (y2 - y1) * (y2 - y1));
4313 }
4314 getPathLength() {
4315 if (this.pathLength === -1) {
4316 this.pathLength = this.dataArray.reduce((length, command) => (command.pathLength > 0
4317 ? length + command.pathLength
4318 : length), 0);
4319 }
4320 return this.pathLength;
4321 }
4322 getPointOnCubicBezier(pct, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) {
4323 const x = p4x * CB1(pct) + p3x * CB2(pct) + p2x * CB3(pct) + p1x * CB4(pct);
4324 const y = p4y * CB1(pct) + p3y * CB2(pct) + p2y * CB3(pct) + p1y * CB4(pct);
4325 return {
4326 x,
4327 y
4328 };
4329 }
4330 getPointOnQuadraticBezier(pct, p1x, p1y, p2x, p2y, p3x, p3y) {
4331 const x = p3x * QB1(pct) + p2x * QB2(pct) + p1x * QB3(pct);
4332 const y = p3y * QB1(pct) + p2y * QB2(pct) + p1y * QB3(pct);
4333 return {
4334 x,
4335 y
4336 };
4337 }
4338 getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) {
4339 const cosPsi = Math.cos(psi);
4340 const sinPsi = Math.sin(psi);
4341 const pt = {
4342 x: rx * Math.cos(theta),
4343 y: ry * Math.sin(theta)
4344 };
4345 return {
4346 x: cx + (pt.x * cosPsi - pt.y * sinPsi),
4347 y: cy + (pt.x * sinPsi + pt.y * cosPsi)
4348 };
4349 }
4350 // TODO need some optimisations. possibly build cache only for curved segments?
4351 buildEquidistantCache(inputStep, inputPrecision) {
4352 const fullLen = this.getPathLength();
4353 const precision = inputPrecision || 0.25; // accuracy vs performance
4354 const step = inputStep || fullLen / 100;
4355 if (!this.equidistantCache
4356 || this.equidistantCache.step !== step
4357 || this.equidistantCache.precision !== precision) {
4358 // Prepare cache
4359 this.equidistantCache = {
4360 step,
4361 precision,
4362 points: []
4363 };
4364 // Calculate points
4365 let s = 0;
4366 for (let l = 0; l <= fullLen; l += precision) {
4367 const p0 = this.getPointOnPath(l);
4368 const p1 = this.getPointOnPath(l + precision);
4369 if (!p0 || !p1) {
4370 continue;
4371 }
4372 s += this.getLineLength(p0.x, p0.y, p1.x, p1.y);
4373 if (s >= step) {
4374 this.equidistantCache.points.push({
4375 x: p0.x,
4376 y: p0.y,
4377 distance: l
4378 });
4379 s -= step;
4380 }
4381 }
4382 }
4383 }
4384 getEquidistantPointOnPath(targetDistance, step, precision) {
4385 this.buildEquidistantCache(step, precision);
4386 if (targetDistance < 0
4387 || targetDistance - this.getPathLength() > 0.00005) {
4388 return null;
4389 }
4390 const idx = Math.round(targetDistance
4391 / this.getPathLength()
4392 * (this.equidistantCache.points.length - 1));
4393 return this.equidistantCache.points[idx] || null;
4394 }
4395}
4396
4397// groups: 1: mime-type (+ charset), 2: mime-type (w/o charset), 3: charset, 4: base64?, 5: body
4398const dataUriRegex = /^\s*data:(([^/,;]+\/[^/,;]+)(?:;([^,;=]+=[^,;=]+))?)?(?:;(base64))?,(.*)$/i;
4399class ImageElement extends RenderedElement {
4400 constructor(document, node, captureTextNodes) {
4401 super(document, node, captureTextNodes);
4402 this.type = 'image';
4403 this.loaded = false;
4404 const href = this.getHrefAttribute().getString();
4405 if (!href) {
4406 return;
4407 }
4408 const isSvg = href.endsWith('.svg') || /^\s*data:image\/svg\+xml/i.test(href);
4409 document.images.push(this);
4410 if (!isSvg) {
4411 void this.loadImage(href);
4412 }
4413 else {
4414 void this.loadSvg(href);
4415 }
4416 this.isSvg = isSvg;
4417 }
4418 async loadImage(href) {
4419 try {
4420 const image = await this.document.createImage(href);
4421 this.image = image;
4422 }
4423 catch (err) {
4424 console.error(`Error while loading image "${href}":`, err);
4425 }
4426 this.loaded = true;
4427 }
4428 async loadSvg(href) {
4429 const match = dataUriRegex.exec(href);
4430 if (match) {
4431 const data = match[5];
4432 if (match[4] === 'base64') {
4433 this.image = atob(data);
4434 }
4435 else {
4436 this.image = decodeURIComponent(data);
4437 }
4438 }
4439 else {
4440 try {
4441 const response = await this.document.fetch(href);
4442 const svg = await response.text();
4443 this.image = svg;
4444 }
4445 catch (err) {
4446 console.error(`Error while loading image "${href}":`, err);
4447 }
4448 }
4449 this.loaded = true;
4450 }
4451 renderChildren(ctx) {
4452 const { document, image, loaded } = this;
4453 const x = this.getAttribute('x').getPixels('x');
4454 const y = this.getAttribute('y').getPixels('y');
4455 const width = this.getStyle('width').getPixels('x');
4456 const height = this.getStyle('height').getPixels('y');
4457 if (!loaded || !image
4458 || !width || !height) {
4459 return;
4460 }
4461 ctx.save();
4462 ctx.translate(x, y);
4463 if (this.isSvg) {
4464 const subDocument = document.canvg.forkString(ctx, this.image, {
4465 ignoreMouse: true,
4466 ignoreAnimation: true,
4467 ignoreDimensions: true,
4468 ignoreClear: true,
4469 offsetX: 0,
4470 offsetY: 0,
4471 scaleWidth: width,
4472 scaleHeight: height
4473 });
4474 subDocument.document.documentElement.parent = this;
4475 void subDocument.render();
4476 }
4477 else {
4478 const image = this.image;
4479 document.setViewBox({
4480 ctx,
4481 aspectRatio: this.getAttribute('preserveAspectRatio').getString(),
4482 width,
4483 desiredWidth: image.width,
4484 height,
4485 desiredHeight: image.height
4486 });
4487 if (this.loaded) {
4488 if (typeof image.complete === 'undefined' || image.complete) {
4489 ctx.drawImage(image, 0, 0);
4490 }
4491 }
4492 }
4493 ctx.restore();
4494 }
4495 getBoundingBox() {
4496 const x = this.getAttribute('x').getPixels('x');
4497 const y = this.getAttribute('y').getPixels('y');
4498 const width = this.getStyle('width').getPixels('x');
4499 const height = this.getStyle('height').getPixels('y');
4500 return new BoundingBox(x, y, x + width, y + height);
4501 }
4502}
4503
4504class SymbolElement extends RenderedElement {
4505 constructor() {
4506 super(...arguments);
4507 this.type = 'symbol';
4508 }
4509 render(_) {
4510 // NO RENDER
4511 }
4512}
4513
4514class SVGFontLoader {
4515 constructor(document) {
4516 this.document = document;
4517 this.loaded = false;
4518 document.fonts.push(this);
4519 }
4520 async load(fontFamily, url) {
4521 try {
4522 const { document } = this;
4523 const svgDocument = await document.canvg.parser.load(url);
4524 const fonts = svgDocument.getElementsByTagName('font');
4525 Array.from(fonts).forEach((fontNode) => {
4526 const font = document.createElement(fontNode);
4527 document.definitions[fontFamily] = font;
4528 });
4529 }
4530 catch (err) {
4531 console.error(`Error while loading font "${url}":`, err);
4532 }
4533 this.loaded = true;
4534 }
4535}
4536
4537class StyleElement extends Element {
4538 constructor(document, node, captureTextNodes) {
4539 super(document, node, captureTextNodes);
4540 this.type = 'style';
4541 const css = compressSpaces(Array.from(node.childNodes)
4542 // NEED TEST
4543 .map(_ => _.textContent)
4544 .join('')
4545 .replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, '') // remove comments
4546 .replace(/@import.*;/g, '') // remove imports
4547 );
4548 const cssDefs = css.split('}');
4549 cssDefs.forEach((_) => {
4550 const def = _.trim();
4551 if (!def) {
4552 return;
4553 }
4554 const cssParts = def.split('{');
4555 const cssClasses = cssParts[0].split(',');
4556 const cssProps = cssParts[1].split(';');
4557 cssClasses.forEach((_) => {
4558 const cssClass = _.trim();
4559 if (!cssClass) {
4560 return;
4561 }
4562 const props = document.styles[cssClass] || {};
4563 cssProps.forEach((cssProp) => {
4564 const prop = cssProp.indexOf(':');
4565 const name = cssProp.substr(0, prop).trim();
4566 const value = cssProp.substr(prop + 1, cssProp.length - prop).trim();
4567 if (name && value) {
4568 props[name] = new Property(document, name, value);
4569 }
4570 });
4571 document.styles[cssClass] = props;
4572 document.stylesSpecificity[cssClass] = getSelectorSpecificity(cssClass);
4573 if (cssClass === '@font-face') { // && !nodeEnv
4574 const fontFamily = props['font-family'].getString().replace(/"|'/g, '');
4575 const srcs = props.src.getString().split(',');
4576 srcs.forEach((src) => {
4577 if (src.indexOf('format("svg")') > 0) {
4578 const url = parseExternalUrl(src);
4579 if (url) {
4580 void new SVGFontLoader(document).load(fontFamily, url);
4581 }
4582 }
4583 });
4584 }
4585 });
4586 });
4587 }
4588}
4589StyleElement.parseExternalUrl = parseExternalUrl;
4590
4591class UseElement extends RenderedElement {
4592 constructor() {
4593 super(...arguments);
4594 this.type = 'use';
4595 }
4596 setContext(ctx) {
4597 super.setContext(ctx);
4598 const xAttr = this.getAttribute('x');
4599 const yAttr = this.getAttribute('y');
4600 if (xAttr.hasValue()) {
4601 ctx.translate(xAttr.getPixels('x'), 0);
4602 }
4603 if (yAttr.hasValue()) {
4604 ctx.translate(0, yAttr.getPixels('y'));
4605 }
4606 }
4607 path(ctx) {
4608 const { element } = this;
4609 if (element) {
4610 element.path(ctx);
4611 }
4612 }
4613 renderChildren(ctx) {
4614 const { document, element } = this;
4615 if (element) {
4616 let tempSvg = element;
4617 if (element.type === 'symbol') {
4618 // render me using a temporary svg element in symbol cases (http://www.w3.org/TR/SVG/struct.html#UseElement)
4619 tempSvg = new SVGElement(document, null);
4620 tempSvg.attributes.viewBox = new Property(document, 'viewBox', element.getAttribute('viewBox').getString());
4621 tempSvg.attributes.preserveAspectRatio = new Property(document, 'preserveAspectRatio', element.getAttribute('preserveAspectRatio').getString());
4622 tempSvg.attributes.overflow = new Property(document, 'overflow', element.getAttribute('overflow').getString());
4623 tempSvg.children = element.children;
4624 // element is still the parent of the children
4625 element.styles.opacity = new Property(document, 'opacity', this.calculateOpacity());
4626 }
4627 if (tempSvg.type === 'svg') {
4628 const widthStyle = this.getStyle('width', false, true);
4629 const heightStyle = this.getStyle('height', false, true);
4630 // if symbol or svg, inherit width/height from me
4631 if (widthStyle.hasValue()) {
4632 tempSvg.attributes.width = new Property(document, 'width', widthStyle.getString());
4633 }
4634 if (heightStyle.hasValue()) {
4635 tempSvg.attributes.height = new Property(document, 'height', heightStyle.getString());
4636 }
4637 }
4638 const oldParent = tempSvg.parent;
4639 tempSvg.parent = this;
4640 tempSvg.render(ctx);
4641 tempSvg.parent = oldParent;
4642 }
4643 }
4644 getBoundingBox(ctx) {
4645 const { element } = this;
4646 if (element) {
4647 return element.getBoundingBox(ctx);
4648 }
4649 return null;
4650 }
4651 elementTransform() {
4652 const { document, element } = this;
4653 return Transform.fromElement(document, element);
4654 }
4655 get element() {
4656 if (!this.cachedElement) {
4657 this.cachedElement = this.getHrefAttribute().getDefinition();
4658 }
4659 return this.cachedElement;
4660 }
4661}
4662
4663function imGet(img, x, y, width, _height, rgba) {
4664 return img[y * width * 4 + x * 4 + rgba];
4665}
4666function imSet(img, x, y, width, _height, rgba, val) {
4667 img[y * width * 4 + x * 4 + rgba] = val;
4668}
4669function m(matrix, i, v) {
4670 const mi = matrix[i];
4671 return mi * v;
4672}
4673function c(a, m1, m2, m3) {
4674 return m1 + Math.cos(a) * m2 + Math.sin(a) * m3;
4675}
4676class FeColorMatrixElement extends Element {
4677 constructor(document, node, captureTextNodes) {
4678 super(document, node, captureTextNodes);
4679 this.type = 'feColorMatrix';
4680 let matrix = toNumbers(this.getAttribute('values').getString());
4681 switch (this.getAttribute('type').getString('matrix')) { // http://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
4682 case 'saturate': {
4683 const s = matrix[0];
4684 /* eslint-disable array-element-newline */
4685 matrix = [
4686 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0,
4687 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0,
4688 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0,
4689 0, 0, 0, 1, 0,
4690 0, 0, 0, 0, 1
4691 ];
4692 /* eslint-enable array-element-newline */
4693 break;
4694 }
4695 case 'hueRotate': {
4696 const a = matrix[0] * Math.PI / 180.0;
4697 /* eslint-disable array-element-newline */
4698 matrix = [
4699 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,
4700 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,
4701 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,
4702 0, 0, 0, 1, 0,
4703 0, 0, 0, 0, 1
4704 ];
4705 /* eslint-enable array-element-newline */
4706 break;
4707 }
4708 case 'luminanceToAlpha':
4709 /* eslint-disable array-element-newline */
4710 matrix = [
4711 0, 0, 0, 0, 0,
4712 0, 0, 0, 0, 0,
4713 0, 0, 0, 0, 0,
4714 0.2125, 0.7154, 0.0721, 0, 0,
4715 0, 0, 0, 0, 1
4716 ];
4717 /* eslint-enable array-element-newline */
4718 break;
4719 }
4720 this.matrix = matrix;
4721 this.includeOpacity = this.getAttribute('includeOpacity').hasValue();
4722 }
4723 apply(ctx, _x, _y, width, height) {
4724 // assuming x==0 && y==0 for now
4725 const { includeOpacity, matrix } = this;
4726 const srcData = ctx.getImageData(0, 0, width, height);
4727 for (let y = 0; y < height; y++) {
4728 for (let x = 0; x < width; x++) {
4729 const r = imGet(srcData.data, x, y, width, height, 0);
4730 const g = imGet(srcData.data, x, y, width, height, 1);
4731 const b = imGet(srcData.data, x, y, width, height, 2);
4732 const a = imGet(srcData.data, x, y, width, height, 3);
4733 let nr = m(matrix, 0, r) + m(matrix, 1, g) + m(matrix, 2, b) + m(matrix, 3, a) + m(matrix, 4, 1);
4734 let ng = m(matrix, 5, r) + m(matrix, 6, g) + m(matrix, 7, b) + m(matrix, 8, a) + m(matrix, 9, 1);
4735 let nb = m(matrix, 10, r) + m(matrix, 11, g) + m(matrix, 12, b) + m(matrix, 13, a) + m(matrix, 14, 1);
4736 let na = m(matrix, 15, r) + m(matrix, 16, g) + m(matrix, 17, b) + m(matrix, 18, a) + m(matrix, 19, 1);
4737 if (includeOpacity) {
4738 nr = 0;
4739 ng = 0;
4740 nb = 0;
4741 na *= a / 255;
4742 }
4743 imSet(srcData.data, x, y, width, height, 0, nr);
4744 imSet(srcData.data, x, y, width, height, 1, ng);
4745 imSet(srcData.data, x, y, width, height, 2, nb);
4746 imSet(srcData.data, x, y, width, height, 3, na);
4747 }
4748 }
4749 ctx.clearRect(0, 0, width, height);
4750 ctx.putImageData(srcData, 0, 0);
4751 }
4752}
4753
4754class MaskElement extends Element {
4755 constructor() {
4756 super(...arguments);
4757 this.type = 'mask';
4758 }
4759 apply(ctx, element) {
4760 const { document } = this;
4761 // render as temp svg
4762 let x = this.getAttribute('x').getPixels('x');
4763 let y = this.getAttribute('y').getPixels('y');
4764 let width = this.getStyle('width').getPixels('x');
4765 let height = this.getStyle('height').getPixels('y');
4766 if (!width && !height) {
4767 const boundingBox = new BoundingBox();
4768 this.children.forEach((child) => {
4769 boundingBox.addBoundingBox(child.getBoundingBox(ctx));
4770 });
4771 x = Math.floor(boundingBox.x1);
4772 y = Math.floor(boundingBox.y1);
4773 width = Math.floor(boundingBox.width);
4774 height = Math.floor(boundingBox.height);
4775 }
4776 const ignoredStyles = this.removeStyles(element, MaskElement.ignoreStyles);
4777 const maskCanvas = document.createCanvas(x + width, y + height);
4778 const maskCtx = maskCanvas.getContext('2d');
4779 document.screen.setDefaults(maskCtx);
4780 this.renderChildren(maskCtx);
4781 // convert mask to alpha with a fake node
4782 // TODO: refactor out apply from feColorMatrix
4783 new FeColorMatrixElement(document, ({
4784 nodeType: 1,
4785 childNodes: [],
4786 attributes: [
4787 {
4788 nodeName: 'type',
4789 value: 'luminanceToAlpha'
4790 },
4791 {
4792 nodeName: 'includeOpacity',
4793 value: 'true'
4794 }
4795 ]
4796 })).apply(maskCtx, 0, 0, x + width, y + height);
4797 const tmpCanvas = document.createCanvas(x + width, y + height);
4798 const tmpCtx = tmpCanvas.getContext('2d');
4799 document.screen.setDefaults(tmpCtx);
4800 element.render(tmpCtx);
4801 tmpCtx.globalCompositeOperation = 'destination-in';
4802 tmpCtx.fillStyle = maskCtx.createPattern(maskCanvas, 'no-repeat');
4803 tmpCtx.fillRect(0, 0, x + width, y + height);
4804 ctx.fillStyle = tmpCtx.createPattern(tmpCanvas, 'no-repeat');
4805 ctx.fillRect(0, 0, x + width, y + height);
4806 // reassign mask
4807 this.restoreStyles(element, ignoredStyles);
4808 }
4809 render(_) {
4810 // NO RENDER
4811 }
4812}
4813MaskElement.ignoreStyles = [
4814 'mask',
4815 'transform',
4816 'clip-path'
4817];
4818
4819const noop = () => {
4820 // NOOP
4821};
4822class ClipPathElement extends Element {
4823 constructor() {
4824 super(...arguments);
4825 this.type = 'clipPath';
4826 }
4827 apply(ctx) {
4828 const { document } = this;
4829 const contextProto = Reflect.getPrototypeOf(ctx);
4830 const { beginPath, closePath } = ctx;
4831 if (contextProto) {
4832 contextProto.beginPath = noop;
4833 contextProto.closePath = noop;
4834 }
4835 Reflect.apply(beginPath, ctx, []);
4836 this.children.forEach((child) => {
4837 if (typeof child.path === 'undefined') {
4838 return;
4839 }
4840 let transform = typeof child.elementTransform !== 'undefined'
4841 ? child.elementTransform()
4842 : null; // handle <use />
4843 if (!transform) {
4844 transform = Transform.fromElement(document, child);
4845 }
4846 if (transform) {
4847 transform.apply(ctx);
4848 }
4849 child.path(ctx);
4850 if (contextProto) {
4851 contextProto.closePath = closePath;
4852 }
4853 if (transform) {
4854 transform.unapply(ctx);
4855 }
4856 });
4857 Reflect.apply(closePath, ctx, []);
4858 ctx.clip();
4859 if (contextProto) {
4860 contextProto.beginPath = beginPath;
4861 contextProto.closePath = closePath;
4862 }
4863 }
4864 render(_) {
4865 // NO RENDER
4866 }
4867}
4868
4869class FilterElement extends Element {
4870 constructor() {
4871 super(...arguments);
4872 this.type = 'filter';
4873 }
4874 apply(ctx, element) {
4875 // render as temp svg
4876 const { document, children } = this;
4877 const boundingBox = element.getBoundingBox(ctx);
4878 if (!boundingBox) {
4879 return;
4880 }
4881 let px = 0;
4882 let py = 0;
4883 children.forEach((child) => {
4884 const efd = child.extraFilterDistance || 0;
4885 px = Math.max(px, efd);
4886 py = Math.max(py, efd);
4887 });
4888 const width = Math.floor(boundingBox.width);
4889 const height = Math.floor(boundingBox.height);
4890 const tmpCanvasWidth = width + 2 * px;
4891 const tmpCanvasHeight = height + 2 * py;
4892 if (tmpCanvasWidth < 1 || tmpCanvasHeight < 1) {
4893 return;
4894 }
4895 const x = Math.floor(boundingBox.x);
4896 const y = Math.floor(boundingBox.y);
4897 const ignoredStyles = this.removeStyles(element, FilterElement.ignoreStyles);
4898 const tmpCanvas = document.createCanvas(tmpCanvasWidth, tmpCanvasHeight);
4899 const tmpCtx = tmpCanvas.getContext('2d');
4900 document.screen.setDefaults(tmpCtx);
4901 tmpCtx.translate(-x + px, -y + py);
4902 element.render(tmpCtx);
4903 // apply filters
4904 children.forEach((child) => {
4905 if (typeof child.apply === 'function') {
4906 child.apply(tmpCtx, 0, 0, tmpCanvasWidth, tmpCanvasHeight);
4907 }
4908 });
4909 // render on me
4910 ctx.drawImage(tmpCanvas, 0, 0, tmpCanvasWidth, tmpCanvasHeight, x - px, y - py, tmpCanvasWidth, tmpCanvasHeight);
4911 this.restoreStyles(element, ignoredStyles);
4912 }
4913 render(_) {
4914 // NO RENDER
4915 }
4916}
4917FilterElement.ignoreStyles = [
4918 'filter',
4919 'transform',
4920 'clip-path'
4921];
4922
4923class FeDropShadowElement extends Element {
4924 constructor(document, node, captureTextNodes) {
4925 super(document, node, captureTextNodes);
4926 this.type = 'feDropShadow';
4927 this.addStylesFromStyleDefinition();
4928 }
4929 apply(_, _x, _y, _width, _height) {
4930 // TODO: implement
4931 }
4932}
4933
4934class FeMorphologyElement extends Element {
4935 constructor() {
4936 super(...arguments);
4937 this.type = 'feMorphology';
4938 }
4939 apply(_, _x, _y, _width, _height) {
4940 // TODO: implement
4941 }
4942}
4943
4944class FeCompositeElement extends Element {
4945 constructor() {
4946 super(...arguments);
4947 this.type = 'feComposite';
4948 }
4949 apply(_, _x, _y, _width, _height) {
4950 // TODO: implement
4951 }
4952}
4953
4954class FeGaussianBlurElement extends Element {
4955 constructor(document, node, captureTextNodes) {
4956 super(document, node, captureTextNodes);
4957 this.type = 'feGaussianBlur';
4958 this.blurRadius = Math.floor(this.getAttribute('stdDeviation').getNumber());
4959 this.extraFilterDistance = this.blurRadius;
4960 }
4961 apply(ctx, x, y, width, height) {
4962 const { document, blurRadius } = this;
4963 const body = document.window
4964 ? document.window.document.body
4965 : null;
4966 const canvas = ctx.canvas;
4967 // StackBlur requires canvas be on document
4968 canvas.id = document.getUniqueId();
4969 if (body) {
4970 canvas.style.display = 'none';
4971 body.appendChild(canvas);
4972 }
4973 canvasRGBA(canvas, x, y, width, height, blurRadius);
4974 if (body) {
4975 body.removeChild(canvas);
4976 }
4977 }
4978}
4979
4980class TitleElement extends Element {
4981 constructor() {
4982 super(...arguments);
4983 this.type = 'title';
4984 }
4985}
4986
4987class DescElement extends Element {
4988 constructor() {
4989 super(...arguments);
4990 this.type = 'desc';
4991 }
4992}
4993
4994const elements = {
4995 'svg': SVGElement,
4996 'rect': RectElement,
4997 'circle': CircleElement,
4998 'ellipse': EllipseElement,
4999 'line': LineElement,
5000 'polyline': PolylineElement,
5001 'polygon': PolygonElement,
5002 'path': PathElement,
5003 'pattern': PatternElement,
5004 'marker': MarkerElement,
5005 'defs': DefsElement,
5006 'linearGradient': LinearGradientElement,
5007 'radialGradient': RadialGradientElement,
5008 'stop': StopElement,
5009 'animate': AnimateElement,
5010 'animateColor': AnimateColorElement,
5011 'animateTransform': AnimateTransformElement,
5012 'font': FontElement,
5013 'font-face': FontFaceElement,
5014 'missing-glyph': MissingGlyphElement,
5015 'glyph': GlyphElement,
5016 'text': TextElement,
5017 'tspan': TSpanElement,
5018 'tref': TRefElement,
5019 'a': AElement,
5020 'textPath': TextPathElement,
5021 'image': ImageElement,
5022 'g': GElement,
5023 'symbol': SymbolElement,
5024 'style': StyleElement,
5025 'use': UseElement,
5026 'mask': MaskElement,
5027 'clipPath': ClipPathElement,
5028 'filter': FilterElement,
5029 'feDropShadow': FeDropShadowElement,
5030 'feMorphology': FeMorphologyElement,
5031 'feComposite': FeCompositeElement,
5032 'feColorMatrix': FeColorMatrixElement,
5033 'feGaussianBlur': FeGaussianBlurElement,
5034 'title': TitleElement,
5035 'desc': DescElement
5036};
5037
5038function createCanvas(width, height) {
5039 const canvas = document.createElement('canvas');
5040 canvas.width = width;
5041 canvas.height = height;
5042 return canvas;
5043}
5044async function createImage(src, anonymousCrossOrigin = false) {
5045 const image = document.createElement('img');
5046 if (anonymousCrossOrigin) {
5047 image.crossOrigin = 'Anonymous';
5048 }
5049 return new Promise((resolve, reject) => {
5050 image.onload = () => {
5051 resolve(image);
5052 };
5053 image.onerror = (_event, _source, _lineno, _colno, error) => {
5054 reject(error);
5055 };
5056 image.src = src;
5057 });
5058}
5059class Document {
5060 constructor(canvg, { rootEmSize = 12, emSize = 12, createCanvas = Document.createCanvas, createImage = Document.createImage, anonymousCrossOrigin } = {}) {
5061 this.canvg = canvg;
5062 this.definitions = {};
5063 this.styles = {};
5064 this.stylesSpecificity = {};
5065 this.images = [];
5066 this.fonts = [];
5067 this.emSizeStack = [];
5068 this.uniqueId = 0;
5069 this.screen = canvg.screen;
5070 this.rootEmSize = rootEmSize;
5071 this.emSize = emSize;
5072 this.createCanvas = createCanvas;
5073 this.createImage = this.bindCreateImage(createImage, anonymousCrossOrigin);
5074 this.screen.wait(this.isImagesLoaded.bind(this));
5075 this.screen.wait(this.isFontsLoaded.bind(this));
5076 }
5077 bindCreateImage(createImage, anonymousCrossOrigin) {
5078 if (typeof anonymousCrossOrigin === 'boolean') {
5079 return (source, forceAnonymousCrossOrigin) => createImage(source, typeof forceAnonymousCrossOrigin === 'boolean'
5080 ? forceAnonymousCrossOrigin
5081 : anonymousCrossOrigin);
5082 }
5083 return createImage;
5084 }
5085 get window() {
5086 return this.screen.window;
5087 }
5088 get fetch() {
5089 return this.screen.fetch;
5090 }
5091 get ctx() {
5092 return this.screen.ctx;
5093 }
5094 get emSize() {
5095 const { emSizeStack } = this;
5096 return emSizeStack[emSizeStack.length - 1];
5097 }
5098 set emSize(value) {
5099 const { emSizeStack } = this;
5100 emSizeStack.push(value);
5101 }
5102 popEmSize() {
5103 const { emSizeStack } = this;
5104 emSizeStack.pop();
5105 }
5106 getUniqueId() {
5107 return `canvg${++this.uniqueId}`;
5108 }
5109 isImagesLoaded() {
5110 return this.images.every(_ => _.loaded);
5111 }
5112 isFontsLoaded() {
5113 return this.fonts.every(_ => _.loaded);
5114 }
5115 createDocumentElement(document) {
5116 const documentElement = this.createElement(document.documentElement);
5117 documentElement.root = true;
5118 documentElement.addStylesFromStyleDefinition();
5119 this.documentElement = documentElement;
5120 return documentElement;
5121 }
5122 createElement(node) {
5123 const elementType = node.nodeName.replace(/^[^:]+:/, '');
5124 const ElementType = Document.elementTypes[elementType];
5125 if (typeof ElementType !== 'undefined') {
5126 return new ElementType(this, node);
5127 }
5128 return new UnknownElement(this, node);
5129 }
5130 createTextNode(node) {
5131 return new TextNode(this, node);
5132 }
5133 setViewBox(config) {
5134 this.screen.setViewBox({
5135 document: this,
5136 ...config
5137 });
5138 }
5139}
5140Document.createCanvas = createCanvas;
5141Document.createImage = createImage;
5142Document.elementTypes = elements;
5143
5144/**
5145 * SVG renderer on canvas.
5146 */
5147class Canvg {
5148 /**
5149 * Main constructor.
5150 * @param ctx - Rendering context.
5151 * @param svg - SVG Document.
5152 * @param options - Rendering options.
5153 */
5154 constructor(ctx, svg, options = {}) {
5155 this.parser = new Parser(options);
5156 this.screen = new Screen(ctx, options);
5157 this.options = options;
5158 const document = new Document(this, options);
5159 const documentElement = document.createDocumentElement(svg);
5160 this.document = document;
5161 this.documentElement = documentElement;
5162 }
5163 /**
5164 * Create Canvg instance from SVG source string or URL.
5165 * @param ctx - Rendering context.
5166 * @param svg - SVG source string or URL.
5167 * @param options - Rendering options.
5168 * @returns Canvg instance.
5169 */
5170 static async from(ctx, svg, options = {}) {
5171 const parser = new Parser(options);
5172 const svgDocument = await parser.parse(svg);
5173 return new Canvg(ctx, svgDocument, options);
5174 }
5175 /**
5176 * Create Canvg instance from SVG source string.
5177 * @param ctx - Rendering context.
5178 * @param svg - SVG source string.
5179 * @param options - Rendering options.
5180 * @returns Canvg instance.
5181 */
5182 static fromString(ctx, svg, options = {}) {
5183 const parser = new Parser(options);
5184 const svgDocument = parser.parseFromString(svg);
5185 return new Canvg(ctx, svgDocument, options);
5186 }
5187 /**
5188 * Create new Canvg instance with inherited options.
5189 * @param ctx - Rendering context.
5190 * @param svg - SVG source string or URL.
5191 * @param options - Rendering options.
5192 * @returns Canvg instance.
5193 */
5194 fork(ctx, svg, options = {}) {
5195 return Canvg.from(ctx, svg, {
5196 ...this.options,
5197 ...options
5198 });
5199 }
5200 /**
5201 * Create new Canvg instance with inherited options.
5202 * @param ctx - Rendering context.
5203 * @param svg - SVG source string.
5204 * @param options - Rendering options.
5205 * @returns Canvg instance.
5206 */
5207 forkString(ctx, svg, options = {}) {
5208 return Canvg.fromString(ctx, svg, {
5209 ...this.options,
5210 ...options
5211 });
5212 }
5213 /**
5214 * Document is ready promise.
5215 * @returns Ready promise.
5216 */
5217 ready() {
5218 return this.screen.ready();
5219 }
5220 /**
5221 * Document is ready value.
5222 * @returns Is ready or not.
5223 */
5224 isReady() {
5225 return this.screen.isReady();
5226 }
5227 /**
5228 * Render only first frame, ignoring animations and mouse.
5229 * @param options - Rendering options.
5230 */
5231 async render(options = {}) {
5232 this.start({
5233 enableRedraw: true,
5234 ignoreAnimation: true,
5235 ignoreMouse: true,
5236 ...options
5237 });
5238 await this.ready();
5239 this.stop();
5240 }
5241 /**
5242 * Start rendering.
5243 * @param options - Render options.
5244 */
5245 start(options = {}) {
5246 const { documentElement, screen, options: baseOptions } = this;
5247 screen.start(documentElement, {
5248 enableRedraw: true,
5249 ...baseOptions,
5250 ...options
5251 });
5252 }
5253 /**
5254 * Stop rendering.
5255 */
5256 stop() {
5257 this.screen.stop();
5258 }
5259 /**
5260 * Resize SVG to fit in given size.
5261 * @param width
5262 * @param height
5263 * @param preserveAspectRatio
5264 */
5265 resize(width, height = width, preserveAspectRatio = false) {
5266 this.documentElement.resize(width, height, preserveAspectRatio);
5267 }
5268}
5269
5270export { 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, Canvg as default, getSelectorSpecificity, normalizeAttributeName, normalizeColor, parseExternalUrl, index as presets, toNumbers, trimLeft, trimRight, vectorMagnitude, vectorsAngle, vectorsRatio };
5271//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguYmFiZWwuanMiLCJzb3VyY2VzIjpbXSwic291cmNlc0NvbnRlbnQiOltdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7In0=
Note: See TracBrowser for help on using the repository browser.