DEV Community 👩‍💻👨‍💻

Eckhardt
Eckhardt

Posted on

Downloading images in the browser with Node.js

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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;
  // ...
});
Enter fullscreen mode Exit fullscreen mode

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)}`;
Enter fullscreen mode Exit fullscreen mode
  • 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)

Collapse
charlesr1971 profile image
Charles Robertson

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.

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.