@scratch/scratch-svg-renderer 14.0.0-accessibility-improvements → 14.0.0-accessibility-improvements.2

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.
@@ -1,1912 +1,2 @@
1
- (function webpackUniversalModuleDefinition(root, factory) {
2
- if(typeof exports === 'object' && typeof module === 'object')
3
- module.exports = factory();
4
- else if(typeof define === 'function' && define.amd)
5
- define([], factory);
6
- else if(typeof exports === 'object')
7
- exports["ScratchSVGRenderer"] = factory();
8
- else
9
- root["ScratchSVGRenderer"] = factory();
10
- })(global, () => {
11
- return /******/ (() => { // webpackBootstrap
12
- /******/ var __webpack_modules__ = ({
13
-
14
- /***/ "./src/bitmap-adapter.js"
15
- /*!*******************************!*\
16
- !*** ./src/bitmap-adapter.js ***!
17
- \*******************************/
18
- (module, __unused_webpack_exports, __webpack_require__) {
19
-
20
- const base64js = __webpack_require__(/*! base64-js */ "base64-js");
21
-
22
- /**
23
- * Adapts Scratch 2.0 bitmaps for use in scratch 3.0
24
- */
25
- class BitmapAdapter {
26
- /**
27
- * @param {?Function} makeImage HTML image constructor. Tests can provide this.
28
- * @param {?Function} makeCanvas HTML canvas constructor. Tests can provide this.
29
- */
30
- constructor(makeImage, makeCanvas) {
31
- this._makeImage = makeImage ? makeImage : () => new Image();
32
- this._makeCanvas = makeCanvas ? makeCanvas : () => document.createElement('canvas');
33
- }
34
-
35
- /**
36
- * Return a canvas with the resized version of the given image, done using nearest-neighbor interpolation
37
- * @param {CanvasImageSource} image The image to resize
38
- * @param {int} newWidth The desired post-resize width of the image
39
- * @param {int} newHeight The desired post-resize height of the image
40
- * @returns {HTMLCanvasElement} A canvas with the resized image drawn on it.
41
- */
42
- resize(image, newWidth, newHeight) {
43
- // We want to always resize using nearest-neighbor interpolation. However, canvas implementations are free to
44
- // use linear interpolation (or other "smooth" interpolation methods) when downscaling:
45
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1360415
46
- // It seems we can get around this by resizing in two steps: first width, then height. This will always result
47
- // in nearest-neighbor interpolation, even when downscaling.
48
- const stretchWidthCanvas = this._makeCanvas();
49
- stretchWidthCanvas.width = newWidth;
50
- stretchWidthCanvas.height = image.height;
51
- let context = stretchWidthCanvas.getContext('2d');
52
- context.imageSmoothingEnabled = false;
53
- context.drawImage(image, 0, 0, stretchWidthCanvas.width, stretchWidthCanvas.height);
54
- const stretchHeightCanvas = this._makeCanvas();
55
- stretchHeightCanvas.width = newWidth;
56
- stretchHeightCanvas.height = newHeight;
57
- context = stretchHeightCanvas.getContext('2d');
58
- context.imageSmoothingEnabled = false;
59
- context.drawImage(stretchWidthCanvas, 0, 0, stretchHeightCanvas.width, stretchHeightCanvas.height);
60
- return stretchHeightCanvas;
61
- }
62
-
63
- /**
64
- * Scratch 2.0 had resolution 1 and 2 bitmaps. All bitmaps in Scratch 3.0 are equivalent
65
- * to resolution 2 bitmaps. Therefore, converting a resolution 1 bitmap means doubling
66
- * it in width and height.
67
- * @param {!string} dataURI Base 64 encoded image data of the bitmap
68
- * @param {!Function} callback Node-style callback that returns updated dataURI if conversion succeeded
69
- */
70
- convertResolution1Bitmap(dataURI, callback) {
71
- const image = this._makeImage();
72
- image.src = dataURI;
73
- image.onload = () => {
74
- callback(null, this.resize(image, image.width * 2, image.height * 2).toDataURL());
75
- };
76
- image.onerror = () => {
77
- callback('Image load failed');
78
- };
79
- }
80
-
81
- /**
82
- * Given width/height of an uploaded item, return width/height the image will be resized
83
- * to in Scratch 3.0
84
- * @param {!number} oldWidth original width
85
- * @param {!number} oldHeight original height
86
- * @returns {object} Array of new width, new height
87
- */
88
- getResizedWidthHeight(oldWidth, oldHeight) {
89
- const STAGE_WIDTH = 480;
90
- const STAGE_HEIGHT = 360;
91
- const STAGE_RATIO = STAGE_WIDTH / STAGE_HEIGHT;
92
-
93
- // If both dimensions are smaller than or equal to corresponding stage dimension,
94
- // double both dimensions
95
- if (oldWidth <= STAGE_WIDTH && oldHeight <= STAGE_HEIGHT) {
96
- return {
97
- width: oldWidth * 2,
98
- height: oldHeight * 2
99
- };
100
- }
101
-
102
- // If neither dimension is larger than 2x corresponding stage dimension,
103
- // this is an in-between image, return it as is
104
- if (oldWidth <= STAGE_WIDTH * 2 && oldHeight <= STAGE_HEIGHT * 2) {
105
- return {
106
- width: oldWidth,
107
- height: oldHeight
108
- };
109
- }
110
- const imageRatio = oldWidth / oldHeight;
111
- // Otherwise, figure out how to resize
112
- if (imageRatio >= STAGE_RATIO) {
113
- // Wide Image
114
- return {
115
- width: STAGE_WIDTH * 2,
116
- height: STAGE_WIDTH * 2 / imageRatio
117
- };
118
- }
119
- // In this case we have either:
120
- // - A wide image, but not with as big a ratio between width and height,
121
- // making it so that fitting the width to double stage size would leave
122
- // the height too big to fit in double the stage height
123
- // - A square image that's still larger than the double at least
124
- // one of the stage dimensions, so pick the smaller of the two dimensions (to fit)
125
- // - A tall image
126
- // In any of these cases, resize the image to fit the height to double the stage height
127
- return {
128
- width: STAGE_HEIGHT * 2 * imageRatio,
129
- height: STAGE_HEIGHT * 2
130
- };
131
- }
132
-
133
- /**
134
- * Given bitmap data, resize as necessary.
135
- * @param {ArrayBuffer | string} fileData Base 64 encoded image data of the bitmap
136
- * @param {string} fileType The MIME type of this file
137
- * @returns {Promise} Resolves to resized image data Uint8Array
138
- */
139
- importBitmap(fileData, fileType) {
140
- let dataURI = fileData;
141
- if (fileData instanceof ArrayBuffer) {
142
- dataURI = this.convertBinaryToDataURI(fileData, fileType);
143
- }
144
- return new Promise((resolve, reject) => {
145
- const image = this._makeImage();
146
- image.src = dataURI;
147
- image.onload = () => {
148
- const newSize = this.getResizedWidthHeight(image.width, image.height);
149
- if (newSize.width === image.width && newSize.height === image.height) {
150
- // No change
151
- resolve(this.convertDataURIToBinary(dataURI));
152
- } else {
153
- const resizedDataURI = this.resize(image, newSize.width, newSize.height).toDataURL();
154
- resolve(this.convertDataURIToBinary(resizedDataURI));
155
- }
156
- };
157
- image.onerror = () => {
158
- // TODO: reject with an Error (breaking API change!)
159
- // eslint-disable-next-line prefer-promise-reject-errors
160
- reject('Image load failed');
161
- };
162
- });
163
- }
164
-
165
- // TODO consolidate with scratch-vm/src/util/base64-util.js
166
- // From https://gist.github.com/borismus/1032746
167
- convertDataURIToBinary(dataURI) {
168
- const BASE64_MARKER = ';base64,';
169
- const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
170
- const base64 = dataURI.substring(base64Index);
171
- const raw = window.atob(base64);
172
- const rawLength = raw.length;
173
- const array = new Uint8Array(new ArrayBuffer(rawLength));
174
- for (let i = 0; i < rawLength; i++) {
175
- array[i] = raw.charCodeAt(i);
176
- }
177
- return array;
178
- }
179
- convertBinaryToDataURI(arrayBuffer, contentType) {
180
- return "data:".concat(contentType, ";base64,").concat(base64js.fromByteArray(new Uint8Array(arrayBuffer)));
181
- }
182
- }
183
- module.exports = BitmapAdapter;
184
-
185
- /***/ },
186
-
187
- /***/ "./src/fixup-svg-string.js"
188
- /*!*********************************!*\
189
- !*** ./src/fixup-svg-string.js ***!
190
- \*********************************/
191
- (module) {
192
-
193
- /**
194
- * Fixup svg string prior to parsing.
195
- * @param {!string} svgString String of the svg to fix.
196
- * @returns {!string} fixed svg that should be parseable.
197
- */
198
- module.exports = function (svgString) {
199
- // Add root svg namespace if it does not exist.
200
- const svgAttrs = svgString.match(/<svg [^>]*>/);
201
- if (svgAttrs && svgAttrs[0].indexOf('xmlns=') === -1) {
202
- svgString = svgString.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
203
- }
204
-
205
- // There are some SVGs from Illustrator that use undeclared entities.
206
- // Just replace those entities with fake namespace references to prevent
207
- // DOMParser from crashing
208
- if (svgAttrs && svgAttrs[0].indexOf('&ns_') !== -1 && svgString.indexOf('<!DOCTYPE') === -1) {
209
- svgString = svgString.replace(svgAttrs[0], svgAttrs[0].replace(/&ns_[^;]+;/g, 'http://ns.adobe.com/Extensibility/1.0/'));
210
- }
211
-
212
- // Some SVGs exported from Photoshop have been found to have an invalid mime type
213
- // Chrome and Safari won't render these SVGs, so we correct it here
214
- if (svgString.includes('data:img/png')) {
215
- svgString = svgString.replace(
216
- // capture entire image tag with xlink:href=and the quote - dont capture data: bit
217
- /(<image[^>]+?xlink:href=["'])data:img\/png/g,
218
- // use the captured <image ..... xlink:href=" then append the right data uri mime type
219
- ($0, $1) => "".concat($1, "data:image/png"));
220
- }
221
-
222
- // Some SVGs from Inkscape attempt to bind a prefix to a reserved namespace name.
223
- // This will cause SVG parsing to fail, so replace these with a dummy namespace name.
224
- // This namespace name is only valid for "xml", and if we bind "xmlns:xml" to the dummy namespace,
225
- // parsing will fail yet again, so exclude "xmlns:xml" declarations.
226
- const xmlnsRegex = /(<[^>]+?xmlns:(?!xml=)[^ ]+=)"http:\/\/www.w3.org\/XML\/1998\/namespace"/g;
227
- if (svgString.match(xmlnsRegex) !== null) {
228
- svgString = svgString.replace(
229
- // capture the entire attribute
230
- xmlnsRegex,
231
- // use the captured attribute name; replace only the URL
232
- ($0, $1) => "".concat($1, "\"http://dummy.namespace\""));
233
- }
234
-
235
- // Strip `svg:` prefix (sometimes added by Inkscape) from all tags. They interfere with DOMPurify (prefixed tag
236
- // names are not recognized) and the paint editor.
237
- // This matches opening and closing tags--the capture group captures the slash if it exists, and it is reinserted
238
- // in the replacement text.
239
- svgString = svgString.replace(/<(\/?)\s*svg:/g, '<$1');
240
-
241
- // The <metadata> element is not needed for rendering and sometimes contains
242
- // unparseable garbage from Illustrator :( Empty out the contents.
243
- // Note: [\s\S] matches everything including newlines, which .* does not
244
- svgString = svgString.replace(/<metadata>[\s\S]*<\/metadata>/, '<metadata></metadata>');
245
-
246
- // Empty script tags and javascript executing
247
- svgString = svgString.replace(/<script[\s\S]*>[\s\S]*<\/script>/, '<script></script>');
248
- return svgString;
249
- };
250
-
251
- /***/ },
252
-
253
- /***/ "./src/font-converter.js"
254
- /*!*******************************!*\
255
- !*** ./src/font-converter.js ***!
256
- \*******************************/
257
- (module) {
258
-
259
- /**
260
- * @fileOverview Convert 2.0 fonts to 3.0 fonts.
261
- */
262
-
263
- /**
264
- * Given an SVG, replace Scratch 2.0 fonts with new 3.0 fonts. Add defaults where there are none.
265
- * @param {SVGElement} svgTag The SVG dom object
266
- * @returns {void}
267
- */
268
- const convertFonts = function convertFonts(svgTag) {
269
- // Collect all text elements into a list.
270
- const textElements = [];
271
- const collectText = domElement => {
272
- if (domElement.localName === 'text') {
273
- textElements.push(domElement);
274
- }
275
- for (let i = 0; i < domElement.childNodes.length; i++) {
276
- collectText(domElement.childNodes[i]);
277
- }
278
- };
279
- collectText(svgTag);
280
- // If there's an old font-family, switch to the new one.
281
- for (const textElement of textElements) {
282
- // If there's no font-family provided, provide one.
283
- if (!textElement.getAttribute('font-family') || textElement.getAttribute('font-family') === 'Helvetica') {
284
- textElement.setAttribute('font-family', 'Sans Serif');
285
- } else if (textElement.getAttribute('font-family') === 'Mystery') {
286
- textElement.setAttribute('font-family', 'Curly');
287
- } else if (textElement.getAttribute('font-family') === 'Gloria') {
288
- textElement.setAttribute('font-family', 'Handwriting');
289
- } else if (textElement.getAttribute('font-family') === 'Donegal') {
290
- textElement.setAttribute('font-family', 'Serif');
291
- }
292
- }
293
- };
294
- module.exports = convertFonts;
295
-
296
- /***/ },
297
-
298
- /***/ "./src/font-inliner.js"
299
- /*!*****************************!*\
300
- !*** ./src/font-inliner.js ***!
301
- \*****************************/
302
- (module, __unused_webpack_exports, __webpack_require__) {
303
-
304
- /**
305
- * @fileOverview Import bitmap data into Scratch 3.0, resizing image as necessary.
306
- */
307
- const getFonts = __webpack_require__(/*! scratch-render-fonts */ "scratch-render-fonts");
308
-
309
- /**
310
- * Given SVG data, inline the fonts. This allows them to be rendered correctly when set
311
- * as the source of an HTMLImageElement. Here is a note from tmickel:
312
- * // Inject fonts that are needed.
313
- * // It would be nice if there were another way to get the SVG-in-canvas
314
- * // to render the correct font family, but I couldn't find any other way.
315
- * // Other things I tried:
316
- * // Just injecting the font-family into the document: no effect.
317
- * // External stylesheet linked to by SVG: no effect.
318
- * // Using a <link> or <style>@import</style> to link to font-family
319
- * // injected into the document: no effect.
320
- * @param {string} svgString The string representation of the svg to modify
321
- * @returns {string} The svg with any needed fonts inlined
322
- */
323
- const inlineSvgFonts = function inlineSvgFonts(svgString) {
324
- const FONTS = getFonts();
325
- // Make it clear that this function only operates on strings.
326
- // If we don't explicitly throw this here, the function silently fails.
327
- if (typeof svgString !== 'string') {
328
- throw new Error('SVG to be inlined is not a string');
329
- }
330
-
331
- // Collect fonts that need injection.
332
- const fontsNeeded = new Set();
333
- const fontRegex = /font-family="([^"]*)"/g;
334
- let matches = fontRegex.exec(svgString);
335
- while (matches) {
336
- fontsNeeded.add(matches[1]);
337
- matches = fontRegex.exec(svgString);
338
- }
339
- if (fontsNeeded.size > 0) {
340
- let str = '<defs><style>';
341
- for (const font of fontsNeeded) {
342
- if (Object.prototype.hasOwnProperty.call(FONTS, font)) {
343
- str += "".concat(FONTS[font]);
344
- }
345
- }
346
- str += '</style></defs>';
347
- svgString = svgString.replace(/<svg[^>]*>/, "$&".concat(str));
348
- return svgString;
349
- }
350
- return svgString;
351
- };
352
- module.exports = inlineSvgFonts;
353
-
354
- /***/ },
355
-
356
- /***/ "./src/index.js"
357
- /*!**********************!*\
358
- !*** ./src/index.js ***!
359
- \**********************/
360
- (module, __unused_webpack_exports, __webpack_require__) {
361
-
362
- const SVGRenderer = __webpack_require__(/*! ./svg-renderer */ "./src/svg-renderer.js");
363
- const BitmapAdapter = __webpack_require__(/*! ./bitmap-adapter */ "./src/bitmap-adapter.js");
364
- const inlineSvgFonts = __webpack_require__(/*! ./font-inliner */ "./src/font-inliner.js");
365
- const loadSvgString = __webpack_require__(/*! ./load-svg-string */ "./src/load-svg-string.js");
366
- const sanitizeSvg = __webpack_require__(/*! ./sanitize-svg */ "./src/sanitize-svg.js");
367
- const serializeSvgToString = __webpack_require__(/*! ./serialize-svg-to-string */ "./src/serialize-svg-to-string.js");
368
- const SvgElement = __webpack_require__(/*! ./svg-element */ "./src/svg-element.js");
369
- const convertFonts = __webpack_require__(/*! ./font-converter */ "./src/font-converter.js");
370
- // /**
371
- // * Export for NPM & Node.js
372
- // * @type {RenderWebGL}
373
- // */
374
- module.exports = {
375
- BitmapAdapter: BitmapAdapter,
376
- convertFonts: convertFonts,
377
- inlineSvgFonts: inlineSvgFonts,
378
- loadSvgString: loadSvgString,
379
- sanitizeSvg: sanitizeSvg,
380
- serializeSvgToString: serializeSvgToString,
381
- SvgElement: SvgElement,
382
- SVGRenderer: SVGRenderer
383
- };
384
-
385
- /***/ },
386
-
387
- /***/ "./src/load-svg-string.js"
388
- /*!********************************!*\
389
- !*** ./src/load-svg-string.js ***!
390
- \********************************/
391
- (module, __unused_webpack_exports, __webpack_require__) {
392
-
393
- const SvgElement = __webpack_require__(/*! ./svg-element */ "./src/svg-element.js");
394
- const convertFonts = __webpack_require__(/*! ./font-converter */ "./src/font-converter.js");
395
- const transformStrokeWidths = __webpack_require__(/*! ./transform-applier */ "./src/transform-applier.js");
396
- const {
397
- sanitizeSvgText
398
- } = __webpack_require__(/*! ./sanitize-svg */ "./src/sanitize-svg.js");
399
-
400
- /**
401
- * @param {SVGElement} svgTag the tag to search within
402
- * @param {string} [tagName] svg tag to search for (or collect all elements if not given)
403
- * @returns {Array} a list of elements with the given tagname
404
- */
405
- const collectElements = (svgTag, tagName) => {
406
- const elts = [];
407
- const collectElementsInner = domElement => {
408
- if ((domElement.localName === tagName || typeof tagName === 'undefined') && domElement.getAttribute) {
409
- elts.push(domElement);
410
- }
411
- for (let i = 0; i < domElement.childNodes.length; i++) {
412
- collectElementsInner(domElement.childNodes[i]);
413
- }
414
- };
415
- collectElementsInner(svgTag);
416
- return elts;
417
- };
418
-
419
- /**
420
- * Fix SVGs to comply with SVG spec. Scratch 2 defaults to x2 = 0 when x2 is missing, but
421
- * SVG defaults to x2 = 1 when missing.
422
- * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to
423
- */
424
- const transformGradients = svgTag => {
425
- const linearGradientElements = collectElements(svgTag, 'linearGradient');
426
-
427
- // For each gradient element, supply x2 if necessary.
428
- for (const gradientElement of linearGradientElements) {
429
- if (!gradientElement.getAttribute('x2')) {
430
- gradientElement.setAttribute('x2', '0');
431
- }
432
- }
433
- };
434
-
435
- /**
436
- * Fix SVGs to match appearance in Scratch 2, which used nearest neighbor scaling for bitmaps
437
- * within SVGs.
438
- * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to
439
- */
440
- const transformImages = svgTag => {
441
- const imageElements = collectElements(svgTag, 'image');
442
-
443
- // For each image element, set image rendering to pixelated
444
- const pixelatedImages = 'image-rendering: optimizespeed; image-rendering: pixelated;';
445
- for (const elt of imageElements) {
446
- if (elt.getAttribute('style')) {
447
- elt.setAttribute('style', "".concat(pixelatedImages, " ").concat(elt.getAttribute('style')));
448
- } else {
449
- elt.setAttribute('style', pixelatedImages);
450
- }
451
- }
452
- };
453
-
454
- /**
455
- * Transforms an SVG's text elements for Scratch 2.0 quirks.
456
- * These quirks include:
457
- * 1. `x` and `y` properties are removed/ignored.
458
- * 2. Alignment is set to `text-before-edge`.
459
- * 3. Line-breaks are converted to explicit <tspan> elements.
460
- * 4. Any required fonts are injected.
461
- * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to
462
- */
463
- const transformText = svgTag => {
464
- // Collect all text elements into a list.
465
- const textElements = [];
466
- const collectText = domElement => {
467
- if (domElement.localName === 'text') {
468
- textElements.push(domElement);
469
- }
470
- for (let i = 0; i < domElement.childNodes.length; i++) {
471
- collectText(domElement.childNodes[i]);
472
- }
473
- };
474
- collectText(svgTag);
475
- convertFonts(svgTag);
476
- // For each text element, apply quirks.
477
- for (const textElement of textElements) {
478
- // Remove x and y attributes - they are not used in Scratch.
479
- textElement.removeAttribute('x');
480
- textElement.removeAttribute('y');
481
- // Set text-before-edge alignment:
482
- // Scratch renders all text like this.
483
- textElement.setAttribute('alignment-baseline', 'text-before-edge');
484
- textElement.setAttribute('xml:space', 'preserve');
485
- // If there's no font size provided, provide one.
486
- if (!textElement.getAttribute('font-size')) {
487
- textElement.setAttribute('font-size', '18');
488
- }
489
- let text = textElement.textContent;
490
-
491
- // Fix line breaks in text, which are not natively supported by SVG.
492
- // Only fix if text does not have child tspans.
493
- // @todo this will not work for font sizes with units such as em, percent
494
- // However, text made in scratch 2 should only ever export size 22 font.
495
- const fontSize = parseFloat(textElement.getAttribute('font-size'));
496
- const tx = 2;
497
- let ty = 0;
498
- let spacing = 1.2;
499
- // Try to match the position and spacing of Scratch 2.0's fonts.
500
- // Different fonts seem to use different line spacing.
501
- // Scratch 2 always uses alignment-baseline=text-before-edge
502
- // However, most SVG readers don't support this attribute
503
- // or don't support it alongside use of tspan, so the translations
504
- // here are to make up for that.
505
- if (textElement.getAttribute('font-family') === 'Handwriting') {
506
- spacing = 2;
507
- ty = -11 * fontSize / 22;
508
- } else if (textElement.getAttribute('font-family') === 'Scratch') {
509
- spacing = 0.89;
510
- ty = -3 * fontSize / 22;
511
- } else if (textElement.getAttribute('font-family') === 'Curly') {
512
- spacing = 1.38;
513
- ty = -6 * fontSize / 22;
514
- } else if (textElement.getAttribute('font-family') === 'Marker') {
515
- spacing = 1.45;
516
- ty = -6 * fontSize / 22;
517
- } else if (textElement.getAttribute('font-family') === 'Sans Serif') {
518
- spacing = 1.13;
519
- ty = -3 * fontSize / 22;
520
- } else if (textElement.getAttribute('font-family') === 'Serif') {
521
- spacing = 1.25;
522
- ty = -4 * fontSize / 22;
523
- }
524
- if (textElement.transform.baseVal.numberOfItems === 0) {
525
- const transform = svgTag.createSVGTransform();
526
- textElement.transform.baseVal.appendItem(transform);
527
- }
528
-
529
- // Right multiply matrix by a translation of (tx, ty)
530
- const mtx = textElement.transform.baseVal.getItem(0).matrix;
531
- mtx.e += mtx.a * tx + mtx.c * ty;
532
- mtx.f += mtx.b * tx + mtx.d * ty;
533
- if (text && textElement.childElementCount === 0) {
534
- textElement.textContent = '';
535
- const lines = text.split('\n');
536
- text = '';
537
- for (const line of lines) {
538
- const tspanNode = SvgElement.create('tspan');
539
- tspanNode.setAttribute('x', '0');
540
- tspanNode.setAttribute('style', 'white-space: pre');
541
- tspanNode.setAttribute('dy', "".concat(spacing, "em"));
542
- tspanNode.textContent = line ? line : ' ';
543
- textElement.appendChild(tspanNode);
544
- }
545
- }
546
- }
547
- };
548
-
549
- /**
550
- * Find the largest stroke width in the svg. If a shape has no
551
- * `stroke` property, it has a stroke-width of 0. If it has a `stroke`,
552
- * it is by default a stroke-width of 1.
553
- * This is used to enlarge the computed bounding box, which doesn't take
554
- * stroke width into account.
555
- * @param {SVGSVGElement} rootNode The root SVG node to traverse.
556
- * @returns {number} The largest stroke width in the SVG.
557
- */
558
- const findLargestStrokeWidth = rootNode => {
559
- let largestStrokeWidth = 0;
560
- const collectStrokeWidths = domElement => {
561
- if (domElement.getAttribute) {
562
- if (domElement.getAttribute('stroke')) {
563
- largestStrokeWidth = Math.max(largestStrokeWidth, 1);
564
- }
565
- if (domElement.getAttribute('stroke-width')) {
566
- largestStrokeWidth = Math.max(largestStrokeWidth, Number(domElement.getAttribute('stroke-width')) || 0);
567
- }
568
- }
569
- for (let i = 0; i < domElement.childNodes.length; i++) {
570
- collectStrokeWidths(domElement.childNodes[i]);
571
- }
572
- };
573
- collectStrokeWidths(rootNode);
574
- return largestStrokeWidth;
575
- };
576
-
577
- /**
578
- * Transform the measurements of the SVG.
579
- * In Scratch 2.0, SVGs are drawn without respect to the width,
580
- * height, and viewBox attribute on the tag. The exporter
581
- * does output these properties - but they appear to be incorrect often.
582
- * To address the incorrect measurements, we append the DOM to the
583
- * document, and then use SVG's native `getBBox` to find the real
584
- * drawn dimensions. This ensures things drawn in negative dimensions,
585
- * outside the given viewBox, etc., are all eventually drawn to the canvas.
586
- * I tried to do this several other ways: stripping the width/height/viewBox
587
- * attributes and then drawing (Firefox won't draw anything),
588
- * or inflating them and then measuring a canvas. But this seems to be
589
- * a natural and performant way.
590
- * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to
591
- */
592
- const transformMeasurements = svgTag => {
593
- // Append the SVG dom to the document.
594
- // This allows us to use `getBBox` on the page,
595
- // which returns the full bounding-box of all drawn SVG
596
- // elements, similar to how Scratch 2.0 did measurement.
597
- const svgSpot = document.createElement('span');
598
- let bbox;
599
- try {
600
- // Insert sanitized value.
601
- svgSpot.innerHTML = svgTag.outerHTML;
602
- document.body.appendChild(svgSpot);
603
- // Take the bounding box. We have to get elements via svgSpot
604
- // because we added it via innerHTML.
605
- bbox = svgSpot.children[0].getBBox();
606
- } finally {
607
- // Always destroy the element, even if, for example, getBBox throws.
608
- document.body.removeChild(svgSpot);
609
- }
610
-
611
- // Enlarge the bbox from the largest found stroke width
612
- // This may have false-positives, but at least the bbox will always
613
- // contain the full graphic including strokes.
614
- // If the width or height is zero however, don't enlarge since
615
- // they won't have a stroke width that needs to be enlarged.
616
- let halfStrokeWidth;
617
- if (bbox.width === 0 || bbox.height === 0) {
618
- halfStrokeWidth = 0;
619
- } else {
620
- halfStrokeWidth = findLargestStrokeWidth(svgTag) / 2;
621
- }
622
- const width = bbox.width + halfStrokeWidth * 2;
623
- const height = bbox.height + halfStrokeWidth * 2;
624
- const x = bbox.x - halfStrokeWidth;
625
- const y = bbox.y - halfStrokeWidth;
626
-
627
- // Set the correct measurements on the SVG tag
628
- svgTag.setAttribute('width', width);
629
- svgTag.setAttribute('height', height);
630
- svgTag.setAttribute('viewBox', "".concat(x, " ").concat(y, " ").concat(width, " ").concat(height));
631
- };
632
-
633
- /**
634
- * Find all instances of a URL-referenced `stroke` in the svg. In 2.0, all gradient strokes
635
- * have a round `stroke-linejoin` and `stroke-linecap`... for some reason.
636
- * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to
637
- */
638
- const setGradientStrokeRoundedness = svgTag => {
639
- const elements = collectElements(svgTag);
640
- for (const elt of elements) {
641
- if (!elt.style) continue;
642
- const stroke = elt.style.stroke || elt.getAttribute('stroke');
643
- if (stroke && stroke.match(/^url\(#.*\)$/)) {
644
- elt.style['stroke-linejoin'] = 'round';
645
- elt.style['stroke-linecap'] = 'round';
646
- }
647
- }
648
- };
649
-
650
- /**
651
- * In-place, convert passed SVG to something consistent that will be rendered the way we want them to be.
652
- * @param {SVGSvgElement} svgTag root SVG node to operate upon
653
- * @param {boolean} [fromVersion2] True if we should perform conversion from version 2 to version 3 svg.
654
- */
655
- const normalizeSvg = (svgTag, fromVersion2) => {
656
- if (fromVersion2) {
657
- // Fix gradients. Scratch 2 exports no x2 when x2 = 0, but
658
- // SVG default is that x2 is 1. This must be done before
659
- // transformStrokeWidths since transformStrokeWidths affects
660
- // gradients.
661
- transformGradients(svgTag);
662
- }
663
- transformStrokeWidths(svgTag, window);
664
- transformImages(svgTag);
665
- if (fromVersion2) {
666
- // Transform all text elements.
667
- transformText(svgTag);
668
- // Transform measurements.
669
- transformMeasurements(svgTag);
670
- // Fix stroke roundedness.
671
- setGradientStrokeRoundedness(svgTag);
672
- } else if (!svgTag.getAttribute('viewBox')) {
673
- // Renderer expects a view box.
674
- transformMeasurements(svgTag);
675
- } else if (!svgTag.getAttribute('width') || !svgTag.getAttribute('height')) {
676
- svgTag.setAttribute('width', svgTag.viewBox.baseVal.width);
677
- svgTag.setAttribute('height', svgTag.viewBox.baseVal.height);
678
- }
679
- };
680
-
681
- /**
682
- * Load an SVG string and normalize it. All the steps before drawing/measuring.
683
- * Currently, this will normalize stroke widths (see transform-applier.js) and render all embedded images pixelated.
684
- * The returned SVG will be guaranteed to always have a `width`, `height` and `viewBox`.
685
- * In addition, if the `fromVersion2` parameter is `true`, several "quirks-mode" transformations will be applied which
686
- * mimic Scratch 2.0's SVG rendering.
687
- * @param {!string} svgString String of SVG data to draw in quirks-mode.
688
- * @param {boolean} [fromVersion2] True if we should perform conversion from version 2 to version 3 svg.
689
- * @returns {SVGSVGElement} The normalized SVG element.
690
- */
691
- const loadSvgString = (svgString, fromVersion2) => {
692
- // Parse string into SVG XML.
693
- const parser = new DOMParser();
694
-
695
- // Since we're adding user-provided SVG to document.body as part of normalization,
696
- // sanitization is required. This should not affect bounding box calculation.
697
- const sanitizedSvgString = sanitizeSvgText(svgString);
698
- const svgDom = parser.parseFromString(sanitizedSvgString, 'text/xml');
699
- if (svgDom.childNodes.length < 1 || svgDom.documentElement.localName !== 'svg') {
700
- throw new Error('Document does not appear to be SVG.');
701
- }
702
- const svgTag = svgDom.documentElement;
703
- normalizeSvg(svgTag, fromVersion2);
704
- return svgTag;
705
- };
706
- module.exports = loadSvgString;
707
-
708
- /***/ },
709
-
710
- /***/ "./src/sanitize-svg.js"
711
- /*!*****************************!*\
712
- !*** ./src/sanitize-svg.js ***!
713
- \*****************************/
714
- (module, __unused_webpack_exports, __webpack_require__) {
715
-
716
- /**
717
- * @fileOverview Sanitize the content of an SVG aggressively, to make it as safe
718
- * as possible
719
- */
720
- const fixupSvgString = __webpack_require__(/*! ./fixup-svg-string */ "./src/fixup-svg-string.js");
721
- const {
722
- generate,
723
- parse,
724
- walk
725
- } = __webpack_require__(/*! css-tree */ "css-tree");
726
- const DOMPurify = __webpack_require__(/*! isomorphic-dompurify */ "isomorphic-dompurify");
727
- const sanitizeSvg = {};
728
- const isInternalRef = ref => ref.startsWith('#') || ref.startsWith('data:');
729
- DOMPurify.addHook('beforeSanitizeAttributes', currentNode => {
730
- if (currentNode && currentNode.href && currentNode.href.baseVal) {
731
- const href = currentNode.href.baseVal.replace(/\s/g, '');
732
- // "data:" and "#" are valid hrefs
733
- if (!isInternalRef(href)) {
734
- // TODO: Those can be in different namespaces than `xlink:`
735
- if (currentNode.attributes.getNamedItem('xlink:href')) {
736
- currentNode.attributes.removeNamedItem('xlink:href');
737
- delete currentNode['xlink:href'];
738
- }
739
- if (currentNode.attributes.getNamedItem('href')) {
740
- currentNode.attributes.removeNamedItem('href');
741
- delete currentNode.href;
742
- }
743
- }
744
- }
745
-
746
- // Remove url(...) usages with external references
747
- if (currentNode && currentNode.attributes) {
748
- for (let i = currentNode.attributes.length - 1; i >= 0; i--) {
749
- const attr = currentNode.attributes[i];
750
- const rawValue = attr.value || '';
751
- const value = rawValue.toLowerCase().replace(/\s/g, '');
752
- const urlMatch = value.match(/url\((.+?)\)/);
753
- if (urlMatch) {
754
- const ref = urlMatch[1].replace(/['"]/g, '');
755
- if (!isInternalRef(ref)) {
756
- currentNode.removeAttribute(attr.name);
757
- }
758
- }
759
- }
760
- }
761
- return currentNode;
762
- });
763
- DOMPurify.addHook('uponSanitizeElement', (node, data) => {
764
- if (data.tagName === 'style') {
765
- const ast = parse(node.textContent);
766
- let isModified = false;
767
- walk(ast, (astNode, item, list) => {
768
- // @import rules
769
- if (astNode.type === 'Atrule' && astNode.name.toLowerCase() === 'import') {
770
- list.remove(item);
771
- isModified = true;
772
- }
773
-
774
- // Elements using url(...) for external resources
775
- if (astNode.type === 'Declaration' && astNode.value) {
776
- let shouldRemove = false;
777
- walk(astNode.value, valueNode => {
778
- if (valueNode.type === 'Url') {
779
- const urlValue = (valueNode.value.value || '').trim().replace(/['"]/g, '');
780
- if (!isInternalRef(urlValue)) {
781
- shouldRemove = true;
782
- }
783
- }
784
- });
785
- if (shouldRemove) {
786
- list.remove(item);
787
- isModified = true;
788
- }
789
- }
790
- });
791
- if (isModified) {
792
- node.textContent = generate(ast);
793
- }
794
- }
795
- });
796
-
797
- // Use JS implemented TextDecoder and TextEncoder if it is not provided by the
798
- // browser.
799
- let _TextDecoder;
800
- let _TextEncoder;
801
- if (typeof TextDecoder === 'undefined' || typeof TextEncoder === 'undefined') {
802
- // Wait to require the text encoding polyfill until we know it's needed.
803
-
804
- const encoding = __webpack_require__(/*! fastestsmallesttextencoderdecoder */ "fastestsmallesttextencoderdecoder");
805
- _TextDecoder = encoding.TextDecoder;
806
- _TextEncoder = encoding.TextEncoder;
807
- } else {
808
- _TextDecoder = TextDecoder;
809
- _TextEncoder = TextEncoder;
810
- }
811
-
812
- /**
813
- * Load an SVG Uint8Array of bytes and "sanitize" it
814
- * @param {!Uint8Array} rawData unsanitized SVG daata
815
- * @returns {Uint8Array} sanitized SVG data
816
- */
817
- sanitizeSvg.sanitizeByteStream = function (rawData) {
818
- const decoder = new _TextDecoder();
819
- const encoder = new _TextEncoder();
820
- const sanitizedText = sanitizeSvg.sanitizeSvgText(decoder.decode(rawData));
821
- return encoder.encode(sanitizedText);
822
- };
823
-
824
- /**
825
- * Load an SVG string and "sanitize" it. This is more aggressive than the handling in
826
- * fixup-svg-string.js, and thus more risky; there are known examples of SVGs that
827
- * it will clobber. We use DOMPurify's svg profile, which restricts many types of tag.
828
- * @param {!string} rawSvgText unsanitized SVG string
829
- * @returns {string} sanitized SVG text
830
- */
831
- sanitizeSvg.sanitizeSvgText = function (rawSvgText) {
832
- let sanitizedText = DOMPurify.sanitize(rawSvgText, {
833
- USE_PROFILES: {
834
- svg: true
835
- },
836
- FORBID_TAGS: ['a', 'audio', 'canvas', 'video'],
837
- // Allow data URI in image tags (e.g. SVGs converted from bitmap)
838
- ADD_DATA_URI_TAGS: ['image']
839
- });
840
-
841
- // Remove partial XML comment that is sometimes left in the HTML
842
- const badTag = sanitizedText.indexOf(']&gt;');
843
- if (badTag >= 0) {
844
- sanitizedText = sanitizedText.substring(5, sanitizedText.length);
845
- }
846
-
847
- // also use our custom fixup rules
848
- sanitizedText = fixupSvgString(sanitizedText);
849
- return sanitizedText;
850
- };
851
- module.exports = sanitizeSvg;
852
-
853
- /***/ },
854
-
855
- /***/ "./src/serialize-svg-to-string.js"
856
- /*!****************************************!*\
857
- !*** ./src/serialize-svg-to-string.js ***!
858
- \****************************************/
859
- (module, __unused_webpack_exports, __webpack_require__) {
860
-
861
- const inlineSvgFonts = __webpack_require__(/*! ./font-inliner */ "./src/font-inliner.js");
862
-
863
- /**
864
- * Serialize a given SVG DOM to a string.
865
- * @param {SVGSVGElement} svgTag The SVG element to serialize.
866
- * @param {?boolean} shouldInjectFonts True if fonts should be included in the SVG as
867
- * base64 data.
868
- * @returns {string} String representing current SVG data.
869
- */
870
- const serializeSvgToString = (svgTag, shouldInjectFonts) => {
871
- const serializer = new XMLSerializer();
872
- let string = serializer.serializeToString(svgTag);
873
- if (shouldInjectFonts) {
874
- string = inlineSvgFonts(string);
875
- }
876
- return string;
877
- };
878
- module.exports = serializeSvgToString;
879
-
880
- /***/ },
881
-
882
- /***/ "./src/svg-element.js"
883
- /*!****************************!*\
884
- !*** ./src/svg-element.js ***!
885
- \****************************/
886
- (module) {
887
-
888
- /* Adapted from
889
- * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
890
- * http://paperjs.org/
891
- *
892
- * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
893
- * http://scratchdisk.com/ & http://jonathanpuckey.com/
894
- *
895
- * Distributed under the MIT license. See LICENSE file for details.
896
- *
897
- * All rights reserved.
898
- */
899
-
900
- /**
901
- * @name SvgElement
902
- * @namespace
903
- * @private
904
- */
905
- class SvgElement {
906
- // SVG related namespaces
907
- static get svg() {
908
- return 'http://www.w3.org/2000/svg';
909
- }
910
- static get xmlns() {
911
- return 'http://www.w3.org/2000/xmlns';
912
- }
913
- static get xlink() {
914
- return 'http://www.w3.org/1999/xlink';
915
- }
916
-
917
- // Mapping of attribute names to required namespaces:
918
- static attributeNamespace() {
919
- return {
920
- 'href': SvgElement.xlink,
921
- 'xlink': SvgElement.xmlns,
922
- // Only the xmlns attribute needs the trailing slash. See #984
923
- 'xmlns': "".concat(SvgElement.xmlns, "/"),
924
- // IE needs the xmlns namespace when setting 'xmlns:xlink'. See #984
925
- 'xmlns:xlink': "".concat(SvgElement.xmlns, "/")
926
- };
927
- }
928
- static create(tag, attributes, formatter) {
929
- return SvgElement.set(document.createElementNS(SvgElement.svg, tag), attributes, formatter);
930
- }
931
- static get(node, name) {
932
- const namespace = SvgElement.attributeNamespace[name];
933
- const value = namespace ? node.getAttributeNS(namespace, name) : node.getAttribute(name);
934
- return value === 'null' ? null : value;
935
- }
936
- static set(node, attributes, formatter) {
937
- for (const name in attributes) {
938
- let value = attributes[name];
939
- const namespace = SvgElement.attributeNamespace[name];
940
- if (typeof value === 'number' && formatter) {
941
- value = formatter.number(value);
942
- }
943
- if (namespace) {
944
- node.setAttributeNS(namespace, name, value);
945
- } else {
946
- node.setAttribute(name, value);
947
- }
948
- }
949
- return node;
950
- }
951
- }
952
- module.exports = SvgElement;
953
-
954
- /***/ },
955
-
956
- /***/ "./src/svg-renderer.js"
957
- /*!*****************************!*\
958
- !*** ./src/svg-renderer.js ***!
959
- \*****************************/
960
- (module, __unused_webpack_exports, __webpack_require__) {
961
-
962
- const loadSvgString = __webpack_require__(/*! ./load-svg-string */ "./src/load-svg-string.js");
963
- const serializeSvgToString = __webpack_require__(/*! ./serialize-svg-to-string */ "./src/serialize-svg-to-string.js");
964
-
965
- /**
966
- * Main quirks-mode SVG rendering code.
967
- * @deprecated Call into individual methods exported from this library instead.
968
- */
969
- class SvgRenderer {
970
- /**
971
- * Create a quirks-mode SVG renderer for a particular canvas.
972
- * @param {HTMLCanvasElement} [canvas] An optional canvas element to draw to. If this is not provided, the renderer
973
- * will create a new canvas.
974
- * @class
975
- */
976
- constructor(canvas) {
977
- /**
978
- * The canvas that this SVG renderer will render to.
979
- * @type {HTMLCanvasElement}
980
- * @private
981
- */
982
- this._canvas = canvas || document.createElement('canvas');
983
- this._context = this._canvas.getContext('2d');
984
-
985
- /**
986
- * A measured SVG "viewbox"
987
- * @typedef {object} SvgRenderer#SvgMeasurements
988
- * @property {number} x - The left edge of the SVG viewbox.
989
- * @property {number} y - The top edge of the SVG viewbox.
990
- * @property {number} width - The width of the SVG viewbox.
991
- * @property {number} height - The height of the SVG viewbox.
992
- */
993
-
994
- /**
995
- * The measurement box of the currently loaded SVG.
996
- * @type {SvgRenderer#SvgMeasurements}
997
- * @private
998
- */
999
- this._measurements = {
1000
- x: 0,
1001
- y: 0,
1002
- width: 0,
1003
- height: 0
1004
- };
1005
-
1006
- /**
1007
- * The `<img>` element with the contents of the currently loaded SVG.
1008
- * @type {?HTMLImageElement}
1009
- * @private
1010
- */
1011
- this._cachedImage = null;
1012
-
1013
- /**
1014
- * True if this renderer's current SVG is loaded and can be rendered to the canvas.
1015
- * @type {boolean}
1016
- */
1017
- this.loaded = false;
1018
- }
1019
-
1020
- /**
1021
- * @returns {!HTMLCanvasElement} this renderer's target canvas.
1022
- */
1023
- get canvas() {
1024
- return this._canvas;
1025
- }
1026
-
1027
- /**
1028
- * @returns {Array<number>} the natural size, in Scratch units, of this SVG.
1029
- */
1030
- get size() {
1031
- return [this._measurements.width, this._measurements.height];
1032
- }
1033
-
1034
- /**
1035
- * @returns {Array<number>} the offset (upper left corner) of the SVG's view box.
1036
- */
1037
- get viewOffset() {
1038
- return [this._measurements.x, this._measurements.y];
1039
- }
1040
-
1041
- /**
1042
- * Load an SVG string and normalize it. All the steps before drawing/measuring.
1043
- * @param {!string} svgString String of SVG data to draw in quirks-mode.
1044
- * @param {?boolean} fromVersion2 True if we should perform conversion from
1045
- * version 2 to version 3 svg.
1046
- */
1047
- loadString(svgString, fromVersion2) {
1048
- // New svg string invalidates the cached image
1049
- this._cachedImage = null;
1050
- const svgTag = loadSvgString(svgString, fromVersion2);
1051
- this._svgTag = svgTag;
1052
- this._measurements = {
1053
- width: svgTag.viewBox.baseVal.width,
1054
- height: svgTag.viewBox.baseVal.height,
1055
- x: svgTag.viewBox.baseVal.x,
1056
- y: svgTag.viewBox.baseVal.y
1057
- };
1058
- }
1059
-
1060
- /**
1061
- * Load an SVG string, normalize it, and prepare it for (synchronous) rendering.
1062
- * @param {!string} svgString String of SVG data to draw in quirks-mode.
1063
- * @param {?boolean} fromVersion2 True if we should perform conversion from version 2 to version 3 svg.
1064
- * @param {Function} [onFinish] - An optional callback to call when the SVG is loaded and can be rendered.
1065
- */
1066
- loadSVG(svgString, fromVersion2, onFinish) {
1067
- this.loadString(svgString, fromVersion2);
1068
- this._createSVGImage(onFinish);
1069
- }
1070
-
1071
- /**
1072
- * Creates an <img> element for the currently loaded SVG string, then calls the callback once it's loaded.
1073
- * @param {Function} [onFinish] - An optional callback to call when the <img> has loaded.
1074
- */
1075
- _createSVGImage(onFinish) {
1076
- if (this._cachedImage === null) this._cachedImage = new Image();
1077
- const img = this._cachedImage;
1078
- img.onload = () => {
1079
- this.loaded = true;
1080
- if (onFinish) onFinish();
1081
- };
1082
- const svgText = this.toString(true /* shouldInjectFonts */);
1083
- img.src = "data:image/svg+xml;utf8,".concat(encodeURIComponent(svgText));
1084
- this.loaded = false;
1085
- }
1086
-
1087
- /**
1088
- * Serialize the active SVG DOM to a string.
1089
- * @param {?boolean} shouldInjectFonts True if fonts should be included in the SVG as
1090
- * base64 data.
1091
- * @returns {string} String representing current SVG data.
1092
- * @deprecated Use the standalone `serializeSvgToString` export instead.
1093
- */
1094
- toString(shouldInjectFonts) {
1095
- return serializeSvgToString(this._svgTag, shouldInjectFonts);
1096
- }
1097
-
1098
- /**
1099
- * Synchronously draw the loaded SVG to this renderer's `canvas`.
1100
- * @param {number} [scale] - Optionally, also scale the image by this factor.
1101
- */
1102
- draw(scale) {
1103
- if (!this.loaded) throw new Error('SVG image has not finished loading');
1104
- this._drawFromImage(scale);
1105
- }
1106
-
1107
- /**
1108
- * Draw to the canvas from a loaded image element.
1109
- * @param {number} [scale] - Optionally, also scale the image by this factor.
1110
- */
1111
- _drawFromImage(scale) {
1112
- if (this._cachedImage === null) return;
1113
- const ratio = Number.isFinite(scale) ? scale : 1;
1114
- const bbox = this._measurements;
1115
- this._canvas.width = bbox.width * ratio;
1116
- this._canvas.height = bbox.height * ratio;
1117
- // Even if the canvas at the current scale has a nonzero size, the image's dimensions are floored pre-scaling.
1118
- // e.g. if an image has a width of 0.4 and is being rendered at 3x scale, the canvas will have a width of 1, but
1119
- // the image's width will be rounded down to 0 on some browsers (Firefox) prior to being drawn at that scale.
1120
- if (this._canvas.width <= 0 || this._canvas.height <= 0 || this._cachedImage.naturalWidth <= 0 || this._cachedImage.naturalHeight <= 0) return;
1121
- this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
1122
- this._context.setTransform(ratio, 0, 0, ratio, 0, 0);
1123
- this._context.drawImage(this._cachedImage, 0, 0);
1124
- }
1125
- }
1126
- module.exports = SvgRenderer;
1127
-
1128
- /***/ },
1129
-
1130
- /***/ "./src/transform-applier.js"
1131
- /*!**********************************!*\
1132
- !*** ./src/transform-applier.js ***!
1133
- \**********************************/
1134
- (module, __unused_webpack_exports, __webpack_require__) {
1135
-
1136
- const Matrix = __webpack_require__(/*! transformation-matrix */ "transformation-matrix");
1137
- const SvgElement = __webpack_require__(/*! ./svg-element */ "./src/svg-element.js");
1138
- const log = __webpack_require__(/*! ./util/log */ "./src/util/log.js");
1139
-
1140
- /**
1141
- * @fileOverview Apply transforms to match stroke width appearance in 2.0 and 3.0
1142
- */
1143
-
1144
- // Adapted from paper.js's Path.applyTransform
1145
- const _parseTransform = function _parseTransform(domElement) {
1146
- let matrix = Matrix.identity();
1147
- const string = domElement.attributes && domElement.attributes.transform && domElement.attributes.transform.value;
1148
- if (!string) return matrix;
1149
- // https://www.w3.org/TR/SVG/types.html#DataTypeTransformList
1150
- // Parse SVG transform string. First we split at /)\s*/, to separate
1151
- // commands
1152
- const transforms = string.split(/\)\s*/g);
1153
- for (const transform of transforms) {
1154
- if (!transform) break;
1155
- // Command come before the '(', values after
1156
- const parts = transform.split(/\(\s*/);
1157
- const command = parts[0].trim();
1158
- const v = parts[1].split(/[\s,]+/g);
1159
- // Convert values to floats
1160
- for (let j = 0; j < v.length; j++) {
1161
- v[j] = parseFloat(v[j]);
1162
- }
1163
- switch (command) {
1164
- case 'matrix':
1165
- matrix = Matrix.compose(matrix, {
1166
- a: v[0],
1167
- b: v[1],
1168
- c: v[2],
1169
- d: v[3],
1170
- e: v[4],
1171
- f: v[5]
1172
- });
1173
- break;
1174
- case 'rotate':
1175
- matrix = Matrix.compose(matrix, Matrix.rotateDEG(v[0], v[1] || 0, v[2] || 0));
1176
- break;
1177
- case 'translate':
1178
- matrix = Matrix.compose(matrix, Matrix.translate(v[0], v[1] || 0));
1179
- break;
1180
- case 'scale':
1181
- matrix = Matrix.compose(matrix, Matrix.scale(v[0], v[1] || v[0]));
1182
- break;
1183
- case 'skewX':
1184
- matrix = Matrix.compose(matrix, Matrix.skewDEG(v[0], 0));
1185
- break;
1186
- case 'skewY':
1187
- matrix = Matrix.compose(matrix, Matrix.skewDEG(0, v[0]));
1188
- break;
1189
- default:
1190
- log.error("Couldn't parse: ".concat(command));
1191
- }
1192
- }
1193
- return matrix;
1194
- };
1195
-
1196
- // Adapted from paper.js's Matrix.decompose
1197
- // Given a matrix, return the x and y scale factors of the matrix
1198
- const _getScaleFactor = function _getScaleFactor(matrix) {
1199
- const a = matrix.a;
1200
- const b = matrix.b;
1201
- const c = matrix.c;
1202
- const d = matrix.d;
1203
- const det = a * d - b * c;
1204
- if (a !== 0 || b !== 0) {
1205
- const r = Math.sqrt(a * a + b * b);
1206
- return {
1207
- x: r,
1208
- y: det / r
1209
- };
1210
- }
1211
- if (c !== 0 || d !== 0) {
1212
- const s = Math.sqrt(c * c + d * d);
1213
- return {
1214
- x: det / s,
1215
- y: s
1216
- };
1217
- }
1218
- // a = b = c = d = 0
1219
- return {
1220
- x: 0,
1221
- y: 0
1222
- };
1223
- };
1224
-
1225
- // Returns null if matrix is not invertible. Otherwise returns given ellipse
1226
- // transformed by transform, an object {radiusX, radiusY, rotation}.
1227
- const _calculateTransformedEllipse = function _calculateTransformedEllipse(radiusX, radiusY, theta, transform) {
1228
- theta = -theta * Math.PI / 180;
1229
- const a = transform.a;
1230
- const b = -transform.c;
1231
- const c = -transform.b;
1232
- const d = transform.d;
1233
- // Since other parameters determine the translation of the ellipse in SVG, we do not need to worry
1234
- // about what e and f are.
1235
- const det = a * d - b * c;
1236
- // Non-invertible matrix
1237
- if (det === 0) return null;
1238
-
1239
- // rotA, rotB, and rotC represent Ax^2 + Bxy + Cy^2 = 1 coefficients for a rotated ellipse formula
1240
- const sinT = Math.sin(theta);
1241
- const cosT = Math.cos(theta);
1242
- const sin2T = Math.sin(2 * theta);
1243
- const rotA = cosT * cosT / radiusX / radiusX + sinT * sinT / radiusY / radiusY;
1244
- const rotB = sin2T / radiusX / radiusX - sin2T / radiusY / radiusY;
1245
- const rotC = sinT * sinT / radiusX / radiusX + cosT * cosT / radiusY / radiusY;
1246
-
1247
- // Calculate the ellipse formula of the transformed ellipse
1248
- // A, B, and C represent Ax^2 + Bxy + Cy^2 = 1 / det / det coefficients in a transformed ellipse formula
1249
- // scaled by inverse det squared (to preserve accuracy)
1250
- const A = rotA * d * d - rotB * d * c + rotC * c * c;
1251
- const B = -2 * rotA * b * d + rotB * a * d + rotB * b * c - 2 * rotC * a * c;
1252
- const C = rotA * b * b - rotB * a * b + rotC * a * a;
1253
-
1254
- // Derive new radii and theta from the transformed ellipse formula
1255
- const newRadiusXOverDet = Math.sqrt(2) * Math.sqrt((A + C - Math.sqrt(A * A + B * B - 2 * A * C + C * C)) / (-B * B + 4 * A * C));
1256
- const newRadiusYOverDet = 1 / Math.sqrt(A + C - 1 / newRadiusXOverDet / newRadiusXOverDet);
1257
- let temp = (A - 1 / newRadiusXOverDet / newRadiusXOverDet) / (1 / newRadiusYOverDet / newRadiusYOverDet - 1 / newRadiusXOverDet / newRadiusXOverDet);
1258
- if (temp < 0 && Math.abs(temp) < 1e-8) temp = 0; // Fix floating point issue
1259
- temp = Math.sqrt(temp);
1260
- if (Math.abs(1 - temp) < 1e-8) temp = 1; // Fix floating point issue
1261
- // Solve for which of the two possible thetas is correct
1262
- let newTheta = Math.asin(temp);
1263
- temp = B / (1 / newRadiusXOverDet / newRadiusXOverDet - 1 / newRadiusYOverDet / newRadiusYOverDet);
1264
- const newTheta2 = -newTheta;
1265
- if (Math.abs(Math.sin(2 * newTheta2) - temp) < Math.abs(Math.sin(2 * newTheta) - temp)) {
1266
- newTheta = newTheta2;
1267
- }
1268
- return {
1269
- radiusX: newRadiusXOverDet * det,
1270
- radiusY: newRadiusYOverDet * det,
1271
- rotation: -newTheta * 180 / Math.PI
1272
- };
1273
- };
1274
-
1275
- // Adapted from paper.js's PathItem.setPathData
1276
- const _transformPath = function _transformPath(pathString, transform) {
1277
- if (!transform || Matrix.toString(transform) === Matrix.toString(Matrix.identity())) return pathString;
1278
- // First split the path data into parts of command-coordinates pairs
1279
- // Commands are any of these characters: mzlhvcsqta
1280
- const parts = pathString && pathString.match(/[mlhvcsqtaz][^mlhvcsqtaz]*/ig);
1281
- let coords;
1282
- let relative = false;
1283
- let previous;
1284
- let control;
1285
- let current = {
1286
- x: 0,
1287
- y: 0
1288
- };
1289
- let start = {
1290
- x: 0,
1291
- y: 0
1292
- };
1293
- let result = '';
1294
- const getCoord = function getCoord(index, coord) {
1295
- let val = +coords[index];
1296
- if (relative) {
1297
- val += current[coord];
1298
- }
1299
- return val;
1300
- };
1301
- const getPoint = function getPoint(index) {
1302
- return {
1303
- x: getCoord(index, 'x'),
1304
- y: getCoord(index + 1, 'y')
1305
- };
1306
- };
1307
- const roundTo4Places = function roundTo4Places(num) {
1308
- return Number(num.toFixed(4));
1309
- };
1310
-
1311
- // Returns the transformed point as a string
1312
- const getString = function getString(point) {
1313
- const transformed = Matrix.applyToPoint(transform, point);
1314
- return "".concat(roundTo4Places(transformed.x), " ").concat(roundTo4Places(transformed.y), " ");
1315
- };
1316
- for (let i = 0, l = parts && parts.length; i < l; i++) {
1317
- const part = parts[i];
1318
- const command = part[0];
1319
- const lower = command.toLowerCase();
1320
- // Match all coordinate values
1321
- coords = part.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);
1322
- const length = coords && coords.length;
1323
- relative = command === lower;
1324
- // Fix issues with z in the middle of SVG path data, not followed by
1325
- // a m command, see paper.js#413:
1326
- if (previous === 'z' && !/[mz]/.test(lower)) {
1327
- result += "M ".concat(current.x, " ").concat(current.y, " ");
1328
- }
1329
- switch (lower) {
1330
- case 'm': // Move to
1331
- case 'l':
1332
- // Line to
1333
- {
1334
- let move = lower === 'm';
1335
- for (let j = 0; j < length; j += 2) {
1336
- result += move ? 'M ' : 'L ';
1337
- current = getPoint(j);
1338
- result += getString(current);
1339
- if (move) {
1340
- start = current;
1341
- move = false;
1342
- }
1343
- }
1344
- control = current;
1345
- break;
1346
- }
1347
- case 'h': // Horizontal line
1348
- case 'v':
1349
- // Vertical line
1350
- {
1351
- const coord = lower === 'h' ? 'x' : 'y';
1352
- current = {
1353
- x: current.x,
1354
- y: current.y
1355
- }; // Clone as we're going to modify it.
1356
- for (let j = 0; j < length; j++) {
1357
- current[coord] = getCoord(j, coord);
1358
- result += "L ".concat(getString(current));
1359
- }
1360
- control = current;
1361
- break;
1362
- }
1363
- case 'c':
1364
- // Cubic Bezier curve
1365
- for (let j = 0; j < length; j += 6) {
1366
- const handle1 = getPoint(j);
1367
- control = getPoint(j + 2);
1368
- current = getPoint(j + 4);
1369
- result += "C ".concat(getString(handle1)).concat(getString(control)).concat(getString(current));
1370
- }
1371
- break;
1372
- case 's':
1373
- // Smooth cubic Bezier curve
1374
- for (let j = 0; j < length; j += 4) {
1375
- const handle1 = /[cs]/.test(previous) ? {
1376
- x: current.x * 2 - control.x,
1377
- y: current.y * 2 - control.y
1378
- } : current;
1379
- control = getPoint(j);
1380
- current = getPoint(j + 2);
1381
- result += "C ".concat(getString(handle1)).concat(getString(control)).concat(getString(current));
1382
- previous = lower;
1383
- }
1384
- break;
1385
- case 'q':
1386
- // Quadratic Bezier curve
1387
- for (let j = 0; j < length; j += 4) {
1388
- control = getPoint(j);
1389
- current = getPoint(j + 2);
1390
- result += "Q ".concat(getString(control)).concat(getString(current));
1391
- }
1392
- break;
1393
- case 't':
1394
- // Smooth quadratic Bezier curve
1395
- for (let j = 0; j < length; j += 2) {
1396
- control = /[qt]/.test(previous) ? {
1397
- x: current.x * 2 - control.x,
1398
- y: current.y * 2 - control.y
1399
- } : current;
1400
- current = getPoint(j);
1401
- result += "Q ".concat(getString(control)).concat(getString(current));
1402
- previous = lower;
1403
- }
1404
- break;
1405
- case 'a':
1406
- // Elliptical arc curve
1407
- for (let j = 0; j < length; j += 7) {
1408
- current = getPoint(j + 5);
1409
- const rx = +coords[j];
1410
- const ry = +coords[j + 1];
1411
- const rotation = +coords[j + 2];
1412
- const largeArcFlag = +coords[j + 3];
1413
- let clockwiseFlag = +coords[j + 4];
1414
- const newEllipse = _calculateTransformedEllipse(rx, ry, rotation, transform);
1415
- const matrixScale = _getScaleFactor(transform);
1416
- if (newEllipse) {
1417
- if (matrixScale.x > 0 && matrixScale.y < 0 || matrixScale.x < 0 && matrixScale.y > 0) {
1418
- clockwiseFlag = clockwiseFlag ^ 1;
1419
- }
1420
- result += "A ".concat(roundTo4Places(Math.abs(newEllipse.radiusX)), " ") + "".concat(roundTo4Places(Math.abs(newEllipse.radiusY)), " ") + "".concat(roundTo4Places(newEllipse.rotation), " ").concat(largeArcFlag, " ") + "".concat(clockwiseFlag, " ").concat(getString(current));
1421
- } else {
1422
- result += "L ".concat(getString(current));
1423
- }
1424
- }
1425
- break;
1426
- case 'z':
1427
- // Close path
1428
- result += "Z ";
1429
- // Correctly handle relative m commands, see paper.js#1101:
1430
- current = start;
1431
- break;
1432
- }
1433
- previous = lower;
1434
- }
1435
- return result;
1436
- };
1437
- const GRAPHICS_ELEMENTS = ['circle', 'ellipse', 'image', 'line', 'path', 'polygon', 'polyline', 'rect', 'text', 'use'];
1438
- const CONTAINER_ELEMENTS = ['a', 'defs', 'g', 'marker', 'glyph', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol'];
1439
- const _isContainerElement = function _isContainerElement(element) {
1440
- return element.tagName && CONTAINER_ELEMENTS.includes(element.tagName.toLowerCase());
1441
- };
1442
- const _isGraphicsElement = function _isGraphicsElement(element) {
1443
- return element.tagName && GRAPHICS_ELEMENTS.includes(element.tagName.toLowerCase());
1444
- };
1445
- const _isPathWithTransformAndStroke = function _isPathWithTransformAndStroke(element, strokeWidth) {
1446
- if (!element.attributes) return false;
1447
- strokeWidth = element.attributes['stroke-width'] ? Number(element.attributes['stroke-width'].value) : Number(strokeWidth);
1448
- return strokeWidth && element.tagName && element.tagName.toLowerCase() === 'path' && element.attributes.d && element.attributes.d.value;
1449
- };
1450
- const _quadraticMean = function _quadraticMean(a, b) {
1451
- return Math.sqrt((a * a + b * b) / 2);
1452
- };
1453
- const _createGradient = function _createGradient(gradientId, svgTag, bbox, matrix) {
1454
- // Adapted from Paper.js's SvgImport.getValue
1455
- const getValue = function getValue(node, name, isString, allowNull, allowPercent, defaultValue) {
1456
- // Interpret value as number. Never return NaN, but 0 instead.
1457
- // If the value is a sequence of numbers, parseFloat will
1458
- // return the first occurring number, which is enough for now.
1459
- let value = SvgElement.get(node, name);
1460
- let res;
1461
- if (value === null) {
1462
- if (defaultValue) {
1463
- res = defaultValue;
1464
- if (/%\s*$/.test(res)) {
1465
- value = defaultValue;
1466
- res = parseFloat(value);
1467
- }
1468
- } else if (allowNull) {
1469
- res = null;
1470
- } else if (isString) {
1471
- res = '';
1472
- } else {
1473
- res = 0;
1474
- }
1475
- } else if (isString) {
1476
- res = value;
1477
- } else {
1478
- res = parseFloat(value);
1479
- }
1480
- // Support for dimensions in percentage of the root size. If root-size
1481
- // is not set (e.g. during <defs>), just scale the percentage value to
1482
- // 0..1, as required by gradients with gradientUnits="objectBoundingBox"
1483
- if (/%\s*$/.test(value)) {
1484
- const size = allowPercent ? 1 : bbox[/x|^width/.test(name) ? 'width' : 'height'];
1485
- return res / 100 * size;
1486
- }
1487
- return res;
1488
- };
1489
- const getPoint = function getPoint(node, x, y, allowNull, allowPercent, defaultX, defaultY) {
1490
- x = getValue(node, x || 'x', false, allowNull, allowPercent, defaultX);
1491
- y = getValue(node, y || 'y', false, allowNull, allowPercent, defaultY);
1492
- return allowNull && (x === null || y === null) ? null : {
1493
- x,
1494
- y
1495
- };
1496
- };
1497
- let defs = svgTag.getElementsByTagName('defs');
1498
- if (defs.length === 0) {
1499
- defs = SvgElement.create('defs');
1500
- svgTag.appendChild(defs);
1501
- } else {
1502
- defs = defs[0];
1503
- }
1504
-
1505
- // Clone the old gradient. We'll make a new one, since the gradient might be reused elsewhere
1506
- // with different transform matrix
1507
- const oldGradient = svgTag.getElementById(gradientId);
1508
- if (!oldGradient) return;
1509
- const radial = oldGradient.tagName.toLowerCase() === 'radialgradient';
1510
- const newGradient = svgTag.getElementById(gradientId).cloneNode(true /* deep */);
1511
-
1512
- // Give the new gradient a new ID
1513
- let matrixString = Matrix.toString(matrix);
1514
- matrixString = matrixString.substring(8, matrixString.length - 1);
1515
- const newGradientId = "".concat(gradientId, "-").concat(matrixString);
1516
- newGradient.setAttribute('id', newGradientId);
1517
-
1518
- // This gradient already exists and was transformed before. Just reuse the already-transformed one.
1519
- if (svgTag.getElementById(newGradientId)) {
1520
- // This is the same code as in the end of the function, but I don't feel like wrapping the next 80 lines
1521
- // in an `if (!svgTag.getElementById(newGradientId))` block
1522
- return "url(#".concat(newGradientId, ")");
1523
- }
1524
- const scaleToBounds = getValue(newGradient, 'gradientUnits', true) !== 'userSpaceOnUse';
1525
- let origin;
1526
- let destination;
1527
- let radius;
1528
- let focal;
1529
- if (radial) {
1530
- origin = getPoint(newGradient, 'cx', 'cy', false, scaleToBounds, '50%', '50%');
1531
- radius = getValue(newGradient, 'r', false, false, scaleToBounds, '50%');
1532
- focal = getPoint(newGradient, 'fx', 'fy', true, scaleToBounds);
1533
- } else {
1534
- origin = getPoint(newGradient, 'x1', 'y1', false, scaleToBounds);
1535
- destination = getPoint(newGradient, 'x2', 'y2', false, scaleToBounds, '1');
1536
- if (origin.x === destination.x && origin.y === destination.y) {
1537
- // If it's degenerate, use the color of the last stop, as described by
1538
- // https://www.w3.org/TR/SVG/pservers.html#LinearGradientNotes
1539
- const stops = newGradient.getElementsByTagName('stop');
1540
- if (!stops.length || !stops[stops.length - 1].attributes || !stops[stops.length - 1].attributes['stop-color']) {
1541
- return null;
1542
- }
1543
- return stops[stops.length - 1].attributes['stop-color'].value;
1544
- }
1545
- }
1546
-
1547
- // Transform points
1548
- // Emulate SVG's gradientUnits="objectBoundingBox"
1549
- if (scaleToBounds) {
1550
- const boundsMatrix = Matrix.compose(Matrix.translate(bbox.x, bbox.y), Matrix.scale(bbox.width, bbox.height));
1551
- origin = Matrix.applyToPoint(boundsMatrix, origin);
1552
- if (destination) destination = Matrix.applyToPoint(boundsMatrix, destination);
1553
- if (radius) {
1554
- radius = _quadraticMean(bbox.width, bbox.height) * radius;
1555
- }
1556
- if (focal) focal = Matrix.applyToPoint(boundsMatrix, focal);
1557
- }
1558
- if (radial) {
1559
- origin = Matrix.applyToPoint(matrix, origin);
1560
- const matrixScale = _getScaleFactor(matrix);
1561
- radius = _quadraticMean(matrixScale.x, matrixScale.y) * radius;
1562
- if (focal) focal = Matrix.applyToPoint(matrix, focal);
1563
- } else {
1564
- const dot = (a, b) => a.x * b.x + a.y * b.y;
1565
- const multiply = (coefficient, v) => ({
1566
- x: coefficient * v.x,
1567
- y: coefficient * v.y
1568
- });
1569
- const add = (a, b) => ({
1570
- x: a.x + b.x,
1571
- y: a.y + b.y
1572
- });
1573
- const subtract = (a, b) => ({
1574
- x: a.x - b.x,
1575
- y: a.y - b.y
1576
- });
1577
-
1578
- // The line through origin and gradientPerpendicular is the line at which the gradient starts
1579
- let gradientPerpendicular = Math.abs(origin.x - destination.x) < 1e-8 ? add(origin, {
1580
- x: 1,
1581
- y: (origin.x - destination.x) / (destination.y - origin.y)
1582
- }) : add(origin, {
1583
- x: (destination.y - origin.y) / (origin.x - destination.x),
1584
- y: 1
1585
- });
1586
-
1587
- // Transform points
1588
- gradientPerpendicular = Matrix.applyToPoint(matrix, gradientPerpendicular);
1589
- origin = Matrix.applyToPoint(matrix, origin);
1590
- destination = Matrix.applyToPoint(matrix, destination);
1591
-
1592
- // Calculate the direction that the gradient has changed to
1593
- const originToPerpendicular = subtract(gradientPerpendicular, origin);
1594
- const originToDestination = subtract(destination, origin);
1595
- const gradientDirection = Math.abs(originToPerpendicular.x) < 1e-8 ? {
1596
- x: 1,
1597
- y: -originToPerpendicular.x / originToPerpendicular.y
1598
- } : {
1599
- x: -originToPerpendicular.y / originToPerpendicular.x,
1600
- y: 1
1601
- };
1602
-
1603
- // Set the destination so that the gradient moves in the correct direction, by projecting the destination vector
1604
- // onto the gradient direction vector
1605
- const projectionCoeff = dot(originToDestination, gradientDirection) / dot(gradientDirection, gradientDirection);
1606
- const projection = multiply(projectionCoeff, gradientDirection);
1607
- destination = {
1608
- x: origin.x + projection.x,
1609
- y: origin.y + projection.y
1610
- };
1611
- }
1612
-
1613
- // Put values back into svg
1614
- if (radial) {
1615
- newGradient.setAttribute('cx', Number(origin.x.toFixed(4)));
1616
- newGradient.setAttribute('cy', Number(origin.y.toFixed(4)));
1617
- newGradient.setAttribute('r', Number(radius.toFixed(4)));
1618
- if (focal) {
1619
- newGradient.setAttribute('fx', Number(focal.x.toFixed(4)));
1620
- newGradient.setAttribute('fy', Number(focal.y.toFixed(4)));
1621
- }
1622
- } else {
1623
- newGradient.setAttribute('x1', Number(origin.x.toFixed(4)));
1624
- newGradient.setAttribute('y1', Number(origin.y.toFixed(4)));
1625
- newGradient.setAttribute('x2', Number(destination.x.toFixed(4)));
1626
- newGradient.setAttribute('y2', Number(destination.y.toFixed(4)));
1627
- }
1628
- newGradient.setAttribute('gradientUnits', 'userSpaceOnUse');
1629
- defs.appendChild(newGradient);
1630
- return "url(#".concat(newGradientId, ")");
1631
- };
1632
-
1633
- // Adapted from paper.js's SvgImport.getDefinition
1634
- const _parseUrl = (value, windowRef) => {
1635
- // When url() comes from a style property, '#'' seems to be missing on
1636
- // WebKit. We also get variations of quotes or no quotes, single or
1637
- // double, so handle it all with one regular expression:
1638
- const match = value && value.match(/\((?:["'#]*)([^"')]+)/);
1639
- const name = match && match[1];
1640
- const res = name && windowRef ?
1641
- // This is required by Firefox, which can produce absolute
1642
- // urls for local gradients, see paperjs#1001:
1643
- name.replace("".concat(windowRef.location.href.split('#')[0], "#"), '') : name;
1644
- return res;
1645
- };
1646
-
1647
- /**
1648
- * Scratch 2.0 displays stroke widths in a "normalized" way, that is,
1649
- * if a shape with a stroke width has a transform applied, it will be
1650
- * rendered with a stroke that is the same width all the way around,
1651
- * instead of stretched looking.
1652
- *
1653
- * The vector paint editor also prefers to normalize the stroke width,
1654
- * rather than keep track of transforms at the group level, as this
1655
- * simplifies editing (e.g. stroke width 3 always means the same thickness)
1656
- *
1657
- * This function performs that normalization process, pushing transforms
1658
- * on groups down to the leaf level and averaging out the stroke width
1659
- * around the shapes. Note that this doens't just change stroke widths, it
1660
- * changes path data and attributes throughout the SVG.
1661
- * @param {SVGElement} svgTag The SVG dom object
1662
- * @param {Window} windowRef The window to use. Need to pass in for
1663
- * tests to work, as they get angry at even the mention of window.
1664
- * @param {object} bboxForTesting The bounds to use. Need to pass in for
1665
- * tests only, because getBBox doesn't work in Node. This should
1666
- * be the bounds of the svgTag without including stroke width or transforms.
1667
- * @returns {void}
1668
- */
1669
- const transformStrokeWidths = function transformStrokeWidths(svgTag, windowRef, bboxForTesting) {
1670
- const inherited = Matrix.identity();
1671
- const applyTransforms = (element, matrix, strokeWidth, fill, stroke) => {
1672
- if (_isContainerElement(element)) {
1673
- // Push fills and stroke width down to leaves
1674
- if (element.attributes['stroke-width']) {
1675
- strokeWidth = element.attributes['stroke-width'].value;
1676
- }
1677
- if (element.attributes) {
1678
- if (element.attributes.fill) fill = element.attributes.fill.value;
1679
- if (element.attributes.stroke) stroke = element.attributes.stroke.value;
1680
- }
1681
-
1682
- // If any child nodes don't take attributes, leave the attributes
1683
- // at the parent level.
1684
- for (let i = 0; i < element.childNodes.length; i++) {
1685
- applyTransforms(element.childNodes[i], Matrix.compose(matrix, _parseTransform(element)), strokeWidth, fill, stroke);
1686
- }
1687
- element.removeAttribute('transform');
1688
- element.removeAttribute('stroke-width');
1689
- element.removeAttribute('fill');
1690
- element.removeAttribute('stroke');
1691
- } else if (_isPathWithTransformAndStroke(element, strokeWidth)) {
1692
- if (element.attributes['stroke-width']) {
1693
- strokeWidth = element.attributes['stroke-width'].value;
1694
- }
1695
- if (element.attributes.fill) fill = element.attributes.fill.value;
1696
- if (element.attributes.stroke) stroke = element.attributes.stroke.value;
1697
- matrix = Matrix.compose(matrix, _parseTransform(element));
1698
- if (Matrix.toString(matrix) === Matrix.toString(Matrix.identity())) {
1699
- element.removeAttribute('transform');
1700
- element.setAttribute('stroke-width', strokeWidth);
1701
- if (fill) element.setAttribute('fill', fill);
1702
- if (stroke) element.setAttribute('stroke', stroke);
1703
- return;
1704
- }
1705
-
1706
- // Transform gradient
1707
- const fillGradientId = _parseUrl(fill, windowRef);
1708
- const strokeGradientId = _parseUrl(stroke, windowRef);
1709
- if (fillGradientId || strokeGradientId) {
1710
- const doc = windowRef.document;
1711
- // Need path bounds to transform gradient
1712
- const svgSpot = doc.createElement('span');
1713
- let bbox;
1714
- if (bboxForTesting) {
1715
- bbox = bboxForTesting;
1716
- } else {
1717
- try {
1718
- doc.body.appendChild(svgSpot);
1719
- const svg = SvgElement.set(doc.createElementNS(SvgElement.svg, 'svg'));
1720
- const path = SvgElement.set(doc.createElementNS(SvgElement.svg, 'path'));
1721
- path.setAttribute('d', element.attributes.d.value);
1722
- svg.appendChild(path);
1723
- svgSpot.appendChild(svg);
1724
- // Take the bounding box.
1725
- bbox = svg.getBBox();
1726
- } finally {
1727
- // Always destroy the element, even if, for example, getBBox throws.
1728
- doc.body.removeChild(svgSpot);
1729
- }
1730
- }
1731
- if (fillGradientId) {
1732
- const newFillRef = _createGradient(fillGradientId, svgTag, bbox, matrix);
1733
- if (newFillRef) fill = newFillRef;
1734
- }
1735
- if (strokeGradientId) {
1736
- const newStrokeRef = _createGradient(strokeGradientId, svgTag, bbox, matrix);
1737
- if (newStrokeRef) stroke = newStrokeRef;
1738
- }
1739
- }
1740
-
1741
- // Transform path data
1742
- element.setAttribute('d', _transformPath(element.attributes.d.value, matrix));
1743
- element.removeAttribute('transform');
1744
-
1745
- // Transform stroke width
1746
- const matrixScale = _getScaleFactor(matrix);
1747
- element.setAttribute('stroke-width', _quadraticMean(matrixScale.x, matrixScale.y) * strokeWidth);
1748
- if (fill) element.setAttribute('fill', fill);
1749
- if (stroke) element.setAttribute('stroke', stroke);
1750
- } else if (_isGraphicsElement(element)) {
1751
- // Push stroke width, fill, and stroke down to leaves
1752
- if (strokeWidth && !element.attributes['stroke-width']) {
1753
- element.setAttribute('stroke-width', strokeWidth);
1754
- }
1755
- if (fill && !element.attributes.fill) {
1756
- element.setAttribute('fill', fill);
1757
- }
1758
- if (stroke && !element.attributes.stroke) {
1759
- element.setAttribute('stroke', stroke);
1760
- }
1761
-
1762
- // Push transform down to leaves
1763
- matrix = Matrix.compose(matrix, _parseTransform(element));
1764
- if (Matrix.toString(matrix) === Matrix.toString(Matrix.identity())) {
1765
- element.removeAttribute('transform');
1766
- } else {
1767
- element.setAttribute('transform', Matrix.toString(matrix));
1768
- }
1769
- }
1770
- };
1771
- applyTransforms(svgTag, inherited, 1 /* default SVG stroke width */);
1772
- };
1773
- module.exports = transformStrokeWidths;
1774
-
1775
- /***/ },
1776
-
1777
- /***/ "./src/util/log.js"
1778
- /*!*************************!*\
1779
- !*** ./src/util/log.js ***!
1780
- \*************************/
1781
- (module, __unused_webpack_exports, __webpack_require__) {
1782
-
1783
- const {
1784
- Logger
1785
- } = __webpack_require__(/*! tslog */ "tslog");
1786
- module.exports = new Logger({
1787
- name: 'scratch-svg-renderer'
1788
- });
1789
-
1790
- /***/ },
1791
-
1792
- /***/ "base64-js"
1793
- /*!****************************!*\
1794
- !*** external "base64-js" ***!
1795
- \****************************/
1796
- (module) {
1797
-
1798
- "use strict";
1799
- module.exports = require("base64-js");
1800
-
1801
- /***/ },
1802
-
1803
- /***/ "css-tree"
1804
- /*!***************************!*\
1805
- !*** external "css-tree" ***!
1806
- \***************************/
1807
- (module) {
1808
-
1809
- "use strict";
1810
- module.exports = require("css-tree");
1811
-
1812
- /***/ },
1813
-
1814
- /***/ "fastestsmallesttextencoderdecoder"
1815
- /*!****************************************************!*\
1816
- !*** external "fastestsmallesttextencoderdecoder" ***!
1817
- \****************************************************/
1818
- (module) {
1819
-
1820
- "use strict";
1821
- module.exports = require("fastestsmallesttextencoderdecoder");
1822
-
1823
- /***/ },
1824
-
1825
- /***/ "isomorphic-dompurify"
1826
- /*!***************************************!*\
1827
- !*** external "isomorphic-dompurify" ***!
1828
- \***************************************/
1829
- (module) {
1830
-
1831
- "use strict";
1832
- module.exports = require("isomorphic-dompurify");
1833
-
1834
- /***/ },
1835
-
1836
- /***/ "scratch-render-fonts"
1837
- /*!***************************************!*\
1838
- !*** external "scratch-render-fonts" ***!
1839
- \***************************************/
1840
- (module) {
1841
-
1842
- "use strict";
1843
- module.exports = require("scratch-render-fonts");
1844
-
1845
- /***/ },
1846
-
1847
- /***/ "transformation-matrix"
1848
- /*!****************************************!*\
1849
- !*** external "transformation-matrix" ***!
1850
- \****************************************/
1851
- (module) {
1852
-
1853
- "use strict";
1854
- module.exports = require("transformation-matrix");
1855
-
1856
- /***/ },
1857
-
1858
- /***/ "tslog"
1859
- /*!************************!*\
1860
- !*** external "tslog" ***!
1861
- \************************/
1862
- (module) {
1863
-
1864
- "use strict";
1865
- module.exports = require("tslog");
1866
-
1867
- /***/ }
1868
-
1869
- /******/ });
1870
- /************************************************************************/
1871
- /******/ // The module cache
1872
- /******/ var __webpack_module_cache__ = {};
1873
- /******/
1874
- /******/ // The require function
1875
- /******/ function __webpack_require__(moduleId) {
1876
- /******/ // Check if module is in cache
1877
- /******/ var cachedModule = __webpack_module_cache__[moduleId];
1878
- /******/ if (cachedModule !== undefined) {
1879
- /******/ return cachedModule.exports;
1880
- /******/ }
1881
- /******/ // Create a new module (and put it into the cache)
1882
- /******/ var module = __webpack_module_cache__[moduleId] = {
1883
- /******/ // no module.id needed
1884
- /******/ // no module.loaded needed
1885
- /******/ exports: {}
1886
- /******/ };
1887
- /******/
1888
- /******/ // Execute the module function
1889
- /******/ if (!(moduleId in __webpack_modules__)) {
1890
- /******/ delete __webpack_module_cache__[moduleId];
1891
- /******/ var e = new Error("Cannot find module '" + moduleId + "'");
1892
- /******/ e.code = 'MODULE_NOT_FOUND';
1893
- /******/ throw e;
1894
- /******/ }
1895
- /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
1896
- /******/
1897
- /******/ // Return the exports of the module
1898
- /******/ return module.exports;
1899
- /******/ }
1900
- /******/
1901
- /************************************************************************/
1902
- /******/
1903
- /******/ // startup
1904
- /******/ // Load entry module and return exports
1905
- /******/ // This entry module is referenced by other modules so it can't be inlined
1906
- /******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
1907
- /******/
1908
- /******/ return __webpack_exports__;
1909
- /******/ })()
1910
- ;
1911
- });
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ScratchSVGRenderer=e():t.ScratchSVGRenderer=e()}(global,()=>{return t={619(t,e,r){const s=r(661);t.exports=class{constructor(t,e){this._makeImage=t||(()=>new Image),this._makeCanvas=e||(()=>document.createElement("canvas"))}resize(t,e,r){const s=this._makeCanvas();s.width=e,s.height=t.height;let n=s.getContext("2d");n.imageSmoothingEnabled=!1,n.drawImage(t,0,0,s.width,s.height);const i=this._makeCanvas();return i.width=e,i.height=r,n=i.getContext("2d"),n.imageSmoothingEnabled=!1,n.drawImage(s,0,0,i.width,i.height),i}convertResolution1Bitmap(t,e){const r=this._makeImage();r.src=t,r.onload=()=>{e(null,this.resize(r,2*r.width,2*r.height).toDataURL())},r.onerror=()=>{e("Image load failed")}}getResizedWidthHeight(t,e){const r=480,s=360;if(t<=r&&e<=s)return{width:2*t,height:2*e};if(t<=960&&e<=720)return{width:t,height:e};const n=t/e;return n>=1.3333333333333333?{width:960,height:960/n}:{width:720*n,height:720}}importBitmap(t,e){let r=t;return t instanceof ArrayBuffer&&(r=this.convertBinaryToDataURI(t,e)),new Promise((t,e)=>{const s=this._makeImage();s.src=r,s.onload=()=>{const e=this.getResizedWidthHeight(s.width,s.height);if(e.width===s.width&&e.height===s.height)t(this.convertDataURIToBinary(r));else{const r=this.resize(s,e.width,e.height).toDataURL();t(this.convertDataURIToBinary(r))}},s.onerror=()=>{e("Image load failed")}})}convertDataURIToBinary(t){const e=";base64,",r=t.indexOf(e)+8,s=t.substring(r),n=window.atob(s),i=n.length,a=new Uint8Array(new ArrayBuffer(i));for(let t=0;t<i;t++)a[t]=n.charCodeAt(t);return a}convertBinaryToDataURI(t,e){return"data:".concat(e,";base64,").concat(s.fromByteArray(new Uint8Array(t)))}}},247(t){t.exports=function(t){const e=t.match(/<svg [^>]*>/);e&&-1===e[0].indexOf("xmlns=")&&(t=t.replace("<svg ",'<svg xmlns="http://www.w3.org/2000/svg" ')),e&&-1!==e[0].indexOf("&ns_")&&-1===t.indexOf("<!DOCTYPE")&&(t=t.replace(e[0],e[0].replace(/&ns_[^;]+;/g,"http://ns.adobe.com/Extensibility/1.0/"))),t.includes("data:img/png")&&(t=t.replace(/(<image[^>]+?xlink:href=["'])data:img\/png/g,(t,e)=>"".concat(e,"data:image/png")));const r=/(<[^>]+?xmlns:(?!xml=)[^ ]+=)"http:\/\/www.w3.org\/XML\/1998\/namespace"/g;return null!==t.match(r)&&(t=t.replace(r,(t,e)=>"".concat(e,'"http://dummy.namespace"'))),t=(t=(t=t.replace(/<(\/?)\s*svg:/g,"<$1")).replace(/<metadata>[\s\S]*<\/metadata>/,"<metadata></metadata>")).replace(/<script[\s\S]*>[\s\S]*<\/script>/,"<script><\/script>")}},642(t){t.exports=function(t){const e=[],r=t=>{"text"===t.localName&&e.push(t);for(let e=0;e<t.childNodes.length;e++)r(t.childNodes[e])};r(t);for(const t of e)t.getAttribute("font-family")&&"Helvetica"!==t.getAttribute("font-family")?"Mystery"===t.getAttribute("font-family")?t.setAttribute("font-family","Curly"):"Gloria"===t.getAttribute("font-family")?t.setAttribute("font-family","Handwriting"):"Donegal"===t.getAttribute("font-family")&&t.setAttribute("font-family","Serif"):t.setAttribute("font-family","Sans Serif")}},519(t,e,r){const s=r(770);t.exports=function(t){const e=s();if("string"!=typeof t)throw new Error("SVG to be inlined is not a string");const r=new Set,n=/font-family="([^"]*)"/g;let i=n.exec(t);for(;i;)r.add(i[1]),i=n.exec(t);if(r.size>0){let s="<defs><style>";for(const t of r)Object.prototype.hasOwnProperty.call(e,t)&&(s+="".concat(e[t]));return s+="</style></defs>",t=t.replace(/<svg[^>]*>/,"$&".concat(s))}return t}},276(t,e,r){const s=r(940),n=r(619),i=r(519),a=r(707),o=r(958),c=r(253),l=r(735),u=r(642);t.exports={BitmapAdapter:n,convertFonts:u,inlineSvgFonts:i,loadSvgString:a,sanitizeSvg:o,serializeSvgToString:c,SvgElement:l,SVGRenderer:s}},707(t,e,r){const s=r(735),n=r(642),i=r(386),{sanitizeSvgText:a}=r(958),o=(t,e)=>{const r=[],s=t=>{t.localName!==e&&void 0!==e||!t.getAttribute||r.push(t);for(let e=0;e<t.childNodes.length;e++)s(t.childNodes[e])};return s(t),r},c=t=>{const e=document.createElement("span");let r,s;try{e.innerHTML=t.outerHTML,document.body.appendChild(e),r=e.children[0].getBBox()}finally{document.body.removeChild(e)}s=0===r.width||0===r.height?0:(t=>{let e=0;const r=t=>{t.getAttribute&&(t.getAttribute("stroke")&&(e=Math.max(e,1)),t.getAttribute("stroke-width")&&(e=Math.max(e,Number(t.getAttribute("stroke-width"))||0)));for(let e=0;e<t.childNodes.length;e++)r(t.childNodes[e])};return r(t),e})(t)/2;const n=r.width+2*s,i=r.height+2*s,a=r.x-s,o=r.y-s;t.setAttribute("width",n),t.setAttribute("height",i),t.setAttribute("viewBox","".concat(a," ").concat(o," ").concat(n," ").concat(i))},l=(t,e)=>{e&&(t=>{const e=o(t,"linearGradient");for(const t of e)t.getAttribute("x2")||t.setAttribute("x2","0")})(t),i(t,window),(t=>{const e=o(t,"image"),r="image-rendering: optimizespeed; image-rendering: pixelated;";for(const t of e)t.getAttribute("style")?t.setAttribute("style","".concat(r," ").concat(t.getAttribute("style"))):t.setAttribute("style",r)})(t),e?((t=>{const e=[],r=t=>{"text"===t.localName&&e.push(t);for(let e=0;e<t.childNodes.length;e++)r(t.childNodes[e])};r(t),n(t);for(const r of e){r.removeAttribute("x"),r.removeAttribute("y"),r.setAttribute("alignment-baseline","text-before-edge"),r.setAttribute("xml:space","preserve"),r.getAttribute("font-size")||r.setAttribute("font-size","18");let e=r.textContent;const n=parseFloat(r.getAttribute("font-size")),i=2;let a=0,o=1.2;if("Handwriting"===r.getAttribute("font-family")?(o=2,a=-11*n/22):"Scratch"===r.getAttribute("font-family")?(o=.89,a=-3*n/22):"Curly"===r.getAttribute("font-family")?(o=1.38,a=-6*n/22):"Marker"===r.getAttribute("font-family")?(o=1.45,a=-6*n/22):"Sans Serif"===r.getAttribute("font-family")?(o=1.13,a=-3*n/22):"Serif"===r.getAttribute("font-family")&&(o=1.25,a=-4*n/22),0===r.transform.baseVal.numberOfItems){const e=t.createSVGTransform();r.transform.baseVal.appendItem(e)}const c=r.transform.baseVal.getItem(0).matrix;if(c.e+=c.a*i+c.c*a,c.f+=c.b*i+c.d*a,e&&0===r.childElementCount){r.textContent="";const t=e.split("\n");e="";for(const e of t){const t=s.create("tspan");t.setAttribute("x","0"),t.setAttribute("style","white-space: pre"),t.setAttribute("dy","".concat(o,"em")),t.textContent=e||" ",r.appendChild(t)}}}})(t),c(t),(t=>{const e=o(t);for(const t of e){if(!t.style)continue;const e=t.style.stroke||t.getAttribute("stroke");e&&e.match(/^url\(#.*\)$/)&&(t.style["stroke-linejoin"]="round",t.style["stroke-linecap"]="round")}})(t)):t.getAttribute("viewBox")?t.getAttribute("width")&&t.getAttribute("height")||(t.setAttribute("width",t.viewBox.baseVal.width),t.setAttribute("height",t.viewBox.baseVal.height)):c(t)};t.exports=(t,e)=>{const r=new DOMParser,s=a(t),n=r.parseFromString(s,"text/xml");if(n.childNodes.length<1||"svg"!==n.documentElement.localName)throw new Error("Document does not appear to be SVG.");const i=n.documentElement;return l(i,e),i}},958(t,e,r){const s=r(247),{generate:n,parse:i,walk:a}=r(606),{ident:o}=r(368),c=r(975),l={},u=t=>t.startsWith("#")||t.toLowerCase().startsWith("data:"),h=t=>{const e=t.toLowerCase().replace(/\s/g,""),r=/url\((.+?)\)/g;let s;for(;null!==(s=r.exec(e));){const t=s[1].replace(/['"]/g,"");if(!u(t))return!0}return!1},d=t=>{let e=!1;return a(t,t=>{if("Url"===t.type){const r=t.value.trim().replace(/['"]/g,"");u(r)||(e=!0)}"Raw"===t.type&&h(t.value)&&(e=!0)}),e},m=(t,e)=>{const r=o.decode(t);try{return d(i(r,{context:e}))}catch(t){return h(r)}},g=new Set(["href","xlink:href"]);let f,b;if(c.addHook("beforeSanitizeAttributes",t=>{if(!t||!t.attributes)return t;for(let e=t.attributes.length-1;e>=0;e--){const r=t.attributes[e];if(r.value)if(g.has(r.name))u(r.value.replace(/\s/g,""))||t.removeAttribute(r.name);else{const e="style"===r.name?"declarationList":"value";m(r.value,e)&&t.removeAttribute(r.name)}}return t}),c.addHook("uponSanitizeElement",(t,e)=>{if("style"===e.tagName)try{const e=o.decode(t.textContent),r=i(e);let s=e!==t.textContent;a(r,(t,e,r)=>{"Atrule"===t.type&&"import"===t.name.toLowerCase()&&(r.remove(e),s=!0),"Declaration"===t.type&&t.value&&d(t.value)&&(r.remove(e),s=!0)}),s&&(t.textContent=n(r))}catch(e){t.textContent=""}}),"undefined"==typeof TextDecoder||"undefined"==typeof TextEncoder){const t=r(980);f=t.TextDecoder,b=t.TextEncoder}else f=TextDecoder,b=TextEncoder;l.sanitizeByteStream=function(t){const e=new f,r=new b,s=l.sanitizeSvgText(e.decode(t));return r.encode(s)},l.sanitizeSvgText=function(t){let e=c.sanitize(t,{USE_PROFILES:{svg:!0},FORBID_TAGS:["a","audio","canvas","video"],ADD_DATA_URI_TAGS:["image"]});return e.indexOf("]&gt;")>=0&&(e=e.substring(5,e.length)),e=s(e),e},t.exports=l},253(t,e,r){const s=r(519);t.exports=(t,e)=>{let r=(new XMLSerializer).serializeToString(t);return e&&(r=s(r)),r}},735(t){class e{static get svg(){return"http://www.w3.org/2000/svg"}static get xmlns(){return"http://www.w3.org/2000/xmlns"}static get xlink(){return"http://www.w3.org/1999/xlink"}static attributeNamespace(){return{href:e.xlink,xlink:e.xmlns,xmlns:"".concat(e.xmlns,"/"),"xmlns:xlink":"".concat(e.xmlns,"/")}}static create(t,r,s){return e.set(document.createElementNS(e.svg,t),r,s)}static get(t,r){const s=e.attributeNamespace[r],n=s?t.getAttributeNS(s,r):t.getAttribute(r);return"null"===n?null:n}static set(t,r,s){for(const n in r){let i=r[n];const a=e.attributeNamespace[n];"number"==typeof i&&s&&(i=s.number(i)),a?t.setAttributeNS(a,n,i):t.setAttribute(n,i)}return t}}t.exports=e},940(t,e,r){const s=r(707),n=r(253);t.exports=class{constructor(t){this._canvas=t||document.createElement("canvas"),this._context=this._canvas.getContext("2d"),this._measurements={x:0,y:0,width:0,height:0},this._cachedImage=null,this.loaded=!1}get canvas(){return this._canvas}get size(){return[this._measurements.width,this._measurements.height]}get viewOffset(){return[this._measurements.x,this._measurements.y]}loadString(t,e){this._cachedImage=null;const r=s(t,e);this._svgTag=r,this._measurements={width:r.viewBox.baseVal.width,height:r.viewBox.baseVal.height,x:r.viewBox.baseVal.x,y:r.viewBox.baseVal.y}}loadSVG(t,e,r){this.loadString(t,e),this._createSVGImage(r)}_createSVGImage(t){null===this._cachedImage&&(this._cachedImage=new Image);const e=this._cachedImage;e.onload=()=>{this.loaded=!0,t&&t()};const r=this.toString(!0);e.src="data:image/svg+xml;utf8,".concat(encodeURIComponent(r)),this.loaded=!1}toString(t){return n(this._svgTag,t)}draw(t){if(!this.loaded)throw new Error("SVG image has not finished loading");this._drawFromImage(t)}_drawFromImage(t){if(null===this._cachedImage)return;const e=Number.isFinite(t)?t:1,r=this._measurements;this._canvas.width=r.width*e,this._canvas.height=r.height*e,this._canvas.width<=0||this._canvas.height<=0||this._cachedImage.naturalWidth<=0||this._cachedImage.naturalHeight<=0||(this._context.clearRect(0,0,this._canvas.width,this._canvas.height),this._context.setTransform(e,0,0,e,0,0),this._context.drawImage(this._cachedImage,0,0))}}},386(t,e,r){const s=r(853),n=r(735),i=r(88),a=function(t){let e=s.identity();const r=t.attributes&&t.attributes.transform&&t.attributes.transform.value;if(!r)return e;const n=r.split(/\)\s*/g);for(const t of n){if(!t)break;const r=t.split(/\(\s*/),n=r[0].trim(),a=r[1].split(/[\s,]+/g);for(let t=0;t<a.length;t++)a[t]=parseFloat(a[t]);switch(n){case"matrix":e=s.compose(e,{a:a[0],b:a[1],c:a[2],d:a[3],e:a[4],f:a[5]});break;case"rotate":e=s.compose(e,s.rotateDEG(a[0],a[1]||0,a[2]||0));break;case"translate":e=s.compose(e,s.translate(a[0],a[1]||0));break;case"scale":e=s.compose(e,s.scale(a[0],a[1]||a[0]));break;case"skewX":e=s.compose(e,s.skewDEG(a[0],0));break;case"skewY":e=s.compose(e,s.skewDEG(0,a[0]));break;default:i.error("Couldn't parse: ".concat(n))}}return e},o=function(t){const e=t.a,r=t.b,s=t.c,n=t.d,i=e*n-r*s;if(0!==e||0!==r){const t=Math.sqrt(e*e+r*r);return{x:t,y:i/t}}if(0!==s||0!==n){const t=Math.sqrt(s*s+n*n);return{x:i/t,y:t}}return{x:0,y:0}},c=function(t,e,r,s){r=-r*Math.PI/180;const n=s.a,i=-s.c,a=-s.b,o=s.d,c=n*o-i*a;if(0===c)return null;const l=Math.sin(r),u=Math.cos(r),h=Math.sin(2*r),d=u*u/t/t+l*l/e/e,m=h/t/t-h/e/e,g=l*l/t/t+u*u/e/e,f=d*o*o-m*o*a+g*a*a,b=-2*d*i*o+m*n*o+m*i*a-2*g*n*a,x=d*i*i-m*n*i+g*n*n,p=Math.sqrt(2)*Math.sqrt((f+x-Math.sqrt(f*f+b*b-2*f*x+x*x))/(-b*b+4*f*x)),y=1/Math.sqrt(f+x-1/p/p);let w=(f-1/p/p)/(1/y/y-1/p/p);w<0&&Math.abs(w)<1e-8&&(w=0),w=Math.sqrt(w),Math.abs(1-w)<1e-8&&(w=1);let v=Math.asin(w);w=b/(1/p/p-1/y/y);const A=-v;return Math.abs(Math.sin(2*A)-w)<Math.abs(Math.sin(2*v)-w)&&(v=A),{radiusX:p*c,radiusY:y*c,rotation:180*-v/Math.PI}},l=["circle","ellipse","image","line","path","polygon","polyline","rect","text","use"],u=["a","defs","g","marker","glyph","missing-glyph","pattern","svg","switch","symbol"],h=function(t,e){return Math.sqrt((t*t+e*e)/2)},d=function(t,e,r,i){const a=function(t,e,s,i,a,o){let c,l=n.get(t,e);return null===l?o?(c=o,/%\s*$/.test(c)&&(l=o,c=parseFloat(l))):c=i?null:s?"":0:c=s?l:parseFloat(l),/%\s*$/.test(l)?c/100*(a?1:r[/x|^width/.test(e)?"width":"height"]):c},c=function(t,e,r,s,n,i,o){return e=a(t,e||"x",!1,s,n,i),r=a(t,r||"y",!1,s,n,o),!s||null!==e&&null!==r?{x:e,y:r}:null};let l=e.getElementsByTagName("defs");0===l.length?(l=n.create("defs"),e.appendChild(l)):l=l[0];const u=e.getElementById(t);if(!u)return;const d="radialgradient"===u.tagName.toLowerCase(),m=e.getElementById(t).cloneNode(!0);let g=s.toString(i);g=g.substring(8,g.length-1);const f="".concat(t,"-").concat(g);if(m.setAttribute("id",f),e.getElementById(f))return"url(#".concat(f,")");const b="userSpaceOnUse"!==a(m,"gradientUnits",!0);let x,p,y,w;if(d)x=c(m,"cx","cy",!1,b,"50%","50%"),y=a(m,"r",!1,!1,b,"50%"),w=c(m,"fx","fy",!0,b);else if(x=c(m,"x1","y1",!1,b),p=c(m,"x2","y2",!1,b,"1"),x.x===p.x&&x.y===p.y){const t=m.getElementsByTagName("stop");return t.length&&t[t.length-1].attributes&&t[t.length-1].attributes["stop-color"]?t[t.length-1].attributes["stop-color"].value:null}if(b){const t=s.compose(s.translate(r.x,r.y),s.scale(r.width,r.height));x=s.applyToPoint(t,x),p&&(p=s.applyToPoint(t,p)),y&&(y=h(r.width,r.height)*y),w&&(w=s.applyToPoint(t,w))}if(d){x=s.applyToPoint(i,x);const t=o(i);y=h(t.x,t.y)*y,w&&(w=s.applyToPoint(i,w))}else{const t=(t,e)=>t.x*e.x+t.y*e.y,e=(t,e)=>({x:t*e.x,y:t*e.y}),r=(t,e)=>({x:t.x+e.x,y:t.y+e.y}),n=(t,e)=>({x:t.x-e.x,y:t.y-e.y});let a=Math.abs(x.x-p.x)<1e-8?r(x,{x:1,y:(x.x-p.x)/(p.y-x.y)}):r(x,{x:(p.y-x.y)/(x.x-p.x),y:1});a=s.applyToPoint(i,a),x=s.applyToPoint(i,x),p=s.applyToPoint(i,p);const o=n(a,x),c=n(p,x),l=Math.abs(o.x)<1e-8?{x:1,y:-o.x/o.y}:{x:-o.y/o.x,y:1},u=e(t(c,l)/t(l,l),l);p={x:x.x+u.x,y:x.y+u.y}}return d?(m.setAttribute("cx",Number(x.x.toFixed(4))),m.setAttribute("cy",Number(x.y.toFixed(4))),m.setAttribute("r",Number(y.toFixed(4))),w&&(m.setAttribute("fx",Number(w.x.toFixed(4))),m.setAttribute("fy",Number(w.y.toFixed(4))))):(m.setAttribute("x1",Number(x.x.toFixed(4))),m.setAttribute("y1",Number(x.y.toFixed(4))),m.setAttribute("x2",Number(p.x.toFixed(4))),m.setAttribute("y2",Number(p.y.toFixed(4)))),m.setAttribute("gradientUnits","userSpaceOnUse"),l.appendChild(m),"url(#".concat(f,")")},m=(t,e)=>{const r=t&&t.match(/\((?:["'#]*)([^"')]+)/),s=r&&r[1];return s&&e?s.replace("".concat(e.location.href.split("#")[0],"#"),""):s};t.exports=function(t,e,r){const i=s.identity(),g=(i,f,b,x,p)=>{if(function(t){return t.tagName&&u.includes(t.tagName.toLowerCase())}(i)){i.attributes["stroke-width"]&&(b=i.attributes["stroke-width"].value),i.attributes&&(i.attributes.fill&&(x=i.attributes.fill.value),i.attributes.stroke&&(p=i.attributes.stroke.value));for(let t=0;t<i.childNodes.length;t++)g(i.childNodes[t],s.compose(f,a(i)),b,x,p);i.removeAttribute("transform"),i.removeAttribute("stroke-width"),i.removeAttribute("fill"),i.removeAttribute("stroke")}else if(function(t,e){return!!t.attributes&&(e=t.attributes["stroke-width"]?Number(t.attributes["stroke-width"].value):Number(e))&&t.tagName&&"path"===t.tagName.toLowerCase()&&t.attributes.d&&t.attributes.d.value}(i,b)){if(i.attributes["stroke-width"]&&(b=i.attributes["stroke-width"].value),i.attributes.fill&&(x=i.attributes.fill.value),i.attributes.stroke&&(p=i.attributes.stroke.value),f=s.compose(f,a(i)),s.toString(f)===s.toString(s.identity()))return i.removeAttribute("transform"),i.setAttribute("stroke-width",b),x&&i.setAttribute("fill",x),void(p&&i.setAttribute("stroke",p));const l=m(x,e),u=m(p,e);if(l||u){const s=e.document,a=s.createElement("span");let o;if(r)o=r;else try{s.body.appendChild(a);const t=n.set(s.createElementNS(n.svg,"svg")),e=n.set(s.createElementNS(n.svg,"path"));e.setAttribute("d",i.attributes.d.value),t.appendChild(e),a.appendChild(t),o=t.getBBox()}finally{s.body.removeChild(a)}if(l){const e=d(l,t,o,f);e&&(x=e)}if(u){const e=d(u,t,o,f);e&&(p=e)}}i.setAttribute("d",function(t,e){if(!e||s.toString(e)===s.toString(s.identity()))return t;const r=t&&t.match(/[mlhvcsqtaz][^mlhvcsqtaz]*/gi);let n,i,a,l=!1,u={x:0,y:0},h={x:0,y:0},d="";const m=function(t,e){let r=+n[t];return l&&(r+=u[e]),r},g=function(t){return{x:m(t,"x"),y:m(t+1,"y")}},f=function(t){return Number(t.toFixed(4))},b=function(t){const r=s.applyToPoint(e,t);return"".concat(f(r.x)," ").concat(f(r.y)," ")};for(let t=0,s=r&&r.length;t<s;t++){const s=r[t],x=s[0],p=x.toLowerCase();n=s.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);const y=n&&n.length;switch(l=x===p,"z"!==i||/[mz]/.test(p)||(d+="M ".concat(u.x," ").concat(u.y," ")),p){case"m":case"l":{let t="m"===p;for(let e=0;e<y;e+=2)d+=t?"M ":"L ",u=g(e),d+=b(u),t&&(h=u,t=!1);a=u;break}case"h":case"v":{const t="h"===p?"x":"y";u={x:u.x,y:u.y};for(let e=0;e<y;e++)u[t]=m(e,t),d+="L ".concat(b(u));a=u;break}case"c":for(let t=0;t<y;t+=6){const e=g(t);a=g(t+2),u=g(t+4),d+="C ".concat(b(e)).concat(b(a)).concat(b(u))}break;case"s":for(let t=0;t<y;t+=4){const e=/[cs]/.test(i)?{x:2*u.x-a.x,y:2*u.y-a.y}:u;a=g(t),u=g(t+2),d+="C ".concat(b(e)).concat(b(a)).concat(b(u)),i=p}break;case"q":for(let t=0;t<y;t+=4)a=g(t),u=g(t+2),d+="Q ".concat(b(a)).concat(b(u));break;case"t":for(let t=0;t<y;t+=2)a=/[qt]/.test(i)?{x:2*u.x-a.x,y:2*u.y-a.y}:u,u=g(t),d+="Q ".concat(b(a)).concat(b(u)),i=p;break;case"a":for(let t=0;t<y;t+=7){u=g(t+5);const r=+n[t],s=+n[t+1],i=+n[t+2],a=+n[t+3];let l=+n[t+4];const h=c(r,s,i,e),m=o(e);h?((m.x>0&&m.y<0||m.x<0&&m.y>0)&&(l^=1),d+="A ".concat(f(Math.abs(h.radiusX))," ")+"".concat(f(Math.abs(h.radiusY))," ")+"".concat(f(h.rotation)," ").concat(a," ")+"".concat(l," ").concat(b(u))):d+="L ".concat(b(u))}break;case"z":d+="Z ",u=h}i=p}return d}(i.attributes.d.value,f)),i.removeAttribute("transform");const g=o(f);i.setAttribute("stroke-width",h(g.x,g.y)*b),x&&i.setAttribute("fill",x),p&&i.setAttribute("stroke",p)}else(function(t){return t.tagName&&l.includes(t.tagName.toLowerCase())})(i)&&(b&&!i.attributes["stroke-width"]&&i.setAttribute("stroke-width",b),x&&!i.attributes.fill&&i.setAttribute("fill",x),p&&!i.attributes.stroke&&i.setAttribute("stroke",p),f=s.compose(f,a(i)),s.toString(f)===s.toString(s.identity())?i.removeAttribute("transform"):i.setAttribute("transform",s.toString(f)))};g(t,i,1)}},88(t,e,r){const{Logger:s}=r(441);t.exports=new s({name:"scratch-svg-renderer"})},661(t){"use strict";t.exports=require("base64-js")},606(t){"use strict";t.exports=require("css-tree")},368(t){"use strict";t.exports=require("css-tree/utils")},980(t){"use strict";t.exports=require("fastestsmallesttextencoderdecoder")},975(t){"use strict";t.exports=require("isomorphic-dompurify")},770(t){"use strict";t.exports=require("scratch-render-fonts")},853(t){"use strict";t.exports=require("transformation-matrix")},441(t){"use strict";t.exports=require("tslog")}},e={},function r(s){var n=e[s];if(void 0!==n)return n.exports;var i=e[s]={exports:{}};return t[s](i,i.exports,r),i.exports}(276);var t,e});
1912
2
  //# sourceMappingURL=scratch-svg-renderer.js.map