19

First of all there is a question with the same title here on SO but its not what I'm looking for and it doesn't have a complete answer either.

So here's my question. Say I have this URL which directs to an image.

https://fbcdn-photos-a.akamaihd.net/hphotos-ak-ash4/299595_10150290138650735_543370734_8021370_355110168_n.jpg

Once I put this parameter ?dl=1 to the end of the URL, it becomes downloadable.

https://fbcdn-photos-a.akamaihd.net/hphotos-ak-ash4/299595_10150290138650735_543370734_8021370_355110168_n.jpg?dl=1

I'm trying to do this task through a userscript. So I used XMLHttpRequest for that.

var url = "https://fbcdn-photos-a.akamaihd.net/hphotos-ak-ash4/299595_10150290138650735_543370734_8021370_355110168_n.jpg?dl=1";

var request = new XMLHttpRequest();  
request.open("GET", url, false);   
request.send(null);  

if (request.status === 200) 
{  
    alert(request.statusText);
}

Here is a fiddle.

But it does not work.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Isuru
  • 30,617
  • 60
  • 187
  • 303

4 Answers4

25

XMLHttpRequest will not work cross-domain, but since this is a userscript Chrome now supports GM_xmlhttpRequest() in userscripts only.

Something like this should work, note that it is asynchronous:

GM_xmlhttpRequest ( {
    method:         'GET',
    url:            'https://fbcdn-photos-a.akamaihd.net/hphotos-ak-ash4/299595_10150290138650735_543370734_8021370_355110168_n.jpg?dl=1',
    onload:         function (responseDetails) {
                        alert(responseDetails.statusText);
                    }
} );




As for getting and using the actual image data, that is a major pain to work out.

  • You can use the new .responseType = "blob"; functionality in Firefox but Chrome does not yet support it.

  • In Chrome or Firefox, for the same domain only, you can use the new XHR2 like so:
    See it in action at jsBin.

    BlobBuilder             = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;
    
    var url                 = "http://jsbin.com/images/gear.png";
    var request             = new XMLHttpRequest();
    request.open ("GET", url, false);
    request.responseType    = "arraybuffer";
    request.send (null);
    
    if (request.status === 200) {
        var bb              = new BlobBuilder ();
        bb.append (request.response); // Note: not request.responseText
    
        var blob            = bb.getBlob ('image/png');
        var reader          = new FileReader ();
        reader.onload       = function (zFR_Event) {
            $("body").prepend ('<p>New image: <img src="' + zFR_Event.target.result + '"></p>')
        };
    
        reader.readAsDataURL (blob);
    }
    


  • Unfortunately, GM_xmlhttpRequest() does not (yet) support setting responseType.


So, for GM script or userscript applications, we have to use a custom base64 encoding scheme like in "Javascript Hacks: Using XHR to load binary data".

The script code becomes something like:

var imgUrl              = "http://jsbin.com/images/gear.png";

GM_xmlhttpRequest ( {
    method:         'GET',
    url:            imgUrl,
    onload:         function (respDetails) {
                        var binResp     = customBase64Encode (respDetails.responseText);

                        /*-- Here, we just demo that we have a valid base64 encoding
                            by inserting the image into the page.
                            We could just as easily AJAX-off the data instead.
                        */
                        var zImgPara    = document.createElement ('p');
                        var zTargetNode = document.querySelector ("body *"); //1st child

                        zImgPara.innerHTML = 'Image: <img src="data:image/png;base64,'
                                           + binResp + '">';
                        zTargetNode.parentNode.insertBefore (zImgPara, zTargetNode);
                    },
    overrideMimeType: 'text/plain; charset=x-user-defined'
} );


function customBase64Encode (inputStr) {
    var
        bbLen               = 3,
        enCharLen           = 4,
        inpLen              = inputStr.length,
        inx                 = 0,
        jnx,
        keyStr              = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
                            + "0123456789+/=",
        output              = "",
        paddingBytes        = 0;
    var
        bytebuffer          = new Array (bbLen),
        encodedCharIndexes  = new Array (enCharLen);

    while (inx < inpLen) {
        for (jnx = 0;  jnx < bbLen;  ++jnx) {
            /*--- Throw away high-order byte, as documented at:
              https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data
            */
            if (inx < inpLen)
                bytebuffer[jnx] = inputStr.charCodeAt (inx++) & 0xff;
            else
                bytebuffer[jnx] = 0;
        }

        /*--- Get each encoded character, 6 bits at a time.
            index 0: first  6 bits
            index 1: second 6 bits
                        (2 least significant bits from inputStr byte 1
                         + 4 most significant bits from byte 2)
            index 2: third  6 bits
                        (4 least significant bits from inputStr byte 2
                         + 2 most significant bits from byte 3)
            index 3: forth  6 bits (6 least significant bits from inputStr byte 3)
        */
        encodedCharIndexes[0] = bytebuffer[0] >> 2;
        encodedCharIndexes[1] = ( (bytebuffer[0] & 0x3) << 4)   |  (bytebuffer[1] >> 4);
        encodedCharIndexes[2] = ( (bytebuffer[1] & 0x0f) << 2)  |  (bytebuffer[2] >> 6);
        encodedCharIndexes[3] = bytebuffer[2] & 0x3f;

        //--- Determine whether padding happened, and adjust accordingly.
        paddingBytes          = inx - (inpLen - 1);
        switch (paddingBytes) {
            case 1:
                // Set last character to padding char
                encodedCharIndexes[3] = 64;
                break;
            case 2:
                // Set last 2 characters to padding char
                encodedCharIndexes[3] = 64;
                encodedCharIndexes[2] = 64;
                break;
            default:
                break; // No padding - proceed
        }

        /*--- Now grab each appropriate character out of our keystring,
            based on our index array and append it to the output string.
        */
        for (jnx = 0;  jnx < enCharLen;  ++jnx)
            output += keyStr.charAt ( encodedCharIndexes[jnx] );
    }
    return output;
}
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • yes, it works! Thank you. :) small question, how can you actually download the image now? I tried all the other methods but they all return info, state of the request etc. – Isuru Jan 09 '12 at 17:51
  • okay.. I read about `overrideMimeType` argument on greasespot but it doesn't offer much details either. Thank you :) – Isuru Jan 10 '12 at 17:43
  • 1
    Okay, updated the answer showing how to grab and base64 encode the data (which allows it to be used in the page or AJAXed off to a server in usable form). – Brock Adams Jan 11 '12 at 04:20
  • whoa! you weren't kidding about this being a pain! :D I will go through the code and try to make it work. btw if I put `GM_XMLHttpRequest` (or any GM API method for that matter) inside a function, wouldn't it work? you once answered about this for `GM_addStyle`in this [question](http://stackoverflow.com/a/8628630/1077789), I just want to know whether that's applicable to all the GM API methods. And I'm really grateful for all the time and effort you've put into this. Thank you very much :) – Isuru Jan 11 '12 at 16:11
  • 1
    You can put the `GM_` functions inside other functions, but only ones that run in the script scope. If the wrapping function gets injected or otherwise runs in the page scope, then the `GM_` functions won't work. This is by design (for "security"). – Brock Adams Jan 11 '12 at 21:58
  • now this output is a string, right? So is it possible to use it and construct `Blob` objects using `BlobBuilder` and `Files` using `FileSystem API` on Chrome?? – Isuru Jan 14 '12 at 13:35
  • The output becomes a standard format object only after it has been filtered through `customBase64Encode()`. After that, then yes, you could feed `binResp` to `BlobBuilder` or wherever base64 is sold. You may need to use `atob()` on `binResp` first, depending on what you are doing. – Brock Adams Jan 14 '12 at 21:13
  • Thanks. Can you please explain what this line does? `overrideMimeType: 'text/plain; charset=x-user-defined'` – Isuru Jan 15 '12 at 16:31
  • 1
    That line stops the server response from being truncated -- which would otherwise happen when trying to treat binary data as text -- which we have to do since `GM_xmlhttpRequest()` does not (yet) support `responseType`. – Brock Adams Jan 15 '12 at 21:20
  • One last question. I know this comment thread is getting longer than it should be. I just need a clarification. I can pass `binResp` string to [this](http://stackoverflow.com/a/5100158/1077789) function and convert the dataURI to a `Blob`, right?? – Isuru Jan 17 '12 at 19:04
  • It's best not to ask follow-on questions in comments, yes. As for that function, you may need to slightly modify it since it expects `binResp` to have a dataURI preamble (EG: `data:image/png;base64,`). You can *probably* also skip it and use [append()](https://developer.mozilla.org/en/DOM/BlobBuilder#append%28%29). – Brock Adams Jan 17 '12 at 22:58
  • You could skip the base64 part if you need a blob and build it directly with BlobBuilder. I had a similar question (http://stackoverflow.com/questions/9612229/can-i-get-the-data-of-an-img-tag-as-a-blob) and came up with several answers. See fiddle @ http://jsfiddle.net/KEnVR/ – Yuval Mar 08 '12 at 15:23
  • Why customBase64Encode, rather than just using atob and btoa? – BrianFreud May 25 '12 at 12:12
  • @BrianFreud, It is because the `GM_xmlhttpRequest` response is not (or wasn't) normal base-64 encoding. See the comments in the source. – Brock Adams May 25 '12 at 12:40
  • @Brock: Found the same thing last night while trying to do something similar in a userscript of my own. Ended up just using this same workaround, rather than try to figure out why btoa() was tossing out errors. Thanks for your question; the answers edited in have saved me a lot of time! :) – BrianFreud May 25 '12 at 23:31
  • @BrianFreud: Glad it helped! Feel free to upvote early and upvote often... Wink wink, nudge nudge. ;) – Brock Adams May 26 '12 at 00:02
  • Note that this only works for `@require`, [not for `@match`](https://code.google.com/p/chromium/issues/detail?id=112746). The latter will throw `XMLHttpRequest cannot load [...] Origin chrome-extension://[...] is not allowed by Access-Control-Allow-Origin.` – Arjan Feb 26 '13 at 23:02
  • Unlucky Safari has no WebKitBlobBuilder. – loretoparisi Oct 26 '15 at 14:35
4

Modern browsers have the Blob object:

GM_xmlhttpRequest({
  method: "GET",
  url: url,
  headers: { referer: url, origin: url },
  responseType: 'blob',
  onload: function(resp) {
    var img = document.createElement('img');
    img.src = window.URL.createObjectURL(resp.response);
    document.body.appendChild(img);
  }
});

The headers param will set the referrer so you can load referrer locked images.

ariel
  • 15,620
  • 12
  • 61
  • 73
4

You are trying to request a resource using XHR that is on a different domain and is thus blocked. Use CORS for cross-domain messaging using XHR.

Klemen Slavič
  • 19,661
  • 3
  • 34
  • 43
0

Krof Drakula is right, you cannot load an image from a different domain, but do you really need to do this? You can create and append an img tag and wait for it to load (with something like jQuery load()).

var img = document.createElement( 'img' );
img.setAttribute( 'src', url );
document.getElementsByTagName('body')[0].appendChild( img );
Daniel J F
  • 1,054
  • 2
  • 15
  • 29
  • 2
    no no... I don't want to embed the image on a page. I want to download it using a XHR. – Isuru Jan 08 '12 at 16:13
  • 1
    I don't really understand what you are trying to do, but if you need to use the image on a page, you can append it in a hidden `div`, for instance, just to cache it, and after loading do with it whatever you need. If you need to show a save dialogue (see your link above) than you can just use `window.location = 'http://foo.com/bar?dl=1'`. And if you really need to use XHR than CORS is probably your only option. – Daniel J F Jan 08 '12 at 16:27