Предисловие
Всем привет. Не так давно я написал статью о создании загрузчика изображений на флеше. Там я упомянул, что загрузчик можно реализовать и с помощью 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.
