@scratch/scratch-svg-renderer 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +15 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +10 -0
- package/.gitattributes +38 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +1300 -0
- package/LICENSE +12 -0
- package/README.md +89 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/package.json +76 -0
- package/playground/index.html +132 -0
- package/playground/scratch-svg-renderer.js +17830 -0
- package/playground/scratch-svg-renderer.js.map +1 -0
- package/release.config.js +14 -0
- package/src/bitmap-adapter.js +156 -0
- package/src/fixup-svg-string.js +61 -0
- package/src/font-converter.js +38 -0
- package/src/font-inliner.js +50 -0
- package/src/index.js +22 -0
- package/src/load-svg-string.js +334 -0
- package/src/playground/index.html +132 -0
- package/src/sanitize-svg.js +104 -0
- package/src/serialize-svg-to-string.js +19 -0
- package/src/svg-element.js +71 -0
- package/src/svg-renderer.js +169 -0
- package/src/transform-applier.js +628 -0
- package/src/util/log.js +4 -0
- package/test/bitmapAdapter_getResized.js +102 -0
- package/test/fixtures/css-import.sanitized.svg +5 -0
- package/test/fixtures/css-import.svg +11 -0
- package/test/fixtures/embedded-cat-foo.sanitized.svg +3 -0
- package/test/fixtures/embedded-cat-foo.svg +3 -0
- package/test/fixtures/embedded-cat-xlink.svg +3 -0
- package/test/fixtures/hearts.svg +31 -0
- package/test/fixtures/invalid-cloud.svg +1 -0
- package/test/fixtures/metadata-body.svg +6 -0
- package/test/fixtures/metadata-onload.sanitized.svg +3 -0
- package/test/fixtures/metadata-onload.svg +7 -0
- package/test/fixtures/onload-script.svg +7 -0
- package/test/fixtures/red-and-white-carousel-pound-in-href.sanitized.svg +74 -0
- package/test/fixtures/red-and-white-carousel-pound-in-href.svg +74 -0
- package/test/fixtures/reserved-namespace.sanitized.svg +3 -0
- package/test/fixtures/reserved-namespace.svg +4 -0
- package/test/fixtures/scratch_cat_bitmap_within_svg.sanitized.svg +1 -0
- package/test/fixtures/scratch_cat_bitmap_within_svg.svg +1 -0
- package/test/fixtures/script.sanitized.svg +4 -0
- package/test/fixtures/script.svg +8 -0
- package/test/fixtures/svg-tag-prefixes.svg +37 -0
- package/test/fixup-svg-string.js +144 -0
- package/test/sanitize-svg.js +34 -0
- package/test/test-output/transform-applier-test.html +373 -0
- package/test/transform-applier.js +796 -0
- package/webpack.config.js +72 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Scratch SVG rendering playground</title>
|
|
6
|
+
<style>
|
|
7
|
+
.result {
|
|
8
|
+
background-color:gray;
|
|
9
|
+
}
|
|
10
|
+
#reference {
|
|
11
|
+
display:inline-block;
|
|
12
|
+
font-size:0;
|
|
13
|
+
}
|
|
14
|
+
.column {
|
|
15
|
+
display:inline-block;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<p>
|
|
21
|
+
<input type="file" id="svg-file-upload", accept="image/svg+xml">
|
|
22
|
+
</p>
|
|
23
|
+
<p>
|
|
24
|
+
<label for="render-scale">Scale:</label>
|
|
25
|
+
<input type="range" style="width:50%;" id="render-scale" value="1" min="0.5" max="3" step="any">
|
|
26
|
+
<label for="render-scale" id="scale-display"></label>
|
|
27
|
+
</p>
|
|
28
|
+
<p>
|
|
29
|
+
<input type="button" id="trigger-render" value="Render">
|
|
30
|
+
<label for="shouldRenderReference">
|
|
31
|
+
<input type="checkbox" id="shouldRenderReference" checked />
|
|
32
|
+
Render Reference?
|
|
33
|
+
</label>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<div class="columns">
|
|
37
|
+
<div class="column">
|
|
38
|
+
<div>Rendered Result</div>
|
|
39
|
+
<canvas id="render-canvas" class="result"></canvas>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="column">
|
|
42
|
+
<div>Reference</div>
|
|
43
|
+
<span id="reference"></span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="columns">
|
|
47
|
+
<div class="column">
|
|
48
|
+
<div>Rendered Content</div>
|
|
49
|
+
<textarea id="renderedContent" wrap="off" cols="50" rows="50"></textarea>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="column">
|
|
52
|
+
<div>Reference</div>
|
|
53
|
+
<span id="reference"></span>
|
|
54
|
+
<textarea id="referenceContent" wrap="off" cols="50" rows="50"></textarea>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<script src="scratch-svg-renderer.js"></script>
|
|
59
|
+
<script>
|
|
60
|
+
const renderCanvas = document.getElementById("render-canvas");
|
|
61
|
+
const referenceImage = document.getElementById("reference");
|
|
62
|
+
const fileChooser = document.getElementById("svg-file-upload");
|
|
63
|
+
const scaleSlider = document.getElementById("render-scale");
|
|
64
|
+
const scaleDisplay = document.getElementById("scale-display");
|
|
65
|
+
const renderButton = document.getElementById("trigger-render");
|
|
66
|
+
|
|
67
|
+
const renderer = new ScratchSVGRenderer.SVGRenderer(renderCanvas);
|
|
68
|
+
|
|
69
|
+
let loadedSVGString = "";
|
|
70
|
+
|
|
71
|
+
if (fileChooser.value) {
|
|
72
|
+
loadSVGString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderSVGString() {
|
|
76
|
+
if (renderer.loaded) {
|
|
77
|
+
renderer.draw(parseFloat(scaleSlider.value));
|
|
78
|
+
}
|
|
79
|
+
renderedContent.value = renderer.toString(true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function updateReferenceImage() {
|
|
83
|
+
referenceImage.innerHTML = loadedSVGString;
|
|
84
|
+
scalePercent = (parseFloat(scaleSlider.value) * 100) + "%"
|
|
85
|
+
referenceSVG = referenceImage.children[0];
|
|
86
|
+
referenceSVG.style.width = referenceSVG.style.height = scalePercent;
|
|
87
|
+
referenceSVG.classList.add("result");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readFileAsText(file) {
|
|
91
|
+
return new Promise((res, rej) => {
|
|
92
|
+
const reader = new FileReader();
|
|
93
|
+
|
|
94
|
+
reader.onload = function(event) {
|
|
95
|
+
res(reader.result);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
reader.onerror = console.log;
|
|
99
|
+
|
|
100
|
+
reader.readAsText(file);
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function loadSVGString() {
|
|
105
|
+
readFileAsText(fileChooser.files[0]).then(str => {
|
|
106
|
+
loadedSVGString = str;
|
|
107
|
+
renderer.loadSVG(str, false);
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderLoadedString() {
|
|
112
|
+
renderSVGString();
|
|
113
|
+
referenceContent.value = loadedSVGString;
|
|
114
|
+
shouldRenderReference.checked && updateReferenceImage();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scaleSliderChanged() {
|
|
118
|
+
renderLoadedString();
|
|
119
|
+
scaleDisplay.innerText = scaleSlider.value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fileChooser.addEventListener("change", loadSVGString);
|
|
123
|
+
|
|
124
|
+
scaleSlider.addEventListener("change", scaleSliderChanged);
|
|
125
|
+
scaleSlider.addEventListener("input", scaleSliderChanged);
|
|
126
|
+
|
|
127
|
+
renderButton.addEventListener("click", (event => {
|
|
128
|
+
renderLoadedString();
|
|
129
|
+
}));
|
|
130
|
+
</script>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview Sanitize the content of an SVG aggressively, to make it as safe
|
|
3
|
+
* as possible
|
|
4
|
+
*/
|
|
5
|
+
const fixupSvgString = require('./fixup-svg-string');
|
|
6
|
+
const {generate, parse, walk} = require('css-tree');
|
|
7
|
+
const DOMPurify = require('isomorphic-dompurify');
|
|
8
|
+
|
|
9
|
+
const sanitizeSvg = {};
|
|
10
|
+
|
|
11
|
+
DOMPurify.addHook(
|
|
12
|
+
'beforeSanitizeAttributes',
|
|
13
|
+
currentNode => {
|
|
14
|
+
|
|
15
|
+
if (currentNode && currentNode.href && currentNode.href.baseVal) {
|
|
16
|
+
const href = currentNode.href.baseVal.replace(/\s/g, '');
|
|
17
|
+
// "data:" and "#" are valid hrefs
|
|
18
|
+
if ((href.slice(0, 5) !== 'data:') && (href.slice(0, 1) !== '#')) {
|
|
19
|
+
|
|
20
|
+
if (currentNode.attributes.getNamedItem('xlink:href')) {
|
|
21
|
+
currentNode.attributes.removeNamedItem('xlink:href');
|
|
22
|
+
delete currentNode['xlink:href'];
|
|
23
|
+
}
|
|
24
|
+
if (currentNode.attributes.getNamedItem('href')) {
|
|
25
|
+
currentNode.attributes.removeNamedItem('href');
|
|
26
|
+
delete currentNode.href;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return currentNode;
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
DOMPurify.addHook(
|
|
35
|
+
'uponSanitizeElement',
|
|
36
|
+
(node, data) => {
|
|
37
|
+
if (data.tagName === 'style') {
|
|
38
|
+
const ast = parse(node.textContent);
|
|
39
|
+
let isModified = false;
|
|
40
|
+
// Remove any @import rules as it could leak HTTP requests
|
|
41
|
+
walk(ast, (astNode, item, list) => {
|
|
42
|
+
if (astNode.type === 'Atrule' && astNode.name === 'import') {
|
|
43
|
+
list.remove(item);
|
|
44
|
+
isModified = true;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (isModified) {
|
|
48
|
+
node.textContent = generate(ast);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Use JS implemented TextDecoder and TextEncoder if it is not provided by the
|
|
55
|
+
// browser.
|
|
56
|
+
let _TextDecoder;
|
|
57
|
+
let _TextEncoder;
|
|
58
|
+
if (typeof TextDecoder === 'undefined' || typeof TextEncoder === 'undefined') {
|
|
59
|
+
// Wait to require the text encoding polyfill until we know it's needed.
|
|
60
|
+
// eslint-disable-next-line global-require
|
|
61
|
+
const encoding = require('fastestsmallesttextencoderdecoder');
|
|
62
|
+
_TextDecoder = encoding.TextDecoder;
|
|
63
|
+
_TextEncoder = encoding.TextEncoder;
|
|
64
|
+
} else {
|
|
65
|
+
_TextDecoder = TextDecoder;
|
|
66
|
+
_TextEncoder = TextEncoder;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load an SVG Uint8Array of bytes and "sanitize" it
|
|
71
|
+
* @param {!Uint8Array} rawData unsanitized SVG daata
|
|
72
|
+
* @return {Uint8Array} sanitized SVG data
|
|
73
|
+
*/
|
|
74
|
+
sanitizeSvg.sanitizeByteStream = function (rawData) {
|
|
75
|
+
const decoder = new _TextDecoder();
|
|
76
|
+
const encoder = new _TextEncoder();
|
|
77
|
+
const sanitizedText = sanitizeSvg.sanitizeSvgText(decoder.decode(rawData));
|
|
78
|
+
return encoder.encode(sanitizedText);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load an SVG string and "sanitize" it. This is more aggressive than the handling in
|
|
83
|
+
* fixup-svg-string.js, and thus more risky; there are known examples of SVGs that
|
|
84
|
+
* it will clobber. We use DOMPurify's svg profile, which restricts many types of tag.
|
|
85
|
+
* @param {!string} rawSvgText unsanitized SVG string
|
|
86
|
+
* @return {string} sanitized SVG text
|
|
87
|
+
*/
|
|
88
|
+
sanitizeSvg.sanitizeSvgText = function (rawSvgText) {
|
|
89
|
+
let sanitizedText = DOMPurify.sanitize(rawSvgText, {
|
|
90
|
+
USE_PROFILES: {svg: true}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Remove partial XML comment that is sometimes left in the HTML
|
|
94
|
+
const badTag = sanitizedText.indexOf(']>');
|
|
95
|
+
if (badTag >= 0) {
|
|
96
|
+
sanitizedText = sanitizedText.substring(5, sanitizedText.length);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// also use our custom fixup rules
|
|
100
|
+
sanitizedText = fixupSvgString(sanitizedText);
|
|
101
|
+
return sanitizedText;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = sanitizeSvg;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const inlineSvgFonts = require('./font-inliner');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serialize a given SVG DOM to a string.
|
|
5
|
+
* @param {SVGSVGElement} svgTag The SVG element to serialize.
|
|
6
|
+
* @param {?boolean} shouldInjectFonts True if fonts should be included in the SVG as
|
|
7
|
+
* base64 data.
|
|
8
|
+
* @returns {string} String representing current SVG data.
|
|
9
|
+
*/
|
|
10
|
+
const serializeSvgToString = (svgTag, shouldInjectFonts) => {
|
|
11
|
+
const serializer = new XMLSerializer();
|
|
12
|
+
let string = serializer.serializeToString(svgTag);
|
|
13
|
+
if (shouldInjectFonts) {
|
|
14
|
+
string = inlineSvgFonts(string);
|
|
15
|
+
}
|
|
16
|
+
return string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = serializeSvgToString;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* Adapted from
|
|
2
|
+
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
3
|
+
* http://paperjs.org/
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
|
|
6
|
+
* http://scratchdisk.com/ & http://jonathanpuckey.com/
|
|
7
|
+
*
|
|
8
|
+
* Distributed under the MIT license. See LICENSE file for details.
|
|
9
|
+
*
|
|
10
|
+
* All rights reserved.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @name SvgElement
|
|
15
|
+
* @namespace
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
class SvgElement {
|
|
19
|
+
// SVG related namespaces
|
|
20
|
+
static get svg () {
|
|
21
|
+
return 'http://www.w3.org/2000/svg';
|
|
22
|
+
}
|
|
23
|
+
static get xmlns () {
|
|
24
|
+
return 'http://www.w3.org/2000/xmlns';
|
|
25
|
+
}
|
|
26
|
+
static get xlink () {
|
|
27
|
+
return 'http://www.w3.org/1999/xlink';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Mapping of attribute names to required namespaces:
|
|
31
|
+
static attributeNamespace () {
|
|
32
|
+
return {
|
|
33
|
+
'href': SvgElement.xlink,
|
|
34
|
+
'xlink': SvgElement.xmlns,
|
|
35
|
+
// Only the xmlns attribute needs the trailing slash. See #984
|
|
36
|
+
'xmlns': `${SvgElement.xmlns}/`,
|
|
37
|
+
// IE needs the xmlns namespace when setting 'xmlns:xlink'. See #984
|
|
38
|
+
'xmlns:xlink': `${SvgElement.xmlns}/`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static create (tag, attributes, formatter) {
|
|
43
|
+
return SvgElement.set(document.createElementNS(SvgElement.svg, tag), attributes, formatter);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static get (node, name) {
|
|
47
|
+
const namespace = SvgElement.attributeNamespace[name];
|
|
48
|
+
const value = namespace ?
|
|
49
|
+
node.getAttributeNS(namespace, name) :
|
|
50
|
+
node.getAttribute(name);
|
|
51
|
+
return value === 'null' ? null : value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static set (node, attributes, formatter) {
|
|
55
|
+
for (const name in attributes) {
|
|
56
|
+
let value = attributes[name];
|
|
57
|
+
const namespace = SvgElement.attributeNamespace[name];
|
|
58
|
+
if (typeof value === 'number' && formatter) {
|
|
59
|
+
value = formatter.number(value);
|
|
60
|
+
}
|
|
61
|
+
if (namespace) {
|
|
62
|
+
node.setAttributeNS(namespace, name, value);
|
|
63
|
+
} else {
|
|
64
|
+
node.setAttribute(name, value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return node;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = SvgElement;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const loadSvgString = require('./load-svg-string');
|
|
2
|
+
const serializeSvgToString = require('./serialize-svg-to-string');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Main quirks-mode SVG rendering code.
|
|
6
|
+
* @deprecated Call into individual methods exported from this library instead.
|
|
7
|
+
*/
|
|
8
|
+
class SvgRenderer {
|
|
9
|
+
/**
|
|
10
|
+
* Create a quirks-mode SVG renderer for a particular canvas.
|
|
11
|
+
* @param {HTMLCanvasElement} [canvas] An optional canvas element to draw to. If this is not provided, the renderer
|
|
12
|
+
* will create a new canvas.
|
|
13
|
+
* @constructor
|
|
14
|
+
*/
|
|
15
|
+
constructor (canvas) {
|
|
16
|
+
/**
|
|
17
|
+
* The canvas that this SVG renderer will render to.
|
|
18
|
+
* @type {HTMLCanvasElement}
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
this._canvas = canvas || document.createElement('canvas');
|
|
22
|
+
this._context = this._canvas.getContext('2d');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A measured SVG "viewbox"
|
|
26
|
+
* @typedef {object} SvgRenderer#SvgMeasurements
|
|
27
|
+
* @property {number} x - The left edge of the SVG viewbox.
|
|
28
|
+
* @property {number} y - The top edge of the SVG viewbox.
|
|
29
|
+
* @property {number} width - The width of the SVG viewbox.
|
|
30
|
+
* @property {number} height - The height of the SVG viewbox.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The measurement box of the currently loaded SVG.
|
|
35
|
+
* @type {SvgRenderer#SvgMeasurements}
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
this._measurements = {x: 0, y: 0, width: 0, height: 0};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The `<img>` element with the contents of the currently loaded SVG.
|
|
42
|
+
* @type {?HTMLImageElement}
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
this._cachedImage = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True if this renderer's current SVG is loaded and can be rendered to the canvas.
|
|
49
|
+
* @type {boolean}
|
|
50
|
+
*/
|
|
51
|
+
this.loaded = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @returns {!HTMLCanvasElement} this renderer's target canvas.
|
|
56
|
+
*/
|
|
57
|
+
get canvas () {
|
|
58
|
+
return this._canvas;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @return {Array<number>} the natural size, in Scratch units, of this SVG.
|
|
63
|
+
*/
|
|
64
|
+
get size () {
|
|
65
|
+
return [this._measurements.width, this._measurements.height];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @return {Array<number>} the offset (upper left corner) of the SVG's view box.
|
|
70
|
+
*/
|
|
71
|
+
get viewOffset () {
|
|
72
|
+
return [this._measurements.x, this._measurements.y];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load an SVG string and normalize it. All the steps before drawing/measuring.
|
|
77
|
+
* @param {!string} svgString String of SVG data to draw in quirks-mode.
|
|
78
|
+
* @param {?boolean} fromVersion2 True if we should perform conversion from
|
|
79
|
+
* version 2 to version 3 svg.
|
|
80
|
+
*/
|
|
81
|
+
loadString (svgString, fromVersion2) {
|
|
82
|
+
// New svg string invalidates the cached image
|
|
83
|
+
this._cachedImage = null;
|
|
84
|
+
const svgTag = loadSvgString(svgString, fromVersion2);
|
|
85
|
+
|
|
86
|
+
this._svgTag = svgTag;
|
|
87
|
+
this._measurements = {
|
|
88
|
+
width: svgTag.viewBox.baseVal.width,
|
|
89
|
+
height: svgTag.viewBox.baseVal.height,
|
|
90
|
+
x: svgTag.viewBox.baseVal.x,
|
|
91
|
+
y: svgTag.viewBox.baseVal.y
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load an SVG string, normalize it, and prepare it for (synchronous) rendering.
|
|
97
|
+
* @param {!string} svgString String of SVG data to draw in quirks-mode.
|
|
98
|
+
* @param {?boolean} fromVersion2 True if we should perform conversion from version 2 to version 3 svg.
|
|
99
|
+
* @param {Function} [onFinish] - An optional callback to call when the SVG is loaded and can be rendered.
|
|
100
|
+
*/
|
|
101
|
+
loadSVG (svgString, fromVersion2, onFinish) {
|
|
102
|
+
this.loadString(svgString, fromVersion2);
|
|
103
|
+
this._createSVGImage(onFinish);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates an <img> element for the currently loaded SVG string, then calls the callback once it's loaded.
|
|
108
|
+
* @param {Function} [onFinish] - An optional callback to call when the <img> has loaded.
|
|
109
|
+
*/
|
|
110
|
+
_createSVGImage (onFinish) {
|
|
111
|
+
if (this._cachedImage === null) this._cachedImage = new Image();
|
|
112
|
+
const img = this._cachedImage;
|
|
113
|
+
|
|
114
|
+
img.onload = () => {
|
|
115
|
+
this.loaded = true;
|
|
116
|
+
if (onFinish) onFinish();
|
|
117
|
+
};
|
|
118
|
+
const svgText = this.toString(true /* shouldInjectFonts */);
|
|
119
|
+
img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
|
|
120
|
+
this.loaded = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Serialize the active SVG DOM to a string.
|
|
125
|
+
* @param {?boolean} shouldInjectFonts True if fonts should be included in the SVG as
|
|
126
|
+
* base64 data.
|
|
127
|
+
* @returns {string} String representing current SVG data.
|
|
128
|
+
* @deprecated Use the standalone `serializeSvgToString` export instead.
|
|
129
|
+
*/
|
|
130
|
+
toString (shouldInjectFonts) {
|
|
131
|
+
return serializeSvgToString(this._svgTag, shouldInjectFonts);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Synchronously draw the loaded SVG to this renderer's `canvas`.
|
|
136
|
+
* @param {number} [scale] - Optionally, also scale the image by this factor.
|
|
137
|
+
*/
|
|
138
|
+
draw (scale) {
|
|
139
|
+
if (!this.loaded) throw new Error('SVG image has not finished loading');
|
|
140
|
+
this._drawFromImage(scale);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Draw to the canvas from a loaded image element.
|
|
145
|
+
* @param {number} [scale] - Optionally, also scale the image by this factor.
|
|
146
|
+
**/
|
|
147
|
+
_drawFromImage (scale) {
|
|
148
|
+
if (this._cachedImage === null) return;
|
|
149
|
+
|
|
150
|
+
const ratio = Number.isFinite(scale) ? scale : 1;
|
|
151
|
+
const bbox = this._measurements;
|
|
152
|
+
this._canvas.width = bbox.width * ratio;
|
|
153
|
+
this._canvas.height = bbox.height * ratio;
|
|
154
|
+
// Even if the canvas at the current scale has a nonzero size, the image's dimensions are floored pre-scaling.
|
|
155
|
+
// e.g. if an image has a width of 0.4 and is being rendered at 3x scale, the canvas will have a width of 1, but
|
|
156
|
+
// the image's width will be rounded down to 0 on some browsers (Firefox) prior to being drawn at that scale.
|
|
157
|
+
if (
|
|
158
|
+
this._canvas.width <= 0 ||
|
|
159
|
+
this._canvas.height <= 0 ||
|
|
160
|
+
this._cachedImage.naturalWidth <= 0 ||
|
|
161
|
+
this._cachedImage.naturalHeight <= 0
|
|
162
|
+
) return;
|
|
163
|
+
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
|
164
|
+
this._context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
165
|
+
this._context.drawImage(this._cachedImage, 0, 0);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = SvgRenderer;
|