XMLHttpRequest: Multipart/Related POST with XML and image as payload - javascript

I'm trying to POST an image (with Metadata) to Picasa Webalbums from within a Chrome-Extension. Note that a regular post with Content-Type image/xyz works, as I described here. However, I wish to include a description/keywords and the protocol specification describes a multipart/related format with a XML and data part.
I'm getting the Data through HTML5 FileReader and user file input. I retrieve a binary
String using
FileReader.readAsBinaryString(file);
Assume this is my callback code once the FileReader has loaded the string:
function upload_to_album(binaryString, filetype, albumid) {
var method = 'POST';
var url = 'http://picasaweb.google.com/data/feed/api/user/default/albumid/' + albumid;
var request = gen_multipart('Title', 'Description', binaryString, filetype);
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.setRequestHeader("GData-Version", '3.0');
xhr.setRequestHeader("Content-Type", 'multipart/related; boundary="END_OF_PART"');
xhr.setRequestHeader("MIME-version", "1.0");
// Add OAuth Token
xhr.setRequestHeader("Authorization", oauth.getAuthorizationHeader(url, method, ''));
xhr.onreadystatechange = function(data) {
if (xhr.readyState == 4) {
// .. handle response
}
};
xhr.send(request);
}
The gen_multipart function just generates the multipart from the input values and the XML template and produces the exact same output as the specification (apart from ..binary image data..), but for sake of completeness, here it is:
function gen_multipart(title, description, image, mimetype) {
var multipart = ['Media multipart posting', " \n", '--END_OF_PART', "\n",
'Content-Type: application/atom+xml',"\n","\n",
"<entry xmlns='http://www.w3.org/2005/Atom'>", '<title>', title, '</title>',
'<summary>', description, '</summary>',
'<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/photos/2007#photo" />',
'</entry>', "\n", '--END_OF_PART', "\n",
'Content-Type:', mimetype, "\n\n",
image, "\n", '--END_OF_PART--'];
return multipart.join("");
}
The problem is, that the POST payload differs from the raw image data, and thus leads to a Bad Request (Picasa won't accept the image), although it worked fine when using
xhr.send(file) // With content-type set to file.type
My question is, how do I get the real binary image to include it in the multipart? I assume it is mangled by just appending it to the xml string, but I can't seem to get it fixed.
Note that due to an old bug in Picasa, base64 is not the solution.

The XMLHttpRequest specification states that the data send using the .send() method is converted to unicode, and encoded as UTF-8.
The recommended way to upload binary data is through FormData API. However, since you're not just uploading a file, but wrapping the binary data within XML, this option is not useful.
The solution can be found in the source code of the FormData for Web Workers Polyfill, which I've written when I encountered a similar problem. To prevent the Unicode-conversion, all data is added to an array, and finally transmitted as an ArrayBuffer. The byte sequences are not touched on transmission, per specification.
The code below is a specific derivative, based on the FormData for Web Workers Polyfill:
function gen_multipart(title, description, image, mimetype) {
var multipart = [ "..." ].join(''); // See question for the source
var uint8array = new Uint8Array(multipart.length);
for (var i=0; i<multipart.length; i++) {
uint8array[i] = multipart.charCodeAt(i) & 0xff;
}
return uint8array.buffer; // <-- This is an ArrayBuffer object!
}
The script becomes more efficient when you use .readAsArrayBuffer instead of .readAsBinaryString:
function gen_multipart(title, description, image, mimetype) {
image = new Uint8Array(image); // Wrap in view to get data
var before = ['Media ... ', 'Content-Type:', mimetype, "\n\n"].join('');
var after = '\n--END_OF_PART--';
var size = before.length + image.byteLength + after.length;
var uint8array = new Uint8Array(size);
var i = 0;
// Append the string.
for (; i<before.length; i++) {
uint8array[i] = before.charCodeAt(i) & 0xff;
}
// Append the binary data.
for (var j=0; j<image.byteLength; i++, j++) {
uint8array[i] = image[j];
}
// Append the remaining string
for (var j=0; j<after.length; i++, j++) {
uint8array[i] = after.charCodeAt(j) & 0xff;
}
return uint8array.buffer; // <-- This is an ArrayBuffer object!
}

Related

Decode XMLHTTPResponseText into dataUrl without base encoding on server side

How can the plain text response of an XMLHTTPRequest be converted to a dataUrl on client side?
Image data is being send from the server to an Iframe and the only option to retrieve the data is the default encoded data from a GET request.*
I do not have any control over the server. I can not specify overrideMimeType nor the responseType of the request.
I tried to utf8 encode the returned data:
const utf8 = new TextEncoder();
const bytes = utf8.encode(imageDataAsText);
//Convert to data url
const fr = new FileReader();
const blob = new Blob([bytes], { type: attachment.metadata.mediaType });
fr.onload = (e) => {
setImageData(fr.result as string);
};
fr.readAsDataURL(blob);
Converting via the char code didn't work either:
https://stackoverflow.com/a/6226756/3244464
let bytesv2 = []; // char codes
for (var i = 0; i < imageDataAsString.length; ++i) {
var code = imageDataAsString.charCodeAt(i);
bytesv2 = bytesv2.concat([code & 0xff, code / 256 >>> 0]);
}
Raw data as it is displayed by console out. What is the actual default encoding of the data I am working with here?
Context:
* The data in question is recieved inside an iframe inside the atlassian (jira/confuence) ecosystem. They do not support piping binary data from the parent frame to the iframe, nor can I make my own request due to the authorization flow which requires cookies stored on the parent page. All other options mention to override some encoding, or changing it on the server side do not apply in this specific case.
When you are using XMLHttpRequest to get binary data then don't return it as a string. Use xhr.responseType = 'blob' (or arrayBuffer if you intend to read/modify it)
var xhr = new XMLHttpRequest()
xhr.responseType = 'blob'
xhr.onload = () => {
// Convert xhr blob to data url
const fr = new FileReader()
fr.onload = () => {
setImageData(fr.result)
}
fr.readAsDataURL(xhr.response)
}
Better yet use the fetch api instead of the old XMLHttpRequest to get the binary
It's more popular among web workers and servers. It's also simpler and based on promises
fetch(url)
.then(res => res.blob())
.then(blob => { ... })
And why do you need it to be a base64 url? if it's just to show a preview of an <img> then it's a waste of time, cpu and memory.
It would be better just to do:
img.src = URL.createObjectURL(blob)

image/png response from restapi not displaying in browser

I am getting corrupted image icon while displaying b64 encoded png image response from rest API.
javascript-
function getcap(){
var http = new XMLHttpRequest()
http.open("GET", "http://localhost:8888/newcaptcha",true)
http.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
http.setRequestHeader("Access-Control-Allow-Origin", "http://localhost:8888");
http.send()
http.onload = () => {
var resp=unescape(encodeURIComponent(http.responseText));
var b64Response = window.btoa(resp);
console.log('data:image/png;base64,'+b64Response);
document.getElementById("capimg").src = 'data:image/png;base64,'+b64Response;
}
}
html -
<div id="newCaptcha" onClick="getcap()" ><h5>new captcha:</h5><img id="capimg" width="30" height ="30"/></div>
b64 encoded response-
server code -
#CrossOrigin(origins = "http://localhost:8080")
#RequestMapping(value = "/newcaptcha", method = RequestMethod.GET, produces = "image/png")
public #ResponseBody byte[] getnewCaptcha() {
try {
Random random = new Random();
imgkey= random.nextInt(3);
InputStream is = this.getClass().getResourceAsStream("/"+captcheMap.get(imgkey)+".png");
BufferedImage img = ImageIO.read(is);
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ImageIO.write(img, "png", bao);
return bao.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
The base 64 response attached doesn't seem to actually load the image, if I open it in browser.
Secondly, I can see that that one problem that can cause this is reloading of DOM element img, if its not handled by any framework, you may have to manually intervene. To check this, you can test using a local image and load that. If it doesn't work, then you got your root cause. And if it does, then this base64 response is an issue.
Also, check the console for any errors and do update here.
As I pointed out in comments, probably you don't need b64. However, if you really want, read this.
There are tons on questions on Stackoverflow on this subject, and few answers. I have put together all pieces.
The point is that btoa() badly supports binary data.
Here: convert binary data to base-64 javaScript you find the suggestion to use arraybuffers as responseType, instead of just text.
Here: ArrayBuffer to base64 encoded string you find a function that converts arraybuffers to b64.
Putting all togheter:
function getcap(){
var http = new XMLHttpRequest();
http.open("GET", "/newcaptcha",true);
http.responseType = 'arraybuffer';
http.send();
http.onload = () => {
console.log(http.response);
var b64Response = _arrayBufferToBase64(http.response);
document.getElementById("capimg").src = 'data:image/png;base64,'+b64Response;
}
}
function _arrayBufferToBase64( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}

Retrieving file data in chunks using Web API for display in browser (WIP)

I have this working but I want to share this out to see if I missed anything obvious and to solve a mystery as to why my file chunk size has to be a multiple of 2049. The main requirements are:
Files uploaded from website must be stored in SQL server, not as files
Website must be able to download and display file data as a file (opened in a separate window.
Website is angularjs/javascript SPA, no server side code, no MVC
API is Web API 2 (again not MVC)
I'm just going to focus on the download part here. Basically what I'm doing is:
Read a chunk of data from SQL server varbinary field
Web API 2 api returns file name, mime type and byte data as a base64 string. NOTE - tried returning byte array but Web API just serializes it into base64 string anyway.
concatenate the chunks, convert the chunks to a blob and display
VB library function that returns a dataset with the chunk (I have to use this library which handles the database connection but doesn't support parameter queries)
Public Function GetWebApplicationAttachment(ByVal intId As Integer, ByVal intChunkNumber As Integer, ByVal intChunkSize As Integer) As DataSet
' the starting number is NOT 0 based
Dim intStart As Integer = 1
If intChunkNumber > 1 Then intStart = ((intChunkNumber - 1) * intChunkSize) + 1
Dim strQuery As String = ""
strQuery += "SELECT FileName, "
strQuery += "SUBSTRING(ByteData," & intStart.ToString & "," & intChunkSize.ToString & ") AS ByteData "
strQuery += "FROM FileAttachments WHERE Id = " + intId.ToString + " "
Try
Return Query(strQuery)
Catch ex As Exception
...
End Try
End Function
Web API business rules bit that creates the file object from the dataset
...
result.FileName = ds.Tables[0].Rows[0]["FileName"].ToString();
// NOTE: Web API converts a byte array to base 64 string so the result is the same either way
// the result of this is that the returned data will be about 30% bigger than the chunk size requested
result.StringData = Convert.ToBase64String((byte[])ds.Tables[0].Rows[0]["ByteData"]);
//result.ByteData = (byte[])ds.Tables[0].Rows[0]["ByteData"];
... some code to get the mime type
result.MIMEType = ...
Web API controller (simplified - all security and error handling removed)
public IHttpActionResult GetFileAttachment([FromUri] int id, int chunkSize, int chunkNumber) {
brs = new Files(...);
fileResult file = brs.GetFileAttachment(appID, chunkNumber, chunkSize);
return Ok(file);
}
angularjs Service that gets the chunks recurively and puts them together
function getFileAttachment2(id, chunkSize, chunkNumber, def, fileData, mimeType) {
var deferred = def || $q.defer();
$http.get(webServicesPath + "api/files/get-file-attachment?id=" + id + "&chunkSize=" + chunkSize + "&chunkNumber=" + chunkNumber).then(
function (response) {
// when completed string data will be empty
if (response.data.StringData === "") {
response.data.MIMEType = mimeType;
response.data.StringData = fileData;
deferred.resolve(response.data);
} else {
if (chunkNumber === 1) {
// only the first chunk computes the mime type
mimeType = response.data.MIMEType;
}
fileData += response.data.StringData;
chunkNumber += 1;
getFileAttachment2(appID, detailID, orgID, GUID, type, chunkSize, chunkNumber, deferred, fileData, mimeType);
}
},
function (response) {
... error stuff
}
);
return deferred.promise;
}
angular controller method that makes the calls.
function viewFile(id) {
sharedInfo.getWebPortalSetting("FileChunkSize").then(function (result) {
// chunk size must be a multiple of 2049 ???
var chunkSize = 0;
if (result !== null) chunkSize = parseInt(result);
fileHelper.getFileAttachment2(id, chunkSize, 1, null, "", "").then(function (result) {
if (result.error === null) {
if (!fileHelper.viewAsFile(result.StringData, result.FileName, result.MIMEType)) {
... error
}
result = {};
} else {
... error;
}
});
});
}
And finally the bit of javascript that displays the file as a download
function viewAsFile(fileData, fileName, fileType) {
try {
fileData = window.atob(fileData);
var ab = new ArrayBuffer(fileData.length);
var ia = new Uint8Array(ab); // ia provides window into array buffer
for (var i = 0; i < fileData.length; i++) {
ia[i] = fileData.charCodeAt(i);
}
var file = new Blob([ab], { type: fileType });
fileData = "";
if (window.navigator.msSaveOrOpenBlob) // IE10+
window.navigator.msSaveOrOpenBlob(file, fileName);
else { // Others
var a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
return true;
} catch (e) {
... error stuff
}
}
I see already that a more RESTful approach would be to use headers to indicate chunk range and to separate the file meta data from the file chunks. Also I could try returning a data stream rather than Base64 encoded string. If anyone has tips on that let me know.
Well that was entirely the wrong way to go about that. In case it helps here's what I ended up doing.
Dynamically create the href address of an anchor tag to return a file (security token and parameters in query string)
get byte array from database
web api call return response message (see code below)
This is much faster and more reliable, but provides less in the way of progress monitoring.
business rule method uses...
...
file.ByteData = (byte[])ds.Tables[0].Rows[0]["ByteData"];
...
web api controller
public HttpResponseMessage ViewFileAttachment([FromUri] int id, string token) {
HttpResponseMessage response = new HttpResponseMessage();
... security stuff
fileInfoClass file = ... code to get file info
response.Content = new ByteArrayContent(file.ByteData);
response.Content.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment") {
FileName = file.FileName
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue(file.MIMEType);
return response;
This could even be improved with streaming

Use the base64 preview of the binary data response (zip file) in angularjs

I always get this error in the downloaded zip file C:\Users\me\Downloads\test.zip: Unexpected end of archive
My current code is:
var blob = new Blob([data], { // data here is the binary content
type: 'octet/stream',
});
var zipUrl = window.URL.createObjectURL(blob);
var fileName = orderNo;
fileName += '.zip';
downloadFile(null, fileName, null, zipUrl, null); // just creates a hidden anchor tag and triggers the download
The response of the call is a binary (I think). Binary Content Here
But the preview is a base64. Base64 Content. And it is the correct one. The way I verify it is by using this fiddle.
You can refer to the screenshot of the network here
I put the base64 content in this line var sampleBytes = base64ToArrayBuffer(''); And the zip downloaded just opens fine.
Things I have tried so far.
Adding this headers to the GET call
var headers = {
Accept: 'application/octet-stream',
responseType: 'blob',
};
But I get Request header field responseType is not allowed by Access-Control-Allow-Headers in preflight response.
We're using an already ajax.service.js in our AngularJS project.
From this answer
var blob = new Blob([yourBinaryDataAsAnArrayOrAsAString], {type: "application/octet-stream"});
var fileName = "myFileName.myExtension";
saveAs(blob, fileName);
There are other things that I have tried that I have not listed. I will edit the questions once I find them again
But where I'm current at right now. The preview is correct base64 of the binary file. Is it possible to use that instead of the binary? (If it is I will not find the other methods that I've tested) I tried some binary to base64 converters but they don't work.
So I just went and ditched using the ajax.service.js, that we have, for this specific call.
I used the xhr snippet from this answer. I just added the headers necessary for our call: tokens and auth stuff.
And I used this code snippet for the conversion thing.
And the code looks like this:
fetchBlob(url, function (blob) {
// Array buffer to Base64:
var base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(blob)));
var blob = new Blob([base64ToArrayBuffer(base64)], {
type: 'octet/stream',
});
var zipUrl = window.URL.createObjectURL(blob);
var fileName = orderNo;
fileName += ' Attachments ';
fileName += moment().format('DD-MMM-YYYY');
fileName += '.zip';
downloadFile(null, fileName, null, zipUrl, null); // create a hidden anchor tag and trigger download
});
function fetchBlob(uri, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', uri, true);
xhr.responseType = 'arraybuffer';
var x = AjaxService.getAuthHeaders();
xhr.setRequestHeader('auth_stuff', x['auth_stuff']);
xhr.setRequestHeader('token_stuff', x['token_stuff']);
xhr.setRequestHeader('Accept', 'application/octet-stream');
xhr.onload = function (e) {
if (this.status == 200) {
var blob = this.response;
if (callback) {
callback(blob);
}
}
};
return xhr.send();
};
function base64ToArrayBuffer(base64) {
var binaryString = window.atob(base64);
var binaryLen = binaryString.length;
var bytes = new Uint8Array(binaryLen);
for (var i = 0; i < binaryLen; i++) {
var ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
};
return bytes;
}

Javascript Binary String to MP3

I'm working on a project in browser that receives a multipart response from a server.
The first part of the response is a bit of JSON metadata
The second part of the response is a binary MP3 file.
I need a way to extract that MP3 file from the multipart response and play it in an HTML 5 audio element.
Has anyone encountered this or something similar?
I ended up solving my problem. I'm providing the issue and my solution to anyone needing to do this in the future.
Background info:
I'm using AngularJS when I make this ajax request, but the idea is the same for both that, jquery ajax, and regular xhr.
Code:
//creating a form object and assigning everything
//to it is so that XHR can automatically
//generate proper multipart formatting
var form = new FormData();
var data = {};
data['messageHeader'] = {};
var jsonData = JSON.stringify(data);
var jsonBlob = new Blob([jsonData],{type: "application/json"});
//assign json metadata blob and audio blob to the form
form.append("request", jsonData);
form.append("audio",response); //Response is the audio blob
//make the post request
//Notes:
//content-type set to undefined so angular can auto assign type
//transformRequest: angular.identity allows for angular to create multipart
//response: arraybuffer so untouched binary data can be received
$http({method:"POST",
url: endpoint + path,
headers: {
'Authorization': 'Bearer ' + $cookies.get('token'),
'Content-Type': undefined
},
transformRequest: angular.identity,
data: form,
responseType: "arraybuffer"
})
.success(function(data){
//data: ArrayBuffer of multipart response
//toss ArrayBuffer into Uint8Array
//lets you iterate over the bytes
var audioArray = new Uint8Array(data);
//toss a UTF-8 version of the response into
//a variable. Used to extract metadata
var holder = "";
for (var i = 0; i < audioArray.length; i++){
holder += String.fromCharCode(audioArray[i]);
}
//get the boundary from the string. Eg contents of first line
var boundary = holder.substr(0, holder.indexOf("\n"));
//break response into array at each boundary string
var temp = holder.split(boundary);
var parts = [];
//loop through array to remove empty parts
for (var i = 0; i < temp.length; i++){
if (temp[i] != ""){
parts.push(temp[i]);
}
}
//PARSE FIRST PART
//get index of first squiggly, indicator of start of JSON
var jsonStart = parts[0].indexOf('{');
//string to JSON on { index to end of part substring
var JSONResponse = JSON.parse(parts[0].substring(jsonStart));
//PARSE SECOND PART
var audioStart = holder.indexOf('mpeg') + 8;
//get an ArrayBuffer from UInt8Buffer from the audio
//start point to the end of the array
var audio = audioArray.buffer.slice(audioStart);
//hand off audio to AudioContext for automatic decoding
audio_context.decodeAudioData(audio, function(buffer) {
var audioBuffer = buffer;
//create a sound source
var source = audio_context.createBufferSource();
//attach audioBuffer to sound source
source.buffer = audioBuffer;
//wire source to speakers
source.connect(audio_context.destination);
//on audio completion, re-enable mic button
source.onended = function() {
console.log("ended");
$scope.$apply(function(){
$scope.playing = false;
});
}
//start playing audio
source.start(0);
}, function (){
//callback for when there is an error
console.log("error decoding audio");
});
})
Overview:
You need to accept the response as pure binary data (ArrayBuffer). Most libraries will give it to you as a string, which is cool for normal requests but bad for binary data.
You then step through the data to find the multipart boundaries.
You then split at the boundaries.
Get the index of the boundary you know is binary data
and then retrieve the original binary from the ArrayBuffer.
In my case I send that binary into the speakers, however if its an image you can build a blob, get a url from FileReader and then set that as a source of an image.

Categories

Resources