Отправка изображений из элементов Canvas с использованием Ajax и PHP $_FILES

Мне нужно иметь возможность отправлять изображения и некоторые поля формы из элемента canvas на стороне клиента в PHP script, заканчивая $_POST и $_FILES. Когда я отправлю его вот так:

<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 и $_FILES возвращаются, но данные изображения в $_FILES все еще нуждаются в декодировании следующим образом:

$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);

..., чтобы сохранить его как читаемый PNG. Это не вариант, поскольку я пытаюсь передать изображение в функцию Wordpress media_handle_upload(), для которой требуется индекс $_FILES, указывающий на читаемое изображение. Я также не могу декодировать, сохранять и изменять "tmp_name" соответственно, так как это подрывает проверки безопасности.

Итак, я нашел это: http://www.webtoolkit.info/javascript-base64.html и попытался выполнить декодирование на стороне клиента:

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

но по какой-то причине я до сих пор не получаю доступный для чтения файл, когда он попадает на PHP. Поэтому вопрос: "Почему?" или "Что я делаю неправильно?" или "Возможно ли это?": -)

Любая помощь очень ценится!

Спасибо, Кейн

Ответ 1

Основываясь на превосходном ответе Натана, я смог его обмануть, чтобы он все еще проходил через jQuery.ajax. Просто добавьте это в запрос ajax:

            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;
            },

В принципе, вы просто возвращаете объект xhr, который переопределяется, так что "send" означает "sendAsBinary". Тогда jQuery делает правильные вещи.

Ответ 2

К сожалению, это невозможно в JavaScript без некоторой промежуточной кодировки. Чтобы понять, почему, допустим, вы base64 расшифровали и разместили данные, как описано в вашем примере. Первые несколько строк в шестнадцатеричном действии файла PHP могут выглядеть так:

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

Если вы посмотрели на тот же диапазон шестнадцатеричного размера загруженного PNG файла, он будет выглядеть следующим образом:

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

Различия тонкие. Сравните второй и третий столбцы второй строки. В действительном файле четыре байта: 0x00 0x80 0x00 0x00. В загруженном файле те же четыре байта: 0x00 0xc2 0x80 0x00. Почему?

Строки JavaScript - это UTF. Это означает, что любые двоичные значения ASCII (0-127) кодируются одним байтом. Однако все, начиная с 128-2047, получает два байта. Этот дополнительный 0xc2 в загруженном файле является артефактом этой многобайтовой кодировки. Если вы хотите точно знать, почему это происходит, вы можете прочитать больше о кодировке UTF в Википедии.

Вы не можете предотвратить это из-за строк JavaScript, поэтому вы не можете загружать эти двоичные данные через AJAX без использования base64.

EDIT: После некоторого дальнейшего копания это возможно с некоторыми современными браузерами. Если браузер поддерживает XMLHttpRequest.prototype.sendAsBinary (Firefox 3 и 4), вы можете использовать его для отправки изображения, например:

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'));
}

Для браузеров, у которых нет sendAsBinary, но есть Uint8Array (Chrome и WebKit), вы можете polyfill сделать это так:

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);
  };
}