Sending images from Canvas elements using Ajax and PHP $_FILES Sending images from Canvas elements using Ajax and PHP $_FILES ajax ajax

Sending images from Canvas elements using Ajax and PHP $_FILES


Unfortunately, this isn't possible in JavaScript without some intermediate encoding. To understand why, let's assume you base64 decoded and posted the data, like you described in your example. The first few lines in hex of a valid PHP file might look like this:

0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR0000010: 0000 0080 0000 0080 0806 0000 00c3 3e61  ..............>a

If you looked at the same range of hex of your uploaded PNG file, it would look like this:

0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR0000010: 0000 00c2 8000 0000 c280 0806 0000 00c3  ................

The differences are subtle. Compare the second and third columns of the second line. In the valid file, the four bytes are 0x00 0x80 0x00 0x00. In your uploaded file, the same four bytes are 0x00 0xc2 0x80 0x00. Why?

JavaScript strings are UTF. This means that any ASCII binary values (0-127) are encoded with one byte. However, anything from 128-2047 gets two bytes. That extra 0xc2 in the uploaded file is an artifact of this multibyte encoding. If you want to know exactly why this happens, you can read more about UTF encoding on Wikipedia.

You can't prevent this from happening with JavaScript strings, so you can't upload this binary data via AJAX without using base64.

EDIT: After some further digging, this is possible with some modern browsers. If a browser supports XMLHttpRequest.prototype.sendAsBinary (Firefox 3 and 4), you can use this to send the image, like so:

function postCanvasToURL(url, name, fn, canvas, type) {  var data = canvas.toDataURL(type);  data = data.replace('data:' + type + ';base64,', '');  var xhr = new XMLHttpRequest();  xhr.open('POST', url, true);  var boundary = 'ohaiimaboundary';  xhr.setRequestHeader(    'Content-Type', 'multipart/form-data; boundary=' + boundary);  xhr.sendAsBinary([    '--' + boundary,    'Content-Disposition: form-data; name="' + name + '"; filename="' + fn + '"',    'Content-Type: ' + type,    '',    atob(data),    '--' + boundary + '--'  ].join('\r\n'));}

For browsers that don't have sendAsBinary, but do have Uint8Array (Chrome and WebKit), you can polyfill it like so:

if (XMLHttpRequest.prototype.sendAsBinary === undefined) {  XMLHttpRequest.prototype.sendAsBinary = function(string) {    var bytes = Array.prototype.map.call(string, function(c) {      return c.charCodeAt(0) & 0xff;    });    this.send(new Uint8Array(bytes).buffer);  };}


Building on Nathan's excellent answer, I was able to finnagle it so that it is still going through jQuery.ajax. Just add this to the ajax request:

            xhr: function () {                var myXHR = new XMLHttpRequest();                if (myXHR.sendAsBinary == undefined) {                    myXHR.legacySend = myXHR.send;                    myXHR.sendAsBinary = function (string) {                        var bytes = Array.prototype.map.call(string, function (c) {                            return c.charCodeAt(0) & 0xff;                        });                        this.legacySend(new Uint8Array(bytes).buffer);                    };                }                myXHR.send = myXHR.sendAsBinary;                return myXHR;            },

Basically, you just return back an xhr object that is overriden so that "send" means "sendAsBinary". Then jQuery does the right thing.