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.
- package/lib/renderingcontext_2d.js +43 -16
- package/lib/widgets/svgview.js +209 -72
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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] ||
|
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') {
|