大多数 svg 到画布库将在外部资源(图像、用途、符号和xlink
名称空间中的任何其他属性或带有<funciri>
( url()
) 的属性中)失败。
我正在编写一个处理这些情况的脚本。
它确实会搜索此类外部资源,并将其附加到要转换的 svg 元素的克隆中,然后使用画布drawImage()
功能呈现 svg。
它的使用非常简单,在不久的将来可能会更简单。
SVG2bitmap(SVGElement, [function([canvasElement],[dataURL]) || IMGElement || CanvasElement] [, Object{optional parameters}])
这是解析具有此类外部属性的元素的函数的转储:
function parseXlinks() {
// some browsers don't support the asterisk namespace selector (guess which ones ...)
// create a test element
var test = document.createElementNS('http://www.w3.org/2000/svg', 'use');
// set its href attribute to something that should be found
test.setAttributeNS(xlinkNS, 'href', '__#__');
// append it to our document
clone.appendChild(test);
// if querySelector returns null then the selector is not supported
var supported = !!clone.querySelector('[*|href*="#"]'),
xl,
i;
// the test is done, remove the element
clone.removeChild(test);
// if the selector is not supported
if (!supported) {
// xl is an array
xl = [];
// iterate through all our elements
var children = clone.querySelectorAll('*');
for (i = 0; i < children.length; i++) {
// search the xlink:href attribute
var xl_attr = children[i].getAttributeNS(xlinkNS, 'href');
// we only want the ones that refer to elements
if (xl_attr && xl_attr.indexOf('#') > -1) {
xl.push(children[i]);
}
}
} else {
// get all our elements using an xlink:href attribute
xl = clone.querySelectorAll('[*|href*="#"]');
}
// the list of all attributes that can have a <funciri> (url()) as value
var url_attrs = ["clip-path", "src", "cursor", "fill", "filter", "marker", "marker-start", "marker-mid", "marker-end", "mask", "stroke"];
// build our selector string
var urlSelector = '[*|' + url_attrs.join('*="url"], *[*|') + '*="url"]';
// query is magic
var url = clone.querySelectorAll(urlSelector);
// we found something
if (xl.length || url.length) {
// create a <defs> or get the svg's one
getDef();
}
// there is no such elements ?
else {
// continue directly with images
parseImages();
return;
}
// create an array for external docs
var externals = [],
inDoc = [];
var getElements = function(arr) {
for (var i = 0; i < inDoc.length; i++) {
var el = inDoc[i];
// not in our actual svg ?
if (!svg.getElementById(el)) {
// get it somewhere else in the page
var ref = document.querySelector('#' + el);
// failed
if (!ref) {
console.warn('could not find this element : ', '#' + el);
continue;
}
// we got it, add a clone to our svg
defs.appendChild(ref.cloneNode(true));
}
}
};
// fetch the external documents
var addFile = function(url) {
var pushed = false;
for (var i = 0; i < externals.length; i++) {
// if we already have this document, just push the element
if (url[0] === externals[i].file_url) {
pushed = true;
externals[i].elements.push(url[1]);
checkParse();
}
}
// that was a new doc
if (!pushed) {
// create the object we'll use for this doc
var that = {
file_url: url[0],
elements: [url[1]],
loading: null
};
// add it to our array
externals.push(that);
// create a new request
var xhr = new XMLHttpRequest();
xhr.onload = function() {
// we're not loading anymore
that.loading = false;
// everything went fine
if (this.status === 200) {
that.response = this.responseText;
} else {
console.warn('could not load this document :', url[0], '\n' +
'Those elements are lost : ', that.elements.join(' , '));
}
// In case we were the last one
checkParse();
};
xhr.onerror = function(e) {
that.loading = false;
console.warn('could not load this document', url[0]);
console.warn('Those elements are lost : ', that.elements.join(' , '));
checkParse();
};
xhr.open('GET', that.file_url);
that.loading = true;
xhr.send();
}
};
var checkParse = function() {
// there are still pending loadings
if (externals.some(function(o) {
return o.loading;
})) {
return;
} else {
// loop through all our documents
for (var i = 0; i < externals.length; i++) {
// if we failed to load it
if (!externals[i].response) {
continue;
}
// create a new doc from the response
var doc = new DOMParser().parseFromString(externals[i].response, 'image/svg+xml');
// loop through the elements we use in this document
var els = externals[i].elements;
for (var j = 0; j < els.length; j++) {
// get the new id we'll use in our svg file
var newId = externals[i].file_url.replace('.svg', '_') + els[j];
// this one was already appended
if (defs.querySelector('#' + newId)) {
continue;
}
// find it in the response doc
var elem = doc.documentElement.querySelector('#' + els[j]);
if (elem) {
// we found it
var clone = elem.cloneNode(true);
clone.setAttribute('id', newId);
defs.appendChild(clone);
} else {
console.warn('could not find this element : ', externals[i].file_url + '#' + els[j]);
}
}
}
// all responses have been parsed
// we can continue with images
parseImages();
}
};
// get the attributes containing the <funciri>
for (i = 0; i < url.length; i++) {
// get all our node's attributes
var att = url[i].attributes;
// store a new array to our node
url[i].external_attr = [];
for (var j = 0; j < att.length; j++) {
// does it have a <funciri> ?
if (att[j].value.indexOf('url(') > -1) {
// add it to the array
url[i].external_attr.push(att[j]);
}
}
}
var split_attr = function(list, type) {
// loop through our list to get the external elements
for (var i = 0; i < list.length; i++) {
var hrefs = [],
j;
if (type === 'xlink') {
// get the href attribute of our element
hrefs.push(list[i].getAttributeNS(xlinkNS, 'href').split('#'));
} else {
// loop through all attributes containing a <funciri>
var attr = list[i].external_attr;
for (j = 0; j < attr.length; j++)
hrefs.push(attr[j].value.substring(4).slice(0, -1).split('#'));
}
for (j = 0; j < hrefs.length; j++) {
var href = hrefs[j];
// it does point to an external doc
if (href[0].indexOf('.svg') > 0) {
addFile(href);
// a new id if different external docs uses the same ids
var newId = '#' + href[0].replace('.svg', '_') + href[1];
// 'xlink' case
if (type === 'xlink')
list[i].setAttributeNS(xlinkNS, 'href', newId);
// <funciri> case
else
list[i].setAttribute(list[i].external_attr[j].name, 'url(' + newId + ')');
}
// it should be inside the page
else if (!href[0]) {
// push it to our array if it's not there already
if (inDoc.indexOf(href[1] < 0)) {
inDoc.push(href[1]);
}
}
}
}
if (inDoc.length) {
getElements(inDoc);
}
};
split_attr(xl, 'xlink');
split_attr(url, 'funciri');
// all was done synchronously or before we finished parsing (not sure this can happen)
if (externals.length === 0 || !externals.some(function(o) {
return o.loading;
})) {
exportDoc();
}
}
现场示例:
var svg = toPixel;
var clone = svg.cloneNode(true);
var doSomethingWith = function(canvas) {
document.body.appendChild(canvas)
};
var xlinkNS = 'http://www.w3.org/1999/xlink';
function parseXlinks() {
// some browsers don't support the asterisk namespace selector (guess which ones ...)
// create a test element
var test = document.createElementNS('http://www.w3.org/2000/svg', 'use');
// set its href attribute to something that should be found
test.setAttributeNS(xlinkNS, 'href', '__#__');
// append it to our document
clone.appendChild(test);
// if querySelector returns null then the selector is not supported
var supported = !!clone.querySelector('[*|href*="#"]'),
xl,
i;
// the test is done, remove the element
clone.removeChild(test);
// if the selector is not supported
if (!supported) {
// xl is an array
xl = [];
// iterate through all our elements
var children = clone.querySelectorAll('*');
for (i = 0; i < children.length; i++) {
// search the xlink:href attribute
var xl_attr = children[i].getAttributeNS(xlinkNS, 'href');
// we only want the ones that refer to elements
if (xl_attr && xl_attr.indexOf('#') > -1) {
xl.push(children[i]);
}
}
} else {
// get all our elements using an xlink:href attribute
xl = clone.querySelectorAll('[*|href*="#"]');
}
// the list of all attributes that can have a <funciri> (url()) as value
var url_attrs = ["clip-path", "src", "cursor", "fill", "filter", "marker", "marker-start", "marker-mid", "marker-end", "mask", "stroke"];
// build our selector string
var urlSelector = '[*|' + url_attrs.join('*="url"], *[*|') + '*="url"]';
// query is magic
var url = clone.querySelectorAll(urlSelector);
// we found something
if (xl.length || url.length) {
// create a <defs> or get the svg's one
getDef();
}
// there is no such elements ?
else {
// continue directly with images
parseImages();
return;
}
// create an array for external docs
var externals = [],
inDoc = [];
var getElements = function(arr) {
for (var i = 0; i < inDoc.length; i++) {
var el = inDoc[i];
// not in our actual svg ?
if (!svg.getElementById(el)) {
// get it somewhere else in the page
var ref = document.querySelector('#' + el);
// failed
if (!ref) {
console.warn('could not find this element : ', '#' + el);
continue;
}
// we got it, add a clone to our svg
defs.appendChild(ref.cloneNode(true));
}
}
};
// fetch the external documents
var addFile = function(url) {
var pushed = false;
for (var i = 0; i < externals.length; i++) {
// if we already have this document, just push the element
if (url[0] === externals[i].file_url) {
pushed = true;
externals[i].elements.push(url[1]);
checkParse();
}
}
// that was a new doc
if (!pushed) {
// create the object we'll use for this doc
var that = {
file_url: url[0],
elements: [url[1]],
loading: null
};
// add it to our array
externals.push(that);
// create a new request
var xhr = new XMLHttpRequest();
xhr.onload = function() {
// we're not loading anymore
that.loading = false;
// everything went fine
if (this.status === 200) {
that.response = this.responseText;
} else {
console.warn('could not load this document :', url[0], '\n' +
'Those elements are lost : ', that.elements.join(' , '));
}
// In case we were the last one
checkParse();
};
xhr.onerror = function(e) {
that.loading = false;
console.warn('could not load this document', url[0]);
console.warn('Those elements are lost : ', that.elements.join(' , '));
checkParse();
};
xhr.open('GET', that.file_url);
that.loading = true;
xhr.send();
}
};
var checkParse = function() {
// there are still pending loadings
if (externals.some(function(o) {
return o.loading;
})) {
return;
} else {
// loop through all our documents
for (var i = 0; i < externals.length; i++) {
// if we failed to load it
if (!externals[i].response) {
continue;
}
// create a new doc from the response
var doc = new DOMParser().parseFromString(externals[i].response, 'image/svg+xml');
// loop through the elements we use in this document
var els = externals[i].elements;
for (var j = 0; j < els.length; j++) {
// get the new id we'll use in our svg file
var newId = externals[i].file_url.replace('.svg', '_') + els[j];
// this one was already appended
if (defs.querySelector('#' + newId)) {
continue;
}
// find it in the response doc
var elem = doc.documentElement.querySelector('#' + els[j]);
if (elem) {
// we found it
var clone = elem.cloneNode(true);
clone.setAttribute('id', newId);
defs.appendChild(clone);
} else {
console.warn('could not find this element : ', externals[i].file_url + '#' + els[j]);
}
}
}
// all responses have been parsed
// we can continue with images
parseImages();
}
};
// get the attributes containing the <funciri>
for (i = 0; i < url.length; i++) {
// get all our node's attributes
var att = url[i].attributes;
// store a new array to our node
url[i].external_attr = [];
for (var j = 0; j < att.length; j++) {
// does it have a <funciri> ?
if (att[j].value.indexOf('url(') > -1) {
// add it to the array
url[i].external_attr.push(att[j]);
}
}
}
var split_attr = function(list, type) {
// loop through our list to get the external elements
for (var i = 0; i < list.length; i++) {
var hrefs = [],
j;
if (type === 'xlink') {
// get the href attribute of our element
hrefs.push(list[i].getAttributeNS(xlinkNS, 'href').split('#'));
} else {
// loop through all attributes containing a <funciri>
var attr = list[i].external_attr;
for (j = 0; j < attr.length; j++)
hrefs.push(attr[j].value.substring(4).slice(0, -1).split('#'));
}
for (j = 0; j < hrefs.length; j++) {
var href = hrefs[j];
// it does point to an external doc
if (href[0].indexOf('.svg') > 0) {
addFile(href);
// a new id if different external docs uses the same ids
var newId = '#' + href[0].replace('.svg', '_') + href[1];
// 'xlink' case
if (type === 'xlink')
list[i].setAttributeNS(xlinkNS, 'href', newId);
// <funciri> case
else
list[i].setAttribute(list[i].external_attr[j].name, 'url(' + newId + ')');
}
// it should be inside the page
else if (!href[0]) {
// push it to our array if it's not there already
if (inDoc.indexOf(href[1] < 0)) {
inDoc.push(href[1]);
}
}
}
}
if (inDoc.length) {
getElements(inDoc);
}
};
split_attr(xl, 'xlink');
split_attr(url, 'funciri');
// all was done synchronously or before we finished parsing (not sure this can happen)
if (externals.length === 0 || !externals.some(function(o) {
return o.loading;
})) {
exportDoc();
}
}
var defs;
var getDef = function() {
// Do we have a `<defs>` element already ?
defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
if (!defs.parentNode) {
svg.insertBefore(defs, svg.firstElementChild);
}
};
var exportDoc = function() {
// check if our svgNode has width and height properties set to absolute values
// otherwise, canvas won't be able to draw it
var bbox = svg.getBoundingClientRect();
if (svg.width.baseVal.unitType !== 1) svg.setAttribute('width', bbox.width);
if (svg.height.baseVal.unitType !== 1) svg.setAttribute('height', bbox.height);
// serialize our node
var svgData = (new XMLSerializer()).serializeToString(svg);
// remember to encode special chars
var svgURL = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgData);
var svgImg = new Image();
svgImg.onload = function() {
var canvas = document.createElement('canvas');
// IE11 doesn't set a width on svg images...
canvas.width = this.width || bbox.width;
canvas.height = this.height || bbox.height;
canvas.getContext('2d').drawImage(svgImg, 0, 0, canvas.width, canvas.height);
doSomethingWith(canvas)
};
svgImg.src = svgURL;
};
parseXlinks();
canvas {
border: 1px solid green !important;
}
<script src="https://rawgit.com/Kaiido/SVG2Bitmap/master/SVG2Bitmap.js"></script>
<svg style="display: none">
<symbol id="sym01" viewBox="0 0 150 110">
<circle cx="50" cy="50" r="40" stroke-width="8" stroke="red" fill="red" />
<circle cx="90" cy="60" r="40" stroke-width="8" stroke="green" fill="white" />
</symbol>
</svg>
<svg id="toPixel">
<use xlink:href="#sym01" />
</svg>
<br>canvas version:
<br>
或使用整个脚本:
SVG2bitmap(toPixel, function(canvas){
document.body.appendChild(canvas);
});
canvas { border: 1px solid green !important;}
<script src="https://rawgit.com/Kaiido/SVG2Bitmap/master/SVG2Bitmap.js"></script>
<svg style="display: none">
<symbol id="sym01" viewBox="0 0 150 110">
<circle cx="50" cy="50" r="40" stroke-width="8" stroke="red" fill="red" />
<circle cx="90" cy="60" r="40" stroke-width="8" stroke="green" fill="white" />
</symbol>
</svg>
<svg id="toPixel">
<use xlink:href="#sym01" />
</svg>
<br>canvas version:
<br>