ntk 8.17.3 → 8.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/fontconfig.js +426 -97
- package/lib/renderingcontext_2d.js +173 -61
- package/lib/text/glyphs.js +34 -13
- package/lib/widgets/svgview.js +209 -72
- package/package.json +1 -1
package/lib/widgets/svgview.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
//
|
|
17
17
|
// Supported: path/rect/circle/ellipse/line/polyline/polygon, <g>, <defs>,
|
|
18
18
|
// <use>, linear/radial gradients (objectBoundingBox and userSpaceOnUse),
|
|
19
|
-
// transform lists, opacity/fill-opacity/stroke-opacity,
|
|
20
|
-
// <text>. See docs/svg.md for the
|
|
19
|
+
// transform lists, opacity/fill-opacity/stroke-opacity, display, visibility,
|
|
20
|
+
// nested <svg> viewports, fill-rule, basic <text>. See docs/svg.md for the
|
|
21
|
+
// full surface and limitations.
|
|
21
22
|
|
|
22
23
|
import { textContent } from 'domutils';
|
|
23
24
|
import { parseDocument } from 'htmlparser2';
|
|
@@ -37,7 +38,8 @@ const INHERITED = {
|
|
|
37
38
|
color: '#000',
|
|
38
39
|
fontFamily: 'sans-serif',
|
|
39
40
|
fontSize: 16,
|
|
40
|
-
textAnchor: 'start'
|
|
41
|
+
textAnchor: 'start',
|
|
42
|
+
visibility: 'visible'
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
const STYLE_ATTRS = {
|
|
@@ -53,10 +55,13 @@ const STYLE_ATTRS = {
|
|
|
53
55
|
color: 'color',
|
|
54
56
|
'font-family': 'fontFamily',
|
|
55
57
|
'font-size': 'fontSize',
|
|
56
|
-
'text-anchor': 'textAnchor'
|
|
58
|
+
'text-anchor': 'textAnchor',
|
|
59
|
+
visibility: 'visibility'
|
|
57
60
|
};
|
|
58
61
|
|
|
59
|
-
const NUMERIC = new Set(['strokeWidth', 'miterLimit', '
|
|
62
|
+
const NUMERIC = new Set(['strokeWidth', 'miterLimit', 'fontSize']);
|
|
63
|
+
const ALPHA = new Set(['fillOpacity', 'strokeOpacity']);
|
|
64
|
+
const VISIBILITY = new Set(['visible', 'hidden', 'collapse']);
|
|
60
65
|
|
|
61
66
|
// documents may come from an XML parse (setSvg: exact case) or from an HTML
|
|
62
67
|
// parse (inline <svg>: tag/attribute names lowercased) — compare
|
|
@@ -76,6 +81,27 @@ function attrNum(node, name, fallback = 0) {
|
|
|
76
81
|
return Number.isFinite(n) ? n : fallback;
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A property that is not inherited, as an element sets it itself: in its
|
|
86
|
+
* `style`, the last declaration of it there that `read` makes something
|
|
87
|
+
* of, or else by its presentation attribute — the order `resolveStyle`
|
|
88
|
+
* applies the inherited ones in. `undefined` where it sets it neither way.
|
|
89
|
+
*/
|
|
90
|
+
function ownProperty(node, name, read) {
|
|
91
|
+
const raw = attr(node, name);
|
|
92
|
+
let v = raw === undefined ? undefined : read(raw);
|
|
93
|
+
const style = node.attribs?.style;
|
|
94
|
+
if (style && style.includes(name)) {
|
|
95
|
+
for (const decl of style.split(';')) {
|
|
96
|
+
const idx = decl.indexOf(':');
|
|
97
|
+
if (idx > 0 && decl.slice(0, idx).trim() === name) v = read(decl.slice(idx + 1)) ?? v;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return v;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const keyword = (raw) => String(raw).trim().toLowerCase() || undefined;
|
|
104
|
+
|
|
79
105
|
/**
|
|
80
106
|
* Whether an element is `display: none`, by its attribute or in its
|
|
81
107
|
* `style`, which wins. Such an element is not rendered, and nor is anything
|
|
@@ -85,15 +111,67 @@ function attrNum(node, name, fallback = 0) {
|
|
|
85
111
|
* around shapes it marks `display="inline"`.
|
|
86
112
|
*/
|
|
87
113
|
function displayNone(node) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
return ownProperty(node, 'display', keyword) === 'none';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* An `<alpha-value>` — `opacity`, `fill-opacity`, `stroke-opacity`: a number
|
|
119
|
+
* or a percentage, clamped to 0..1 (CSS Color 4, 5.2). `undefined` for
|
|
120
|
+
* anything else, so the caller keeps what it had.
|
|
121
|
+
*/
|
|
122
|
+
function alphaValue(raw) {
|
|
123
|
+
const v = String(raw ?? '').trim();
|
|
124
|
+
let n = parseFloat(v);
|
|
125
|
+
if (!Number.isFinite(n)) return undefined;
|
|
126
|
+
if (v.endsWith('%')) n /= 100;
|
|
127
|
+
return Math.min(1, Math.max(0, n));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A `viewBox` as `[minX, minY, width, height]`, or null where it has none. */
|
|
131
|
+
function parseViewBox(node) {
|
|
132
|
+
const vb = (attr(node, 'viewBox') || '')
|
|
133
|
+
.split(/[\s,]+/)
|
|
134
|
+
.filter((s) => s !== '')
|
|
135
|
+
.map(parseFloat);
|
|
136
|
+
return vb.length === 4 && vb.every(Number.isFinite) ? vb : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A length on a viewport-establishing element — `x`, `y`, `width`, `height`
|
|
141
|
+
* — in the user units of the viewport it sits in, `size` along its axis;
|
|
142
|
+
* a percentage is of that. `auto` and a missing or unreadable value are the
|
|
143
|
+
* `fallback`.
|
|
144
|
+
*/
|
|
145
|
+
function viewportLength(node, name, size, fallback) {
|
|
146
|
+
const v = String(attr(node, name) ?? '').trim();
|
|
147
|
+
const n = parseFloat(v);
|
|
148
|
+
if (!Number.isFinite(n)) return fallback;
|
|
149
|
+
return v.endsWith('%') ? (n / 100) * size : n;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ALIGN = /^x(Min|Mid|Max)Y(Min|Mid|Max)$/;
|
|
153
|
+
const ALONG = { Min: 0, Mid: 0.5, Max: 1 };
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The transform that fits a `viewBox` into a viewport `w` by `h` at `x`,`y`,
|
|
157
|
+
* as `preserveAspectRatio` asks (SVG 2, 8.8): scaled evenly to fit inside
|
|
158
|
+
* (`meet`, the default) or to cover it (`slice`), and set where its
|
|
159
|
+
* alignment says, `xMidYMid` by default; or stretched to it for `none`.
|
|
160
|
+
*
|
|
161
|
+
* @returns {[number, number, number, number]} `[sx, sy, tx, ty]`
|
|
162
|
+
*/
|
|
163
|
+
function viewBoxFit([minX, minY, vbW, vbH], x, y, w, h, par) {
|
|
164
|
+
const words = String(par || '').trim().split(/\s+/);
|
|
165
|
+
if (words[0] === 'defer') words.shift();
|
|
166
|
+
const [align, meetOrSlice] = words;
|
|
167
|
+
let sx = w / vbW;
|
|
168
|
+
let sy = h / vbH;
|
|
169
|
+
if (align === 'none') return [sx, sy, x - minX * sx, y - minY * sy];
|
|
170
|
+
// a value it cannot read is the default as a whole, xMidYMid meet
|
|
171
|
+
const m = ALIGN.exec(align || '');
|
|
172
|
+
const [fx, fy] = m ? [ALONG[m[1]], ALONG[m[2]]] : [0.5, 0.5];
|
|
173
|
+
sx = sy = m && meetOrSlice === 'slice' ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
174
|
+
return [sx, sy, x - minX * sx + (w - vbW * sx) * fx, y - minY * sy + (h - vbH * sy) * fy];
|
|
97
175
|
}
|
|
98
176
|
|
|
99
177
|
const NON_RENDERED = new Set([
|
|
@@ -222,6 +300,45 @@ function pathBBox(path) {
|
|
|
222
300
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
223
301
|
}
|
|
224
302
|
|
|
303
|
+
/**
|
|
304
|
+
* The inherited properties an element ends up with: its parent's, where
|
|
305
|
+
* the element sets them neither by presentation attribute nor in its inline
|
|
306
|
+
* `style`, which wins over the attribute. A value it cannot read is dropped,
|
|
307
|
+
* as CSS drops an invalid declaration. Returns `parent` itself where
|
|
308
|
+
* nothing changes.
|
|
309
|
+
*/
|
|
310
|
+
function resolveStyle(node, parent) {
|
|
311
|
+
let style = parent;
|
|
312
|
+
const own = () => (style === parent ? (style = { ...parent }) : style);
|
|
313
|
+
const apply = (name, raw) => {
|
|
314
|
+
const key = STYLE_ATTRS[name];
|
|
315
|
+
if (!key || raw === undefined || raw === '' || raw === 'inherit') return;
|
|
316
|
+
const v = String(raw).trim();
|
|
317
|
+
if (NUMERIC.has(key)) {
|
|
318
|
+
const n = parseFloat(v);
|
|
319
|
+
if (!Number.isFinite(n)) return;
|
|
320
|
+
own()[key] = n;
|
|
321
|
+
} else if (ALPHA.has(key)) {
|
|
322
|
+
const n = alphaValue(v);
|
|
323
|
+
if (n === undefined) return;
|
|
324
|
+
own()[key] = n;
|
|
325
|
+
} else if (key === 'visibility') {
|
|
326
|
+
const k = v.toLowerCase();
|
|
327
|
+
if (!VISIBILITY.has(k)) return;
|
|
328
|
+
own()[key] = k;
|
|
329
|
+
} else {
|
|
330
|
+
own()[key] = v;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
for (const [name, value] of Object.entries(node.attribs || {})) apply(name, value);
|
|
334
|
+
// inline style="" wins over presentation attributes
|
|
335
|
+
for (const decl of (node.attribs?.style || '').split(';')) {
|
|
336
|
+
const idx = decl.indexOf(':');
|
|
337
|
+
if (idx > 0) apply(decl.slice(0, idx).trim(), decl.slice(idx + 1).trim());
|
|
338
|
+
}
|
|
339
|
+
return style;
|
|
340
|
+
}
|
|
341
|
+
|
|
225
342
|
/**
|
|
226
343
|
* How many distinct colours a document actually commits to, decided once at
|
|
227
344
|
* parse time.
|
|
@@ -236,11 +353,13 @@ function pathBBox(path) {
|
|
|
236
353
|
* Opacity does not enter into it: `opacity`, `fill-opacity` and
|
|
237
354
|
* `stroke-opacity` scale coverage, which a mask carries perfectly well.
|
|
238
355
|
*
|
|
239
|
-
* The walk mirrors `
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* target with the *use*
|
|
243
|
-
*
|
|
356
|
+
* The walk mirrors `_renderNode`, and reads style with the same
|
|
357
|
+
* `resolveStyle` — fill and stroke inherit, inline `style` beats the
|
|
358
|
+
* presentation attribute, the initial fill is black and the initial stroke
|
|
359
|
+
* is none, `<line>` never fills, `<use>` paints its target with the *use*
|
|
360
|
+
* element's style, a shape whose inherited `visibility` is not `visible`
|
|
361
|
+
* paints nothing, and non-rendered and `display: none` subtrees contribute
|
|
362
|
+
* nothing. It starts at the root element itself, because `draw`
|
|
244
363
|
* does: the root `<svg>` is an ordinary element for inheritance, and every
|
|
245
364
|
* mainstream icon set puts `fill`/`stroke` there (issue #306). The two walks
|
|
246
365
|
* have to agree — a scan that skipped the root would disagree with what is
|
|
@@ -254,59 +373,48 @@ function scanPaints(root, ids) {
|
|
|
254
373
|
const paints = new Set();
|
|
255
374
|
let multi = false;
|
|
256
375
|
|
|
257
|
-
const paintOf = (node, name, inherited) => {
|
|
258
|
-
let v = attr(node, name);
|
|
259
|
-
for (const decl of (node.attribs?.style || '').split(';')) {
|
|
260
|
-
const idx = decl.indexOf(':');
|
|
261
|
-
if (idx > 0 && decl.slice(0, idx).trim() === name) v = decl.slice(idx + 1);
|
|
262
|
-
}
|
|
263
|
-
if (v === undefined || v === '' || v === 'inherit') return inherited;
|
|
264
|
-
return String(v).trim();
|
|
265
|
-
};
|
|
266
|
-
|
|
267
376
|
const note = (paint) => {
|
|
268
377
|
if (paint === 'none') return;
|
|
269
378
|
if (/^url\(/i.test(paint)) multi = true;
|
|
270
379
|
else paints.add(paint);
|
|
271
380
|
};
|
|
272
381
|
|
|
273
|
-
const kids = (node,
|
|
382
|
+
const kids = (node, style, depth) => {
|
|
274
383
|
for (const child of node.children || []) {
|
|
275
|
-
if (child.type === 'tag') visit(child,
|
|
384
|
+
if (child.type === 'tag') visit(child, style, depth + 1);
|
|
276
385
|
}
|
|
277
386
|
};
|
|
278
387
|
|
|
279
|
-
const visit = (node,
|
|
388
|
+
const visit = (node, parent, depth) => {
|
|
280
389
|
if (multi || depth > 32) return;
|
|
281
390
|
const name = tag(node);
|
|
282
391
|
if (NON_RENDERED.has(name) || displayNone(node)) return;
|
|
283
|
-
const
|
|
284
|
-
const s = paintOf(node, 'stroke', stroke);
|
|
392
|
+
const style = resolveStyle(node, parent);
|
|
285
393
|
switch (name) {
|
|
286
394
|
case 'svg':
|
|
287
395
|
case 'g':
|
|
288
396
|
case 'a':
|
|
289
|
-
kids(node,
|
|
397
|
+
kids(node, style, depth);
|
|
290
398
|
return;
|
|
291
399
|
case 'use': {
|
|
292
400
|
const href = node.attribs?.href || node.attribs?.['xlink:href'] || '';
|
|
293
401
|
const target = href.startsWith('#') ? ids.get(href.slice(1)) : null;
|
|
294
402
|
if (!target) return;
|
|
295
403
|
// <symbol> is non-rendered on its own but renders through <use>
|
|
296
|
-
if (tag(target) === 'symbol') kids(target,
|
|
297
|
-
else visit(target,
|
|
404
|
+
if (tag(target) === 'symbol') kids(target, style, depth);
|
|
405
|
+
else visit(target, style, depth + 1);
|
|
298
406
|
return;
|
|
299
407
|
}
|
|
300
|
-
case 'text':
|
|
301
|
-
note(f);
|
|
302
|
-
return;
|
|
303
408
|
default:
|
|
304
|
-
|
|
305
|
-
|
|
409
|
+
// a hidden shape paints nothing, though what is in a hidden
|
|
410
|
+
// group may show itself again
|
|
411
|
+
if (style.visibility !== 'visible') return;
|
|
412
|
+
if (name !== 'line') note(style.fill);
|
|
413
|
+
if (name !== 'text') note(style.stroke);
|
|
306
414
|
}
|
|
307
415
|
};
|
|
308
416
|
|
|
309
|
-
visit(root, INHERITED
|
|
417
|
+
visit(root, INHERITED, 0);
|
|
310
418
|
return {
|
|
311
419
|
kind: multi || paints.size > 1 ? 'multi' : 'mono',
|
|
312
420
|
solo: paints.size === 1 ? [...paints][0] : null
|
|
@@ -333,6 +441,7 @@ export default class SvgView {
|
|
|
333
441
|
|
|
334
442
|
this._root = null;
|
|
335
443
|
this._ids = new Map();
|
|
444
|
+
this._viewport = [0, 0];
|
|
336
445
|
this.naturalWidth = 0;
|
|
337
446
|
this.naturalHeight = 0;
|
|
338
447
|
this.viewBox = null;
|
|
@@ -390,11 +499,7 @@ export default class SvgView {
|
|
|
390
499
|
this.soloPaint = scanned.solo;
|
|
391
500
|
|
|
392
501
|
const a = this._root.attribs || {};
|
|
393
|
-
|
|
394
|
-
.split(/[\s,]+/)
|
|
395
|
-
.filter((s) => s !== '')
|
|
396
|
-
.map(parseFloat);
|
|
397
|
-
this.viewBox = vb.length === 4 && vb.every(Number.isFinite) ? vb : null;
|
|
502
|
+
this.viewBox = parseViewBox(this._root);
|
|
398
503
|
const w = parseFloat(a.width);
|
|
399
504
|
const h = parseFloat(a.height);
|
|
400
505
|
this.naturalWidth = Number.isFinite(w) && !String(a.width || '').endsWith('%') ? w : this.viewBox ? this.viewBox[2] : 300;
|
|
@@ -443,14 +548,19 @@ export default class SvgView {
|
|
|
443
548
|
if (!this._root) return;
|
|
444
549
|
ctx.save();
|
|
445
550
|
ctx.translate(x, y);
|
|
551
|
+
// the user-space size of the viewport, what a nested <svg>'s
|
|
552
|
+
// percentages are of
|
|
553
|
+
this._viewport = [w, h];
|
|
446
554
|
if (this.viewBox) {
|
|
447
555
|
const [minX, minY, vbW, vbH] = this.viewBox;
|
|
448
556
|
if (vbW > 0 && vbH > 0) {
|
|
449
557
|
ctx.scale(w / vbW, h / vbH);
|
|
450
558
|
ctx.translate(-minX, -minY);
|
|
559
|
+
this._viewport = [vbW, vbH];
|
|
451
560
|
}
|
|
452
561
|
} else if (this.naturalWidth > 0 && this.naturalHeight > 0) {
|
|
453
562
|
ctx.scale(w / this.naturalWidth, h / this.naturalHeight);
|
|
563
|
+
this._viewport = [this.naturalWidth, this.naturalHeight];
|
|
454
564
|
}
|
|
455
565
|
this._renderChildren(
|
|
456
566
|
this._root,
|
|
@@ -465,27 +575,7 @@ export default class SvgView {
|
|
|
465
575
|
// ------------------------------------------------------------------
|
|
466
576
|
|
|
467
577
|
_style(node, parent) {
|
|
468
|
-
|
|
469
|
-
const own = () => (style === parent ? (style = { ...parent }) : style);
|
|
470
|
-
const apply = (name, raw) => {
|
|
471
|
-
const key = STYLE_ATTRS[name];
|
|
472
|
-
if (!key || raw === undefined || raw === '' || raw === 'inherit') return;
|
|
473
|
-
let v = String(raw).trim();
|
|
474
|
-
if (NUMERIC.has(key)) {
|
|
475
|
-
const n = parseFloat(v);
|
|
476
|
-
if (!Number.isFinite(n)) return;
|
|
477
|
-
own()[key] = n;
|
|
478
|
-
} else {
|
|
479
|
-
own()[key] = v;
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
for (const [name, value] of Object.entries(node.attribs || {})) apply(name, value);
|
|
483
|
-
// inline style="" wins over presentation attributes
|
|
484
|
-
for (const decl of (node.attribs?.style || '').split(';')) {
|
|
485
|
-
const idx = decl.indexOf(':');
|
|
486
|
-
if (idx > 0) apply(decl.slice(0, idx).trim(), decl.slice(idx + 1).trim());
|
|
487
|
-
}
|
|
488
|
-
return style;
|
|
578
|
+
return resolveStyle(node, parent);
|
|
489
579
|
}
|
|
490
580
|
|
|
491
581
|
_paint(ctx, value, style, path) {
|
|
@@ -604,14 +694,55 @@ export default class SvgView {
|
|
|
604
694
|
}
|
|
605
695
|
}
|
|
606
696
|
|
|
697
|
+
/**
|
|
698
|
+
* A nested `<svg>`, which is a new viewport and not a group (SVG 2, 8.2):
|
|
699
|
+
* `width` by `height` at `x`,`y` of the user space it sits in, all of the
|
|
700
|
+
* viewport around it where they are unset, and percentages of that. Its
|
|
701
|
+
* `viewBox` is fitted in as `preserveAspectRatio` says, and what it draws
|
|
702
|
+
* is clipped to it unless its `overflow` is `visible` or `auto`. A zero
|
|
703
|
+
* or negative size, or a zero-sized `viewBox`, draws nothing.
|
|
704
|
+
*/
|
|
705
|
+
_renderViewport(node, ctx, style, alpha, depth) {
|
|
706
|
+
const outer = this._viewport;
|
|
707
|
+
const [pw, ph] = outer;
|
|
708
|
+
const x = viewportLength(node, 'x', pw, 0);
|
|
709
|
+
const y = viewportLength(node, 'y', ph, 0);
|
|
710
|
+
const w = viewportLength(node, 'width', pw, pw);
|
|
711
|
+
const h = viewportLength(node, 'height', ph, ph);
|
|
712
|
+
if (!(w > 0) || !(h > 0)) return;
|
|
713
|
+
// a negative size makes the viewBox an error, and it is ignored
|
|
714
|
+
let vb = parseViewBox(node);
|
|
715
|
+
if (vb && (vb[2] === 0 || vb[3] === 0)) return;
|
|
716
|
+
if (vb && (vb[2] < 0 || vb[3] < 0)) vb = null;
|
|
717
|
+
|
|
718
|
+
ctx.save();
|
|
719
|
+
const overflow = ownProperty(node, 'overflow', keyword);
|
|
720
|
+
if (overflow !== 'visible' && overflow !== 'auto') {
|
|
721
|
+
const clip = new Path2D();
|
|
722
|
+
clip.rect(x, y, w, h);
|
|
723
|
+
ctx.clip(clip);
|
|
724
|
+
}
|
|
725
|
+
if (vb) {
|
|
726
|
+
const [sx, sy, tx, ty] = viewBoxFit(vb, x, y, w, h, attr(node, 'preserveAspectRatio'));
|
|
727
|
+
ctx.transform(sx, 0, 0, sy, tx, ty);
|
|
728
|
+
this._viewport = [vb[2], vb[3]];
|
|
729
|
+
} else {
|
|
730
|
+
if (x || y) ctx.translate(x, y);
|
|
731
|
+
this._viewport = [w, h];
|
|
732
|
+
}
|
|
733
|
+
this._renderChildren(node, ctx, style, alpha, depth + 1);
|
|
734
|
+
this._viewport = outer;
|
|
735
|
+
ctx.restore();
|
|
736
|
+
}
|
|
737
|
+
|
|
607
738
|
_renderNode(node, ctx, parentStyle, alpha, depth) {
|
|
608
739
|
if (depth > 32) return;
|
|
609
740
|
const name = tag(node);
|
|
610
741
|
if (NON_RENDERED.has(name) || displayNone(node)) return;
|
|
611
742
|
|
|
612
743
|
const style = this._style(node, parentStyle);
|
|
613
|
-
|
|
614
|
-
const nodeAlpha = alpha * (
|
|
744
|
+
// not inherited: a group's multiplies down through what is in it
|
|
745
|
+
const nodeAlpha = alpha * (ownProperty(node, 'opacity', alphaValue) ?? 1);
|
|
615
746
|
if (nodeAlpha <= 0) return;
|
|
616
747
|
|
|
617
748
|
const transforms = parseSvgTransform(node.attribs?.transform);
|
|
@@ -623,6 +754,8 @@ export default class SvgView {
|
|
|
623
754
|
|
|
624
755
|
switch (name) {
|
|
625
756
|
case 'svg':
|
|
757
|
+
this._renderViewport(node, ctx, style, nodeAlpha, depth);
|
|
758
|
+
break;
|
|
626
759
|
case 'g':
|
|
627
760
|
case 'a':
|
|
628
761
|
this._renderChildren(node, ctx, style, nodeAlpha, depth + 1);
|
|
@@ -642,9 +775,13 @@ export default class SvgView {
|
|
|
642
775
|
break;
|
|
643
776
|
}
|
|
644
777
|
case 'text':
|
|
645
|
-
this._renderText(node, ctx, style, nodeAlpha);
|
|
778
|
+
if (style.visibility === 'visible') this._renderText(node, ctx, style, nodeAlpha);
|
|
646
779
|
break;
|
|
647
780
|
default: {
|
|
781
|
+
// `visibility` hides what an element paints itself, and is
|
|
782
|
+
// inherited: a group walks on, since what is in it may say
|
|
783
|
+
// `visible` again
|
|
784
|
+
if (style.visibility !== 'visible') break;
|
|
648
785
|
const path = shapePath(node);
|
|
649
786
|
if (!path) break;
|
|
650
787
|
if (name !== 'line' && style.fill !== 'none') {
|