8

I have a problem with BlobBuilder (Chrome11) I try to obtain an image from server with XHR request. Then i try to save it to local FS with BlobBuilder / FileWriter. Every example on the internet is about working with text/plain mime type and these examples work fine. But when i try to write binary data obtained with XHR, file size becomes about 1.5-2 times bigger than the original file size. And it cannot be viewed in Picasa / Eye Of Gnome.

var xhr = new XMLHttpRequest();
var photoOrigUrl = 'http://www.google.ru/images/nav_logo72.png';
xhr.open('GET', photoOrigUrl, true);
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        var contentType = xhr.getResponseHeader('Content-type');

        fsLink.root.getFile('nav_logo72.png', {'create': true}, function(fileEntry) {
            fileEntry.createWriter(function(fileWriter) {
                var BlobBuilderObj = new (window.BlobBuilder || window.WebKitBlobBuilder)();
                BlobBuilderObj.append(xhr.responseText);

                fileWriter.write(BlobBuilderObj.getBlob(contentType));
            }, function(resultError) {
                console.log('writing file to file system failed (   code ' + resultError.code + ')');
            });
        });
    }
}

xhr.send();

fsLink exists, this is extension.

Dmitrii Sorin
  • 3,855
  • 4
  • 31
  • 40

5 Answers5

15

The problem is that BlobBuilder.append(xhr.responseText) is detecting its argument as a UTF-8 string, which is what XHR returns, and not binary data, which is what it really is. There's a couple of tricks to get the BlobBuilder reading it as binary data instead of string data:

var xhr = new XMLHttpRequest();
var photoOrigUrl = 'http://www.google.ru/images/nav_logo72.png';
xhr.open('GET', photoOrigUrl, true);

// CHANGE 1: This stops the browser from parsing the data as UTF-8:
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        var contentType = xhr.getResponseHeader('Content-type');

        fsLink.root.getFile('nav_logo72.png', {'create': true}, function(fileEntry) {
            fileEntry.createWriter(function(fileWriter) {

                // CHANGE 2: convert string object into a binary object
                var byteArray = new Uint8Array(xhr.response.length);
                for (var i = 0; i < xhr.response.length; i++) {
                    byteArray[i] = xhr.response.charCodeAt(i) & 0xff;
                }

                var BlobBuilderObj = new (window.BlobBuilder || window.WebKitBlobBuilder)();

                // CHANGE 3: Pass the BlobBuilder an ArrayBuffer instead of a string
                BlobBuilderObj.append(byteArray.buffer);

                // CHANGE 4: not sure if it's needed, but keep only the necessary
                // part of the Internet Media Type string
                fileWriter.write(BlobBuilderObj.getBlob(contentType.split(";")[0]));
            }, function(resultError) {
                console.log('writing file to file system failed (   code ' + resultError.code + ')');
            });
        });
    }
}

xhr.send();

This gave me a file with the same length as what xhr.getResponseHeader('Content-Length') suggests it should have been.

Stoive
  • 11,232
  • 4
  • 24
  • 32
  • Also, remember to accept answers when you think they're correct ;) – Stoive May 30 '11 at 03:59
  • Stolve, you're the MAN!!! You made my day! Many thanks to you! The only thing i don't understand is overriding mimetype with this "text/plain; charset=x-user-defined". I've always thought that one can specify the needed response mimetype here, but why should we put here "text/plain"? – Dmitrii Sorin May 30 '11 at 05:38
  • Glad to help :). That line is just telling the browser not to do anything smart with the data the request returns - otherwise it'll always try and turn it into text, with the text encoding of the current page (I think...). Possibly a historical thing, since XHR was previously only used for "text/xml" or "application/xml", at a guess. – Stoive May 30 '11 at 06:20
  • Don't use `xhr.overrideMimeType(...)`. It's hacky and there are better ways to achieve the same thing. See my response below on using `ArrayBuffer`. – ebidel Sep 04 '11 at 00:29
3

You can use xhr.responseType='arraybuffer' though:

BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  if (this.status == 200) {
    var bb = new BlobBuilder();
    bb.append(this.response); // Note: not xhr.responseText

    var blob = bb.getBlob('image/png');
    ...
  }
};

xhr.send();
ebidel
  • 23,921
  • 3
  • 63
  • 76
  • +1 for a step in the right direction. Looks like BlobBuilder is deprecated, Chrome is asking me to use window.Blob instead. – Shanimal Oct 17 '12 at 13:39
  • 2
    LOL, you know this, a link to your blog post http://www.html5rocks.com/en/tutorials/file/dndfiles/ – Shanimal Oct 17 '12 at 13:41
1

I think Stoive is spot on but I want to point out that instead of BlobBuilder there is now Blob constructor available that will do the trick

 var b = new Blob([byteArray.buffer], {'type': 'application/type'});

I think this is more in keeping with current standards. Thanks much Stoive, very helpful.

Chris Wininger
  • 651
  • 8
  • 13
0

Btw XHR2 sets a better way for implementing my task:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.google.ru/images/nav_logo72.png', true);
xhr.responseType = 'blob';

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        // xhr.responseBlob is needed blob data
    }
}

xhr.send();

The only disappointment is that this is still a bug in Chrome: http://code.google.com/p/chromium/issues/detail?id=52486

Dmitrii Sorin
  • 3,855
  • 4
  • 31
  • 40
0

XMLHttpRequest cannot load http://www.google.ru/images/nav_logo72.png. Origin file:// is not allowed by Access-Control-Allow-Origin.

user820955
  • 81
  • 1
  • 6