Sometimes, in the course of web development, there are situations where
we have content that is generated by the client browser, but the user might like
to download it for use offline. Specifically, graphic elements such as svg
or
canvas
might be prime candidates for this kind of technique.
Downloading a server-generated image is dead easy: just provide an appropriate link to the generated image file on the server.
But when dealing with content that’s inside the document the user is currently looking at, things are a bit more complicated, since it’s not immediately clear where exactly the browser would be downloading the image from. Not only that, but the data the browser would be downloading is within the current document, which would be destroyed when the browser navigates away to the “download.” It seems a bit of a Catch-22.
Fortunately, there are ways to work around this. First let’s define a function that will enable us to download content on the fly, if we have a string that contains the data in question:
var saveInline = function(filename, data, mimetype='application/octet-stream') {
var blob = new Blob([data], {type: mimetype});
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename);
}
else {
var elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = filename;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
elem.href.revokeObjectURL(blob);
}
}
This code works by creating a phantom A
tag, making our data the target of
this element, and then programmatically “clicking” the link. (We also make sure
to remove the phantom element after the download.)
Now we can create a function that will grab the data of an HTML element and make it into a string that we can pass to the download function:
var saveInlineElement = function(element, filename, mimetype) {
let elContent = element.toDataURL(mimetype);
saveInline(filename, elContent, mimetype);
return elContent;
}
The
If we drop these two functions into a file (named, perhaps, saveInline.js
), we
can then call them from an HTML page such as the following very minimal example:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width; initial-scale=1">
</head>
<body>
<div id="svgwrapper">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><circle fill="#4FD1D9" cx="32" cy="32" r="30"/><path fill="#FFF" d="M38 12L18 32l20 20z"/></svg>
</div>
<div>
<button onclick="doDownload();">Download</button>
</div>
<script src="saveInline.js"></script>
<script>
var doDownload = function() {
let el = document.querySelector("div#svgwrapper>svg");
saveInlineElement(el, "el.svg", "text/svg");
};
</script>
</body>
You’ll note that there may be a restriction on saving the contents of a canvas
tag this way if there have been any images drawn onto that canvas which were
loaded from another domain than the one the page is hosted on. You might
get a security error in this case. Take a look at
Cross-Origin Resource Sharing (CORS)
for more details on this kind of thing.
This code works for me with both canvas
and svg
elements, with modern browsers.
I don’t guarantee it will work with any particular browser, especially older ones.
Caveat developor. 😉
Enjoy!