Image downloading in modern browsers seems like a topic trivial enough - why write about it?
The drawback of native HTML downloading
HTML5 has a neat download
attribute readily available for the anchor element - by simply adding the following, a user can easily view the image by clicking the link.
<a href="https://picsum.photos/200/300" download="random.png">
Download this image
</a>
The problem with this approach is that the image simply opens in the browser and requires the user to save as
. This behaviour may not be the preferable user experience. A better flow may be that the user clicks the link and it automatically downloads into the Download Folder configured in the browser's settings.
This is also achievable without any server-side code in the following way:
index.html
<button id="download-link">Download Image</button>
index.js
const downloadButton = document.querySelector("#download-link");
downloadButton.addEventListener("click", async (evt) => {
evt.preventDefault();
// Fetch the image blob
const url = "https://picsum.photos/200/300";
const response = await fetch(url);
const blob = await response.blob();
// Create an objectURL
const blobURL = URL.createObjectURL(blob);
// create a hidden anchor element
const anchor = document.createElement("a");
anchor.style.display = "none";
// Set the <a> tag's href to blob url
// and give it a download name
anchor.href = blobURL;
anchor.download = "image-name.png";
// Append anchor and trigger the download
document.body.appendChild(anchor);
anchor.click();
});
The client-side code above listens to a click on the HTML button, fetches the image as a blob, creates an objectURL, adds it to a newly created (hidden) anchor tag and clicks it to initiate a download. Because the anchor tag has an object URL, the browser will initiate the download to the user's Download Folder.
This experience may be more user-friendly, but don't be surprised if you run into the notorious CORS
wall. CORS
or Cross-Origin Resource Sharing may many times cause the download to fail from the browser if the resource is not on the same origin, or doesn't have the appropriate headers set.
Making image download robust with Node.js
Luckily, for requests not coming from a browser e.g. a Node.js server - CORS
can be safely bypassed. The following example only requires one simple change to the download logic on the client - the URL. Instead of making a fetch directly to the image, you will make it to your Node.js API endpoint, which could be set up as follows:
app.js
const fetch = require("node-fetch");
const express = require("express");
const app = express();
app.get("/image", async (req, res) => {
// Fetch the required image
const imageURL = "https://picsum.photos/200/300";
const response = await fetch(imageURL);
// Set the appropriate headers, to let
// the browser know that it should save
res.writeHead(200, {
"content-disposition": 'attachment; filename="my-image.png"',
"content-type": "image/png",
});
// Pipe the request buffer into
// the response back to the client
return response.body.pipe(res);
});
The example above has a few parts to it, namely:
- Requesting the known image URL to receive the raw body in the response. The URL here could be dynamically set too and that way you could simply prepend your server URL to any image URL, e.g.
app.get("/image/:url", (req, res) => {
const { url } = req.params;
// ...
});
Just remember to encode the URI on the client before appending it to your server URL, e.g.
const finalURL = `https://your-server.com/image/${encodeURIComponent(url)}`;
- Setting the appropriate headers for the response:
content-dispostion
with a value of attachment
will tell the browser to save the file instead of the alternative inline
which will try to render the response in the browser.
Note here too you might want to have some sort of library or checker to determine the image MIME type e.g. image/png
for the content-type
header and file extension to be accurate.
- Piping the result into the response:
This simply takes the data in the result body and feeds it into the body of the response to the client.
Serverless Caveat
If you're using a serverless solution, be mindful of their Request Payload size limits. E.g. AWS limits the size of request bodies to ~6MB. If you're working with large images, consider a static solution.
Conclusion
If you're already calling a Node.js back-end to feed your front-end, why not add an endpoint to help you download remote images with a better experience. You even get the niceties of overcoming the dreaded CORS
error.
If you want to automate this task for your website screenshots, let Stillio do the heavy lifting.
Top comments (1)
This is great, but why are you adding the link using JavaScript. The problem with this approach, is that if someone keeps clicking on the download button, many invisible links get added to the DOM.
I have just added an HTML link with the download attribute. And added the relevant href to my image file. Then when a user clicks on the link, the Save dialog box appears. This is much nicer, than asking users to right click on an image and press Save.