6

I want to make a <a href link which, when clicked, forces your browser to open a "Save As" dialogue. The use of the HTML5 download attribute should have this behavior on all modern browsers.

When the target is an externally hosted image file, this does not work. The <a href link will navigate to the image instead of downloading it. (Tested using images hosted on Imgur and Tumblr)

<a href="https://i.stack.imgur.com/440u9.png" download>
  <img src="https://i.stack.imgur.com/440u9.png" width="200"/>
</a>

Similar HTML code does work if the image is hosted locally on your server, for example

<a href="440u9.png" download>
  <img src="440u9.png" width="200"/>
</a>

Another example - this will work in the W3Schools Tryit Editor, but will not work if you copy the HTML to another sandbox like JSFiddle

<a href="https://www.w3schools.com/images/myw3schoolsimage.jpg" download>
  <img src="https://www.w3schools.com/images/myw3schoolsimage.jpg" width="104" height="142">
</a>


Possible causes explored:

  • Bad image URL: no, the URL is good, you can open the URL as a plain image in your browser
  • Links with https: can't be used this way due to security: no, a URL with https: is good if the URL points to the same domain, tested in W3Schools Tryit Editor
  • Server-side redirect: an http://... link can be redirected to https://... so maybe that could be a cause of failure, but the https://... link also does not work
  • Suppression of download attribute on server by specific hosts? is that even possible? the download attribute should control only client-side browser action surely?

Any workarounds? I have tried Javascript download methods discussed here and here but these all eventually involved a browser action similar to clicking on an href link.

Excellent in-depth discussion of download links including application/force-download setting in PHP on the server

Related StackOverflow question: Force external download url (answer seems to be incorrect)

radfast
  • 538
  • 1
  • 7
  • 14

2 Answers2

4

Turns out the answer is that this cannot be done in the way I was asking. Modern browsers prevent cross-domain downloads as a security feature.

See: Making Firefox and Chrome download image under a specific name

Force <a download /> to download image instead of opening url link to image

If the download must be byte-identical to the original file, the only solutions I can find right now are:

  1. The server must first use an httpClient request to get the target resource, save it to disk locally on the server, then provide an <a href='tmp.jpg' download> link with the URI being that saved file. Or, some server frameworks are able to create a bytestream simulating file access, in which case the resource could be cached to memory instead of saving to disk.

    Drawbacks: data cost equal to twice the filesize (once in, once out), roundtrip delay, use of server memory/filesystem. Probably every possible file with a download link has to be stored locally on the server before the page is served (or maybe it can be done asynchronously using websockets, serving the file after the user clicks the download link, but with a delay). For large files or high usage servers these are serious drawbacks.

  2. Open the target URL in a new tab, and let the client switch to that tab, and manually right-click and Save As...

    Drawbacks: this is exactly what we are trying to avoid!

I'm still wondering if there is some hack for this, like giving a local URL and having the server redirect a request for that particular resource to the external site.

radfast
  • 538
  • 1
  • 7
  • 14
3

My workaround: download an image to canvas, then download canvas image to the local drive.

The code:

    <canvas id="downloadCanvas"></canvas>

    <script>
        var strDataURI = "https://78.media.tumblr.com/33f1b72778ca7352979b0f4555f08f02/tumblr_pfega8eUyH1r68v93o1_640.jpg";
        var img = new Image;
        img.crossOrigin = "Anonymous";
        img.src = strDataURI;


        var canvas = document.getElementById('downloadCanvas');
        var ctx = canvas.getContext('2d');

        img.onload = function(){

          canvas.width = img.width;
          canvas.height = img.height;
          ctx.drawImage(img,0,0);

          var dwBtn = document.getElementById("dw0");
            var dwLink = canvas.toDataURL('image/jpeg', 1.0);

          download(dwLink, "image.jpeg");
        };


    function download(dataurl, filename) {
      var a = document.createElement("a");
      a.href = dataurl;
      a.setAttribute("download", filename);
      var b = document.createEvent("MouseEvents");
      b.initEvent("click", false, true);
      a.dispatchEvent(b);
      return false;
    }

</script>

As the OP asked, he needs captured image to have same extension. (pseudoish code)

Change these lines:

  var ext = ExtensionFromUrl(url);
  var fileName = NameFromUrl(url);

      var dwLink = canvas.toDataURL('image/'+ext, 1.0);
      download(dwLink, fileName+"."+ext);

Note that: This answer won't work with GIF images. Also, as they are captured images, transparency channels will be lost!

Nick
  • 455
  • 9
  • 28
  • 1
    @radfast Thanks. Don't forget to press "accept answer tick" if my code works for you! – Nick Sep 23 '18 at 12:36
  • ok testing in more detail, it's a smart workaround - but it's encoding a new JPEG file from the canvas, not downloading the original served file. So the file saved to disk is different from what I'd get from right-click Save As... – radfast Sep 23 '18 at 12:49
  • @radfast it is indeed a different file. Its an image captured from a canvas. To change name or extension you need to modify a code to get them from the URL, or any other source, as you would wish. But testing in more detail, you could blame me for that this workaround wont work with GIF files. I will point this out in my answer too, so that for somebody to come up with a better solution to mine, he has to know whats wrong with it. – Nick Sep 23 '18 at 13:18
  • Meanwhile I'm looking at ways to have my webapp download the bytestream from the external link server-side (my server will fetch the file from the external link) and then when done, serve it to the client as a local file (now hosted on my server). Not a very good solution as it creates a bottleneck on the server and doubles the data usage. – radfast Sep 23 '18 at 13:24