@scratch/scratch-svg-renderer 11.0.0-UEPR-176

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.
@@ -0,0 +1 @@
1
+ /*! @license DOMPurify 3.2.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.4/LICENSE */
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scratch-svg-renderer.js","mappings":";AAAA","sources":["webpack://ScratchSVGRenderer/webpack/universalModuleDefinition"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ScratchSVGRenderer\"] = factory();\n\telse\n\t\troot[\"ScratchSVGRenderer\"] = factory();\n})(self, () => {\nreturn "],"names":[],"sourceRoot":""}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@scratch/scratch-svg-renderer",
3
+ "version": "11.0.0-UEPR-176",
4
+ "description": "SVG renderer for Scratch",
5
+ "main": "./dist/node/scratch-svg-renderer.js",
6
+ "browser": "./dist/web/scratch-svg-renderer.js",
7
+ "exports": {
8
+ "webpack": "./src/index.js",
9
+ "browser": "./dist/web/scratch-svg-renderer.js",
10
+ "node": "./dist/node/scratch-svg-renderer.js",
11
+ "default": "./src/index.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "build": "npm run clean && webpack",
19
+ "clean": "rimraf dist playground",
20
+ "start": "webpack-dev-server",
21
+ "test": "npm run test:lint && npm run test:unit",
22
+ "test:lint": "eslint . --ext .js",
23
+ "test:unit": "tap ./test/*.js",
24
+ "watch": "webpack --watch"
25
+ },
26
+ "author": "Massachusetts Institute of Technology",
27
+ "license": "AGPL-3.0-only",
28
+ "homepage": "https://github.com/scratchfoundation/scratch-svg-renderer#readme",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/scratchfoundation/scratch-editor.git"
32
+ },
33
+ "peerDependencies": {
34
+ "scratch-render-fonts": "^1.0.0"
35
+ },
36
+ "tap": {
37
+ "branches": 70,
38
+ "functions": 50,
39
+ "lines": 70,
40
+ "statements": 70
41
+ },
42
+ "dependencies": {
43
+ "base64-js": "1.5.1",
44
+ "base64-loader": "1.0.0",
45
+ "css-tree": "1.1.3",
46
+ "fastestsmallesttextencoderdecoder": "1.0.22",
47
+ "isomorphic-dompurify": "2.4.0",
48
+ "minilog": "3.1.0",
49
+ "transformation-matrix": "1.15.3"
50
+ },
51
+ "devDependencies": {
52
+ "@babel/core": "7.26.10",
53
+ "@babel/eslint-parser": "7.26.10",
54
+ "@babel/preset-env": "7.26.9",
55
+ "babel-loader": "9.2.1",
56
+ "copy-webpack-plugin": "4.6.0",
57
+ "eslint": "8.57.1",
58
+ "eslint-config-scratch": "9.0.9",
59
+ "eslint-plugin-import": "2.31.0",
60
+ "jsdom": "13.2.0",
61
+ "json": "9.0.6",
62
+ "mkdirp": "2.1.6",
63
+ "rimraf": "3.0.2",
64
+ "scratch-render-fonts": "1.0.177",
65
+ "scratch-semantic-release-config": "3.0.0",
66
+ "scratch-webpack-configuration": "3.0.0",
67
+ "semantic-release": "19.0.5",
68
+ "tap": "16.3.10",
69
+ "webpack": "5.98.0",
70
+ "webpack-cli": "5.1.4",
71
+ "webpack-dev-server": "5.2.0",
72
+ "xmldom": "0.1.31"
73
+ },
74
+ "browserslist": [
75
+ "Chrome >= 63",
76
+ "Edge >= 15",
77
+ "Firefox >= 57",
78
+ "Safari >= 11"
79
+ ]
80
+ }
@@ -0,0 +1,156 @@
1
+ const base64js = require('base64-js');
2
+
3
+ /**
4
+ * Adapts Scratch 2.0 bitmaps for use in scratch 3.0
5
+ */
6
+ class BitmapAdapter {
7
+ /**
8
+ * @param {?function} makeImage HTML image constructor. Tests can provide this.
9
+ * @param {?function} makeCanvas HTML canvas constructor. Tests can provide this.
10
+ */
11
+ constructor (makeImage, makeCanvas) {
12
+ this._makeImage = makeImage ? makeImage : () => new Image();
13
+ this._makeCanvas = makeCanvas ? makeCanvas : () => document.createElement('canvas');
14
+ }
15
+
16
+ /**
17
+ * Return a canvas with the resized version of the given image, done using nearest-neighbor interpolation
18
+ * @param {CanvasImageSource} image The image to resize
19
+ * @param {int} newWidth The desired post-resize width of the image
20
+ * @param {int} newHeight The desired post-resize height of the image
21
+ * @returns {HTMLCanvasElement} A canvas with the resized image drawn on it.
22
+ */
23
+ resize (image, newWidth, newHeight) {
24
+ // We want to always resize using nearest-neighbor interpolation. However, canvas implementations are free to
25
+ // use linear interpolation (or other "smooth" interpolation methods) when downscaling:
26
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1360415
27
+ // It seems we can get around this by resizing in two steps: first width, then height. This will always result
28
+ // in nearest-neighbor interpolation, even when downscaling.
29
+ const stretchWidthCanvas = this._makeCanvas();
30
+ stretchWidthCanvas.width = newWidth;
31
+ stretchWidthCanvas.height = image.height;
32
+ let context = stretchWidthCanvas.getContext('2d');
33
+ context.imageSmoothingEnabled = false;
34
+ context.drawImage(image, 0, 0, stretchWidthCanvas.width, stretchWidthCanvas.height);
35
+ const stretchHeightCanvas = this._makeCanvas();
36
+ stretchHeightCanvas.width = newWidth;
37
+ stretchHeightCanvas.height = newHeight;
38
+ context = stretchHeightCanvas.getContext('2d');
39
+ context.imageSmoothingEnabled = false;
40
+ context.drawImage(stretchWidthCanvas, 0, 0, stretchHeightCanvas.width, stretchHeightCanvas.height);
41
+ return stretchHeightCanvas;
42
+ }
43
+
44
+ /**
45
+ * Scratch 2.0 had resolution 1 and 2 bitmaps. All bitmaps in Scratch 3.0 are equivalent
46
+ * to resolution 2 bitmaps. Therefore, converting a resolution 1 bitmap means doubling
47
+ * it in width and height.
48
+ * @param {!string} dataURI Base 64 encoded image data of the bitmap
49
+ * @param {!function} callback Node-style callback that returns updated dataURI if conversion succeeded
50
+ */
51
+ convertResolution1Bitmap (dataURI, callback) {
52
+ const image = this._makeImage();
53
+ image.src = dataURI;
54
+ image.onload = () => {
55
+ callback(null, this.resize(image, image.width * 2, image.height * 2).toDataURL());
56
+ };
57
+ image.onerror = () => {
58
+ callback('Image load failed');
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Given width/height of an uploaded item, return width/height the image will be resized
64
+ * to in Scratch 3.0
65
+ * @param {!number} oldWidth original width
66
+ * @param {!number} oldHeight original height
67
+ * @return {object} Array of new width, new height
68
+ */
69
+ getResizedWidthHeight (oldWidth, oldHeight) {
70
+ const STAGE_WIDTH = 480;
71
+ const STAGE_HEIGHT = 360;
72
+ const STAGE_RATIO = STAGE_WIDTH / STAGE_HEIGHT;
73
+
74
+ // If both dimensions are smaller than or equal to corresponding stage dimension,
75
+ // double both dimensions
76
+ if ((oldWidth <= STAGE_WIDTH) && (oldHeight <= STAGE_HEIGHT)) {
77
+ return {width: oldWidth * 2, height: oldHeight * 2};
78
+ }
79
+
80
+ // If neither dimension is larger than 2x corresponding stage dimension,
81
+ // this is an in-between image, return it as is
82
+ if ((oldWidth <= STAGE_WIDTH * 2) && (oldHeight <= STAGE_HEIGHT * 2)) {
83
+ return {width: oldWidth, height: oldHeight};
84
+ }
85
+
86
+ const imageRatio = oldWidth / oldHeight;
87
+ // Otherwise, figure out how to resize
88
+ if (imageRatio >= STAGE_RATIO) {
89
+ // Wide Image
90
+ return {width: STAGE_WIDTH * 2, height: STAGE_WIDTH * 2 / imageRatio};
91
+ }
92
+ // In this case we have either:
93
+ // - A wide image, but not with as big a ratio between width and height,
94
+ // making it so that fitting the width to double stage size would leave
95
+ // the height too big to fit in double the stage height
96
+ // - A square image that's still larger than the double at least
97
+ // one of the stage dimensions, so pick the smaller of the two dimensions (to fit)
98
+ // - A tall image
99
+ // In any of these cases, resize the image to fit the height to double the stage height
100
+ return {width: STAGE_HEIGHT * 2 * imageRatio, height: STAGE_HEIGHT * 2};
101
+ }
102
+
103
+ /**
104
+ * Given bitmap data, resize as necessary.
105
+ * @param {ArrayBuffer | string} fileData Base 64 encoded image data of the bitmap
106
+ * @param {string} fileType The MIME type of this file
107
+ * @returns {Promise} Resolves to resized image data Uint8Array
108
+ */
109
+ importBitmap (fileData, fileType) {
110
+ let dataURI = fileData;
111
+ if (fileData instanceof ArrayBuffer) {
112
+ dataURI = this.convertBinaryToDataURI(fileData, fileType);
113
+ }
114
+ return new Promise((resolve, reject) => {
115
+ const image = this._makeImage();
116
+ image.src = dataURI;
117
+ image.onload = () => {
118
+ const newSize = this.getResizedWidthHeight(image.width, image.height);
119
+ if (newSize.width === image.width && newSize.height === image.height) {
120
+ // No change
121
+ resolve(this.convertDataURIToBinary(dataURI));
122
+ } else {
123
+ const resizedDataURI = this.resize(image, newSize.width, newSize.height).toDataURL();
124
+ resolve(this.convertDataURIToBinary(resizedDataURI));
125
+ }
126
+ };
127
+ image.onerror = () => {
128
+ // TODO: reject with an Error (breaking API change!)
129
+ // eslint-disable-next-line prefer-promise-reject-errors
130
+ reject('Image load failed');
131
+ };
132
+ });
133
+ }
134
+
135
+ // TODO consolidate with scratch-vm/src/util/base64-util.js
136
+ // From https://gist.github.com/borismus/1032746
137
+ convertDataURIToBinary (dataURI) {
138
+ const BASE64_MARKER = ';base64,';
139
+ const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
140
+ const base64 = dataURI.substring(base64Index);
141
+ const raw = window.atob(base64);
142
+ const rawLength = raw.length;
143
+ const array = new Uint8Array(new ArrayBuffer(rawLength));
144
+
145
+ for (let i = 0; i < rawLength; i++) {
146
+ array[i] = raw.charCodeAt(i);
147
+ }
148
+ return array;
149
+ }
150
+
151
+ convertBinaryToDataURI (arrayBuffer, contentType) {
152
+ return `data:${contentType};base64,${base64js.fromByteArray(new Uint8Array(arrayBuffer))}`;
153
+ }
154
+ }
155
+
156
+ module.exports = BitmapAdapter;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Fixup svg string prior to parsing.
3
+ * @param {!string} svgString String of the svg to fix.
4
+ * @returns {!string} fixed svg that should be parseable.
5
+ */
6
+ module.exports = function (svgString) {
7
+ // Add root svg namespace if it does not exist.
8
+ const svgAttrs = svgString.match(/<svg [^>]*>/);
9
+ if (svgAttrs && svgAttrs[0].indexOf('xmlns=') === -1) {
10
+ svgString = svgString.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
11
+ }
12
+
13
+ // There are some SVGs from Illustrator that use undeclared entities.
14
+ // Just replace those entities with fake namespace references to prevent
15
+ // DOMParser from crashing
16
+ if (svgAttrs && svgAttrs[0].indexOf('&ns_') !== -1 && svgString.indexOf('<!DOCTYPE') === -1) {
17
+ svgString = svgString.replace(svgAttrs[0],
18
+ svgAttrs[0].replace(/&ns_[^;]+;/g, 'http://ns.adobe.com/Extensibility/1.0/'));
19
+ }
20
+
21
+ // Some SVGs exported from Photoshop have been found to have an invalid mime type
22
+ // Chrome and Safari won't render these SVGs, so we correct it here
23
+ if (svgString.includes('data:img/png')) {
24
+ svgString = svgString.replace(
25
+ // capture entire image tag with xlink:href=and the quote - dont capture data: bit
26
+ /(<image[^>]+?xlink:href=["'])data:img\/png/g,
27
+ // use the captured <image ..... xlink:href=" then append the right data uri mime type
28
+ ($0, $1) => `${$1}data:image/png`
29
+ );
30
+ }
31
+
32
+ // Some SVGs from Inkscape attempt to bind a prefix to a reserved namespace name.
33
+ // This will cause SVG parsing to fail, so replace these with a dummy namespace name.
34
+ // This namespace name is only valid for "xml", and if we bind "xmlns:xml" to the dummy namespace,
35
+ // parsing will fail yet again, so exclude "xmlns:xml" declarations.
36
+ const xmlnsRegex = /(<[^>]+?xmlns:(?!xml=)[^ ]+=)"http:\/\/www.w3.org\/XML\/1998\/namespace"/g;
37
+ if (svgString.match(xmlnsRegex) !== null) {
38
+ svgString = svgString.replace(
39
+ // capture the entire attribute
40
+ xmlnsRegex,
41
+ // use the captured attribute name; replace only the URL
42
+ ($0, $1) => `${$1}"http://dummy.namespace"`
43
+ );
44
+ }
45
+
46
+ // Strip `svg:` prefix (sometimes added by Inkscape) from all tags. They interfere with DOMPurify (prefixed tag
47
+ // names are not recognized) and the paint editor.
48
+ // This matches opening and closing tags--the capture group captures the slash if it exists, and it is reinserted
49
+ // in the replacement text.
50
+ svgString = svgString.replace(/<(\/?)\s*svg:/g, '<$1');
51
+
52
+ // The <metadata> element is not needed for rendering and sometimes contains
53
+ // unparseable garbage from Illustrator :( Empty out the contents.
54
+ // Note: [\s\S] matches everything including newlines, which .* does not
55
+ svgString = svgString.replace(/<metadata>[\s\S]*<\/metadata>/, '<metadata></metadata>');
56
+
57
+ // Empty script tags and javascript executing
58
+ svgString = svgString.replace(/<script[\s\S]*>[\s\S]*<\/script>/, '<script></script>');
59
+
60
+ return svgString;
61
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @fileOverview Convert 2.0 fonts to 3.0 fonts.
3
+ */
4
+
5
+ /**
6
+ * Given an SVG, replace Scratch 2.0 fonts with new 3.0 fonts. Add defaults where there are none.
7
+ * @param {SVGElement} svgTag The SVG dom object
8
+ * @return {void}
9
+ */
10
+ const convertFonts = function (svgTag) {
11
+ // Collect all text elements into a list.
12
+ const textElements = [];
13
+ const collectText = domElement => {
14
+ if (domElement.localName === 'text') {
15
+ textElements.push(domElement);
16
+ }
17
+ for (let i = 0; i < domElement.childNodes.length; i++) {
18
+ collectText(domElement.childNodes[i]);
19
+ }
20
+ };
21
+ collectText(svgTag);
22
+ // If there's an old font-family, switch to the new one.
23
+ for (const textElement of textElements) {
24
+ // If there's no font-family provided, provide one.
25
+ if (!textElement.getAttribute('font-family') ||
26
+ textElement.getAttribute('font-family') === 'Helvetica') {
27
+ textElement.setAttribute('font-family', 'Sans Serif');
28
+ } else if (textElement.getAttribute('font-family') === 'Mystery') {
29
+ textElement.setAttribute('font-family', 'Curly');
30
+ } else if (textElement.getAttribute('font-family') === 'Gloria') {
31
+ textElement.setAttribute('font-family', 'Handwriting');
32
+ } else if (textElement.getAttribute('font-family') === 'Donegal') {
33
+ textElement.setAttribute('font-family', 'Serif');
34
+ }
35
+ }
36
+ };
37
+
38
+ module.exports = convertFonts;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileOverview Import bitmap data into Scratch 3.0, resizing image as necessary.
3
+ */
4
+ const getFonts = require('scratch-render-fonts');
5
+
6
+ /**
7
+ * Given SVG data, inline the fonts. This allows them to be rendered correctly when set
8
+ * as the source of an HTMLImageElement. Here is a note from tmickel:
9
+ * // Inject fonts that are needed.
10
+ * // It would be nice if there were another way to get the SVG-in-canvas
11
+ * // to render the correct font family, but I couldn't find any other way.
12
+ * // Other things I tried:
13
+ * // Just injecting the font-family into the document: no effect.
14
+ * // External stylesheet linked to by SVG: no effect.
15
+ * // Using a <link> or <style>@import</style> to link to font-family
16
+ * // injected into the document: no effect.
17
+ * @param {string} svgString The string representation of the svg to modify
18
+ * @return {string} The svg with any needed fonts inlined
19
+ */
20
+ const inlineSvgFonts = function (svgString) {
21
+ const FONTS = getFonts();
22
+ // Make it clear that this function only operates on strings.
23
+ // If we don't explicitly throw this here, the function silently fails.
24
+ if (typeof svgString !== 'string') {
25
+ throw new Error('SVG to be inlined is not a string');
26
+ }
27
+
28
+ // Collect fonts that need injection.
29
+ const fontsNeeded = new Set();
30
+ const fontRegex = /font-family="([^"]*)"/g;
31
+ let matches = fontRegex.exec(svgString);
32
+ while (matches) {
33
+ fontsNeeded.add(matches[1]);
34
+ matches = fontRegex.exec(svgString);
35
+ }
36
+ if (fontsNeeded.size > 0) {
37
+ let str = '<defs><style>';
38
+ for (const font of fontsNeeded) {
39
+ if (Object.prototype.hasOwnProperty.call(FONTS, font)) {
40
+ str += `${FONTS[font]}`;
41
+ }
42
+ }
43
+ str += '</style></defs>';
44
+ svgString = svgString.replace(/<svg[^>]*>/, `$&${str}`);
45
+ return svgString;
46
+ }
47
+ return svgString;
48
+ };
49
+
50
+ module.exports = inlineSvgFonts;
package/src/index.js ADDED
@@ -0,0 +1,22 @@
1
+ const SVGRenderer = require('./svg-renderer');
2
+ const BitmapAdapter = require('./bitmap-adapter');
3
+ const inlineSvgFonts = require('./font-inliner');
4
+ const loadSvgString = require('./load-svg-string');
5
+ const sanitizeSvg = require('./sanitize-svg');
6
+ const serializeSvgToString = require('./serialize-svg-to-string');
7
+ const SvgElement = require('./svg-element');
8
+ const convertFonts = require('./font-converter');
9
+ // /**
10
+ // * Export for NPM & Node.js
11
+ // * @type {RenderWebGL}
12
+ // */
13
+ module.exports = {
14
+ BitmapAdapter: BitmapAdapter,
15
+ convertFonts: convertFonts,
16
+ inlineSvgFonts: inlineSvgFonts,
17
+ loadSvgString: loadSvgString,
18
+ sanitizeSvg: sanitizeSvg,
19
+ serializeSvgToString: serializeSvgToString,
20
+ SvgElement: SvgElement,
21
+ SVGRenderer: SVGRenderer
22
+ };