Как стать автором
Обновить

Загрузчик изображений и миниатюр. Новые стандарты, старые проблемы

Время на прочтение6 мин
Количество просмотров12K

Предисловие


Всем привет. Не так давно я написал статью о создании загрузчика изображений на флеше. Там я упомянул, что загрузчик можно реализовать и с помощью html5 File API. Несколько вечеров и — ура — я это сделал. Настало время рассказать, какие приемы я использовал, в каких браузерах это работает, и стоит ли этим вообще пользоваться.
Напомню вкратце требования: необходимо реализовать загрузчик изображений, поддерживающий пакетную загрузку, создание миниатюр(и загрузку их на сервер), и приемлемый интерфейс.

Я прекрасно понимаю, что моя статья использует текущую реализацию ещё не до конца проработанного стандарта, а потому перечислю браузеры, актуальные на сегодняшний момент:
  1. Firefox 8
  2. Chrome 15
  3. Opera 11.60 beta
  4. Safari 5.1.1
  5. 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.
Теги:
Хабы:
Всего голосов 33: ↑33 и ↓0+33
Комментарии39

Публикации