imagescript 1.3.1 → 3.0.0
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/LICENSE +4 -1
- package/README.md +64 -26
- package/THIRD_PARTY_NOTICES.md +74 -0
- package/dist/codecs/crc.d.ts +9 -0
- package/dist/codecs/crc.js +29 -0
- package/dist/codecs/exif.d.ts +11 -0
- package/dist/codecs/exif.js +27 -0
- package/dist/codecs/gif.d.ts +44 -0
- package/dist/codecs/gif.js +401 -0
- package/dist/codecs/jpeg.d.ts +35 -0
- package/dist/codecs/jpeg.js +980 -0
- package/dist/codecs/png.d.ts +44 -0
- package/dist/codecs/png.js +442 -0
- package/dist/codecs/quantize.d.ts +25 -0
- package/dist/codecs/quantize.js +302 -0
- package/dist/codecs/tiff.d.ts +6 -0
- package/dist/codecs/tiff.js +546 -0
- package/dist/codecs/zlib.d.ts +22 -0
- package/dist/codecs/zlib.js +607 -0
- package/dist/frame.d.ts +57 -0
- package/dist/frame.js +65 -0
- package/dist/gif.d.ts +44 -0
- package/dist/gif.js +93 -0
- package/dist/image-type.d.ts +20 -0
- package/dist/image-type.js +49 -0
- package/dist/image.d.ts +521 -0
- package/dist/image.js +1118 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +13 -0
- package/dist/package.json +1 -0
- package/dist/render/font.d.ts +63 -0
- package/dist/render/font.js +857 -0
- package/dist/render/svg.d.ts +11 -0
- package/dist/render/svg.js +1717 -0
- package/dist/render/text.d.ts +61 -0
- package/dist/render/text.js +317 -0
- package/dist/text-layout.d.ts +46 -0
- package/dist/text-layout.js +35 -0
- package/dist/util/blur.d.ts +13 -0
- package/dist/util/blur.js +146 -0
- package/dist/util/mem.d.ts +10 -0
- package/dist/util/mem.js +23 -0
- package/dist/util/resample.d.ts +12 -0
- package/dist/util/resample.js +99 -0
- package/dist/util/utf8.d.ts +5 -0
- package/dist/util/utf8.js +55 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/licenses/Apache-2.0.txt +202 -0
- package/licenses/README.ijg +260 -0
- package/package.json +31 -7
- package/ImageScript.d.ts +0 -954
- package/ImageScript.js +0 -1669
- package/bench/_.mjs +0 -166
- package/bench/all.mjs +0 -4
- package/bench/blur.mjs +0 -59
- package/bench/flip.mjs +0 -63
- package/bench/overlay.mjs +0 -35
- package/bench/package.json +0 -7
- package/bench/readme.md +0 -64
- package/bench/resize.mjs +0 -52
- package/codecs/LICENSE +0 -21
- package/codecs/lib.d.ts +0 -50
- package/codecs/lib.js +0 -6
- package/codecs/lib.mjs +0 -1
- package/codecs/node/bin/arm64-darwin.node +0 -0
- package/codecs/node/bin/arm64-linux.node +0 -0
- package/codecs/node/bin/x64-darwin.node +0 -0
- package/codecs/node/bin/x64-linux.node +0 -0
- package/codecs/node/bin/x64-win32.node +0 -0
- package/codecs/node/index.js +0 -3
- package/codecs/node/index.mjs +0 -6
- package/codecs/package.json +0 -14
- package/codecs/readme.md +0 -38
- package/codecs/wasm/bin/codecs.wasm +0 -0
- package/codecs/wasm/bin/simd.wasm +0 -0
- package/codecs/wasm/bundle/browser.js +0 -1
- package/codecs/wasm/index.js +0 -1
- package/codecs/wasm/index.mjs +0 -1
- package/png/README.md +0 -1
- package/png/node.js +0 -1137
- package/png/src/crc.mjs +0 -49
- package/png/src/mem.mjs +0 -24
- package/png/src/png.mjs +0 -254
- package/png/src/zlib.mjs +0 -618
- package/tsconfig.json +0 -21
- package/utils/mem.js +0 -26
- package/v2/codecs/magic.js +0 -25
- package/v2/framebuffer.d.ts +0 -67
- package/v2/framebuffer.js +0 -2352
- package/v2/framebuffer.mjs +0 -126
- package/v2/framebuffer.rs +0 -1264
- package/v2/index.d.ts +0 -84
- package/v2/index.js +0 -261
- package/v2/ops/blur.mjs +0 -303
- package/v2/ops/color.mjs +0 -277
- package/v2/ops/crop.mjs +0 -64
- package/v2/ops/fill.mjs +0 -33
- package/v2/ops/flip.mjs +0 -33
- package/v2/ops/index.mjs +0 -9
- package/v2/ops/iterator.mjs +0 -34
- package/v2/ops/overlay.mjs +0 -63
- package/v2/ops/resize.mjs +0 -166
- package/v2/ops/rotate.mjs +0 -117
- package/v2/util/mem.mjs +0 -24
- package/wasm/any/font.wasm +0 -0
- package/wasm/any/gif.wasm +0 -0
- package/wasm/any/jpeg.wasm +0 -0
- package/wasm/any/png.wasm +0 -0
- package/wasm/any/svg.wasm +0 -0
- package/wasm/any/tiff.wasm +0 -0
- package/wasm/node/font.js +0 -142
- package/wasm/node/gif.js +0 -76
- package/wasm/node/jpeg.js +0 -92
- package/wasm/node/png.js +0 -49
- package/wasm/node/svg.js +0 -56
- package/wasm/node/tiff.js +0 -47
|
@@ -0,0 +1,1717 @@
|
|
|
1
|
+
import { decode as decodeUTF8 } from "../util/utf8.js";
|
|
2
|
+
const UNSUPPORTED = new Set(['text', 'image', 'filter', 'pattern', 'marker', 'foreignObject', 'script', 'style', 'title', 'desc', 'metadata',
|
|
3
|
+
'defs', 'clipPath', 'mask', 'linearGradient', 'radialGradient', 'stop', 'symbol', 'font', 'font-face', 'animate', 'animateTransform', 'animateMotion', 'set']);
|
|
4
|
+
const INHERITED = new Set(['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity', 'stroke-linecap', 'stroke-linejoin',
|
|
5
|
+
'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'color', 'visibility', 'clip-rule', 'font-size']);
|
|
6
|
+
const SWITCHABLE = new Set(['svg', 'g', 'a', 'switch', 'use', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'text', 'image', 'foreignObject']);
|
|
7
|
+
const PROPERTIES = new Set([...INHERITED, 'opacity', 'display', 'clip-path', 'mask', 'stop-color', 'stop-opacity', 'transform']);
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
'fill': 'black', 'fill-opacity': '1', 'fill-rule': 'nonzero', 'stroke': 'none', 'stroke-width': '1', 'stroke-opacity': '1',
|
|
10
|
+
'stroke-linecap': 'butt', 'stroke-linejoin': 'miter', 'stroke-miterlimit': '4', 'stroke-dasharray': 'none', 'stroke-dashoffset': '0',
|
|
11
|
+
'color': 'black', 'visibility': 'visible', 'clip-rule': 'nonzero', 'font-size': '16', 'opacity': '1', 'display': 'inline',
|
|
12
|
+
'stop-color': 'black', 'stop-opacity': '1'
|
|
13
|
+
};
|
|
14
|
+
const SAMPLES = 16;
|
|
15
|
+
export function render(source, mode, size) {
|
|
16
|
+
if (typeof source !== 'string')
|
|
17
|
+
source = decodeUTF8(source);
|
|
18
|
+
const root = parseXML(source).children.find(node => node.name === 'svg' && node.svg);
|
|
19
|
+
if (!root)
|
|
20
|
+
throw new Error('SVG: no <svg> element');
|
|
21
|
+
const doc = new Document(root);
|
|
22
|
+
const viewBox = parseViewBox(root.attrs.viewBox);
|
|
23
|
+
const lengthOf = (value, fallback) => {
|
|
24
|
+
if (value === undefined || value === 'auto')
|
|
25
|
+
return fallback;
|
|
26
|
+
if (/%\s*$/.test(value))
|
|
27
|
+
return fallback * parseFloat(value) / 100;
|
|
28
|
+
return parseLength(value, null, 16);
|
|
29
|
+
};
|
|
30
|
+
const width = lengthOf(root.attrs.width, viewBox ? viewBox[2] : 100);
|
|
31
|
+
const height = lengthOf(root.attrs.height, viewBox ? viewBox[3] : 100);
|
|
32
|
+
if (!(width > 0) || !(height > 0))
|
|
33
|
+
throw new Error('SVG: invalid dimensions');
|
|
34
|
+
let outWidth, outHeight;
|
|
35
|
+
if (mode === 2) {
|
|
36
|
+
outWidth = Math.round(size);
|
|
37
|
+
outHeight = Math.round(height * size / width);
|
|
38
|
+
}
|
|
39
|
+
else if (mode === 3) {
|
|
40
|
+
outHeight = Math.round(size);
|
|
41
|
+
outWidth = Math.round(width * size / height);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
outWidth = Math.round(width * size);
|
|
45
|
+
outHeight = Math.round(height * size);
|
|
46
|
+
}
|
|
47
|
+
outWidth = Math.max(1, outWidth);
|
|
48
|
+
outHeight = Math.max(1, outHeight);
|
|
49
|
+
const canvas = new Layer(outWidth, outHeight);
|
|
50
|
+
let matrix0 = [outWidth / width, 0, 0, outHeight / height, 0, 0];
|
|
51
|
+
const rootStyle = doc.computeStyle(root, null);
|
|
52
|
+
if (rootStyle.transform)
|
|
53
|
+
matrix0 = multiply(matrix0, parseTransform(rootStyle.transform));
|
|
54
|
+
if (viewBox)
|
|
55
|
+
matrix0 = multiply(matrix0, viewBoxTransform(viewBox, width, height, root.attrs.preserveAspectRatio));
|
|
56
|
+
const viewport = viewBox ? { width: viewBox[2], height: viewBox[3] } : { width, height };
|
|
57
|
+
doc.renderNode(root, { matrix: matrix0, style: null, viewport, layer: canvas, depth: 0 }, true);
|
|
58
|
+
return { width: outWidth, height: outHeight, pixels: canvas.toRGBA() };
|
|
59
|
+
}
|
|
60
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
61
|
+
function parseXML(text) {
|
|
62
|
+
const root = { name: '#root', attrs: {}, children: [], text: '', svg: false };
|
|
63
|
+
const stack = [root];
|
|
64
|
+
const namespaces = [{ '': SVG_NS }];
|
|
65
|
+
const entities = { amp: '&', lt: '<', gt: '>', quot: '"', apos: '\'' };
|
|
66
|
+
const doctype = /<!DOCTYPE[^[>]*\[([\s\S]*?)\]\s*>/i.exec(text);
|
|
67
|
+
if (doctype) {
|
|
68
|
+
const custom = {};
|
|
69
|
+
for (const [, name, a, b] of doctype[1].matchAll(/<!ENTITY\s+([\w.-]+)\s+(?:"([^"]*)"|'([^']*)')\s*>/g))
|
|
70
|
+
custom[name] = a !== undefined ? a : b;
|
|
71
|
+
const limit = Math.max(1 << 20, text.length * 16);
|
|
72
|
+
let rest = text.slice(doctype.index + doctype[0].length);
|
|
73
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
74
|
+
let size = rest.length;
|
|
75
|
+
rest = rest.replace(/&([\w.-]+);/g, (match, name) => {
|
|
76
|
+
if (!(name in custom))
|
|
77
|
+
return match;
|
|
78
|
+
size += custom[name].length - match.length;
|
|
79
|
+
if (size > limit)
|
|
80
|
+
throw new Error('SVG: entity expansion exceeds the size limit');
|
|
81
|
+
return custom[name];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
text = text.slice(0, doctype.index) + rest;
|
|
85
|
+
}
|
|
86
|
+
const decode = (s) => s.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z_][\w.-]*);/gi, (match, name) => {
|
|
87
|
+
if (name[0] === '#')
|
|
88
|
+
return String.fromCodePoint(name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10));
|
|
89
|
+
return name in entities ? entities[name] : match;
|
|
90
|
+
});
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < text.length) {
|
|
93
|
+
if (text[i] !== '<') {
|
|
94
|
+
const end = text.indexOf('<', i);
|
|
95
|
+
stack[stack.length - 1].text += decode(text.slice(i, end === -1 ? text.length : end));
|
|
96
|
+
i = end === -1 ? text.length : end;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (text.startsWith('<!--', i)) {
|
|
100
|
+
const end = text.indexOf('-->', i + 4);
|
|
101
|
+
i = end === -1 ? text.length : end + 3;
|
|
102
|
+
}
|
|
103
|
+
else if (text.startsWith('<![CDATA[', i)) {
|
|
104
|
+
const end = text.indexOf(']]>', i);
|
|
105
|
+
stack[stack.length - 1].text += text.slice(i + 9, end === -1 ? text.length : end);
|
|
106
|
+
i = end === -1 ? text.length : end + 3;
|
|
107
|
+
}
|
|
108
|
+
else if (text.startsWith('<!', i)) {
|
|
109
|
+
let depth = 0, j = i + 2;
|
|
110
|
+
for (; j < text.length; j++) {
|
|
111
|
+
if (text[j] === '[')
|
|
112
|
+
depth++;
|
|
113
|
+
else if (text[j] === ']')
|
|
114
|
+
depth--;
|
|
115
|
+
else if (text[j] === '>' && depth <= 0)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
i = j + 1;
|
|
119
|
+
}
|
|
120
|
+
else if (text.startsWith('<?', i)) {
|
|
121
|
+
const end = text.indexOf('?>', i);
|
|
122
|
+
i = end === -1 ? text.length : end + 2;
|
|
123
|
+
}
|
|
124
|
+
else if (text.startsWith('</', i)) {
|
|
125
|
+
const end = text.indexOf('>', i);
|
|
126
|
+
if (stack.length > 1) {
|
|
127
|
+
stack.pop();
|
|
128
|
+
namespaces.pop();
|
|
129
|
+
}
|
|
130
|
+
i = end === -1 ? text.length : end + 1;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const match = /^<([^\s/>]+)/.exec(text.slice(i, i + 256));
|
|
134
|
+
if (!match) {
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const qualified = match[1];
|
|
139
|
+
const node = { name: '', attrs: {}, children: [], text: '', svg: false };
|
|
140
|
+
i += match[0].length;
|
|
141
|
+
const attribute = /\s*([^\s=/>]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/y;
|
|
142
|
+
for (;;) {
|
|
143
|
+
if (/^\s*\/?>/.test(text.slice(i, i + 64)))
|
|
144
|
+
break;
|
|
145
|
+
attribute.lastIndex = i;
|
|
146
|
+
const a = attribute.exec(text);
|
|
147
|
+
if (!a)
|
|
148
|
+
break;
|
|
149
|
+
node.attrs[a[1]] = decode(a[2] !== undefined ? a[2] : a[3] !== undefined ? a[3] : a[4] !== undefined ? a[4] : '');
|
|
150
|
+
i = attribute.lastIndex;
|
|
151
|
+
}
|
|
152
|
+
const scope = Object.assign({}, namespaces[namespaces.length - 1]);
|
|
153
|
+
for (const name in node.attrs) {
|
|
154
|
+
if (name === 'xmlns')
|
|
155
|
+
scope[''] = node.attrs[name];
|
|
156
|
+
else if (name.startsWith('xmlns:'))
|
|
157
|
+
scope[name.slice(6)] = node.attrs[name];
|
|
158
|
+
}
|
|
159
|
+
const colon = qualified.indexOf(':');
|
|
160
|
+
const prefix = colon === -1 ? '' : qualified.slice(0, colon);
|
|
161
|
+
node.name = colon === -1 ? qualified : qualified.slice(colon + 1);
|
|
162
|
+
node.svg = scope[prefix] === SVG_NS;
|
|
163
|
+
const close = /\s*(\/?)>/y;
|
|
164
|
+
close.lastIndex = i;
|
|
165
|
+
const end = close.exec(text);
|
|
166
|
+
i = end ? close.lastIndex : text.length;
|
|
167
|
+
stack[stack.length - 1].children.push(node);
|
|
168
|
+
if (!end || !end[1]) {
|
|
169
|
+
stack.push(node);
|
|
170
|
+
namespaces.push(scope);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return root;
|
|
175
|
+
}
|
|
176
|
+
function parseStyleSheet(text) {
|
|
177
|
+
const rules = [];
|
|
178
|
+
text = text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
179
|
+
let order = 0;
|
|
180
|
+
const blocks = /([^{}@]+)\{([^{}]*)\}|@[^{;]*(\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}|;)/g;
|
|
181
|
+
for (const [, selectors, body] of text.matchAll(blocks)) {
|
|
182
|
+
if (selectors === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
const declarations = parseDeclarations(body);
|
|
185
|
+
for (const selector of selectors.split(',')) {
|
|
186
|
+
const parsed = parseSelector(selector.trim());
|
|
187
|
+
if (parsed)
|
|
188
|
+
rules.push({ ...parsed, order: order++, declarations });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return rules.sort((a, b) => a.specificity - b.specificity || a.order - b.order);
|
|
192
|
+
}
|
|
193
|
+
function parseSelector(selector) {
|
|
194
|
+
const tokens = selector.match(/\s*[>+~]\s*|\s+|[^\s>+~]+/g);
|
|
195
|
+
if (!tokens)
|
|
196
|
+
return null;
|
|
197
|
+
const compounds = [];
|
|
198
|
+
let combinator = null;
|
|
199
|
+
let specificity = 0;
|
|
200
|
+
for (const token of tokens) {
|
|
201
|
+
const trimmed = token.trim();
|
|
202
|
+
if (!trimmed || trimmed === '>' || trimmed === '+' || trimmed === '~') {
|
|
203
|
+
if (compounds.length)
|
|
204
|
+
combinator = trimmed || ' ';
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const tests = [];
|
|
208
|
+
const part = /([a-zA-Z][\w-]*|\*)|#([\w-]+)|\.([\w-]+)|\[\s*([\w:-]+)\s*(?:([~|^$*]?=)\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]*)))?\s*\]|:(first-child)|(.)/g;
|
|
209
|
+
for (const m of trimmed.matchAll(part)) {
|
|
210
|
+
if (m[10] !== undefined)
|
|
211
|
+
return null;
|
|
212
|
+
if (m[1]) {
|
|
213
|
+
if (m[1] !== '*') {
|
|
214
|
+
tests.push(node => node.name === m[1]);
|
|
215
|
+
specificity += 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (m[2]) {
|
|
219
|
+
tests.push(node => node.attrs.id === m[2]);
|
|
220
|
+
specificity += 100;
|
|
221
|
+
}
|
|
222
|
+
else if (m[3]) {
|
|
223
|
+
tests.push(node => (node.attrs.class || '').split(/\s+/).includes(m[3]));
|
|
224
|
+
specificity += 10;
|
|
225
|
+
}
|
|
226
|
+
else if (m[4]) {
|
|
227
|
+
const name = m[4], op = m[5], value = m[6] !== undefined ? m[6] : m[7] !== undefined ? m[7] : m[8];
|
|
228
|
+
tests.push(node => {
|
|
229
|
+
const v = node.attrs[name];
|
|
230
|
+
if (v === undefined)
|
|
231
|
+
return false;
|
|
232
|
+
if (!op)
|
|
233
|
+
return true;
|
|
234
|
+
if (op === '=')
|
|
235
|
+
return v === value;
|
|
236
|
+
if (op === '~=')
|
|
237
|
+
return v.split(/\s+/).includes(value);
|
|
238
|
+
if (op === '^=')
|
|
239
|
+
return v.startsWith(value);
|
|
240
|
+
if (op === '$=')
|
|
241
|
+
return v.endsWith(value);
|
|
242
|
+
if (op === '*=')
|
|
243
|
+
return v.includes(value);
|
|
244
|
+
return v === value || v.startsWith(value + '-');
|
|
245
|
+
});
|
|
246
|
+
specificity += 10;
|
|
247
|
+
}
|
|
248
|
+
else if (m[9]) {
|
|
249
|
+
tests.push(node => !node.parent || node.parent.children.find(c => c.name) === node);
|
|
250
|
+
specificity += 10;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
compounds.push({ combinator: compounds.length ? combinator || ' ' : null, test: node => tests.every(t => t(node)) });
|
|
254
|
+
combinator = null;
|
|
255
|
+
}
|
|
256
|
+
if (!compounds.length)
|
|
257
|
+
return null;
|
|
258
|
+
const matchAt = (node, index) => {
|
|
259
|
+
if (!node || !compounds[index].test(node))
|
|
260
|
+
return false;
|
|
261
|
+
if (index === 0)
|
|
262
|
+
return true;
|
|
263
|
+
const { combinator } = compounds[index];
|
|
264
|
+
if (combinator === '>')
|
|
265
|
+
return matchAt(node.parent, index - 1);
|
|
266
|
+
if (combinator === '+' || combinator === '~') {
|
|
267
|
+
const siblings = node.parent ? node.parent.children : [];
|
|
268
|
+
const position = siblings.indexOf(node);
|
|
269
|
+
if (combinator === '+')
|
|
270
|
+
return matchAt(siblings[position - 1], index - 1);
|
|
271
|
+
return siblings.slice(0, position).some(sibling => matchAt(sibling, index - 1));
|
|
272
|
+
}
|
|
273
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent)
|
|
274
|
+
if (matchAt(ancestor, index - 1))
|
|
275
|
+
return true;
|
|
276
|
+
return false;
|
|
277
|
+
};
|
|
278
|
+
return { specificity, match: node => matchAt(node, compounds.length - 1) };
|
|
279
|
+
}
|
|
280
|
+
function parseDeclarations(text) {
|
|
281
|
+
const declarations = { normal: {}, important: {} };
|
|
282
|
+
text = text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
283
|
+
for (const part of text.split(';')) {
|
|
284
|
+
const colon = part.indexOf(':');
|
|
285
|
+
if (colon === -1)
|
|
286
|
+
continue;
|
|
287
|
+
const name = part.slice(0, colon).trim().toLowerCase();
|
|
288
|
+
let value = part.slice(colon + 1).trim();
|
|
289
|
+
const important = /!\s*important\s*$/i.test(value);
|
|
290
|
+
if (important)
|
|
291
|
+
value = value.replace(/!\s*important\s*$/i, '').trim();
|
|
292
|
+
if (name && value)
|
|
293
|
+
(important ? declarations.important : declarations.normal)[name] = value;
|
|
294
|
+
}
|
|
295
|
+
return declarations;
|
|
296
|
+
}
|
|
297
|
+
function validProperty(name, value) {
|
|
298
|
+
switch (name) {
|
|
299
|
+
case 'fill':
|
|
300
|
+
case 'stroke':
|
|
301
|
+
return value === 'none' || value === 'currentColor' || value === 'inherit' || /^context-(fill|stroke)$/.test(value)
|
|
302
|
+
|| /^url\(/.test(value) || parseColor(value) !== null;
|
|
303
|
+
case 'color':
|
|
304
|
+
case 'stop-color':
|
|
305
|
+
return value === 'currentColor' || value === 'inherit' || parseColor(value) !== null;
|
|
306
|
+
case 'opacity':
|
|
307
|
+
case 'fill-opacity':
|
|
308
|
+
case 'stroke-opacity':
|
|
309
|
+
case 'stop-opacity':
|
|
310
|
+
return value === 'inherit' || parseStrictNumber(value) !== null;
|
|
311
|
+
default:
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
class Document {
|
|
316
|
+
ids;
|
|
317
|
+
active;
|
|
318
|
+
root;
|
|
319
|
+
rules;
|
|
320
|
+
constructor(root) {
|
|
321
|
+
this.ids = new Map();
|
|
322
|
+
this.active = new Set();
|
|
323
|
+
const styles = [];
|
|
324
|
+
const walk = (node) => {
|
|
325
|
+
if (node.attrs.id && !this.ids.has(node.attrs.id))
|
|
326
|
+
this.ids.set(node.attrs.id, node);
|
|
327
|
+
if (node.name === 'style' && node.svg && (!node.attrs.type || node.attrs.type === 'text/css'))
|
|
328
|
+
styles.push(node.text);
|
|
329
|
+
for (const child of node.children) {
|
|
330
|
+
child.parent = node;
|
|
331
|
+
walk(child);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
walk(root);
|
|
335
|
+
this.root = root;
|
|
336
|
+
this.rules = parseStyleSheet(styles.join('\n'));
|
|
337
|
+
}
|
|
338
|
+
reference(value) {
|
|
339
|
+
if (!value)
|
|
340
|
+
return null;
|
|
341
|
+
const match = /url\(\s*['"]?\s*#([^)'"]+?)\s*['"]?\s*\)/.exec(value) || /^\s*#(.+?)\s*$/.exec(value);
|
|
342
|
+
return match ? this.ids.get(match[1]) || null : null;
|
|
343
|
+
}
|
|
344
|
+
href(node) {
|
|
345
|
+
return node.attrs.href || node.attrs['xlink:href'];
|
|
346
|
+
}
|
|
347
|
+
computeStyle(node, parent) {
|
|
348
|
+
if (node.computed && node.computedParent === parent)
|
|
349
|
+
return node.computed;
|
|
350
|
+
const own = {};
|
|
351
|
+
const apply = (declarations) => {
|
|
352
|
+
for (const name in declarations)
|
|
353
|
+
if (validProperty(name, declarations[name]))
|
|
354
|
+
own[name] = declarations[name];
|
|
355
|
+
};
|
|
356
|
+
const presentation = {};
|
|
357
|
+
for (const name in node.attrs)
|
|
358
|
+
if (PROPERTIES.has(name))
|
|
359
|
+
presentation[name] = node.attrs[name];
|
|
360
|
+
apply(presentation);
|
|
361
|
+
const rules = this.rules.filter(rule => rule.match(node));
|
|
362
|
+
const inline = node.attrs.style ? parseDeclarations(node.attrs.style) : null;
|
|
363
|
+
for (const rule of rules)
|
|
364
|
+
apply(rule.declarations.normal);
|
|
365
|
+
if (inline)
|
|
366
|
+
apply(inline.normal);
|
|
367
|
+
for (const rule of rules)
|
|
368
|
+
apply(rule.declarations.important);
|
|
369
|
+
if (inline)
|
|
370
|
+
apply(inline.important);
|
|
371
|
+
const style = {};
|
|
372
|
+
for (const name of PROPERTIES) {
|
|
373
|
+
if (name === 'transform')
|
|
374
|
+
continue;
|
|
375
|
+
let value = own[name];
|
|
376
|
+
if (value === 'inherit' || (value === undefined && INHERITED.has(name)))
|
|
377
|
+
value = parent ? parent[name] : DEFAULTS[name];
|
|
378
|
+
if (value === undefined || value === 'initial')
|
|
379
|
+
value = DEFAULTS[name];
|
|
380
|
+
style[name] = value === undefined ? undefined : String(value).trim();
|
|
381
|
+
}
|
|
382
|
+
style['mask-type'] = own['mask-type'] || node.attrs['mask-type'];
|
|
383
|
+
style.transform = own.transform;
|
|
384
|
+
node.computed = style;
|
|
385
|
+
node.computedParent = parent;
|
|
386
|
+
return style;
|
|
387
|
+
}
|
|
388
|
+
styleOf(node) {
|
|
389
|
+
return this.computeStyle(node, node.parent && node.parent.name !== '#root' ? this.styleOf(node.parent) : null);
|
|
390
|
+
}
|
|
391
|
+
renderChildren(node, context) {
|
|
392
|
+
for (const child of node.children)
|
|
393
|
+
this.renderNode(child, context);
|
|
394
|
+
}
|
|
395
|
+
passesConditions(node) {
|
|
396
|
+
if (node.attrs.requiredExtensions !== undefined)
|
|
397
|
+
return false;
|
|
398
|
+
const languages = node.attrs.systemLanguage;
|
|
399
|
+
if (languages === undefined)
|
|
400
|
+
return true;
|
|
401
|
+
return languages.split(',').some(lang => {
|
|
402
|
+
lang = lang.trim().toLowerCase();
|
|
403
|
+
return lang === 'en' || lang.startsWith('en-');
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
renderNode(node, context, root = false) {
|
|
407
|
+
if (!node.svg || UNSUPPORTED.has(node.name) || context.depth > 32 || !this.passesConditions(node))
|
|
408
|
+
return;
|
|
409
|
+
const style = this.computeStyle(node, context.style);
|
|
410
|
+
if (style.display === 'none')
|
|
411
|
+
return;
|
|
412
|
+
let matrix = context.matrix;
|
|
413
|
+
if (style.transform && !root)
|
|
414
|
+
matrix = multiply(matrix, parseTransform(style.transform));
|
|
415
|
+
const opacity = clamp01(parseNumber(style.opacity, 1));
|
|
416
|
+
if (opacity <= 0)
|
|
417
|
+
return;
|
|
418
|
+
const clip = this.reference(style['clip-path']);
|
|
419
|
+
const mask = this.reference(style.mask);
|
|
420
|
+
if ((clip && clip.name !== 'clipPath') || (mask && mask.name !== 'mask'))
|
|
421
|
+
return;
|
|
422
|
+
const clipping = clip && !this.active.has(clip);
|
|
423
|
+
const masking = mask && !this.active.has(mask);
|
|
424
|
+
const draw = (layer) => {
|
|
425
|
+
const ctx = { ...context, matrix, style, layer, depth: context.depth + 1 };
|
|
426
|
+
switch (node.name) {
|
|
427
|
+
case 'svg': return root ? this.renderChildren(node, ctx) : this.renderNestedSvg(node, ctx);
|
|
428
|
+
case 'g':
|
|
429
|
+
case 'a': return this.renderChildren(node, ctx);
|
|
430
|
+
case 'switch': {
|
|
431
|
+
const first = node.children.find(child => child.svg && SWITCHABLE.has(child.name) && this.passesConditions(child));
|
|
432
|
+
if (first)
|
|
433
|
+
this.renderNode(first, ctx);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
case 'use': return this.renderUse(node, ctx);
|
|
437
|
+
default: {
|
|
438
|
+
const path = shapePath(node, ctx.viewport, parseNumber(style['font-size'], 16));
|
|
439
|
+
if (path)
|
|
440
|
+
this.paintPath(path, node, ctx);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
if (opacity >= 1 && !clipping && !masking)
|
|
445
|
+
return draw(context.layer);
|
|
446
|
+
const layer = new Layer(context.layer.width, context.layer.height);
|
|
447
|
+
draw(layer);
|
|
448
|
+
let alpha = null;
|
|
449
|
+
if (clipping)
|
|
450
|
+
alpha = this.clipMask(clip, node, matrix, context);
|
|
451
|
+
if (masking) {
|
|
452
|
+
const values = this.maskValues(mask, node, matrix, context);
|
|
453
|
+
if (alpha)
|
|
454
|
+
for (let i = 0; i < alpha.length; i++)
|
|
455
|
+
alpha[i] *= values[i];
|
|
456
|
+
else
|
|
457
|
+
alpha = values;
|
|
458
|
+
}
|
|
459
|
+
context.layer.composite(layer, opacity, alpha);
|
|
460
|
+
}
|
|
461
|
+
clipped(ctx, x, y, width, height, draw) {
|
|
462
|
+
const layer = new Layer(ctx.layer.width, ctx.layer.height);
|
|
463
|
+
draw(layer);
|
|
464
|
+
const { width: w } = ctx.layer;
|
|
465
|
+
const alpha = new Float32Array(w * ctx.layer.height);
|
|
466
|
+
const coverage = rasterize([transformPoints([x, y, x + width, y, x + width, y + height, x, y + height], ctx.matrix)], false, w, ctx.layer.height);
|
|
467
|
+
if (coverage)
|
|
468
|
+
for (let row = 0; row < coverage.h; row++)
|
|
469
|
+
for (let column = 0; column < coverage.w; column++)
|
|
470
|
+
alpha[(coverage.y0 + row) * w + coverage.x0 + column] = coverage.data[row * coverage.w + column];
|
|
471
|
+
ctx.layer.composite(layer, 1, alpha);
|
|
472
|
+
}
|
|
473
|
+
renderNestedSvg(node, ctx) {
|
|
474
|
+
const { viewport } = ctx;
|
|
475
|
+
const x = parseLength(node.attrs.x, viewport, 16, 'x');
|
|
476
|
+
const y = parseLength(node.attrs.y, viewport, 16, 'y');
|
|
477
|
+
const width = node.attrs.width === undefined ? viewport.width : parseLength(node.attrs.width, viewport, 16, 'x');
|
|
478
|
+
const height = node.attrs.height === undefined ? viewport.height : parseLength(node.attrs.height, viewport, 16, 'y');
|
|
479
|
+
if (!(width > 0) || !(height > 0))
|
|
480
|
+
return;
|
|
481
|
+
const viewBox = parseViewBox(node.attrs.viewBox);
|
|
482
|
+
let matrix = multiply(ctx.matrix, [1, 0, 0, 1, x, y]);
|
|
483
|
+
if (viewBox)
|
|
484
|
+
matrix = multiply(matrix, viewBoxTransform(viewBox, width, height, node.attrs.preserveAspectRatio));
|
|
485
|
+
const inner = { ...ctx, matrix, viewport: viewBox ? { width: viewBox[2], height: viewBox[3] } : { width, height } };
|
|
486
|
+
if (/^(visible|auto)$/.test(node.attrs.overflow || ''))
|
|
487
|
+
return this.renderChildren(node, inner);
|
|
488
|
+
this.clipped(ctx, x, y, width, height, layer => this.renderChildren(node, { ...inner, layer }));
|
|
489
|
+
}
|
|
490
|
+
renderUse(node, ctx) {
|
|
491
|
+
const target = this.reference(this.href(node));
|
|
492
|
+
if (!target || ctx.depth > 24)
|
|
493
|
+
return;
|
|
494
|
+
for (let p = node; p; p = p.parent)
|
|
495
|
+
if (p === target)
|
|
496
|
+
return;
|
|
497
|
+
const x = parseLength(node.attrs.x, ctx.viewport, 16, 'x');
|
|
498
|
+
const y = parseLength(node.attrs.y, ctx.viewport, 16, 'y');
|
|
499
|
+
const matrix = multiply(ctx.matrix, [1, 0, 0, 1, x, y]);
|
|
500
|
+
if (target.name === 'symbol' || target.name === 'svg') {
|
|
501
|
+
const style = this.computeStyle(target, ctx.style);
|
|
502
|
+
if (style.display === 'none')
|
|
503
|
+
return;
|
|
504
|
+
const width = node.attrs.width !== undefined ? parseLength(node.attrs.width, ctx.viewport, 16, 'x')
|
|
505
|
+
: target.attrs.width !== undefined ? parseLength(target.attrs.width, ctx.viewport, 16, 'x') : ctx.viewport.width;
|
|
506
|
+
const height = node.attrs.height !== undefined ? parseLength(node.attrs.height, ctx.viewport, 16, 'y')
|
|
507
|
+
: target.attrs.height !== undefined ? parseLength(target.attrs.height, ctx.viewport, 16, 'y') : ctx.viewport.height;
|
|
508
|
+
if (!(width > 0) || !(height > 0))
|
|
509
|
+
return;
|
|
510
|
+
const viewBox = parseViewBox(target.attrs.viewBox);
|
|
511
|
+
let outer = matrix;
|
|
512
|
+
if (target.name === 'svg')
|
|
513
|
+
outer = multiply(outer, [1, 0, 0, 1, parseLength(target.attrs.x, ctx.viewport, 16, 'x'), parseLength(target.attrs.y, ctx.viewport, 16, 'y')]);
|
|
514
|
+
const inner = viewBox ? multiply(outer, viewBoxTransform(viewBox, width, height, target.attrs.preserveAspectRatio)) : outer;
|
|
515
|
+
const opacity = clamp01(parseNumber(style.opacity, 1));
|
|
516
|
+
const innerContext = { ...ctx, matrix: inner, style, viewport: viewBox ? { width: viewBox[2], height: viewBox[3] } : { width, height } };
|
|
517
|
+
const draw = (layer) => /^(visible|auto)$/.test(target.attrs.overflow || '')
|
|
518
|
+
? this.renderChildren(target, { ...innerContext, layer })
|
|
519
|
+
: this.clipped({ ...ctx, matrix: outer, layer }, 0, 0, width, height, inner => this.renderChildren(target, { ...innerContext, layer: inner }));
|
|
520
|
+
if (opacity >= 1)
|
|
521
|
+
return draw(ctx.layer);
|
|
522
|
+
const layer = new Layer(ctx.layer.width, ctx.layer.height);
|
|
523
|
+
draw(layer);
|
|
524
|
+
ctx.layer.composite(layer, opacity, null);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const computed = target.computed, computedParent = target.computedParent;
|
|
528
|
+
target.computed = null;
|
|
529
|
+
this.renderNode(target, { ...ctx, matrix });
|
|
530
|
+
target.computed = computed;
|
|
531
|
+
target.computedParent = computedParent;
|
|
532
|
+
}
|
|
533
|
+
paintPath(path, node, ctx) {
|
|
534
|
+
const { matrix, layer } = ctx;
|
|
535
|
+
const style = ctx.style;
|
|
536
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse')
|
|
537
|
+
return;
|
|
538
|
+
const scale = Math.sqrt(Math.abs(matrix[0] * matrix[3] - matrix[1] * matrix[2])) || 1e-9;
|
|
539
|
+
const tolerance = 0.1 / scale;
|
|
540
|
+
const subpaths = flatten(path, tolerance);
|
|
541
|
+
const bbox = boundingBox(subpaths);
|
|
542
|
+
const fill = this.paint(style.fill, style, bbox, matrix, style['fill-opacity'], ctx);
|
|
543
|
+
if (fill && node.name !== 'line') {
|
|
544
|
+
const polygons = subpaths.map(points => transformPoints(points.points, matrix));
|
|
545
|
+
const coverage = rasterize(polygons, style['fill-rule'] === 'evenodd', layer.width, layer.height);
|
|
546
|
+
if (coverage)
|
|
547
|
+
layer.fill(coverage, fill);
|
|
548
|
+
}
|
|
549
|
+
const stroke = this.paint(style.stroke, style, bbox, matrix, style['stroke-opacity'], ctx);
|
|
550
|
+
const strokeWidth = parseLength(style['stroke-width'], ctx.viewport, 16, 'diagonal');
|
|
551
|
+
if (stroke && strokeWidth > 0) {
|
|
552
|
+
let lines = subpaths;
|
|
553
|
+
const fontSize = parseNumber(style['font-size'], 16);
|
|
554
|
+
const dashes = parseDashArray(style['stroke-dasharray'], ctx.viewport, fontSize);
|
|
555
|
+
if (dashes)
|
|
556
|
+
lines = dash(subpaths, dashes, parseLength(style['stroke-dashoffset'], ctx.viewport, fontSize, 'diagonal'));
|
|
557
|
+
const polygons = strokePolygons(lines, strokeWidth / 2, style['stroke-linecap'], style['stroke-linejoin'], Math.max(1, parseNumber(style['stroke-miterlimit'], 4)), tolerance).map(points => transformPoints(points, matrix));
|
|
558
|
+
const coverage = rasterize(polygons, false, layer.width, layer.height);
|
|
559
|
+
if (coverage)
|
|
560
|
+
layer.fill(coverage, stroke);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
paint(value, style, bbox, matrix, opacityValue, ctx) {
|
|
564
|
+
if (!value || value === 'none')
|
|
565
|
+
return null;
|
|
566
|
+
const opacity = clamp01(parseNumber(opacityValue, 1));
|
|
567
|
+
let color = value;
|
|
568
|
+
if (/^url\(/.test(value)) {
|
|
569
|
+
const server = this.reference(value);
|
|
570
|
+
const fallback = value.replace(/^\s*url\([^)]*\)\s*/, '');
|
|
571
|
+
if (server && (server.name === 'linearGradient' || server.name === 'radialGradient')) {
|
|
572
|
+
const gradient = this.gradient(server, bbox, matrix, opacity, ctx);
|
|
573
|
+
if (gradient !== undefined || !fallback)
|
|
574
|
+
return gradient || null;
|
|
575
|
+
}
|
|
576
|
+
color = fallback;
|
|
577
|
+
if (!color || color === 'none')
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const parsed = parseColor(color === 'currentColor' ? style.color : color);
|
|
581
|
+
if (!parsed || parsed[3] * opacity <= 0)
|
|
582
|
+
return null;
|
|
583
|
+
return solidPaint([parsed[0], parsed[1], parsed[2], parsed[3] * opacity]);
|
|
584
|
+
}
|
|
585
|
+
gradientAttributes(node) {
|
|
586
|
+
const attrs = {};
|
|
587
|
+
let stops = null;
|
|
588
|
+
const geometry = node.name === 'linearGradient' ? ['x1', 'y1', 'x2', 'y2'] : ['cx', 'cy', 'r', 'fx', 'fy', 'fr'];
|
|
589
|
+
for (let current = node, depth = 0; current && depth < 16; current = this.reference(this.href(current)), depth++) {
|
|
590
|
+
if (current.name !== 'linearGradient' && current.name !== 'radialGradient')
|
|
591
|
+
break;
|
|
592
|
+
const names = current.name === node.name ? [...geometry, 'gradientUnits', 'gradientTransform', 'spreadMethod'] : ['gradientUnits', 'gradientTransform', 'spreadMethod'];
|
|
593
|
+
for (const name of names)
|
|
594
|
+
if (attrs[name] === undefined && current.attrs[name] !== undefined)
|
|
595
|
+
attrs[name] = current.attrs[name];
|
|
596
|
+
if (!stops) {
|
|
597
|
+
const own = current.children.filter(child => child.name === 'stop');
|
|
598
|
+
if (own.length)
|
|
599
|
+
stops = own.map(stop => {
|
|
600
|
+
const style = this.styleOf(stop);
|
|
601
|
+
const offset = clamp01(parseStrictNumber(stop.attrs.offset) || 0);
|
|
602
|
+
const color = parseColor(style['stop-color'] === 'currentColor' ? style.color : style['stop-color']) || [0, 0, 0, 1];
|
|
603
|
+
return { offset, color: [color[0], color[1], color[2], color[3] * clamp01(parseNumber(style['stop-opacity'], 1))] };
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { attrs, stops: stops || [] };
|
|
608
|
+
}
|
|
609
|
+
gradient(node, bbox, matrix, opacity, ctx) {
|
|
610
|
+
const { attrs, stops } = this.gradientAttributes(node);
|
|
611
|
+
if (!stops.length)
|
|
612
|
+
return undefined;
|
|
613
|
+
for (let i = 1; i < stops.length; i++)
|
|
614
|
+
stops[i].offset = Math.max(stops[i].offset, stops[i - 1].offset);
|
|
615
|
+
if (stops.length === 1) {
|
|
616
|
+
const c = stops[0].color;
|
|
617
|
+
return c[3] * opacity > 0 ? solidPaint([c[0], c[1], c[2], c[3] * opacity]) : null;
|
|
618
|
+
}
|
|
619
|
+
const objectBox = attrs.gradientUnits !== 'userSpaceOnUse';
|
|
620
|
+
if (objectBox && (!bbox || bbox.width <= 0 || bbox.height <= 0))
|
|
621
|
+
return null;
|
|
622
|
+
const coordinate = (value, fallback, axis) => {
|
|
623
|
+
if (value === undefined)
|
|
624
|
+
return fallback;
|
|
625
|
+
if (objectBox)
|
|
626
|
+
return /%\s*$/.test(value) ? parseFloat(value) / 100 : parseNumber(value, fallback);
|
|
627
|
+
return parseLength(value, ctx.viewport, 16, axis);
|
|
628
|
+
};
|
|
629
|
+
let gradientMatrix = matrix;
|
|
630
|
+
if (objectBox && bbox)
|
|
631
|
+
gradientMatrix = multiply(gradientMatrix, [bbox.width, 0, 0, bbox.height, bbox.x, bbox.y]);
|
|
632
|
+
if (attrs.gradientTransform)
|
|
633
|
+
gradientMatrix = multiply(gradientMatrix, parseTransform(attrs.gradientTransform));
|
|
634
|
+
const inverse = invert(gradientMatrix);
|
|
635
|
+
if (!inverse)
|
|
636
|
+
return null;
|
|
637
|
+
const spread = attrs.spreadMethod === 'reflect' ? 1 : attrs.spreadMethod === 'repeat' ? 2 : 0;
|
|
638
|
+
const lookup = gradientLookup(stops, opacity);
|
|
639
|
+
const color = (t) => {
|
|
640
|
+
if (spread === 2)
|
|
641
|
+
t -= Math.floor(t);
|
|
642
|
+
else if (spread === 1) {
|
|
643
|
+
t = Math.abs(t) % 2;
|
|
644
|
+
if (t > 1)
|
|
645
|
+
t = 2 - t;
|
|
646
|
+
}
|
|
647
|
+
return lookup[Math.round(clamp01(t) * 1023)];
|
|
648
|
+
};
|
|
649
|
+
if (node.name === 'linearGradient') {
|
|
650
|
+
const x1 = coordinate(attrs.x1, 0, 'x'), y1 = coordinate(attrs.y1, 0, 'y');
|
|
651
|
+
const x2 = coordinate(attrs.x2, objectBox ? 1 : ctx.viewport.width, 'x'), y2 = coordinate(attrs.y2, 0, 'y');
|
|
652
|
+
const dx = x2 - x1, dy = y2 - y1;
|
|
653
|
+
const length = dx * dx + dy * dy;
|
|
654
|
+
if (length === 0)
|
|
655
|
+
return this.lastStopPaint(stops, opacity);
|
|
656
|
+
return (px, py) => {
|
|
657
|
+
const x = inverse[0] * px + inverse[2] * py + inverse[4];
|
|
658
|
+
const y = inverse[1] * px + inverse[3] * py + inverse[5];
|
|
659
|
+
return color(((x - x1) * dx + (y - y1) * dy) / length);
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const cx = coordinate(attrs.cx, objectBox ? 0.5 : ctx.viewport.width / 2, 'x');
|
|
663
|
+
const cy = coordinate(attrs.cy, objectBox ? 0.5 : ctx.viewport.height / 2, 'y');
|
|
664
|
+
const r = coordinate(attrs.r, objectBox ? 0.5 : Math.hypot(ctx.viewport.width, ctx.viewport.height) / Math.SQRT2 / 2, 'diagonal');
|
|
665
|
+
let fx = coordinate(attrs.fx, cx, 'x'), fy = coordinate(attrs.fy, cy, 'y');
|
|
666
|
+
const fr = coordinate(attrs.fr, 0, 'diagonal');
|
|
667
|
+
if (!(r > 0))
|
|
668
|
+
return this.lastStopPaint(stops, opacity);
|
|
669
|
+
const fd = Math.hypot(fx - cx, fy - cy);
|
|
670
|
+
if (fd > r * 0.999) {
|
|
671
|
+
fx = cx + (fx - cx) * r * 0.999 / fd;
|
|
672
|
+
fy = cy + (fy - cy) * r * 0.999 / fd;
|
|
673
|
+
}
|
|
674
|
+
const cdx = cx - fx, cdy = cy - fy, dr = r - fr;
|
|
675
|
+
const a = cdx * cdx + cdy * cdy - dr * dr;
|
|
676
|
+
return (px, py) => {
|
|
677
|
+
const x = inverse[0] * px + inverse[2] * py + inverse[4] - fx;
|
|
678
|
+
const y = inverse[1] * px + inverse[3] * py + inverse[5] - fy;
|
|
679
|
+
const b = x * cdx + y * cdy + fr * dr;
|
|
680
|
+
const c = x * x + y * y - fr * fr;
|
|
681
|
+
let t;
|
|
682
|
+
if (Math.abs(a) < 1e-9)
|
|
683
|
+
t = c / (2 * b);
|
|
684
|
+
else {
|
|
685
|
+
const discriminant = b * b - a * c;
|
|
686
|
+
if (discriminant < 0)
|
|
687
|
+
return TRANSPARENT;
|
|
688
|
+
const root = Math.sqrt(discriminant);
|
|
689
|
+
const t1 = (b + root) / a, t2 = (b - root) / a;
|
|
690
|
+
t = Math.max(t1, t2);
|
|
691
|
+
if (fr + t * dr < 0)
|
|
692
|
+
t = Math.min(t1, t2);
|
|
693
|
+
}
|
|
694
|
+
if (!isFinite(t) || fr + t * dr < 0)
|
|
695
|
+
return TRANSPARENT;
|
|
696
|
+
return color(t);
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
lastStopPaint(stops, opacity) {
|
|
700
|
+
const c = stops[stops.length - 1].color;
|
|
701
|
+
return solidPaint([c[0], c[1], c[2], c[3] * opacity]);
|
|
702
|
+
}
|
|
703
|
+
clipMask(clip, node, matrix, context) {
|
|
704
|
+
const { width, height } = context.layer;
|
|
705
|
+
const alpha = new Float32Array(width * height);
|
|
706
|
+
const style = this.styleOf(clip);
|
|
707
|
+
this.active.add(clip);
|
|
708
|
+
let clipMatrix = matrix;
|
|
709
|
+
if (style.transform)
|
|
710
|
+
clipMatrix = multiply(clipMatrix, parseTransform(style.transform));
|
|
711
|
+
if (clip.attrs.clipPathUnits === 'objectBoundingBox') {
|
|
712
|
+
const path = shapePath(node, context.viewport, 16);
|
|
713
|
+
const bbox = path ? boundingBox(flatten(path, 0.1)) : this.groupBoundingBox(node, context.viewport);
|
|
714
|
+
if (!bbox || bbox.width <= 0 || bbox.height <= 0)
|
|
715
|
+
return alpha;
|
|
716
|
+
clipMatrix = multiply(clipMatrix, [bbox.width, 0, 0, bbox.height, bbox.x, bbox.y]);
|
|
717
|
+
}
|
|
718
|
+
for (const child of clip.children) {
|
|
719
|
+
if (UNSUPPORTED.has(child.name) && child.name !== 'text')
|
|
720
|
+
continue;
|
|
721
|
+
const childStyle = this.computeStyle(child, style);
|
|
722
|
+
if (childStyle.display === 'none' || childStyle.visibility === 'hidden')
|
|
723
|
+
continue;
|
|
724
|
+
let childMatrix = clipMatrix;
|
|
725
|
+
if (childStyle.transform)
|
|
726
|
+
childMatrix = multiply(childMatrix, parseTransform(childStyle.transform));
|
|
727
|
+
let shape = child;
|
|
728
|
+
if (child.name === 'use') {
|
|
729
|
+
const target = this.reference(this.href(child));
|
|
730
|
+
if (!target || !['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon'].includes(target.name))
|
|
731
|
+
continue;
|
|
732
|
+
childMatrix = multiply(childMatrix, [1, 0, 0, 1, parseLength(child.attrs.x, context.viewport, 16, 'x'), parseLength(child.attrs.y, context.viewport, 16, 'y')]);
|
|
733
|
+
const targetStyle = this.computeStyle(target, childStyle);
|
|
734
|
+
if (targetStyle.transform)
|
|
735
|
+
childMatrix = multiply(childMatrix, parseTransform(targetStyle.transform));
|
|
736
|
+
shape = target;
|
|
737
|
+
}
|
|
738
|
+
const path = shapePath(shape, context.viewport, 16);
|
|
739
|
+
if (!path)
|
|
740
|
+
continue;
|
|
741
|
+
const scale = Math.sqrt(Math.abs(childMatrix[0] * childMatrix[3] - childMatrix[1] * childMatrix[2])) || 1e-9;
|
|
742
|
+
const polygons = flatten(path, 0.1 / scale).map(points => transformPoints(points.points, childMatrix));
|
|
743
|
+
const coverage = rasterize(polygons, this.computeStyle(shape, childStyle)['clip-rule'] === 'evenodd', width, height);
|
|
744
|
+
if (!coverage)
|
|
745
|
+
continue;
|
|
746
|
+
const own = this.reference(childStyle['clip-path']);
|
|
747
|
+
const ownMask = own && own.name === 'clipPath' && !this.active.has(own) ? this.clipMask(own, shape, childMatrix, context) : null;
|
|
748
|
+
const { x0, y0, w, h, data } = coverage;
|
|
749
|
+
for (let y = 0; y < h; y++)
|
|
750
|
+
for (let x = 0; x < w; x++) {
|
|
751
|
+
let c = data[y * w + x];
|
|
752
|
+
const i = (y0 + y) * width + x0 + x;
|
|
753
|
+
if (ownMask)
|
|
754
|
+
c *= ownMask[i];
|
|
755
|
+
alpha[i] = alpha[i] + c - alpha[i] * c;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const nested = this.reference(style['clip-path']);
|
|
759
|
+
if (nested && nested.name === 'clipPath' && !this.active.has(nested)) {
|
|
760
|
+
const outer = this.clipMask(nested, node, matrix, context);
|
|
761
|
+
for (let i = 0; i < alpha.length; i++)
|
|
762
|
+
alpha[i] *= outer[i];
|
|
763
|
+
}
|
|
764
|
+
this.active.delete(clip);
|
|
765
|
+
return alpha;
|
|
766
|
+
}
|
|
767
|
+
groupBoundingBox(node, viewport) {
|
|
768
|
+
let box = null;
|
|
769
|
+
const visit = (n, matrix) => {
|
|
770
|
+
const path = shapePath(n, viewport, 16);
|
|
771
|
+
if (path) {
|
|
772
|
+
const points = flatten(path, 0.1).map(p => transformPoints(p.points, matrix));
|
|
773
|
+
const b = boundingBox(points.map(p => ({ points: p })));
|
|
774
|
+
if (b)
|
|
775
|
+
box = box ? union(box, b) : b;
|
|
776
|
+
}
|
|
777
|
+
for (const child of n.children)
|
|
778
|
+
visit(child, child.attrs.transform ? multiply(matrix, parseTransform(child.attrs.transform)) : matrix);
|
|
779
|
+
};
|
|
780
|
+
visit(node, [1, 0, 0, 1, 0, 0]);
|
|
781
|
+
return box;
|
|
782
|
+
}
|
|
783
|
+
maskValues(mask, node, matrix, context) {
|
|
784
|
+
const { width, height } = context.layer;
|
|
785
|
+
const values = new Float32Array(width * height);
|
|
786
|
+
const path = shapePath(node, context.viewport, 16);
|
|
787
|
+
const bbox = path ? boundingBox(flatten(path, 0.1)) : this.groupBoundingBox(node, context.viewport);
|
|
788
|
+
const userSpace = mask.attrs.maskUnits === 'userSpaceOnUse';
|
|
789
|
+
const contentBox = mask.attrs.maskContentUnits === 'objectBoundingBox';
|
|
790
|
+
if ((!userSpace || contentBox) && (!bbox || bbox.width <= 0 || bbox.height <= 0))
|
|
791
|
+
return values;
|
|
792
|
+
const box = bbox || { x: 0, y: 0, width: 0, height: 0 };
|
|
793
|
+
const fraction = (name, fallback) => {
|
|
794
|
+
const value = mask.attrs[name];
|
|
795
|
+
if (value === undefined)
|
|
796
|
+
return fallback;
|
|
797
|
+
return /%\s*$/.test(value) ? parseFloat(value) / 100 : parseNumber(value, fallback);
|
|
798
|
+
};
|
|
799
|
+
let rx, ry, rw, rh;
|
|
800
|
+
if (userSpace) {
|
|
801
|
+
const { viewport } = context;
|
|
802
|
+
const length = (name, fallback, axis) => mask.attrs[name] === undefined ? fallback : parseLength(mask.attrs[name], viewport, 16, axis);
|
|
803
|
+
rx = length('x', -0.1 * viewport.width, 'x');
|
|
804
|
+
ry = length('y', -0.1 * viewport.height, 'y');
|
|
805
|
+
rw = length('width', 1.2 * viewport.width, 'x');
|
|
806
|
+
rh = length('height', 1.2 * viewport.height, 'y');
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
rx = box.x + fraction('x', -0.1) * box.width;
|
|
810
|
+
ry = box.y + fraction('y', -0.1) * box.height;
|
|
811
|
+
rw = fraction('width', 1.2) * box.width;
|
|
812
|
+
rh = fraction('height', 1.2) * box.height;
|
|
813
|
+
}
|
|
814
|
+
const layer = new Layer(width, height);
|
|
815
|
+
let contentMatrix = matrix;
|
|
816
|
+
if (contentBox)
|
|
817
|
+
contentMatrix = multiply(contentMatrix, [box.width, 0, 0, box.height, box.x, box.y]);
|
|
818
|
+
const maskStyle = this.styleOf(mask);
|
|
819
|
+
this.active.add(mask);
|
|
820
|
+
this.renderChildren(mask, { ...context, matrix: contentMatrix, style: maskStyle, layer, depth: context.depth + 1 });
|
|
821
|
+
const alphaMask = maskStyle['mask-type'] === 'alpha';
|
|
822
|
+
const regionCoverage = rasterize([transformPoints([rx, ry, rx + rw, ry, rx + rw, ry + rh, rx, ry + rh], matrix)], false, width, height);
|
|
823
|
+
if (!regionCoverage) {
|
|
824
|
+
this.active.delete(mask);
|
|
825
|
+
return values;
|
|
826
|
+
}
|
|
827
|
+
const { x0, y0, w, h, data } = regionCoverage;
|
|
828
|
+
const d = layer.data;
|
|
829
|
+
for (let y = 0; y < h; y++)
|
|
830
|
+
for (let x = 0; x < w; x++) {
|
|
831
|
+
const i = (y0 + y) * width + x0 + x;
|
|
832
|
+
values[i] = data[y * w + x] * (alphaMask ? d[i * 4 + 3] : 0.2125 * d[i * 4] + 0.7154 * d[i * 4 + 1] + 0.0721 * d[i * 4 + 2]);
|
|
833
|
+
}
|
|
834
|
+
const nested = this.reference(maskStyle.mask);
|
|
835
|
+
if (nested && nested.name === 'mask' && !this.active.has(nested)) {
|
|
836
|
+
const outer = this.maskValues(nested, node, matrix, context);
|
|
837
|
+
for (let i = 0; i < values.length; i++)
|
|
838
|
+
values[i] *= outer[i];
|
|
839
|
+
}
|
|
840
|
+
else if (nested)
|
|
841
|
+
values.fill(0);
|
|
842
|
+
this.active.delete(mask);
|
|
843
|
+
return values;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const TRANSPARENT = [0, 0, 0, 0];
|
|
847
|
+
function solidPaint(color) {
|
|
848
|
+
const paint = () => color;
|
|
849
|
+
paint.solid = color;
|
|
850
|
+
return paint;
|
|
851
|
+
}
|
|
852
|
+
function gradientLookup(stops, opacity) {
|
|
853
|
+
const table = new Array(1024);
|
|
854
|
+
for (let i = 0; i < 1024; i++) {
|
|
855
|
+
const t = i / 1023;
|
|
856
|
+
let k = 0;
|
|
857
|
+
while (k < stops.length - 1 && stops[k + 1].offset < t)
|
|
858
|
+
k++;
|
|
859
|
+
let c;
|
|
860
|
+
if (t <= stops[0].offset)
|
|
861
|
+
c = stops[0].color;
|
|
862
|
+
else if (k >= stops.length - 1)
|
|
863
|
+
c = stops[stops.length - 1].color;
|
|
864
|
+
else {
|
|
865
|
+
const a = stops[k], b = stops[k + 1];
|
|
866
|
+
const span = b.offset - a.offset;
|
|
867
|
+
const f = span > 0 ? (t - a.offset) / span : 1;
|
|
868
|
+
c = [0, 1, 2, 3].map(j => a.color[j] + (b.color[j] - a.color[j]) * f);
|
|
869
|
+
}
|
|
870
|
+
table[i] = [c[0], c[1], c[2], c[3] * opacity];
|
|
871
|
+
}
|
|
872
|
+
return table;
|
|
873
|
+
}
|
|
874
|
+
class Layer {
|
|
875
|
+
width;
|
|
876
|
+
height;
|
|
877
|
+
data;
|
|
878
|
+
constructor(width, height) {
|
|
879
|
+
this.width = width;
|
|
880
|
+
this.height = height;
|
|
881
|
+
this.data = new Float32Array(width * height * 4);
|
|
882
|
+
}
|
|
883
|
+
fill({ x0, y0, w, h, data }, paint) {
|
|
884
|
+
const d = this.data;
|
|
885
|
+
const solid = paint.solid;
|
|
886
|
+
for (let y = 0; y < h; y++) {
|
|
887
|
+
let i = ((y0 + y) * this.width + x0) * 4;
|
|
888
|
+
for (let x = 0, o = y * w; x < w; x++, o++, i += 4) {
|
|
889
|
+
const c = data[o];
|
|
890
|
+
if (c <= 0)
|
|
891
|
+
continue;
|
|
892
|
+
const color = solid || paint(x0 + x + 0.5, y0 + y + 0.5);
|
|
893
|
+
const a = color[3] * c;
|
|
894
|
+
if (a <= 0)
|
|
895
|
+
continue;
|
|
896
|
+
if (a >= 1) {
|
|
897
|
+
d[i] = color[0];
|
|
898
|
+
d[i + 1] = color[1];
|
|
899
|
+
d[i + 2] = color[2];
|
|
900
|
+
d[i + 3] = 1;
|
|
901
|
+
if (solid) {
|
|
902
|
+
let run = 1;
|
|
903
|
+
while (x + run < w && data[o + run] >= 1)
|
|
904
|
+
run++;
|
|
905
|
+
if (run > 1) {
|
|
906
|
+
for (let filled = 1; filled < run; filled *= 2)
|
|
907
|
+
d.copyWithin(i + filled * 4, i, i + Math.min(filled, run - filled) * 4);
|
|
908
|
+
x += run - 1;
|
|
909
|
+
o += run - 1;
|
|
910
|
+
i += (run - 1) * 4;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const inv = 1 - a;
|
|
916
|
+
d[i] = color[0] * a + d[i] * inv;
|
|
917
|
+
d[i + 1] = color[1] * a + d[i + 1] * inv;
|
|
918
|
+
d[i + 2] = color[2] * a + d[i + 2] * inv;
|
|
919
|
+
d[i + 3] = a + d[i + 3] * inv;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
composite(layer, opacity, mask) {
|
|
924
|
+
const s = layer.data, d = this.data;
|
|
925
|
+
for (let p = 0, i = 0; p < this.width * this.height; p++, i += 4) {
|
|
926
|
+
if (s[i + 3] <= 0)
|
|
927
|
+
continue;
|
|
928
|
+
const f = mask ? opacity * mask[p] : opacity;
|
|
929
|
+
if (f <= 0)
|
|
930
|
+
continue;
|
|
931
|
+
const a = s[i + 3] * f;
|
|
932
|
+
const inv = 1 - a;
|
|
933
|
+
d[i] = s[i] * f + d[i] * inv;
|
|
934
|
+
d[i + 1] = s[i + 1] * f + d[i + 1] * inv;
|
|
935
|
+
d[i + 2] = s[i + 2] * f + d[i + 2] * inv;
|
|
936
|
+
d[i + 3] = a + d[i + 3] * inv;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
toRGBA() {
|
|
940
|
+
const out = new Uint8ClampedArray(this.width * this.height * 4);
|
|
941
|
+
const d = this.data;
|
|
942
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
943
|
+
const a = d[i + 3];
|
|
944
|
+
if (a <= 0)
|
|
945
|
+
continue;
|
|
946
|
+
const f = a >= 1 ? 255 : 255 / a;
|
|
947
|
+
out[i] = d[i] * f;
|
|
948
|
+
out[i + 1] = d[i + 1] * f;
|
|
949
|
+
out[i + 2] = d[i + 2] * f;
|
|
950
|
+
out[i + 3] = a * 255;
|
|
951
|
+
}
|
|
952
|
+
return new Uint8Array(out.buffer);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function rasterize(polygons, evenOdd, width, height) {
|
|
956
|
+
const edges = [];
|
|
957
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
958
|
+
for (const points of polygons) {
|
|
959
|
+
const n = points.length >> 1;
|
|
960
|
+
if (n < 2)
|
|
961
|
+
continue;
|
|
962
|
+
for (let i = 0; i < n; i++) {
|
|
963
|
+
const ax = points[i * 2], ay = points[i * 2 + 1];
|
|
964
|
+
const j = (i + 1) % n;
|
|
965
|
+
const bx = points[j * 2], by = points[j * 2 + 1];
|
|
966
|
+
if (!isFinite(ax) || !isFinite(ay) || !isFinite(bx) || !isFinite(by) || ay === by)
|
|
967
|
+
continue;
|
|
968
|
+
const down = ay < by;
|
|
969
|
+
const top = down ? ay : by, bottom = down ? by : ay;
|
|
970
|
+
const topX = down ? ax : bx;
|
|
971
|
+
edges.push({ top, bottom, x: topX, slope: (down ? bx - ax : ax - bx) / (bottom - top), dir: down ? 1 : -1 });
|
|
972
|
+
minY = Math.min(minY, top);
|
|
973
|
+
maxY = Math.max(maxY, bottom);
|
|
974
|
+
minX = Math.min(minX, ax, bx);
|
|
975
|
+
maxX = Math.max(maxX, ax, bx);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!edges.length)
|
|
979
|
+
return null;
|
|
980
|
+
const x0 = Math.max(0, Math.floor(minX)), x1 = Math.min(width, Math.ceil(maxX));
|
|
981
|
+
const y0 = Math.max(0, Math.floor(minY)), y1 = Math.min(height, Math.ceil(maxY));
|
|
982
|
+
if (x0 >= x1 || y0 >= y1)
|
|
983
|
+
return null;
|
|
984
|
+
const w = x1 - x0, h = y1 - y0;
|
|
985
|
+
const data = new Float32Array(w * h);
|
|
986
|
+
const row = new Float32Array(w + 2);
|
|
987
|
+
const steps = new Float32Array(w + 2);
|
|
988
|
+
const weight = 1 / SAMPLES;
|
|
989
|
+
edges.sort((a, b) => a.top - b.top);
|
|
990
|
+
let next = 0;
|
|
991
|
+
let active = [];
|
|
992
|
+
const crossings = [];
|
|
993
|
+
const span = (a, b) => {
|
|
994
|
+
a = Math.max(a, x0) - x0;
|
|
995
|
+
b = Math.min(b, x1) - x0;
|
|
996
|
+
if (b <= a)
|
|
997
|
+
return;
|
|
998
|
+
const ia = Math.floor(a), ib = Math.floor(b);
|
|
999
|
+
if (ia === ib)
|
|
1000
|
+
row[ia] += (b - a) * weight;
|
|
1001
|
+
else {
|
|
1002
|
+
row[ia] += (ia + 1 - a) * weight;
|
|
1003
|
+
steps[ia + 1] += weight;
|
|
1004
|
+
steps[ib] -= weight;
|
|
1005
|
+
row[ib] += (b - ib) * weight;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
for (let py = y0; py < y1; py++) {
|
|
1009
|
+
row.fill(0);
|
|
1010
|
+
steps.fill(0);
|
|
1011
|
+
while (next < edges.length && edges[next].top < py + 1)
|
|
1012
|
+
active.push(edges[next++]);
|
|
1013
|
+
active = active.filter(edge => edge.bottom > py);
|
|
1014
|
+
for (let s = 0; s < SAMPLES; s++) {
|
|
1015
|
+
const sy = py + (s + 0.5) / SAMPLES;
|
|
1016
|
+
crossings.length = 0;
|
|
1017
|
+
for (const edge of active)
|
|
1018
|
+
if (sy >= edge.top && sy < edge.bottom)
|
|
1019
|
+
crossings.push(edge.x + (sy - edge.top) * edge.slope, edge.dir);
|
|
1020
|
+
if (crossings.length < 4)
|
|
1021
|
+
continue;
|
|
1022
|
+
for (let i = 2; i < crossings.length; i += 2) {
|
|
1023
|
+
const x = crossings[i], dir = crossings[i + 1];
|
|
1024
|
+
let j = i - 2;
|
|
1025
|
+
for (; j >= 0 && crossings[j] > x; j -= 2) {
|
|
1026
|
+
crossings[j + 2] = crossings[j];
|
|
1027
|
+
crossings[j + 3] = crossings[j + 1];
|
|
1028
|
+
}
|
|
1029
|
+
crossings[j + 2] = x;
|
|
1030
|
+
crossings[j + 3] = dir;
|
|
1031
|
+
}
|
|
1032
|
+
let winding = 0;
|
|
1033
|
+
for (let i = 0; i < crossings.length - 2; i += 2) {
|
|
1034
|
+
winding += crossings[i + 1];
|
|
1035
|
+
if (evenOdd ? winding & 1 : winding !== 0)
|
|
1036
|
+
span(crossings[i], crossings[i + 2]);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
let step = 0;
|
|
1040
|
+
const o = (py - y0) * w;
|
|
1041
|
+
for (let x = 0; x < w; x++) {
|
|
1042
|
+
step += steps[x];
|
|
1043
|
+
const v = row[x] + step;
|
|
1044
|
+
data[o + x] = v > 1 ? 1 : v;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return { x0, y0, w, h, data };
|
|
1048
|
+
}
|
|
1049
|
+
function shapePath(node, viewport, fontSize) {
|
|
1050
|
+
const length = (name, axis, fallback = 0) => node.attrs[name] === undefined ? fallback : parseLength(node.attrs[name], viewport, fontSize, axis);
|
|
1051
|
+
switch (node.name) {
|
|
1052
|
+
case 'path':
|
|
1053
|
+
return parsePath(node.attrs.d || '');
|
|
1054
|
+
case 'rect': {
|
|
1055
|
+
const x = length('x', 'x'), y = length('y', 'y');
|
|
1056
|
+
const w = length('width', 'x'), h = length('height', 'y');
|
|
1057
|
+
if (!(w > 0) || !(h > 0))
|
|
1058
|
+
return null;
|
|
1059
|
+
let rx = node.attrs.rx === undefined || node.attrs.rx === 'auto' ? undefined : length('rx', 'x');
|
|
1060
|
+
let ry = node.attrs.ry === undefined || node.attrs.ry === 'auto' ? undefined : length('ry', 'y');
|
|
1061
|
+
if (rx === undefined || rx < 0)
|
|
1062
|
+
rx = ry !== undefined && ry >= 0 ? ry : 0;
|
|
1063
|
+
if (ry === undefined || ry < 0)
|
|
1064
|
+
ry = rx;
|
|
1065
|
+
rx = Math.min(rx, w / 2);
|
|
1066
|
+
ry = Math.min(ry, h / 2);
|
|
1067
|
+
if (!rx || !ry)
|
|
1068
|
+
return [['M', x, y], ['L', x + w, y], ['L', x + w, y + h], ['L', x, y + h], ['Z']];
|
|
1069
|
+
const k = 0.5522847498;
|
|
1070
|
+
return [
|
|
1071
|
+
['M', x + rx, y], ['L', x + w - rx, y],
|
|
1072
|
+
['C', x + w - rx + k * rx, y, x + w, y + ry - k * ry, x + w, y + ry], ['L', x + w, y + h - ry],
|
|
1073
|
+
['C', x + w, y + h - ry + k * ry, x + w - rx + k * rx, y + h, x + w - rx, y + h], ['L', x + rx, y + h],
|
|
1074
|
+
['C', x + rx - k * rx, y + h, x, y + h - ry + k * ry, x, y + h - ry], ['L', x, y + ry],
|
|
1075
|
+
['C', x, y + ry - k * ry, x + rx - k * rx, y, x + rx, y], ['Z']
|
|
1076
|
+
];
|
|
1077
|
+
}
|
|
1078
|
+
case 'circle':
|
|
1079
|
+
case 'ellipse': {
|
|
1080
|
+
const cx = length('cx', 'x'), cy = length('cy', 'y');
|
|
1081
|
+
let rx, ry;
|
|
1082
|
+
if (node.name === 'circle')
|
|
1083
|
+
rx = ry = length('r', 'diagonal');
|
|
1084
|
+
else {
|
|
1085
|
+
rx = node.attrs.rx === undefined || node.attrs.rx === 'auto' ? undefined : length('rx', 'x');
|
|
1086
|
+
ry = node.attrs.ry === undefined || node.attrs.ry === 'auto' ? undefined : length('ry', 'y');
|
|
1087
|
+
if (rx !== undefined && rx < 0)
|
|
1088
|
+
rx = undefined;
|
|
1089
|
+
if (ry !== undefined && ry < 0)
|
|
1090
|
+
ry = undefined;
|
|
1091
|
+
if (rx === undefined)
|
|
1092
|
+
rx = ry;
|
|
1093
|
+
if (ry === undefined)
|
|
1094
|
+
ry = rx;
|
|
1095
|
+
}
|
|
1096
|
+
if (rx === undefined || ry === undefined || !(rx > 0) || !(ry > 0))
|
|
1097
|
+
return null;
|
|
1098
|
+
const k = 0.5522847498;
|
|
1099
|
+
return [
|
|
1100
|
+
['M', cx + rx, cy],
|
|
1101
|
+
['C', cx + rx, cy + k * ry, cx + k * rx, cy + ry, cx, cy + ry],
|
|
1102
|
+
['C', cx - k * rx, cy + ry, cx - rx, cy + k * ry, cx - rx, cy],
|
|
1103
|
+
['C', cx - rx, cy - k * ry, cx - k * rx, cy - ry, cx, cy - ry],
|
|
1104
|
+
['C', cx + k * rx, cy - ry, cx + rx, cy - k * ry, cx + rx, cy],
|
|
1105
|
+
['Z']
|
|
1106
|
+
];
|
|
1107
|
+
}
|
|
1108
|
+
case 'line':
|
|
1109
|
+
return [['M', length('x1', 'x'), length('y1', 'y')], ['L', length('x2', 'x'), length('y2', 'y')]];
|
|
1110
|
+
case 'polyline':
|
|
1111
|
+
case 'polygon': {
|
|
1112
|
+
const numbers = parseNumberList(node.attrs.points || '');
|
|
1113
|
+
if (numbers.length < 4)
|
|
1114
|
+
return null;
|
|
1115
|
+
const path = [['M', numbers[0], numbers[1]]];
|
|
1116
|
+
for (let i = 2; i + 1 < numbers.length; i += 2)
|
|
1117
|
+
path.push(['L', numbers[i], numbers[i + 1]]);
|
|
1118
|
+
if (node.name === 'polygon')
|
|
1119
|
+
path.push(['Z']);
|
|
1120
|
+
return path;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
function parsePath(d) {
|
|
1126
|
+
const path = [];
|
|
1127
|
+
const tokens = /([MmZzLlHhVvCcSsQqTtAa])|([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/g;
|
|
1128
|
+
const items = [];
|
|
1129
|
+
for (const m of d.matchAll(tokens))
|
|
1130
|
+
items.push(m[1] ? { command: m[1] } : m[2]);
|
|
1131
|
+
let i = 0;
|
|
1132
|
+
let command = null;
|
|
1133
|
+
let x = 0, y = 0, sx = 0, sy = 0;
|
|
1134
|
+
let lastControlX = 0, lastControlY = 0, lastCommand = '';
|
|
1135
|
+
const number = () => typeof items[i] === 'string' ? parseFloat(items[i++]) : NaN;
|
|
1136
|
+
const flag = () => {
|
|
1137
|
+
const v = items[i];
|
|
1138
|
+
if (typeof v !== 'string' || (v[0] !== '0' && v[0] !== '1'))
|
|
1139
|
+
return NaN;
|
|
1140
|
+
if (v.length === 1)
|
|
1141
|
+
i++;
|
|
1142
|
+
else
|
|
1143
|
+
items[i] = v.slice(1);
|
|
1144
|
+
return +v[0];
|
|
1145
|
+
};
|
|
1146
|
+
while (i < items.length) {
|
|
1147
|
+
if (typeof items[i] === 'object')
|
|
1148
|
+
command = items[i++].command;
|
|
1149
|
+
else if (command === null)
|
|
1150
|
+
return path;
|
|
1151
|
+
else if (command === 'M')
|
|
1152
|
+
command = 'L';
|
|
1153
|
+
else if (command === 'm')
|
|
1154
|
+
command = 'l';
|
|
1155
|
+
else if (command === 'Z' || command === 'z')
|
|
1156
|
+
return path;
|
|
1157
|
+
const relative = command === command.toLowerCase();
|
|
1158
|
+
const ox = relative ? x : 0, oy = relative ? y : 0;
|
|
1159
|
+
const upper = command.toUpperCase();
|
|
1160
|
+
if (upper === 'Z') {
|
|
1161
|
+
path.push(['Z']);
|
|
1162
|
+
x = sx;
|
|
1163
|
+
y = sy;
|
|
1164
|
+
lastCommand = 'Z';
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
if (!path.length && upper !== 'M')
|
|
1168
|
+
return path;
|
|
1169
|
+
if (upper === 'M') {
|
|
1170
|
+
const nx = number() + ox, ny = number() + oy;
|
|
1171
|
+
if (isNaN(nx) || isNaN(ny))
|
|
1172
|
+
return path;
|
|
1173
|
+
x = sx = nx;
|
|
1174
|
+
y = sy = ny;
|
|
1175
|
+
path.push(['M', x, y]);
|
|
1176
|
+
}
|
|
1177
|
+
else if (upper === 'L') {
|
|
1178
|
+
const nx = number() + ox, ny = number() + oy;
|
|
1179
|
+
if (isNaN(nx) || isNaN(ny))
|
|
1180
|
+
return path;
|
|
1181
|
+
x = nx;
|
|
1182
|
+
y = ny;
|
|
1183
|
+
path.push(['L', x, y]);
|
|
1184
|
+
}
|
|
1185
|
+
else if (upper === 'H') {
|
|
1186
|
+
const nx = number() + ox;
|
|
1187
|
+
if (isNaN(nx))
|
|
1188
|
+
return path;
|
|
1189
|
+
x = nx;
|
|
1190
|
+
path.push(['L', x, y]);
|
|
1191
|
+
}
|
|
1192
|
+
else if (upper === 'V') {
|
|
1193
|
+
const ny = number() + oy;
|
|
1194
|
+
if (isNaN(ny))
|
|
1195
|
+
return path;
|
|
1196
|
+
y = ny;
|
|
1197
|
+
path.push(['L', x, y]);
|
|
1198
|
+
}
|
|
1199
|
+
else if (upper === 'C' || upper === 'S') {
|
|
1200
|
+
let c1x, c1y;
|
|
1201
|
+
if (upper === 'S') {
|
|
1202
|
+
const smooth = lastCommand === 'C' || lastCommand === 'S';
|
|
1203
|
+
c1x = smooth ? 2 * x - lastControlX : x;
|
|
1204
|
+
c1y = smooth ? 2 * y - lastControlY : y;
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
c1x = number() + ox;
|
|
1208
|
+
c1y = number() + oy;
|
|
1209
|
+
}
|
|
1210
|
+
const c2x = number() + ox, c2y = number() + oy;
|
|
1211
|
+
const nx = number() + ox, ny = number() + oy;
|
|
1212
|
+
if ([c1x, c1y, c2x, c2y, nx, ny].some(isNaN))
|
|
1213
|
+
return path;
|
|
1214
|
+
path.push(['C', c1x, c1y, c2x, c2y, nx, ny]);
|
|
1215
|
+
lastControlX = c2x;
|
|
1216
|
+
lastControlY = c2y;
|
|
1217
|
+
x = nx;
|
|
1218
|
+
y = ny;
|
|
1219
|
+
}
|
|
1220
|
+
else if (upper === 'Q' || upper === 'T') {
|
|
1221
|
+
let cx, cy;
|
|
1222
|
+
if (upper === 'T') {
|
|
1223
|
+
const smooth = lastCommand === 'Q' || lastCommand === 'T';
|
|
1224
|
+
cx = smooth ? 2 * x - lastControlX : x;
|
|
1225
|
+
cy = smooth ? 2 * y - lastControlY : y;
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
cx = number() + ox;
|
|
1229
|
+
cy = number() + oy;
|
|
1230
|
+
}
|
|
1231
|
+
const nx = number() + ox, ny = number() + oy;
|
|
1232
|
+
if ([cx, cy, nx, ny].some(isNaN))
|
|
1233
|
+
return path;
|
|
1234
|
+
path.push(['C', x + 2 / 3 * (cx - x), y + 2 / 3 * (cy - y), nx + 2 / 3 * (cx - nx), ny + 2 / 3 * (cy - ny), nx, ny]);
|
|
1235
|
+
lastControlX = cx;
|
|
1236
|
+
lastControlY = cy;
|
|
1237
|
+
x = nx;
|
|
1238
|
+
y = ny;
|
|
1239
|
+
}
|
|
1240
|
+
else if (upper === 'A') {
|
|
1241
|
+
const rx = number(), ry = number(), rotation = number();
|
|
1242
|
+
const large = flag(), sweep = flag();
|
|
1243
|
+
const nx = number() + ox, ny = number() + oy;
|
|
1244
|
+
if ([rx, ry, rotation, large, sweep, nx, ny].some(isNaN))
|
|
1245
|
+
return path;
|
|
1246
|
+
arcToCubics(path, x, y, rx, ry, rotation, large, sweep, nx, ny);
|
|
1247
|
+
x = nx;
|
|
1248
|
+
y = ny;
|
|
1249
|
+
}
|
|
1250
|
+
else
|
|
1251
|
+
return path;
|
|
1252
|
+
lastCommand = upper;
|
|
1253
|
+
}
|
|
1254
|
+
return path;
|
|
1255
|
+
}
|
|
1256
|
+
function arcToCubics(path, x1, y1, rx, ry, rotation, large, sweep, x2, y2) {
|
|
1257
|
+
if (x1 === x2 && y1 === y2)
|
|
1258
|
+
return;
|
|
1259
|
+
rx = Math.abs(rx);
|
|
1260
|
+
ry = Math.abs(ry);
|
|
1261
|
+
if (!rx || !ry) {
|
|
1262
|
+
path.push(['L', x2, y2]);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const phi = rotation * Math.PI / 180;
|
|
1266
|
+
const cos = Math.cos(phi), sin = Math.sin(phi);
|
|
1267
|
+
const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
|
|
1268
|
+
const x1p = cos * dx + sin * dy, y1p = -sin * dx + cos * dy;
|
|
1269
|
+
const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
1270
|
+
if (lambda > 1) {
|
|
1271
|
+
rx *= Math.sqrt(lambda);
|
|
1272
|
+
ry *= Math.sqrt(lambda);
|
|
1273
|
+
}
|
|
1274
|
+
const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
1275
|
+
const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
1276
|
+
let coefficient = Math.sqrt(Math.max(0, num / den));
|
|
1277
|
+
if (large === sweep)
|
|
1278
|
+
coefficient = -coefficient;
|
|
1279
|
+
const cxp = coefficient * rx * y1p / ry, cyp = -coefficient * ry * x1p / rx;
|
|
1280
|
+
const cx = cos * cxp - sin * cyp + (x1 + x2) / 2;
|
|
1281
|
+
const cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
|
|
1282
|
+
const angle = (ux, uy, vx, vy) => {
|
|
1283
|
+
const a = Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
1284
|
+
return a;
|
|
1285
|
+
};
|
|
1286
|
+
const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
1287
|
+
let delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
1288
|
+
if (!sweep && delta > 0)
|
|
1289
|
+
delta -= 2 * Math.PI;
|
|
1290
|
+
else if (sweep && delta < 0)
|
|
1291
|
+
delta += 2 * Math.PI;
|
|
1292
|
+
const segments = Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9);
|
|
1293
|
+
const step = delta / segments;
|
|
1294
|
+
const k = 4 / 3 * Math.tan(step / 4);
|
|
1295
|
+
const point = (t) => [cx + rx * Math.cos(t) * cos - ry * Math.sin(t) * sin, cy + rx * Math.cos(t) * sin + ry * Math.sin(t) * cos];
|
|
1296
|
+
const derivative = (t) => [-rx * Math.sin(t) * cos - ry * Math.cos(t) * sin, -rx * Math.sin(t) * sin + ry * Math.cos(t) * cos];
|
|
1297
|
+
let t = theta1;
|
|
1298
|
+
for (let i = 0; i < segments; i++) {
|
|
1299
|
+
const t2 = t + step;
|
|
1300
|
+
const [px, py] = point(t), [qx, qy] = i === segments - 1 ? [x2, y2] : point(t2);
|
|
1301
|
+
const [dpx, dpy] = derivative(t), [dqx, dqy] = derivative(t2);
|
|
1302
|
+
path.push(['C', px + k * dpx, py + k * dpy, qx - k * dqx, qy - k * dqy, qx, qy]);
|
|
1303
|
+
t = t2;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function flatten(path, tolerance) {
|
|
1307
|
+
const subpaths = [];
|
|
1308
|
+
let current = null;
|
|
1309
|
+
let x = 0, y = 0, sx = 0, sy = 0;
|
|
1310
|
+
const start = (px, py) => {
|
|
1311
|
+
const subpath = { points: [px, py], closed: false };
|
|
1312
|
+
subpaths.push(subpath);
|
|
1313
|
+
return subpath;
|
|
1314
|
+
};
|
|
1315
|
+
for (const command of path) {
|
|
1316
|
+
switch (command[0]) {
|
|
1317
|
+
case 'M':
|
|
1318
|
+
x = sx = command[1];
|
|
1319
|
+
y = sy = command[2];
|
|
1320
|
+
current = start(x, y);
|
|
1321
|
+
break;
|
|
1322
|
+
case 'L':
|
|
1323
|
+
if (!current)
|
|
1324
|
+
current = start(x, y);
|
|
1325
|
+
x = command[1];
|
|
1326
|
+
y = command[2];
|
|
1327
|
+
current.points.push(x, y);
|
|
1328
|
+
break;
|
|
1329
|
+
case 'C': {
|
|
1330
|
+
if (!current)
|
|
1331
|
+
current = start(x, y);
|
|
1332
|
+
const [, c1x, c1y, c2x, c2y, ex, ey] = command;
|
|
1333
|
+
const ddx = Math.max(Math.abs(x - 2 * c1x + c2x), Math.abs(c1x - 2 * c2x + ex));
|
|
1334
|
+
const ddy = Math.max(Math.abs(y - 2 * c1y + c2y), Math.abs(c1y - 2 * c2y + ey));
|
|
1335
|
+
const n = Math.min(512, Math.max(1, Math.ceil(Math.sqrt(0.75 * Math.hypot(ddx, ddy) / tolerance))));
|
|
1336
|
+
for (let i = 1; i <= n; i++) {
|
|
1337
|
+
const t = i / n, mt = 1 - t;
|
|
1338
|
+
current.points.push(mt * mt * mt * x + 3 * mt * mt * t * c1x + 3 * mt * t * t * c2x + t * t * t * ex, mt * mt * mt * y + 3 * mt * mt * t * c1y + 3 * mt * t * t * c2y + t * t * t * ey);
|
|
1339
|
+
}
|
|
1340
|
+
x = ex;
|
|
1341
|
+
y = ey;
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
case 'Z':
|
|
1345
|
+
if (current) {
|
|
1346
|
+
current.closed = true;
|
|
1347
|
+
x = sx;
|
|
1348
|
+
y = sy;
|
|
1349
|
+
current = null;
|
|
1350
|
+
}
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return subpaths;
|
|
1355
|
+
}
|
|
1356
|
+
function boundingBox(subpaths) {
|
|
1357
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1358
|
+
for (const { points } of subpaths)
|
|
1359
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
1360
|
+
minX = Math.min(minX, points[i]);
|
|
1361
|
+
maxX = Math.max(maxX, points[i]);
|
|
1362
|
+
minY = Math.min(minY, points[i + 1]);
|
|
1363
|
+
maxY = Math.max(maxY, points[i + 1]);
|
|
1364
|
+
}
|
|
1365
|
+
return minX === Infinity ? null : { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1366
|
+
}
|
|
1367
|
+
function union(a, b) {
|
|
1368
|
+
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
|
1369
|
+
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
1370
|
+
}
|
|
1371
|
+
function transformPoints(points, m) {
|
|
1372
|
+
const out = new Array(points.length);
|
|
1373
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
1374
|
+
const x = points[i], y = points[i + 1];
|
|
1375
|
+
out[i] = m[0] * x + m[2] * y + m[4];
|
|
1376
|
+
out[i + 1] = m[1] * x + m[3] * y + m[5];
|
|
1377
|
+
}
|
|
1378
|
+
return out;
|
|
1379
|
+
}
|
|
1380
|
+
function dash(subpaths, pattern, offset) {
|
|
1381
|
+
const total = pattern.reduce((a, b) => a + b, 0);
|
|
1382
|
+
const result = [];
|
|
1383
|
+
for (const { points, closed } of subpaths) {
|
|
1384
|
+
const pts = closed ? [...points, points[0], points[1]] : points;
|
|
1385
|
+
let index = 0;
|
|
1386
|
+
let remaining = ((offset % total) + total) % total;
|
|
1387
|
+
while (remaining >= pattern[index]) {
|
|
1388
|
+
remaining -= pattern[index];
|
|
1389
|
+
index = (index + 1) % pattern.length;
|
|
1390
|
+
}
|
|
1391
|
+
remaining = pattern[index] - remaining;
|
|
1392
|
+
let on = index % 2 === 0;
|
|
1393
|
+
let current = on ? [pts[0], pts[1]] : null;
|
|
1394
|
+
for (let i = 0; i + 3 < pts.length; i += 2) {
|
|
1395
|
+
let ax = pts[i], ay = pts[i + 1];
|
|
1396
|
+
const bx = pts[i + 2], by = pts[i + 3];
|
|
1397
|
+
let length = Math.hypot(bx - ax, by - ay);
|
|
1398
|
+
while (length > 0) {
|
|
1399
|
+
if (remaining >= length) {
|
|
1400
|
+
remaining -= length;
|
|
1401
|
+
if (on && current)
|
|
1402
|
+
current.push(bx, by);
|
|
1403
|
+
length = 0;
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
const t = remaining / length;
|
|
1407
|
+
const mx = ax + (bx - ax) * t, my = ay + (by - ay) * t;
|
|
1408
|
+
if (on) {
|
|
1409
|
+
if (current) {
|
|
1410
|
+
current.push(mx, my);
|
|
1411
|
+
result.push({ points: current, closed: false });
|
|
1412
|
+
}
|
|
1413
|
+
current = null;
|
|
1414
|
+
}
|
|
1415
|
+
else
|
|
1416
|
+
current = [mx, my];
|
|
1417
|
+
on = !on;
|
|
1418
|
+
length -= remaining;
|
|
1419
|
+
ax = mx;
|
|
1420
|
+
ay = my;
|
|
1421
|
+
index = (index + 1) % pattern.length;
|
|
1422
|
+
remaining = pattern[index];
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (on && current && current.length >= 4)
|
|
1427
|
+
result.push({ points: current, closed: false });
|
|
1428
|
+
}
|
|
1429
|
+
return result;
|
|
1430
|
+
}
|
|
1431
|
+
function strokePolygons(subpaths, halfWidth, cap, join, miterLimit, tolerance) {
|
|
1432
|
+
const polygons = [];
|
|
1433
|
+
const add = (polygon) => {
|
|
1434
|
+
let area = 0;
|
|
1435
|
+
for (let i = 0, n = polygon.length; i < n; i += 2) {
|
|
1436
|
+
const j = (i + 2) % n;
|
|
1437
|
+
area += polygon[i] * polygon[j + 1] - polygon[j] * polygon[i + 1];
|
|
1438
|
+
}
|
|
1439
|
+
if (area < 0) {
|
|
1440
|
+
const reversed = [];
|
|
1441
|
+
for (let i = polygon.length - 2; i >= 0; i -= 2)
|
|
1442
|
+
reversed.push(polygon[i], polygon[i + 1]);
|
|
1443
|
+
polygon = reversed;
|
|
1444
|
+
}
|
|
1445
|
+
polygons.push(polygon);
|
|
1446
|
+
};
|
|
1447
|
+
const circleSegments = Math.max(8, Math.min(256, Math.ceil(Math.PI / Math.acos(Math.max(-1, 1 - tolerance / halfWidth)))));
|
|
1448
|
+
const circle = (cx, cy) => {
|
|
1449
|
+
const polygon = [];
|
|
1450
|
+
for (let i = 0; i < circleSegments; i++) {
|
|
1451
|
+
const a = i / circleSegments * 2 * Math.PI;
|
|
1452
|
+
polygon.push(cx + Math.cos(a) * halfWidth, cy + Math.sin(a) * halfWidth);
|
|
1453
|
+
}
|
|
1454
|
+
add(polygon);
|
|
1455
|
+
};
|
|
1456
|
+
for (const { points: raw, closed } of subpaths) {
|
|
1457
|
+
const points = [raw[0], raw[1]];
|
|
1458
|
+
for (let i = 2; i < raw.length; i += 2)
|
|
1459
|
+
if (Math.abs(raw[i] - points[points.length - 2]) > 1e-9 || Math.abs(raw[i + 1] - points[points.length - 1]) > 1e-9)
|
|
1460
|
+
points.push(raw[i], raw[i + 1]);
|
|
1461
|
+
if (closed && points.length > 4 && Math.abs(points[0] - points[points.length - 2]) < 1e-9 && Math.abs(points[1] - points[points.length - 1]) < 1e-9)
|
|
1462
|
+
points.length -= 2;
|
|
1463
|
+
const n = points.length >> 1;
|
|
1464
|
+
if (n === 1) {
|
|
1465
|
+
const [x, y] = points;
|
|
1466
|
+
if (cap === 'round')
|
|
1467
|
+
circle(x, y);
|
|
1468
|
+
else if (cap === 'square')
|
|
1469
|
+
add([x - halfWidth, y - halfWidth, x + halfWidth, y - halfWidth, x + halfWidth, y + halfWidth, x - halfWidth, y + halfWidth]);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
const segmentCount = closed ? n : n - 1;
|
|
1473
|
+
const directions = [];
|
|
1474
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
1475
|
+
const ax = points[i * 2], ay = points[i * 2 + 1];
|
|
1476
|
+
const j = (i + 1) % n;
|
|
1477
|
+
const dx = points[j * 2] - ax, dy = points[j * 2 + 1] - ay;
|
|
1478
|
+
const length = Math.hypot(dx, dy);
|
|
1479
|
+
directions.push([dx / length, dy / length]);
|
|
1480
|
+
const nx = -dy / length * halfWidth, ny = dx / length * halfWidth;
|
|
1481
|
+
const bx = points[j * 2], by = points[j * 2 + 1];
|
|
1482
|
+
add([ax + nx, ay + ny, bx + nx, by + ny, bx - nx, by - ny, ax - nx, ay - ny]);
|
|
1483
|
+
}
|
|
1484
|
+
for (let i = closed ? 0 : 1; i < (closed ? segmentCount : n - 1); i++) {
|
|
1485
|
+
const prev = directions[(i - 1 + segmentCount) % segmentCount], next = directions[i % segmentCount];
|
|
1486
|
+
const vx = points[i * 2], vy = points[i * 2 + 1];
|
|
1487
|
+
const cross = prev[0] * next[1] - prev[1] * next[0];
|
|
1488
|
+
const dot = prev[0] * next[0] + prev[1] * next[1];
|
|
1489
|
+
if (Math.abs(cross) < 1e-9 && dot > 0)
|
|
1490
|
+
continue;
|
|
1491
|
+
if (join === 'round') {
|
|
1492
|
+
circle(vx, vy);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
const side = cross > 0 ? -1 : 1;
|
|
1496
|
+
const ax = vx + side * -prev[1] * halfWidth, ay = vy + side * prev[0] * halfWidth;
|
|
1497
|
+
const bx = vx + side * -next[1] * halfWidth, by = vy + side * next[0] * halfWidth;
|
|
1498
|
+
const theta = Math.acos(Math.max(-1, Math.min(1, dot)));
|
|
1499
|
+
const ratio = 1 / Math.sin((Math.PI - theta) / 2);
|
|
1500
|
+
if ((join === 'miter' || join === 'miter-clip' || join === 'arcs') && ratio <= miterLimit && Math.abs(cross) > 1e-9) {
|
|
1501
|
+
const t = ((bx - ax) * next[1] - (by - ay) * next[0]) / (prev[0] * next[1] - prev[1] * next[0]);
|
|
1502
|
+
add([vx, vy, ax, ay, ax + prev[0] * t, ay + prev[1] * t, bx, by]);
|
|
1503
|
+
}
|
|
1504
|
+
else
|
|
1505
|
+
add([vx, vy, ax, ay, bx, by]);
|
|
1506
|
+
}
|
|
1507
|
+
if (!closed) {
|
|
1508
|
+
for (const [index, direction, sign] of [[0, directions[0], -1], [n - 1, directions[segmentCount - 1], 1]]) {
|
|
1509
|
+
const x = points[index * 2], y = points[index * 2 + 1];
|
|
1510
|
+
if (cap === 'round')
|
|
1511
|
+
circle(x, y);
|
|
1512
|
+
else if (cap === 'square') {
|
|
1513
|
+
const dx = direction[0] * halfWidth * sign, dy = direction[1] * halfWidth * sign;
|
|
1514
|
+
const nx = -direction[1] * halfWidth, ny = direction[0] * halfWidth;
|
|
1515
|
+
add([x + nx, y + ny, x + nx + dx, y + ny + dy, x - nx + dx, y - ny + dy, x - nx, y - ny]);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return polygons;
|
|
1521
|
+
}
|
|
1522
|
+
function multiply(a, b) {
|
|
1523
|
+
return [
|
|
1524
|
+
a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1],
|
|
1525
|
+
a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3],
|
|
1526
|
+
a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]
|
|
1527
|
+
];
|
|
1528
|
+
}
|
|
1529
|
+
function invert(m) {
|
|
1530
|
+
const det = m[0] * m[3] - m[1] * m[2];
|
|
1531
|
+
if (!det || !isFinite(det))
|
|
1532
|
+
return null;
|
|
1533
|
+
return [m[3] / det, -m[1] / det, -m[2] / det, m[0] / det, (m[2] * m[5] - m[3] * m[4]) / det, (m[1] * m[4] - m[0] * m[5]) / det];
|
|
1534
|
+
}
|
|
1535
|
+
function parseTransform(value) {
|
|
1536
|
+
let matrix = [1, 0, 0, 1, 0, 0];
|
|
1537
|
+
const functions = /(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)/g;
|
|
1538
|
+
for (const [, name, args] of value.matchAll(functions)) {
|
|
1539
|
+
const a = parseNumberList(args);
|
|
1540
|
+
let m = [1, 0, 0, 1, 0, 0];
|
|
1541
|
+
switch (name) {
|
|
1542
|
+
case 'matrix':
|
|
1543
|
+
if (a.length !== 6)
|
|
1544
|
+
return [1, 0, 0, 1, 0, 0];
|
|
1545
|
+
m = a;
|
|
1546
|
+
break;
|
|
1547
|
+
case 'translate':
|
|
1548
|
+
m = [1, 0, 0, 1, a[0] || 0, a[1] || 0];
|
|
1549
|
+
break;
|
|
1550
|
+
case 'scale':
|
|
1551
|
+
m = [a[0], 0, 0, a.length > 1 ? a[1] : a[0], 0, 0];
|
|
1552
|
+
break;
|
|
1553
|
+
case 'rotate': {
|
|
1554
|
+
const r = (a[0] || 0) * Math.PI / 180;
|
|
1555
|
+
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1556
|
+
m = [cos, sin, -sin, cos, 0, 0];
|
|
1557
|
+
if (a.length === 3)
|
|
1558
|
+
m = multiply(multiply([1, 0, 0, 1, a[1], a[2]], m), [1, 0, 0, 1, -a[1], -a[2]]);
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
case 'skewX':
|
|
1562
|
+
m = [1, 0, Math.tan((a[0] || 0) * Math.PI / 180), 1, 0, 0];
|
|
1563
|
+
break;
|
|
1564
|
+
case 'skewY':
|
|
1565
|
+
m = [1, Math.tan((a[0] || 0) * Math.PI / 180), 0, 1, 0, 0];
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
if (m.some(v => !isFinite(v)))
|
|
1569
|
+
return [1, 0, 0, 1, 0, 0];
|
|
1570
|
+
matrix = multiply(matrix, m);
|
|
1571
|
+
}
|
|
1572
|
+
return matrix;
|
|
1573
|
+
}
|
|
1574
|
+
function parseViewBox(value) {
|
|
1575
|
+
if (!value)
|
|
1576
|
+
return null;
|
|
1577
|
+
const v = parseNumberList(value);
|
|
1578
|
+
return v.length === 4 && v[2] > 0 && v[3] > 0 ? v : null;
|
|
1579
|
+
}
|
|
1580
|
+
function viewBoxTransform([vx, vy, vw, vh], width, height, preserve = 'xMidYMid meet') {
|
|
1581
|
+
const parts = preserve.trim().split(/\s+/);
|
|
1582
|
+
const align = parts[0] === 'defer' ? parts[1] : parts[0];
|
|
1583
|
+
const slice = parts[parts.length - 1] === 'slice';
|
|
1584
|
+
let sx = width / vw, sy = height / vh;
|
|
1585
|
+
if (align === 'none')
|
|
1586
|
+
return [sx, 0, 0, sy, -vx * sx, -vy * sy];
|
|
1587
|
+
sx = sy = slice ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
1588
|
+
let tx = -vx * sx, ty = -vy * sy;
|
|
1589
|
+
const extraX = width - vw * sx, extraY = height - vh * sy;
|
|
1590
|
+
if (/xMid/.test(align))
|
|
1591
|
+
tx += extraX / 2;
|
|
1592
|
+
else if (/xMax/.test(align))
|
|
1593
|
+
tx += extraX;
|
|
1594
|
+
if (/YMid/.test(align))
|
|
1595
|
+
ty += extraY / 2;
|
|
1596
|
+
else if (/YMax/.test(align))
|
|
1597
|
+
ty += extraY;
|
|
1598
|
+
if (!/^x(Min|Mid|Max)Y(Min|Mid|Max)$/.test(align)) {
|
|
1599
|
+
tx = -vx * sx + extraX / 2;
|
|
1600
|
+
ty = -vy * sy + extraY / 2;
|
|
1601
|
+
}
|
|
1602
|
+
return [sx, 0, 0, sy, tx, ty];
|
|
1603
|
+
}
|
|
1604
|
+
const UNITS = { px: 1, pt: 4 / 3, pc: 16, mm: 96 / 25.4, cm: 96 / 2.54, in: 96 };
|
|
1605
|
+
function parseLength(value, viewport, fontSize = 16, axis = 'x') {
|
|
1606
|
+
if (value === undefined || value === null)
|
|
1607
|
+
return 0;
|
|
1608
|
+
const match = /^\s*([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\s*(px|pt|pc|mm|cm|in|em|ex|%)?\s*$/.exec(String(value));
|
|
1609
|
+
if (!match)
|
|
1610
|
+
return 0;
|
|
1611
|
+
const number = parseFloat(match[1]);
|
|
1612
|
+
const unit = match[2];
|
|
1613
|
+
if (!unit)
|
|
1614
|
+
return number;
|
|
1615
|
+
if (unit === 'em')
|
|
1616
|
+
return number * fontSize;
|
|
1617
|
+
if (unit === 'ex')
|
|
1618
|
+
return number * fontSize / 2;
|
|
1619
|
+
if (unit === '%') {
|
|
1620
|
+
if (!viewport)
|
|
1621
|
+
return 0;
|
|
1622
|
+
const reference = axis === 'x' ? viewport.width : axis === 'y' ? viewport.height
|
|
1623
|
+
: Math.hypot(viewport.width, viewport.height) / Math.SQRT2;
|
|
1624
|
+
return number / 100 * reference;
|
|
1625
|
+
}
|
|
1626
|
+
return number * UNITS[unit];
|
|
1627
|
+
}
|
|
1628
|
+
function parseNumber(value, fallback) {
|
|
1629
|
+
const v = parseStrictNumber(value);
|
|
1630
|
+
return v === null ? fallback : v;
|
|
1631
|
+
}
|
|
1632
|
+
function parseStrictNumber(value) {
|
|
1633
|
+
if (value === undefined || value === null)
|
|
1634
|
+
return null;
|
|
1635
|
+
const match = /^\s*([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\s*(%?)\s*$/.exec(String(value));
|
|
1636
|
+
if (!match)
|
|
1637
|
+
return null;
|
|
1638
|
+
return match[2] ? parseFloat(match[1]) / 100 : parseFloat(match[1]);
|
|
1639
|
+
}
|
|
1640
|
+
function parseNumberList(value) {
|
|
1641
|
+
return [...String(value).matchAll(/[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g)].map(m => parseFloat(m[0]));
|
|
1642
|
+
}
|
|
1643
|
+
function parseDashArray(value, viewport, fontSize) {
|
|
1644
|
+
if (!value || value === 'none')
|
|
1645
|
+
return null;
|
|
1646
|
+
let dashes = value.split(/[\s,]+/).filter(Boolean).map(v => parseLength(v, viewport, fontSize, 'diagonal'));
|
|
1647
|
+
if (!dashes.length || dashes.some(d => d < 0 || isNaN(d)))
|
|
1648
|
+
return null;
|
|
1649
|
+
if (dashes.length % 2)
|
|
1650
|
+
dashes = [...dashes, ...dashes];
|
|
1651
|
+
return dashes.reduce((a, b) => a + b, 0) > 0 ? dashes : null;
|
|
1652
|
+
}
|
|
1653
|
+
function clamp01(v) {
|
|
1654
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
1655
|
+
}
|
|
1656
|
+
function parseColor(value) {
|
|
1657
|
+
if (!value)
|
|
1658
|
+
return null;
|
|
1659
|
+
value = value.trim().toLowerCase();
|
|
1660
|
+
if (value === 'transparent')
|
|
1661
|
+
return [0, 0, 0, 0];
|
|
1662
|
+
let match = /^#([0-9a-f]{3,8})$/.exec(value);
|
|
1663
|
+
if (match) {
|
|
1664
|
+
const hex = match[1];
|
|
1665
|
+
if (hex.length === 3 || hex.length === 4)
|
|
1666
|
+
return [...hex].map(c => parseInt(c + c, 16) / 255).concat(hex.length === 3 ? [1] : []);
|
|
1667
|
+
if (hex.length === 6 || hex.length === 8)
|
|
1668
|
+
return [0, 2, 4, 6].slice(0, hex.length / 2).map(i => parseInt(hex.slice(i, i + 2), 16) / 255).concat(hex.length === 6 ? [1] : []);
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
match = /^(rgba?|hsla?)\(\s*([^)]*)\)$/.exec(value);
|
|
1672
|
+
if (match) {
|
|
1673
|
+
const parts = match[2].split(/\s*[,/]\s*|\s+/).filter(Boolean);
|
|
1674
|
+
if (parts.length < 3)
|
|
1675
|
+
return null;
|
|
1676
|
+
const alpha = parts.length > 3 ? clamp01(parseNumber(parts[3], 1)) : 1;
|
|
1677
|
+
if (match[1].startsWith('rgb')) {
|
|
1678
|
+
const channel = (p) => clamp01(/%$/.test(p) ? parseFloat(p) / 100 : parseFloat(p) / 255);
|
|
1679
|
+
return [channel(parts[0]), channel(parts[1]), channel(parts[2]), alpha];
|
|
1680
|
+
}
|
|
1681
|
+
const h = (((parseFloat(parts[0]) % 360) + 360) % 360) / 360;
|
|
1682
|
+
const s = clamp01(parseFloat(parts[1]) / 100), l = clamp01(parseFloat(parts[2]) / 100);
|
|
1683
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
|
|
1684
|
+
const hue = (t) => {
|
|
1685
|
+
t = (t + 1) % 1;
|
|
1686
|
+
return t < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p;
|
|
1687
|
+
};
|
|
1688
|
+
return [hue(h + 1 / 3), hue(h), hue(h - 1 / 3), alpha];
|
|
1689
|
+
}
|
|
1690
|
+
const named = NAMED_COLORS[value];
|
|
1691
|
+
return named === undefined ? null : [(named >> 16 & 255) / 255, (named >> 8 & 255) / 255, (named & 255) / 255, 1];
|
|
1692
|
+
}
|
|
1693
|
+
const NAMED_COLORS = {
|
|
1694
|
+
aliceblue: 0xf0f8ff, antiquewhite: 0xfaebd7, aqua: 0x00ffff, aquamarine: 0x7fffd4, azure: 0xf0ffff, beige: 0xf5f5dc, bisque: 0xffe4c4,
|
|
1695
|
+
black: 0x000000, blanchedalmond: 0xffebcd, blue: 0x0000ff, blueviolet: 0x8a2be2, brown: 0xa52a2a, burlywood: 0xdeb887, cadetblue: 0x5f9ea0,
|
|
1696
|
+
chartreuse: 0x7fff00, chocolate: 0xd2691e, coral: 0xff7f50, cornflowerblue: 0x6495ed, cornsilk: 0xfff8dc, crimson: 0xdc143c, cyan: 0x00ffff,
|
|
1697
|
+
darkblue: 0x00008b, darkcyan: 0x008b8b, darkgoldenrod: 0xb8860b, darkgray: 0xa9a9a9, darkgreen: 0x006400, darkgrey: 0xa9a9a9, darkkhaki: 0xbdb76b,
|
|
1698
|
+
darkmagenta: 0x8b008b, darkolivegreen: 0x556b2f, darkorange: 0xff8c00, darkorchid: 0x9932cc, darkred: 0x8b0000, darksalmon: 0xe9967a,
|
|
1699
|
+
darkseagreen: 0x8fbc8f, darkslateblue: 0x483d8b, darkslategray: 0x2f4f4f, darkslategrey: 0x2f4f4f, darkturquoise: 0x00ced1, darkviolet: 0x9400d3,
|
|
1700
|
+
deeppink: 0xff1493, deepskyblue: 0x00bfff, dimgray: 0x696969, dimgrey: 0x696969, dodgerblue: 0x1e90ff, firebrick: 0xb22222, floralwhite: 0xfffaf0,
|
|
1701
|
+
forestgreen: 0x228b22, fuchsia: 0xff00ff, gainsboro: 0xdcdcdc, ghostwhite: 0xf8f8ff, gold: 0xffd700, goldenrod: 0xdaa520, gray: 0x808080,
|
|
1702
|
+
grey: 0x808080, green: 0x008000, greenyellow: 0xadff2f, honeydew: 0xf0fff0, hotpink: 0xff69b4, indianred: 0xcd5c5c, indigo: 0x4b0082,
|
|
1703
|
+
ivory: 0xfffff0, khaki: 0xf0e68c, lavender: 0xe6e6fa, lavenderblush: 0xfff0f5, lawngreen: 0x7cfc00, lemonchiffon: 0xfffacd, lightblue: 0xadd8e6,
|
|
1704
|
+
lightcoral: 0xf08080, lightcyan: 0xe0ffff, lightgoldenrodyellow: 0xfafad2, lightgray: 0xd3d3d3, lightgreen: 0x90ee90, lightgrey: 0xd3d3d3,
|
|
1705
|
+
lightpink: 0xffb6c1, lightsalmon: 0xffa07a, lightseagreen: 0x20b2aa, lightskyblue: 0x87cefa, lightslategray: 0x778899, lightslategrey: 0x778899,
|
|
1706
|
+
lightsteelblue: 0xb0c4de, lightyellow: 0xffffe0, lime: 0x00ff00, limegreen: 0x32cd32, linen: 0xfaf0e6, magenta: 0xff00ff, maroon: 0x800000,
|
|
1707
|
+
mediumaquamarine: 0x66cdaa, mediumblue: 0x0000cd, mediumorchid: 0xba55d3, mediumpurple: 0x9370db, mediumseagreen: 0x3cb371,
|
|
1708
|
+
mediumslateblue: 0x7b68ee, mediumspringgreen: 0x00fa9a, mediumturquoise: 0x48d1cc, mediumvioletred: 0xc71585, midnightblue: 0x191970,
|
|
1709
|
+
mintcream: 0xf5fffa, mistyrose: 0xffe4e1, moccasin: 0xffe4b5, navajowhite: 0xffdead, navy: 0x000080, oldlace: 0xfdf5e6, olive: 0x808000,
|
|
1710
|
+
olivedrab: 0x6b8e23, orange: 0xffa500, orangered: 0xff4500, orchid: 0xda70d6, palegoldenrod: 0xeee8aa, palegreen: 0x98fb98,
|
|
1711
|
+
paleturquoise: 0xafeeee, palevioletred: 0xdb7093, papayawhip: 0xffefd5, peachpuff: 0xffdab9, peru: 0xcd853f, pink: 0xffc0cb, plum: 0xdda0dd,
|
|
1712
|
+
powderblue: 0xb0e0e6, purple: 0x800080, rebeccapurple: 0x663399, red: 0xff0000, rosybrown: 0xbc8f8f, royalblue: 0x4169e1, saddlebrown: 0x8b4513,
|
|
1713
|
+
salmon: 0xfa8072, sandybrown: 0xf4a460, seagreen: 0x2e8b57, seashell: 0xfff5ee, sienna: 0xa0522d, silver: 0xc0c0c0, skyblue: 0x87ceeb,
|
|
1714
|
+
slateblue: 0x6a5acd, slategray: 0x708090, slategrey: 0x708090, snow: 0xfffafa, springgreen: 0x00ff7f, steelblue: 0x4682b4, tan: 0xd2b48c,
|
|
1715
|
+
teal: 0x008080, thistle: 0xd8bfd8, tomato: 0xff6347, turquoise: 0x40e0d0, violet: 0xee82ee, wheat: 0xf5deb3, white: 0xffffff,
|
|
1716
|
+
whitesmoke: 0xf5f5f5, yellow: 0xffff00, yellowgreen: 0x9acd32
|
|
1717
|
+
};
|