scratch-render
Version:
WebGL Renderer for Scratch 3.0
1,129 lines (1,053 loc) • 3 MB
JavaScript
(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(']>');
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