UNPKG

scratch-render

Version:
1,129 lines (1,053 loc) • 3 MB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["ScratchRender"] = factory(); else root["ScratchRender"] = factory(); })(self, function() { return /******/ (function() { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./node_modules/scratch-svg-renderer/src/bitmap-adapter.js": /*!*****************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/bitmap-adapter.js ***! \*****************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var base64js = __webpack_require__(/*! base64-js */ "./node_modules/scratch-svg-renderer/node_modules/base64-js/index.js"); /** * Adapts Scratch 2.0 bitmaps for use in scratch 3.0 */ var BitmapAdapter = /*#__PURE__*/function () { /** * @param {?function} makeImage HTML image constructor. Tests can provide this. * @param {?function} makeCanvas HTML canvas constructor. Tests can provide this. */ function BitmapAdapter(makeImage, makeCanvas) { _classCallCheck(this, BitmapAdapter); this._makeImage = makeImage ? makeImage : function () { return new Image(); }; this._makeCanvas = makeCanvas ? makeCanvas : function () { return document.createElement('canvas'); }; } /** * Return a canvas with the resized version of the given image, done using nearest-neighbor interpolation * @param {CanvasImageSource} image The image to resize * @param {int} newWidth The desired post-resize width of the image * @param {int} newHeight The desired post-resize height of the image * @returns {HTMLCanvasElement} A canvas with the resized image drawn on it. */ return _createClass(BitmapAdapter, [{ key: "resize", value: function resize(image, newWidth, newHeight) { // We want to always resize using nearest-neighbor interpolation. However, canvas implementations are free to // use linear interpolation (or other "smooth" interpolation methods) when downscaling: // https://bugzilla.mozilla.org/show_bug.cgi?id=1360415 // It seems we can get around this by resizing in two steps: first width, then height. This will always result // in nearest-neighbor interpolation, even when downscaling. var stretchWidthCanvas = this._makeCanvas(); stretchWidthCanvas.width = newWidth; stretchWidthCanvas.height = image.height; var context = stretchWidthCanvas.getContext('2d'); context.imageSmoothingEnabled = false; context.drawImage(image, 0, 0, stretchWidthCanvas.width, stretchWidthCanvas.height); var stretchHeightCanvas = this._makeCanvas(); stretchHeightCanvas.width = newWidth; stretchHeightCanvas.height = newHeight; context = stretchHeightCanvas.getContext('2d'); context.imageSmoothingEnabled = false; context.drawImage(stretchWidthCanvas, 0, 0, stretchHeightCanvas.width, stretchHeightCanvas.height); return stretchHeightCanvas; } /** * Scratch 2.0 had resolution 1 and 2 bitmaps. All bitmaps in Scratch 3.0 are equivalent * to resolution 2 bitmaps. Therefore, converting a resolution 1 bitmap means doubling * it in width and height. * @param {!string} dataURI Base 64 encoded image data of the bitmap * @param {!function} callback Node-style callback that returns updated dataURI if conversion succeeded */ }, { key: "convertResolution1Bitmap", value: function convertResolution1Bitmap(dataURI, callback) { var _this = this; var image = this._makeImage(); image.src = dataURI; image.onload = function () { callback(null, _this.resize(image, image.width * 2, image.height * 2).toDataURL()); }; image.onerror = function () { callback('Image load failed'); }; } /** * Given width/height of an uploaded item, return width/height the image will be resized * to in Scratch 3.0 * @param {!number} oldWidth original width * @param {!number} oldHeight original height * @return {object} Array of new width, new height */ }, { key: "getResizedWidthHeight", value: function getResizedWidthHeight(oldWidth, oldHeight) { var STAGE_WIDTH = 480; var STAGE_HEIGHT = 360; var STAGE_RATIO = STAGE_WIDTH / STAGE_HEIGHT; // If both dimensions are smaller than or equal to corresponding stage dimension, // double both dimensions if (oldWidth <= STAGE_WIDTH && oldHeight <= STAGE_HEIGHT) { return { width: oldWidth * 2, height: oldHeight * 2 }; } // If neither dimension is larger than 2x corresponding stage dimension, // this is an in-between image, return it as is if (oldWidth <= STAGE_WIDTH * 2 && oldHeight <= STAGE_HEIGHT * 2) { return { width: oldWidth, height: oldHeight }; } var imageRatio = oldWidth / oldHeight; // Otherwise, figure out how to resize if (imageRatio >= STAGE_RATIO) { // Wide Image return { width: STAGE_WIDTH * 2, height: STAGE_WIDTH * 2 / imageRatio }; } // In this case we have either: // - A wide image, but not with as big a ratio between width and height, // making it so that fitting the width to double stage size would leave // the height too big to fit in double the stage height // - A square image that's still larger than the double at least // one of the stage dimensions, so pick the smaller of the two dimensions (to fit) // - A tall image // In any of these cases, resize the image to fit the height to double the stage height return { width: STAGE_HEIGHT * 2 * imageRatio, height: STAGE_HEIGHT * 2 }; } /** * Given bitmap data, resize as necessary. * @param {ArrayBuffer | string} fileData Base 64 encoded image data of the bitmap * @param {string} fileType The MIME type of this file * @returns {Promise} Resolves to resized image data Uint8Array */ }, { key: "importBitmap", value: function importBitmap(fileData, fileType) { var _this2 = this; var dataURI = fileData; if (fileData instanceof ArrayBuffer) { dataURI = this.convertBinaryToDataURI(fileData, fileType); } return new Promise(function (resolve, reject) { var image = _this2._makeImage(); image.src = dataURI; image.onload = function () { var newSize = _this2.getResizedWidthHeight(image.width, image.height); if (newSize.width === image.width && newSize.height === image.height) { // No change resolve(_this2.convertDataURIToBinary(dataURI)); } else { var resizedDataURI = _this2.resize(image, newSize.width, newSize.height).toDataURL(); resolve(_this2.convertDataURIToBinary(resizedDataURI)); } }; image.onerror = function () { // TODO: reject with an Error (breaking API change!) // eslint-disable-next-line prefer-promise-reject-errors reject('Image load failed'); }; }); } // TODO consolidate with scratch-vm/src/util/base64-util.js // From https://gist.github.com/borismus/1032746 }, { key: "convertDataURIToBinary", value: function convertDataURIToBinary(dataURI) { var BASE64_MARKER = ';base64,'; var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length; var base64 = dataURI.substring(base64Index); var raw = window.atob(base64); var rawLength = raw.length; var array = new Uint8Array(new ArrayBuffer(rawLength)); for (var i = 0; i < rawLength; i++) { array[i] = raw.charCodeAt(i); } return array; } }, { key: "convertBinaryToDataURI", value: function convertBinaryToDataURI(arrayBuffer, contentType) { return "data:".concat(contentType, ";base64,").concat(base64js.fromByteArray(new Uint8Array(arrayBuffer))); } }]); }(); module.exports = BitmapAdapter; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/fixup-svg-string.js": /*!*******************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/fixup-svg-string.js ***! \*******************************************************************/ /***/ (function(module) { /** * Fixup svg string prior to parsing. * @param {!string} svgString String of the svg to fix. * @returns {!string} fixed svg that should be parseable. */ module.exports = function (svgString) { // Add root svg namespace if it does not exist. var svgAttrs = svgString.match(/<svg [^>]*>/); if (svgAttrs && svgAttrs[0].indexOf('xmlns=') === -1) { svgString = svgString.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" '); } // There are some SVGs from Illustrator that use undeclared entities. // Just replace those entities with fake namespace references to prevent // DOMParser from crashing if (svgAttrs && svgAttrs[0].indexOf('&ns_') !== -1 && svgString.indexOf('<!DOCTYPE') === -1) { svgString = svgString.replace(svgAttrs[0], svgAttrs[0].replace(/&ns_[^;]+;/g, 'http://ns.adobe.com/Extensibility/1.0/')); } // Some SVGs exported from Photoshop have been found to have an invalid mime type // Chrome and Safari won't render these SVGs, so we correct it here if (svgString.includes('data:img/png')) { svgString = svgString.replace( // capture entire image tag with xlink:href=and the quote - dont capture data: bit /(<image[^>]+?xlink:href=["'])data:img\/png/g, // use the captured <image ..... xlink:href=" then append the right data uri mime type function ($0, $1) { return "".concat($1, "data:image/png"); }); } // Some SVGs from Inkscape attempt to bind a prefix to a reserved namespace name. // This will cause SVG parsing to fail, so replace these with a dummy namespace name. // This namespace name is only valid for "xml", and if we bind "xmlns:xml" to the dummy namespace, // parsing will fail yet again, so exclude "xmlns:xml" declarations. var xmlnsRegex = /(<[^>]+?xmlns:(?!xml=)[^ ]+=)"http:\/\/www.w3.org\/XML\/1998\/namespace"/g; if (svgString.match(xmlnsRegex) !== null) { svgString = svgString.replace( // capture the entire attribute xmlnsRegex, // use the captured attribute name; replace only the URL function ($0, $1) { return "".concat($1, "\"http://dummy.namespace\""); }); } // Strip `svg:` prefix (sometimes added by Inkscape) from all tags. They interfere with DOMPurify (prefixed tag // names are not recognized) and the paint editor. // This matches opening and closing tags--the capture group captures the slash if it exists, and it is reinserted // in the replacement text. svgString = svgString.replace(/<(\/?)\s*svg:/g, '<$1'); // The <metadata> element is not needed for rendering and sometimes contains // unparseable garbage from Illustrator :( Empty out the contents. // Note: [\s\S] matches everything including newlines, which .* does not svgString = svgString.replace(/<metadata>[\s\S]*<\/metadata>/, '<metadata></metadata>'); // Empty script tags and javascript executing svgString = svgString.replace(/<script[\s\S]*>[\s\S]*<\/script>/, '<script></script>'); return svgString; }; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/font-converter.js": /*!*****************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/font-converter.js ***! \*****************************************************************/ /***/ (function(module) { /** * @fileOverview Convert 2.0 fonts to 3.0 fonts. */ /** * Given an SVG, replace Scratch 2.0 fonts with new 3.0 fonts. Add defaults where there are none. * @param {SVGElement} svgTag The SVG dom object * @return {void} */ var convertFonts = function convertFonts(svgTag) { // Collect all text elements into a list. var textElements = []; var _collectText = function collectText(domElement) { if (domElement.localName === 'text') { textElements.push(domElement); } for (var i = 0; i < domElement.childNodes.length; i++) { _collectText(domElement.childNodes[i]); } }; _collectText(svgTag); // If there's an old font-family, switch to the new one. for (var _i = 0, _textElements = textElements; _i < _textElements.length; _i++) { var textElement = _textElements[_i]; // If there's no font-family provided, provide one. if (!textElement.getAttribute('font-family') || textElement.getAttribute('font-family') === 'Helvetica') { textElement.setAttribute('font-family', 'Sans Serif'); } else if (textElement.getAttribute('font-family') === 'Mystery') { textElement.setAttribute('font-family', 'Curly'); } else if (textElement.getAttribute('font-family') === 'Gloria') { textElement.setAttribute('font-family', 'Handwriting'); } else if (textElement.getAttribute('font-family') === 'Donegal') { textElement.setAttribute('font-family', 'Serif'); } } }; module.exports = convertFonts; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/font-inliner.js": /*!***************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/font-inliner.js ***! \***************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } /** * @fileOverview Import bitmap data into Scratch 3.0, resizing image as necessary. */ var getFonts = __webpack_require__(/*! scratch-render-fonts */ "./node_modules/scratch-render-fonts/src/index.js"); /** * Given SVG data, inline the fonts. This allows them to be rendered correctly when set * as the source of an HTMLImageElement. Here is a note from tmickel: * // Inject fonts that are needed. * // It would be nice if there were another way to get the SVG-in-canvas * // to render the correct font family, but I couldn't find any other way. * // Other things I tried: * // Just injecting the font-family into the document: no effect. * // External stylesheet linked to by SVG: no effect. * // Using a <link> or <style>@import</style> to link to font-family * // injected into the document: no effect. * @param {string} svgString The string representation of the svg to modify * @return {string} The svg with any needed fonts inlined */ var inlineSvgFonts = function inlineSvgFonts(svgString) { var FONTS = getFonts(); // Make it clear that this function only operates on strings. // If we don't explicitly throw this here, the function silently fails. if (typeof svgString !== 'string') { throw new Error('SVG to be inlined is not a string'); } // Collect fonts that need injection. var fontsNeeded = new Set(); var fontRegex = /font-family="([^"]*)"/g; var matches = fontRegex.exec(svgString); while (matches) { fontsNeeded.add(matches[1]); matches = fontRegex.exec(svgString); } if (fontsNeeded.size > 0) { var str = '<defs><style>'; var _iterator = _createForOfIteratorHelper(fontsNeeded), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var font = _step.value; if (Object.prototype.hasOwnProperty.call(FONTS, font)) { str += "".concat(FONTS[font]); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } str += '</style></defs>'; svgString = svgString.replace(/<svg[^>]*>/, "$&".concat(str)); return svgString; } return svgString; }; module.exports = inlineSvgFonts; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/index.js": /*!********************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/index.js ***! \********************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { var SVGRenderer = __webpack_require__(/*! ./svg-renderer */ "./node_modules/scratch-svg-renderer/src/svg-renderer.js"); var BitmapAdapter = __webpack_require__(/*! ./bitmap-adapter */ "./node_modules/scratch-svg-renderer/src/bitmap-adapter.js"); var inlineSvgFonts = __webpack_require__(/*! ./font-inliner */ "./node_modules/scratch-svg-renderer/src/font-inliner.js"); var loadSvgString = __webpack_require__(/*! ./load-svg-string */ "./node_modules/scratch-svg-renderer/src/load-svg-string.js"); var sanitizeSvg = __webpack_require__(/*! ./sanitize-svg */ "./node_modules/scratch-svg-renderer/src/sanitize-svg.js"); var serializeSvgToString = __webpack_require__(/*! ./serialize-svg-to-string */ "./node_modules/scratch-svg-renderer/src/serialize-svg-to-string.js"); var SvgElement = __webpack_require__(/*! ./svg-element */ "./node_modules/scratch-svg-renderer/src/svg-element.js"); var convertFonts = __webpack_require__(/*! ./font-converter */ "./node_modules/scratch-svg-renderer/src/font-converter.js"); // /** // * Export for NPM & Node.js // * @type {RenderWebGL} // */ module.exports = { BitmapAdapter: BitmapAdapter, convertFonts: convertFonts, inlineSvgFonts: inlineSvgFonts, loadSvgString: loadSvgString, sanitizeSvg: sanitizeSvg, serializeSvgToString: serializeSvgToString, SvgElement: SvgElement, SVGRenderer: SVGRenderer }; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/load-svg-string.js": /*!******************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/load-svg-string.js ***! \******************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var DOMPurify = __webpack_require__(/*! isomorphic-dompurify */ "./node_modules/isomorphic-dompurify/browser.js"); var SvgElement = __webpack_require__(/*! ./svg-element */ "./node_modules/scratch-svg-renderer/src/svg-element.js"); var convertFonts = __webpack_require__(/*! ./font-converter */ "./node_modules/scratch-svg-renderer/src/font-converter.js"); var fixupSvgString = __webpack_require__(/*! ./fixup-svg-string */ "./node_modules/scratch-svg-renderer/src/fixup-svg-string.js"); var transformStrokeWidths = __webpack_require__(/*! ./transform-applier */ "./node_modules/scratch-svg-renderer/src/transform-applier.js"); /** * @param {SVGElement} svgTag the tag to search within * @param {string} [tagName] svg tag to search for (or collect all elements if not given) * @return {Array} a list of elements with the given tagname */ var collectElements = function collectElements(svgTag, tagName) { var elts = []; var _collectElementsInner = function collectElementsInner(domElement) { if ((domElement.localName === tagName || typeof tagName === 'undefined') && domElement.getAttribute) { elts.push(domElement); } for (var i = 0; i < domElement.childNodes.length; i++) { _collectElementsInner(domElement.childNodes[i]); } }; _collectElementsInner(svgTag); return elts; }; /** * Fix SVGs to comply with SVG spec. Scratch 2 defaults to x2 = 0 when x2 is missing, but * SVG defaults to x2 = 1 when missing. * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to */ var transformGradients = function transformGradients(svgTag) { var linearGradientElements = collectElements(svgTag, 'linearGradient'); // For each gradient element, supply x2 if necessary. var _iterator = _createForOfIteratorHelper(linearGradientElements), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var gradientElement = _step.value; if (!gradientElement.getAttribute('x2')) { gradientElement.setAttribute('x2', '0'); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } }; /** * Fix SVGs to match appearance in Scratch 2, which used nearest neighbor scaling for bitmaps * within SVGs. * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to */ var transformImages = function transformImages(svgTag) { var imageElements = collectElements(svgTag, 'image'); // For each image element, set image rendering to pixelated var pixelatedImages = 'image-rendering: optimizespeed; image-rendering: pixelated;'; var _iterator2 = _createForOfIteratorHelper(imageElements), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var elt = _step2.value; if (elt.getAttribute('style')) { elt.setAttribute('style', "".concat(pixelatedImages, " ").concat(elt.getAttribute('style'))); } else { elt.setAttribute('style', pixelatedImages); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } }; /** * Transforms an SVG's text elements for Scratch 2.0 quirks. * These quirks include: * 1. `x` and `y` properties are removed/ignored. * 2. Alignment is set to `text-before-edge`. * 3. Line-breaks are converted to explicit <tspan> elements. * 4. Any required fonts are injected. * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to */ var transformText = function transformText(svgTag) { // Collect all text elements into a list. var textElements = []; var _collectText = function collectText(domElement) { if (domElement.localName === 'text') { textElements.push(domElement); } for (var i = 0; i < domElement.childNodes.length; i++) { _collectText(domElement.childNodes[i]); } }; _collectText(svgTag); convertFonts(svgTag); // For each text element, apply quirks. for (var _i = 0, _textElements = textElements; _i < _textElements.length; _i++) { var textElement = _textElements[_i]; // Remove x and y attributes - they are not used in Scratch. textElement.removeAttribute('x'); textElement.removeAttribute('y'); // Set text-before-edge alignment: // Scratch renders all text like this. textElement.setAttribute('alignment-baseline', 'text-before-edge'); textElement.setAttribute('xml:space', 'preserve'); // If there's no font size provided, provide one. if (!textElement.getAttribute('font-size')) { textElement.setAttribute('font-size', '18'); } var text = textElement.textContent; // Fix line breaks in text, which are not natively supported by SVG. // Only fix if text does not have child tspans. // @todo this will not work for font sizes with units such as em, percent // However, text made in scratch 2 should only ever export size 22 font. var fontSize = parseFloat(textElement.getAttribute('font-size')); var tx = 2; var ty = 0; var spacing = 1.2; // Try to match the position and spacing of Scratch 2.0's fonts. // Different fonts seem to use different line spacing. // Scratch 2 always uses alignment-baseline=text-before-edge // However, most SVG readers don't support this attribute // or don't support it alongside use of tspan, so the translations // here are to make up for that. if (textElement.getAttribute('font-family') === 'Handwriting') { spacing = 2; ty = -11 * fontSize / 22; } else if (textElement.getAttribute('font-family') === 'Scratch') { spacing = 0.89; ty = -3 * fontSize / 22; } else if (textElement.getAttribute('font-family') === 'Curly') { spacing = 1.38; ty = -6 * fontSize / 22; } else if (textElement.getAttribute('font-family') === 'Marker') { spacing = 1.45; ty = -6 * fontSize / 22; } else if (textElement.getAttribute('font-family') === 'Sans Serif') { spacing = 1.13; ty = -3 * fontSize / 22; } else if (textElement.getAttribute('font-family') === 'Serif') { spacing = 1.25; ty = -4 * fontSize / 22; } if (textElement.transform.baseVal.numberOfItems === 0) { var transform = svgTag.createSVGTransform(); textElement.transform.baseVal.appendItem(transform); } // Right multiply matrix by a translation of (tx, ty) var mtx = textElement.transform.baseVal.getItem(0).matrix; mtx.e += mtx.a * tx + mtx.c * ty; mtx.f += mtx.b * tx + mtx.d * ty; if (text && textElement.childElementCount === 0) { textElement.textContent = ''; var lines = text.split('\n'); text = ''; var _iterator3 = _createForOfIteratorHelper(lines), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var line = _step3.value; var tspanNode = SvgElement.create('tspan'); tspanNode.setAttribute('x', '0'); tspanNode.setAttribute('style', 'white-space: pre'); tspanNode.setAttribute('dy', "".concat(spacing, "em")); tspanNode.textContent = line ? line : ' '; textElement.appendChild(tspanNode); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } } }; /** * Find the largest stroke width in the svg. If a shape has no * `stroke` property, it has a stroke-width of 0. If it has a `stroke`, * it is by default a stroke-width of 1. * This is used to enlarge the computed bounding box, which doesn't take * stroke width into account. * @param {SVGSVGElement} rootNode The root SVG node to traverse. * @return {number} The largest stroke width in the SVG. */ var findLargestStrokeWidth = function findLargestStrokeWidth(rootNode) { var largestStrokeWidth = 0; var _collectStrokeWidths = function collectStrokeWidths(domElement) { if (domElement.getAttribute) { if (domElement.getAttribute('stroke')) { largestStrokeWidth = Math.max(largestStrokeWidth, 1); } if (domElement.getAttribute('stroke-width')) { largestStrokeWidth = Math.max(largestStrokeWidth, Number(domElement.getAttribute('stroke-width')) || 0); } } for (var i = 0; i < domElement.childNodes.length; i++) { _collectStrokeWidths(domElement.childNodes[i]); } }; _collectStrokeWidths(rootNode); return largestStrokeWidth; }; /** * Transform the measurements of the SVG. * In Scratch 2.0, SVGs are drawn without respect to the width, * height, and viewBox attribute on the tag. The exporter * does output these properties - but they appear to be incorrect often. * To address the incorrect measurements, we append the DOM to the * document, and then use SVG's native `getBBox` to find the real * drawn dimensions. This ensures things drawn in negative dimensions, * outside the given viewBox, etc., are all eventually drawn to the canvas. * I tried to do this several other ways: stripping the width/height/viewBox * attributes and then drawing (Firefox won't draw anything), * or inflating them and then measuring a canvas. But this seems to be * a natural and performant way. * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to */ var transformMeasurements = function transformMeasurements(svgTag) { // Append the SVG dom to the document. // This allows us to use `getBBox` on the page, // which returns the full bounding-box of all drawn SVG // elements, similar to how Scratch 2.0 did measurement. var svgSpot = document.createElement('span'); // Since we're adding user-provided SVG to document.body, // sanitizing is required. This should not affect bounding box calculation. // outerHTML is attribute of Element (and not HTMLElement), so use it instead of // calling serializer or toString() // NOTE: svgTag remains untouched! var rawValue = svgTag.outerHTML; var sanitizedValue = DOMPurify.sanitize(rawValue, { // Use SVG profile (no HTML elements) USE_PROFILES: { svg: true }, // Remove some tags that Scratch does not use. FORBID_TAGS: ['a', 'audio', 'canvas', 'video'], // Allow data URI in image tags (e.g. SVGs converted from bitmap) ADD_DATA_URI_TAGS: ['image'] }); var bbox; try { // Insert sanitized value. svgSpot.innerHTML = sanitizedValue; document.body.appendChild(svgSpot); // Take the bounding box. We have to get elements via svgSpot // because we added it via innerHTML. bbox = svgSpot.children[0].getBBox(); } finally { // Always destroy the element, even if, for example, getBBox throws. document.body.removeChild(svgSpot); } // Enlarge the bbox from the largest found stroke width // This may have false-positives, but at least the bbox will always // contain the full graphic including strokes. // If the width or height is zero however, don't enlarge since // they won't have a stroke width that needs to be enlarged. var halfStrokeWidth; if (bbox.width === 0 || bbox.height === 0) { halfStrokeWidth = 0; } else { halfStrokeWidth = findLargestStrokeWidth(svgTag) / 2; } var width = bbox.width + halfStrokeWidth * 2; var height = bbox.height + halfStrokeWidth * 2; var x = bbox.x - halfStrokeWidth; var y = bbox.y - halfStrokeWidth; // Set the correct measurements on the SVG tag svgTag.setAttribute('width', width); svgTag.setAttribute('height', height); svgTag.setAttribute('viewBox', "".concat(x, " ").concat(y, " ").concat(width, " ").concat(height)); }; /** * Find all instances of a URL-referenced `stroke` in the svg. In 2.0, all gradient strokes * have a round `stroke-linejoin` and `stroke-linecap`... for some reason. * @param {SVGSVGElement} svgTag the SVG tag to apply the transformation to */ var setGradientStrokeRoundedness = function setGradientStrokeRoundedness(svgTag) { var elements = collectElements(svgTag); var _iterator4 = _createForOfIteratorHelper(elements), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var elt = _step4.value; if (!elt.style) continue; var stroke = elt.style.stroke || elt.getAttribute('stroke'); if (stroke && stroke.match(/^url\(#.*\)$/)) { elt.style['stroke-linejoin'] = 'round'; elt.style['stroke-linecap'] = 'round'; } } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } }; /** * In-place, convert passed SVG to something consistent that will be rendered the way we want them to be. * @param {SVGSvgElement} svgTag root SVG node to operate upon * @param {boolean} [fromVersion2] True if we should perform conversion from version 2 to version 3 svg. */ var normalizeSvg = function normalizeSvg(svgTag, fromVersion2) { if (fromVersion2) { // Fix gradients. Scratch 2 exports no x2 when x2 = 0, but // SVG default is that x2 is 1. This must be done before // transformStrokeWidths since transformStrokeWidths affects // gradients. transformGradients(svgTag); } transformStrokeWidths(svgTag, window); transformImages(svgTag); if (fromVersion2) { // Transform all text elements. transformText(svgTag); // Transform measurements. transformMeasurements(svgTag); // Fix stroke roundedness. setGradientStrokeRoundedness(svgTag); } else if (!svgTag.getAttribute('viewBox')) { // Renderer expects a view box. transformMeasurements(svgTag); } else if (!svgTag.getAttribute('width') || !svgTag.getAttribute('height')) { svgTag.setAttribute('width', svgTag.viewBox.baseVal.width); svgTag.setAttribute('height', svgTag.viewBox.baseVal.height); } }; /** * Load an SVG string and normalize it. All the steps before drawing/measuring. * Currently, this will normalize stroke widths (see transform-applier.js) and render all embedded images pixelated. * The returned SVG will be guaranteed to always have a `width`, `height` and `viewBox`. * In addition, if the `fromVersion2` parameter is `true`, several "quirks-mode" transformations will be applied which * mimic Scratch 2.0's SVG rendering. * @param {!string} svgString String of SVG data to draw in quirks-mode. * @param {boolean} [fromVersion2] True if we should perform conversion from version 2 to version 3 svg. * @return {SVGSVGElement} The normalized SVG element. */ var loadSvgString = function loadSvgString(svgString, fromVersion2) { // Parse string into SVG XML. var parser = new DOMParser(); svgString = fixupSvgString(svgString); var svgDom = parser.parseFromString(svgString, 'text/xml'); if (svgDom.childNodes.length < 1 || svgDom.documentElement.localName !== 'svg') { throw new Error('Document does not appear to be SVG.'); } var svgTag = svgDom.documentElement; normalizeSvg(svgTag, fromVersion2); return svgTag; }; module.exports = loadSvgString; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/sanitize-svg.js": /*!***************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/sanitize-svg.js ***! \***************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { /** * @fileOverview Sanitize the content of an SVG aggressively, to make it as safe * as possible */ var fixupSvgString = __webpack_require__(/*! ./fixup-svg-string */ "./node_modules/scratch-svg-renderer/src/fixup-svg-string.js"); var _require = __webpack_require__(/*! css-tree */ "./node_modules/css-tree/lib/index.js"), generate = _require.generate, parse = _require.parse, walk = _require.walk; var DOMPurify = __webpack_require__(/*! isomorphic-dompurify */ "./node_modules/isomorphic-dompurify/browser.js"); var sanitizeSvg = {}; DOMPurify.addHook('beforeSanitizeAttributes', function (currentNode) { if (currentNode && currentNode.href && currentNode.href.baseVal) { var href = currentNode.href.baseVal.replace(/\s/g, ''); // "data:" and "#" are valid hrefs if (href.slice(0, 5) !== 'data:' && href.slice(0, 1) !== '#') { if (currentNode.attributes.getNamedItem('xlink:href')) { currentNode.attributes.removeNamedItem('xlink:href'); delete currentNode['xlink:href']; } if (currentNode.attributes.getNamedItem('href')) { currentNode.attributes.removeNamedItem('href'); delete currentNode.href; } } } return currentNode; }); DOMPurify.addHook('uponSanitizeElement', function (node, data) { if (data.tagName === 'style') { var ast = parse(node.textContent); var isModified = false; // Remove any @import rules as it could leak HTTP requests walk(ast, function (astNode, item, list) { if (astNode.type === 'Atrule' && astNode.name === 'import') { list.remove(item); isModified = true; } }); if (isModified) { node.textContent = generate(ast); } } }); // Use JS implemented TextDecoder and TextEncoder if it is not provided by the // browser. var _TextDecoder; var _TextEncoder; if (typeof TextDecoder === 'undefined' || typeof TextEncoder === 'undefined') { // Wait to require the text encoding polyfill until we know it's needed. // eslint-disable-next-line global-require var encoding = __webpack_require__(/*! fastestsmallesttextencoderdecoder */ "./node_modules/fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"); _TextDecoder = encoding.TextDecoder; _TextEncoder = encoding.TextEncoder; } else { _TextDecoder = TextDecoder; _TextEncoder = TextEncoder; } /** * Load an SVG Uint8Array of bytes and "sanitize" it * @param {!Uint8Array} rawData unsanitized SVG daata * @return {Uint8Array} sanitized SVG data */ sanitizeSvg.sanitizeByteStream = function (rawData) { var decoder = new _TextDecoder(); var encoder = new _TextEncoder(); var sanitizedText = sanitizeSvg.sanitizeSvgText(decoder.decode(rawData)); return encoder.encode(sanitizedText); }; /** * Load an SVG string and "sanitize" it. This is more aggressive than the handling in * fixup-svg-string.js, and thus more risky; there are known examples of SVGs that * it will clobber. We use DOMPurify's svg profile, which restricts many types of tag. * @param {!string} rawSvgText unsanitized SVG string * @return {string} sanitized SVG text */ sanitizeSvg.sanitizeSvgText = function (rawSvgText) { var sanitizedText = DOMPurify.sanitize(rawSvgText, { USE_PROFILES: { svg: true } }); // Remove partial XML comment that is sometimes left in the HTML var badTag = sanitizedText.indexOf(']&gt;'); if (badTag >= 0) { sanitizedText = sanitizedText.substring(5, sanitizedText.length); } // also use our custom fixup rules sanitizedText = fixupSvgString(sanitizedText); return sanitizedText; }; module.exports = sanitizeSvg; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/serialize-svg-to-string.js": /*!**************************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/serialize-svg-to-string.js ***! \**************************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { var inlineSvgFonts = __webpack_require__(/*! ./font-inliner */ "./node_modules/scratch-svg-renderer/src/font-inliner.js"); /** * Serialize a given SVG DOM to a string. * @param {SVGSVGElement} svgTag The SVG element to serialize. * @param {?boolean} shouldInjectFonts True if fonts should be included in the SVG as * base64 data. * @returns {string} String representing current SVG data. */ var serializeSvgToString = function serializeSvgToString(svgTag, shouldInjectFonts) { var serializer = new XMLSerializer(); var string = serializer.serializeToString(svgTag); if (shouldInjectFonts) { string = inlineSvgFonts(string); } return string; }; module.exports = serializeSvgToString; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/svg-element.js": /*!**************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/svg-element.js ***! \**************************************************************/ /***/ (function(module) { function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* Adapted from * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey * http://scratchdisk.com/ & http://jonathanpuckey.com/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ /** * @name SvgElement * @namespace * @private */ var SvgElement = /*#__PURE__*/function () { function SvgElement() { _classCallCheck(this, SvgElement); } return _createClass(SvgElement, null, [{ key: "svg", get: // SVG related namespaces function get() { return 'http://www.w3.org/2000/svg'; } }, { key: "xmlns", get: function get() { return 'http://www.w3.org/2000/xmlns'; } }, { key: "xlink", get: function get() { return 'http://www.w3.org/1999/xlink'; } // Mapping of attribute names to required namespaces: }, { key: "attributeNamespace", value: function attributeNamespace() { return { 'href': SvgElement.xlink, 'xlink': SvgElement.xmlns, // Only the xmlns attribute needs the trailing slash. See #984 'xmlns': "".concat(SvgElement.xmlns, "/"), // IE needs the xmlns namespace when setting 'xmlns:xlink'. See #984 'xmlns:xlink': "".concat(SvgElement.xmlns, "/") }; } }, { key: "create", value: function create(tag, attributes, formatter) { return SvgElement.set(document.createElementNS(SvgElement.svg, tag), attributes, formatter); } }, { key: "get", value: function get(node, name) { var namespace = SvgElement.attributeNamespace[name]; var value = namespace ? node.getAttributeNS(namespace, name) : node.getAttribute(name); return value === 'null' ? null : value; } }, { key: "set", value: function set(node, attributes, formatter) { for (var name in attributes) { var value = attributes[name]; var namespace = SvgElement.attributeNamespace[name]; if (typeof value === 'number' && formatter) { value = formatter.number(value); } if (namespace) { node.setAttributeNS(namespace, name, value); } else { node.setAttribute(name, value); } } return node; } }]); }(); module.exports = SvgElement; /***/ }), /***/ "./node_modules/scratch-svg-renderer/src/svg-renderer.js": /*!***************************************************************!*\ !*** ./node_modules/scratch-svg-renderer/src/svg-renderer.js ***! \***************************************************************/ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var loadSvgString = __webpack_require__(/*! ./load-svg-string */ "./node_modules/scratch-svg-renderer/src/load-svg-string.js"); var serializeSvgToString = __webpack_require__(/*! ./serialize-svg-to-string */ "./node_modules/scratch-svg-renderer/src/serialize-svg-to-string.js"); /** * Main quirks-mode SVG rendering code. * @deprecated Call into individual methods exported from this library instead. */ var SvgRenderer = /*#__PURE__*/function () { /** * Create a quirks-mode SVG renderer for a particular canvas. * @param {HTMLCanvasElement} [canvas] An optional canvas element to draw to. If this is not provided, the renderer * will create a new canvas. * @constructor */ function SvgRenderer(canvas) { _classCallCheck(this, SvgRenderer); /** * The canvas that this SVG renderer will render to. * @type {HTMLCanvasElement} * @private */ this._canvas = canvas || document.createElement('canvas'); this._context = this._canvas.getContext('2d'); /** * A measured SVG "viewbox" * @typedef {object} SvgRenderer#SvgMeasurements * @property {number} x - The left edge of the SVG viewbox. * @property {number} y - The top edge of the SVG viewbox. * @property {number} width - The width of the SVG viewbox. * @property {number} height - The height of the SVG viewbox. */ /** * The measurement box of the currently loaded SVG. * @type {SvgRenderer#SvgMeasurements} * @private */ this._measurements = { x: 0, y: 0, width: 0, height: 0 }; /** * The `<img>` element with the contents of the currently loaded SVG. * @type {?HTMLImageElement} * @private */ this._cachedImage = null; /** * True if this renderer's current SVG is loaded and can be rendered to the canvas. * @type {boolean} */ this.loaded = false; } /** * @returns {!HTMLCanvasElement} this renderer's target canvas. */ return _createClass(SvgRenderer, [{ key: "canvas", get: function get() { return this._canvas; } /** * @return {Array<number>} the natural size, in Scratch units, of this SVG. */ }, { key: "size", get: function get() { return [this._measurements.width, this._measurements.height]; } /** * @return {Array<number>} the offset (upper left corner) of the SVG's view box. */ }, { key: "viewOffset