Предисловие
Всем привет. Не так давно я написал статью о создании загрузчика изображений на флеше. Там я упомянул, что загрузчик можно реализовать и с помощью html5 File API. Несколько вечеров и — ура — я это сделал. Настало время рассказать, какие приемы я использовал, в каких браузерах это работает, и стоит ли этим вообще пользоваться.
Напомню вкратце требования: необходимо реализовать загрузчик изображений, поддерживающий пакетную загрузку, создание миниатюр(и загрузку их на сервер), и приемлемый интерфейс.
Я прекрасно понимаю, что моя статья использует текущую реализацию ещё не до конца проработанного стандарта, а потому перечислю браузеры, актуальные на сегодняшний момент:
Firefox 8
Chrome 15
Opera 11.60 beta
Safari 5.1.1
Internet Explorer 9
Теперь о грустном. Для ИЕ 9 нет реализации File API, поэтому его(браузер) я не буду рассматривать. Ну что ж поехали.
Внешний вид
С незапамятных времен стояла задача сделать стильной кнопку для вызова диалога выбора файлов. Поэтому в ход шли яростные костыли. Например, популярное решение — сделать инпут прозрачным и повесить поверх красивого дива. То есть всё зависит от инпута, от его размера. Все вышеперечисленные браузеры поддерживают иное решение. В них можно программно генерировать click инпута. А по сути вызывать диалог выбора файлов. А сам инпут можно легко скрыть:
<input id="input_file" type="file" multiple style="position:absolute; top:-999px; visibility:hidden"/>
<div id="button" style="background-color: blue; width: 100px; height:40px;"></div>
<script type="text/javascript">
var input = document.querySelector("#input_file");
var btn = document.querySelector("#button");
btn.onclick = function () {
input.click();
};
</script>
Такой способ позволяет забыть об инпуте как о элементе управления, что по-моему очень удобно.
О загрузке файла в браузер
Для того, чтобы манипулировать данными файла, например, ресайзить картинку, необходимо получить эти данные. Для этого понадобится FileReader. Для того, чтобы создать миниатюры, возьмем Canvas и загрузим туда данные файла. Это возможно, если представить данные в виде base64:
var files;
var reader = new FileReader();
var cv = document.createElement("canvas");
var cvContext = cv.getContext("2d");
input.onchange = function () {
files = input.files;
reader.readAsDataURL(files[0]);
};
reader.onload = function (e) {
var im = new Image();
im.onload = function (e) {
cv.width = 100;
cv.height = 100;
cvContext.drawImage(im, 0, 0, 100, 100);
// здесь мы должны достать миниатюру из canvas и передать её на сервер вместе с оригиналом
}
im.src = reader.result;
};
Пока всё достаточно прозрачно. Однако надо сразу сказать, что загрузка данных в браузер не поддерживается в Сафари. Самое интересное, что загрузить файл на сервер можно, а в браузер нет. Не поддерживается ни FileReader, ни URL. Впрочем, для нашей задачи есть одно решение, но я бы его, если честно, использовать не стал. Позже я к этому вернусь.
О получении миниатюр и отправке на сервер
Итак. У нас есть оригинал изображения. У нас есть миниатюры в canvas. Нам нужно всё это достать, сгруппировать и отправить на сервер. Чего проще, правда? Вот тут и возникают проблемы. На этом этапе браузеры ведут себя совершенно по-разному. Рассмотрим решения для каждого. Разумеется, от простого к сложному.
Firefox
Тут всё просто. У canvas есть метод mozGetAsFile, название которого говорит само за себя. Фаерфокс также поддерживает FormData. А это значит, что есть контейнер для наших файлов. XMLHttpRequest легко отправит эти данные на сервер, где их можно подцепить. Процесс загрузки можно отслеживать с помощью upload.onprogress.
var blobData = cv.mozGetAsFile(name, files[0].type);
var form = new FormData();
form.append("Filedata0", files[0]);
form.append("Filedata1", blobData);
var xhr = new XMLHttpRequest();
xhr.open("POST", "load.php", true);
xhr.onload = function () {
console.log(this.response);
}
xhr.upload.onprogress = function (e) {
console.log(e.position / e.totalSize) * 100;
}
xhr.send(form);
Минус тут только один. Метод mozGetAsFile не дает возможности выбрать качество выгружаемого изображения.
Chrome
Вот тут никакого mozGetAsFile нет и в помине. Есть возможность получить изображение в base64(Это делает метод toDataURL). Но это меня не устроило, и я всё-таки привел изображение к blob. Комментарии в коде:
var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
//получаем данные в виде base64, второй параметр задает качество (от 0 до 1)
var sBase64 = canva.toDataURL(type, 1);
var aBase64 = sBase64.split(',');
//раскодируем обратно
var sData = atob(aBase64[1]);
var aBufferView = new Uint8Array(sData.length);
//создаем ArrayBuffer на основе строки
for (var i = 0; i < aBufferView.length; i++) {
aBufferView[i] = sData.charCodeAt(i);
}
// с помощью BlobBuilder переводим в blob
var builder = new BlobBuilder();
builder.append(aBufferView.buffer);
var blobData = builder.getBlob(type);
Вот эти данные уже можно записать в FormData и отправлять так же, как в фаерфоксе.
Opera
Вот тут у нас возникнут большие проблемы. Получить миниатюру и превратить её в ArrayBuffer можно так же, как и в Хроме, а вот как отправить? Opera не поддерживает FormData и BlobBuilder. А XMLHttpRequest может отправлять кроме текста только ArrayBuffer. Тут нам поможет опыт создания загрузчика на флеше. Нам придется самим генерировать заголовок формы с данными, записывать его в ArrayBuffer и отправлять.
var sBase64 = canva.toDataURL(type, 1);
var aBase64 = sBase64.split(',');
var sData = atob(aBase64[1]);
var aBufferView = new Uint8Array(sData.length);
for (var i = 0; i < aBufferView.length; i++) {
aBufferView[i] = sData.charCodeAt(i);
}
var fBuilder = new FormBuilder();
fBuilder.addFile(aBufferView);
var form = fBuilder.getForm();
var xhr = new XMLHttpRequest();
xhr.open("POST", "load.php", true);
xhr.onload = function () {
alert(this.response);
}
xhr.setRequestHeader('Content-type', 'multipart/form-data; boundary=' + fBuilder.BOUND);
xhr.send(form);
function FormBuilder() {
this.getBoundary = function () {
var _boundary = "";
for (var i = 0; i < 0x20; i++) {
_boundary += String.fromCharCode(97 + Math.random() * 25);
}
return _boundary;
}
this.addFile = function (name, buffer) {
var sHeader = this.ADDB + this.BOUND;
sHeader += this.ENTER;
sHeader += 'Content-Disposition: form-data; name="Filedata' + this.index + '"; filename="' + name + '"';
sHeader += this.ENTER;
sHeader += 'Content-Type: application/octet-stream';
sHeader += this.ENTER;
sHeader += this.ENTER;
this.index++;
this.header = this.sumBuffers(this.header, this.StrToBuffer(sHeader), buffer, this.EnterBuffer);
}
this.addParam = function (name, value) {
var sHeader = this.ADDB + this.BOUND;
sHeader += this.ENTER;
sHeader += 'Content-Disposition: form-data; name="'+ name + '"';
sHeader += this.ENTER;
sHeader += this.ENTER;
sHeader += value;
sHeader += this.ENTER;
this.header = this.sumBuffers(this.header, this.StrToBuffer(sHeader));
}
this.getForm = function () {
var sHeader = this.ENTER;
sHeader += this.ENTER;
sHeader += (this.ADDB + this.BOUND + this.ADDB);
var aHeader = this.StrToBuffer(sHeader);
return this.sumBuffers(this.header, aHeader).buffer;
}
this.StrToBuffer = function (str) {
var buffer = new Uint8Array(str.length);
for (var i = 0; i < buffer.length; i++) {
buffer[i] = str.charCodeAt(i);
}
return buffer;
}
this.sumBuffers = function () {
var sumLength = 0, position = 0, aSumHeader;
for (var i = 0; i < arguments.length; i++) {
sumLength += arguments[i].length;
}
aSumHeader = new Uint8Array(sumLength);
for (var i = 0; i < arguments.length; i++) {
aSumHeader.set(arguments[i], position);
position += arguments[i].length;
}
return aSumHeader;
}
this.BOUND = this.getBoundary();
this.ENTER = "\r\n";
this.EnterBuffer = this.StrToBuffer(this.ENTER);
this.ADDB = "--";
this.index = 0;
this.header = new Uint8Array(0);
}
Это такой вольный перевод из actionscript в javascript моего класса из первой статьи. С его помощью мы по сути эмулируем FormData. Кстати, в Хроме он прекрасно работает. А вот фаерфокс ругается — он не умеет передавать ArrayBuffer.
Вернемся к Опере. Всё работает, однако отслеживать загрузку не получится: onprogress в Опере не поддерживается(как кстати и во флешевском URLLoader).
Safari
Я уже говорил выше, что в Сафари у нас нет доступа к данным файла, и поэтому сделать практически ничего нельзя. Однако если уж вы решили непременно сделать функциональный загрузчик изображений на html5 и с поддержкой Сафари, то псевдорешение есть. Дело в том, хоть и доступа к данным файла нет, но загрузить его на сервер можно. А уж на сервере можно делать всё, что угодно. Идея проста: получив и сохранив файл, передать его обратно(в виде base64 или просто ссылки с последующей загрузкой в Canvas). А уж тут попытаться реализовать один из предложенных выше вариантов. Естественно, способ нехорош, однако если совсем необходимо, то можно сделать и так.
Заключение
Выводы из всего вышеизложенного выходят довольно простые. Во-первых, File API ещё явно не созрел. Браузеры пытаются как-то поддерживать то, что есть в спецификации, но стандарт ещё на стадии обсуждения и доработки. Несмотря на это, всё-таки мы имеем довольно мощный инструмент, который позволяет решать задачи не только на бумаге.
Надеюсь, статья кому-нибудь поможет.
Пример
Привожу небольшое демо, функциональность там минимальна, однако демонстрирует, как это должно работать.
Да, и ещё. Минимальный код на сервере для этого примера вот такой:
foreach($_FILES as $key => $value){
$filename = substr_replace($key, '.', -4, 1);
move_uploaded_file($value['tmp_name'], $filename);
}
echo 'complete';
Если будет нужен полноценный загрузчик со всеми вилюшками и фичами, то пишите. Может быть и сделаю.
И, конечно, не забываем, что версия Оперы для примера 11.60 beta.
html5, fileapi, загрузчик, arraybuffer, миниатюры