Отправка изображений из элементов 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);
};
}