ntk 8.17.3 → 8.17.4

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.
@@ -396,6 +396,17 @@ function prepareStyle(src, m) {
396
396
  : true;
397
397
  }
398
398
 
399
+ /**
400
+ * Whether every entry can be written as XRender's 16.16 fixed point, whose
401
+ * whole part is a signed 16-bit number. A picture transform is written that
402
+ * way, and the request encoder throws on an entry past it — out of the paint
403
+ * that asked for it, and out of everything drawn after it in the same frame.
404
+ * Not finite fails too.
405
+ */
406
+ function fitsFixed(values) {
407
+ return values.every((v) => Math.abs(v) < 32768);
408
+ }
409
+
399
410
  function isPictureSource(image) {
400
411
  return (
401
412
  image instanceof Image ||
@@ -5503,6 +5514,11 @@ class RenderingContext2d {
5503
5514
  const M = matMultiply(this._m, [dw / sw, 0, 0, dh / sh, dx, dy]);
5504
5515
  const inv = matInvert(M);
5505
5516
  if (!inv) return;
5517
+ // The inverse's linear part is how many source pixels one device pixel
5518
+ // steps across, and it goes on the wire as 16.16 fixed point. Past
5519
+ // 32,767 the image is drawn at under 1/32,768 of its size: less than a
5520
+ // pixel across for any image X can hold, and nothing to see.
5521
+ if (!fitsFixed(inv.slice(0, 4))) return;
5506
5522
 
5507
5523
  // device bbox of the transformed dest rect, clamped to the canvas
5508
5524
  const corners = [
@@ -5523,6 +5539,24 @@ class RenderingContext2d {
5523
5539
  );
5524
5540
  if (x1 <= x0 || y1 <= y0) return;
5525
5541
 
5542
+ // Device -> source sample, measured from the bounding box's corner: the
5543
+ // corner's device position is folded into the transform, and the
5544
+ // composite's source origin is its box's offset from that corner. With
5545
+ // the source origin at device coordinates instead, the translation was
5546
+ // the device position times the downscale — a 50x30 thumbnail of a
5547
+ // 2000x1200 image at x 2,200 is 88,000, past what 16.16 carries, and
5548
+ // the encoder threw. From the corner it is the source coordinate the
5549
+ // corner samples: the image's own size or so, wherever on the surface it
5550
+ // is drawn. The corner is the unclipped one, so a clip that narrows the
5551
+ // box below samples the same points the mask route does.
5552
+ const [a, b, c, d] = inv;
5553
+ const e = a * x0 + c * y0 + inv[4] + sx;
5554
+ const f = b * x0 + d * y0 + inv[5] + sy;
5555
+ // A corner out of reach even so is a draw near the limit above, or an
5556
+ // image over 20,000 pixels across under a rotation; it is not drawn,
5557
+ // rather than thrown out of the paint.
5558
+ if (!fitsFixed([e, f])) return;
5559
+
5526
5560
  // A rectangular clip shrinks the box instead of becoming a mask (issue
5527
5561
  // #307). Decided before the picture transform is installed, so a draw
5528
5562
  // the clip rejects does not have to put it back.
@@ -5531,27 +5565,15 @@ class RenderingContext2d {
5531
5565
  if (direct && !direct.box) return;
5532
5566
  const box = direct ? direct.box : bbox;
5533
5567
 
5534
- // device -> source sample (adding the source-rect origin)
5535
- const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
5536
- this.Render.SetPictureTransform(picture.id, [
5537
- srcM[0],
5538
- srcM[2],
5539
- srcM[4],
5540
- srcM[1],
5541
- srcM[3],
5542
- srcM[5],
5543
- 0,
5544
- 0,
5545
- 1,
5546
- ]);
5568
+ this.Render.SetPictureTransform(picture.id, [a, c, e, b, d, f, 0, 0, 1]);
5547
5569
  picture.setFilter("bilinear");
5548
5570
  this.Render.Composite(
5549
5571
  op,
5550
5572
  picture.id,
5551
5573
  direct ? direct.mask : this._compositeMask(box),
5552
5574
  this._dst(),
5553
- box.x,
5554
- box.y,
5575
+ box.x - x0,
5576
+ box.y - y0,
5555
5577
  box.x,
5556
5578
  box.y,
5557
5579
  box.x,
@@ -5701,7 +5723,7 @@ class CanvasGradient {
5701
5723
  if (k !== 1) inv = inv.map((v) => v / k);
5702
5724
  // the transform is 16.16 fixed point too: one that cannot be carried
5703
5725
  // fills nothing, rather than throwing the rest of the paint away
5704
- if (!inv.every((v) => Math.abs(v) < 32767)) return false;
5726
+ if (!fitsFixed(inv)) return false;
5705
5727
  const a = this._applied;
5706
5728
  if (
5707
5729
  inv[0] !== a[0] ||
@@ -5831,6 +5853,11 @@ class CanvasPattern {
5831
5853
  const m = matMultiply(ctm, this._m);
5832
5854
  const inv = matInvert(m);
5833
5855
  if (!inv) return false;
5856
+ // 16.16 fixed point, as a gradient's is: a tile scaled down far from
5857
+ // the origin puts its device position times the downscale into the
5858
+ // translation, and one that cannot be carried fills nothing rather than
5859
+ // throwing the rest of the paint away
5860
+ if (!fitsFixed(inv)) return false;
5834
5861
  const a = this._applied;
5835
5862
  if (
5836
5863
  inv[0] !== a[0] ||
@@ -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, fill-rule, basic
20
- // <text>. See docs/svg.md for the full surface and limitations.
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', 'fillOpacity', 'strokeOpacity', 'fontSize']);
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
- let v = attr(node, 'display');
89
- const style = node.attribs?.style;
90
- if (style && style.includes('display')) {
91
- for (const decl of style.split(';')) {
92
- const idx = decl.indexOf(':');
93
- if (idx > 0 && decl.slice(0, idx).trim() === 'display') v = decl.slice(idx + 1);
94
- }
95
- }
96
- return v !== undefined && String(v).trim().toLowerCase() === 'none';
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 `_style` and `_renderNode` — fill and stroke inherit,
240
- * inline `style` beats the presentation attribute, the initial fill is black
241
- * and the initial stroke is none, `<line>` never fills, `<use>` paints its
242
- * target with the *use* element's style, and non-rendered subtrees
243
- * contribute nothing. It starts at the root element itself, because `draw`
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, fill, stroke, depth) => {
382
+ const kids = (node, style, depth) => {
274
383
  for (const child of node.children || []) {
275
- if (child.type === 'tag') visit(child, fill, stroke, depth + 1);
384
+ if (child.type === 'tag') visit(child, style, depth + 1);
276
385
  }
277
386
  };
278
387
 
279
- const visit = (node, fill, stroke, depth) => {
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 f = paintOf(node, 'fill', fill);
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, f, s, depth);
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, f, s, depth);
297
- else visit(target, f, s, depth + 1);
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
- if (name !== 'line') note(f);
305
- note(s);
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.fill, INHERITED.stroke, 0);
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
- const vb = (attr(this._root, 'viewBox') || '')
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
- let style = parent;
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
- const opacity = parseFloat(node.attribs?.opacity ?? '1');
614
- const nodeAlpha = alpha * (Number.isFinite(opacity) ? Math.min(1, Math.max(0, opacity)) : 1);
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') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.17.3",
3
+ "version": "8.17.4",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",