1

I'm trying to pull an image from a SharePoint library using REST, construct a Word document using docx.js, insert the image, then save that document back to another SharePoint library.

I have code that "works" in that it doesn't throw any errors, but when I open the new Word doc, it says "This image can't be displayed." I'm guessing it's because I don't know enough about the format of the binary data returned from SharePoint, so I'm not converting/encoding/decoding it in the correct way for docx.js to work with it.

I'm using jQuery to make the REST calls, and I've read various other questions/answers out there that say things like jQuery can't return byte arrays, and you need to be getting the data from the server as a byte array? Again, I'm not very familiar with handling binary data so I don't know if this is true or if it's the culprit, but if it is, any suggestions for how I should be getting the data or if there are any other conversions/encoding/decoding I should be doing would be welcome. Code sample below:

function getFileBuffer(file) {
    var bufferDFD = $.Deferred();
    var reader = new FileReader();
    reader.onload = function (e) {
        bufferDFD.resolve(e.target.result);
    }
    reader.onerror = function (e) {
        bufferDFD.reject(e.target.error);
    }
    reader.readAsArrayBuffer(file);
    return bufferDFD.promise();
}

$.ajax({
    url: _spPageContextInfo.webAbsoluteUrl + "/_api/web/GetFileByServerRelativeUrl('/sites/my/subsite/siteassets/logo.png')/$value",
    method: 'GET',
    headers: {
        accept: 'image/*'
    }
}).then(function (imageData) {
    var imageBlob = new Blob([imageData], {type:'image/png'});
    var doc = new docx.Document();
    var image = docx.Media.addImage(doc, imageBlob);
    doc.addSection({
        children: [
            new docx.Paragraph({
                children: [image],
                alignment: docx.AlignmentType.CENTER
            }),
        ]
    });
    docx.Packer.toBlob(doc).then(function (blob) {
        getFileBuffer(blob).then(function (buffer) {
            getOtherSiteDigest().then(function (digestResp) {
                var uploadUri = _spPageContextInfo.siteAbsoluteUrl + "/othersite/_api/web/GetFolderByServerRelativeUrl('/sites/my/othersite/My Library/Subfolder')/files/add(url='test image.docx', overwrite=true)";
                $.ajax({
                    url: uploadUri,
                    method: 'POST',
                    data: buffer,
                    processData: false,
                    headers: {
                        accept: 'application/json;odata=verbose',
                        'X-RequestDigest': digestResp.d.GetContextWebInformation.FormDigestValue
                    }
                }).then(function (data) {
                    console.log('upload success');
                }).catch(function (err) {
                    console.log(err);
                })
            })
        })
    })
}).catch(function (err) {
    console.log(err);
});
0

So this did indeed turn out to be a jQuery issue, where jQuery (by default) does not get binary data as an ArrayBuffer, but instead returns it essentially as a string.

After reading this great article on handling binary data in JS, which essentially says that when you pass a string that represents binary data directly into the Blob constructor, the Blob constructor will encode that string as UTF-8, which is a variable length encoding scheme, which means that some bytes of data may get truncated or changed, and that your overall binary information will get corrupted.

So the answer there was to force all the binary data to stay positioned properly by converting the string into a typed array, and passing that to the Blob constructor. So I ended up with something like:

$.ajax({
    url: _spPageContextInfo.webAbsoluteUrl + "/_api/web/GetFileByServerRelativeUrl('/sites/my/subsite/siteassets/logo.png')/$value",
    method: 'GET',
    headers: {
        accept: 'image/*'
    }
}).then(function (imageData) {

    // convert the values represented by the string into a Uint8Array
    var u8 = new Uint8Array(imageData.length);
    for (var i = 0; i < imageData.length; i++) {
        u8[i] = imageData[i].charCodeAt(0);
    }

    // use the Uint8Array so the blob costructor does not change anything
    var imageBlob = new Blob([u8], {type:'image/png'});

    // do the rest...
});

But that still didn't work. I downloaded one of the PNG files I made using that conversion, and compared the hex dump of that file with the original, and I could see that some of the bytes were not getting converted properly, so my guess is that jQuery was still doing something to the data before it ultimately spat out the imageData response.

So then the issue was - how to get the binary data as an ArrayBuffer instead of a string? I thought about bypassing jQuery altogether and just using a natvie XMLHttpRequest, but it turns out that one of the options for $.ajax() is xhr. The jQuery documentation for that option is (emphasis mine):

xhr (default: ActiveXObject when available (IE), the XMLHttpRequest otherwise)

Type: Function()

Callback for creating the XMLHttpRequest object. Defaults to the ActiveXObject when available (IE), the XMLHttpRequest otherwise. Override to provide your own implementation for XMLHttpRequest or enhancements to the factory.

Given that you can override the underlying XHR object powering the $.ajax() request, in order to get an ArrayBuffer as a response, all I had to do was this:

var xhrOverride = new XMLHttpRequest();
xhrOverride.responseType = 'arraybuffer';
$.ajax({
    url: _spPageContextInfo.webAbsoluteUrl + "/_api/web/GetFileByServerRelativeUrl('/sites/my/subsite/siteassets/logo.png')/$value",
    method: 'GET',
    headers: {
        accept: 'image/*'
    },
    xhr: function() { return xhrOverride; }
}).then(function (imageData) {

    // imageData is now an ArrayBuffer!
    // i can plug that directly into the Blob constructor,
    // no conversion necessary
    var imageBlob = new Blob([imageData], {type:'image/png'});

    // do the rest...
});

So.... despite all the posts out there that say you can't get an ArrayBuffer response out of jQuery, you actually can, and quite easily, too.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.