Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sending images from Canvas elements using Ajax and PHP $_FILES

I need to be able to send an image and some form fields from a client side canvas element to a PHP script, ending up in $_POST and $_FILES. When I send it like this:

<script type="text/javascript">
var img = canvas.toDataURL("image/png");
...
ajax.setRequestHeader('Content-Type', "multipart/form-data; boundary=" + boundary_str);
var request_body = boundary + '\n' 
+ 'Content-Disposition: form-data; name="formfield"' + '\n' 
+ '\n' 
+ formfield + '\n' 
+ '\n' 
+ boundary + '\n'
+ 'Content-Disposition: form-data; name="async-upload"; filename="' 
+ "ajax_test64_2.png" + '"' + '\n' 
+ 'Content-Type: image/png' + '\n' 
+ '\n' 
+ img
+ '\n' 
+ boundary;
ajax.send(request_body);
</script>

$_POST and $_FILES both come back populated, but the image data in $_FILES still needs decoding like this:

$loc = $_FILES['async-upload']['tmp_name'];
$file = fopen($loc, 'rb');
$contents = fread($file, filesize($loc));
fclose($file);
$filteredData=substr($contents, strpos($contents, ",")+1);
$unencodedData=base64_decode($filteredData);

...in order to save it as a readable PNG. This isn't an option as I'm trying to pass the image to Wordpress's media_handle_upload() function, which requires an index to $_FILES pointing to a readable image. I also can't decode, save and alter 'tmp_name' accordingly, as it falls foul of security checks.

So, I found this: http://www.webtoolkit.info/javascript-base64.html and tried to do the decode on the client side:

img_split = img.split(",",2)[1];
img_decoded = Base64.decode( img_split );

but for some reason I still don't end up with a readable file when it gets to the PHP. So the question is: "Why?" or "What am I doing wrong?" or "Is this even possible?" :-)

Any help very much appreciated!

Thanks, Kane

like image 281
BaronVonKaneHoffen Avatar asked Mar 13 '11 21:03

BaronVonKaneHoffen


1 Answers

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........IHDR
0000010: 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........IHDR
0000010: 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);
  };
}
like image 105
Nathan Ostgard Avatar answered Sep 20 '22 13:09

Nathan Ostgard