On my HTML i have an SVG element. It is rendered with d3js and has stylings applied in CSS.
When i right click in my browser i can select "Save image". This actions saves the image as rendered with all the css stylings applied.
I have been searching for a good way to save the file
However when i get the file to disk the extra stylings from my css is not applied to the saved image.
Question: How can i save my SVG as rendered in the browser with css applied.
The CSS parsing is not an easy task, CSS rules are complicated...
I tried to write something for my SVG2Bitmap little script, but it is still far from being perfect...
Basically, it parses all the stylesheets in the document, and check if any of the svg's nodes match the rule (thanks to querySelector
and Element.matches()
methods).
The problem is that once appended into the svg doc, the rules may not match anymore (e.g body>svg>rect
will fail). I still didn't find an elegant way to deal with it, also if anyone has one, please let me know.
An other issue I faced is that invalid rules will make previously mentioned methods to throw an error. This shouldn't be too much of a concern, but some browsers (Chrome to not tell its name) accept some hacky rules like [xlink\\:href]
but save it in the cssRules
as [xlink\:href]
which will fail and thus throw an error...
The "save as file" part however is way easier thanks to the XMLSerializer
object, which will let the browser create a standalone version of what it parsed, with all that is needed.
To make a 100% valid svg file, you'll also need to set a Doctype at top of your document.
So let's jump in the code :
var exportSVG = function(svg) {
// first create a clone of our svg node so we don't mess the original one
var clone = svg.cloneNode(true);
// parse the styles
parseStyles(clone);
// create a doctype
var svgDocType = document.implementation.createDocumentType('svg', "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd");
// a fresh svg document
var svgDoc = document.implementation.createDocument('http://www.w3.org/2000/svg', 'svg', svgDocType);
// replace the documentElement with our clone
svgDoc.replaceChild(clone, svgDoc.documentElement);
// get the data
var svgData = (new XMLSerializer()).serializeToString(svgDoc);
// now you've got your svg data, the following will depend on how you want to download it
// e.g yo could make a Blob of it for FileSaver.js
/*
var blob = new Blob([svgData.replace(/></g, '>\n\r<')]);
saveAs(blob, 'myAwesomeSVG.svg');
*/
// here I'll just make a simple a with download attribute
var a = document.createElement('a');
a.href = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgData.replace(/></g, '>\n\r<'));
a.download = 'myAwesomeSVG.svg';
a.innerHTML = 'download the svg file';
document.body.appendChild(a);
};
var parseStyles = function(svg) {
var styleSheets = [];
var i;
// get the stylesheets of the document (ownerDocument in case svg is in <iframe> or <object>)
var docStyles = svg.ownerDocument.styleSheets;
// transform the live StyleSheetList to an array to avoid endless loop
for (i = 0; i < docStyles.length; i++) {
styleSheets.push(docStyles[i]);
}
if (!styleSheets.length) {
return;
}
var defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
if (!defs.parentNode) {
svg.insertBefore(defs, svg.firstElementChild);
}
svg.matches = svg.matches || svg.webkitMatchesSelector || svg.mozMatchesSelector || svg.msMatchesSelector || svg.oMatchesSelector;
// iterate through all document's stylesheets
for (i = 0; i < styleSheets.length; i++) {
var currentStyle = styleSheets[i]
var rules;
try {
rules = currentStyle.cssRules;
} catch (e) {
continue;
}
// create a new style element
var style = document.createElement('style');
// some stylesheets can't be accessed and will throw a security error
var l = rules && rules.length;
// iterate through each cssRules of this stylesheet
for (var j = 0; j < l; j++) {
// get the selector of this cssRules
var selector = rules[j].selectorText;
// probably an external stylesheet we can't access
if (!selector) {
continue;
}
// is it our svg node or one of its children ?
if ((svg.matches && svg.matches(selector)) || svg.querySelector(selector)) {
var cssText = rules[j].cssText;
// append it to our <style> node
style.innerHTML += cssText + '\n';
}
}
// if we got some rules
if (style.innerHTML) {
// append the style node to the clone's defs
defs.appendChild(style);
}
}
};
exportSVG(document.getElementById('mySVG'));
svg >rect {
fill: yellow
}
/* this will fail, it could work with a check for document.querySelector instead of svg.querySelector, but that would just be a kill for performances with a lot of cssRules..., and would need to set the elements' style attribute instead of using a <style> tag */
body > svg >rect {
stroke: red
}
<svg width="120" height="120" viewBox="0 0 120 120" id="mySVG">
<rect x="10" y="10" width="100" height="100" />
</svg>
Ps : the download part of this snippet won't work in FF, you can try it in this fiddle though.