Опыт создания загрузчика изображений

Предисловие


Всем привет. Я хочу рассказать о создании загрузчика изображений для своего первого web-проекта. Я постараюсь объяснить, какие решения я видел, и какие подводные камни встретились на моем пути. Ну и, собственно, как эти камни можно обойти. Надеюсь, мой опыт кому-нибудь поможет.

Для начала опишу основную задачу: необходимо создать загрузчик изображений(bmp, png, jpg), с последующим их сохранением на сервере, а также с созданием копий изображений различного размера. Также желательно обеспечить соответствие дизайна загрузчика стилю сайта, и удобный интерфейс пользователя. И самое главное – загрузчик должен максимально поддерживаться браузерами.

Решение первое. HTML4


Конечно же, это самое простое и очевидное. Создать форму, засунуть туда input-file, и обработать на сервере php-скриптом:

if(is_uploaded_file($_FILES["filename"]["tmp_name"])) {
    move_uploaded_file($_FILES["filename"]["tmp_name"], $_FILES["filename"]["name"]);
} 
else {
    echo("Ошибка загрузки файла");
}

Теперь необходимо создать копии меньшего разрешения. Тут можно воспользоваться различными библиотеками обработки изображений. Например, Imagick. Именно так я вначале и сделал. На локальном хосте все работало замечательно. Потом я выбрал недорогой хостинг, поместил туда проект. Начал тестировать. И вот тут вышел главный облом. Для изображений с хорошим разрешением (2500х1900) не создавались маленькие копии. Многие, наверное, догадались почему. Покопавшись в логах, а также приложив умственные усилия, до меня дошло. Когда начинаешь обрабатывать изображения, и работать со всей матрицей, требуется немало оперативной памяти. И мой достаточно скромный тариф на хостинге предоставлял её не очень-то много.

И вот это уже была проблема. Можно, конечно, взять тариф получше. Но кардинально это ситуацию не изменит. При дополнительной нагрузке произойдет то же самое. Подход неверен по сути. Производить манипуляции с изображением надо не на сервере, а на клиенте. И кроссбраузерный html+javascript тут не подходил.

Решение второе. Flash


Конечно, решение тоже не кроссбраузерное. Но у 99% пользователей flash все-таки стоит, поэтому попробовать стоит. Тем более есть очевидные преимущества:
  • асинхронная загрузка
  • возможность пакетной загрузки
  • возможность выводить превью изображений
  • варианты дизайна не ограничены
Нам потребуется FileReferenceList для того, чтобы вызвать диалог и получить список изображений с локальной машины. Далее загружаем каждое и выполняем с ним необходимые действия:

var FileList:Array;

var send_element;

var load_element;

var script_name = "../../ajax.php";

var type_filter:FileFilter = new FileFilter("Изображения (*.jpg, *.jpeg, *.gif, *.png)","*.jpg;*.jpeg;*.gif;*.png");

var OpenFileDialog:FileReferenceList = new FileReferenceList();

    OpenFileDialog.addEventListener(Event.SELECT, onSelectList);

function onSelectList(e:Event){
    Select_check();
}

function Select_check(){
    var element: FileReference = OpenFileDialog.fileList.shift();
    
    load_element["original"] = element;

    FileList.push(load_element);

    element.addEventListener(Event.COMPLETE, onLocal_complete);

    element.load();	
} 

function onLocal_complete(e:Event){
    //здесь действия с загруженным изображением e.target.data
    load_element["original"].removeEventListener(Event.COMPLETE, onLocal_complete);

    if(OpenFileDialog.fileList.length > 0) Select_check();		
}

function open(){
    OpenFileDialog.browse([type_filter]);
}


function save(){
    var send_element = FileList.shift();

    send_element["original"].addEventListener(ProgressEvent.PROGRESS, onPOST_progress);

    send_element["original"].addEventListener(Event.COMPLETE, onPOST_complete);

    send_element["original"].addEventListener(IOErrorEvent.IO_ERROR, onPOST_error);

    var cookie = ExternalInterface.call("function(){                                                                                                                                           var name = 'PHPSESSID';                                                                                                                                          var prefix = name + '=';                                                                                                                                            var cookieStartIndex = document.cookie.indexOf(prefix);                                                                                            if (cookieStartIndex == -1) return null;                                                                                                                        var cookieEndIndex = document.cookie.indexOf(';', cookieStartIndex + prefix.length);                                              if (cookieEndIndex == -1) cookieEndIndex = document.cookie.length;                                                                     return unescape(document.cookie.substring(cookieStartIndex + prefix.length, cookieEndIndex));}");

    var DataVartibles:URLVariables = new URLVariables();

	DataVartibles.PHPSESSID = cookie;
			
    var FileRequest = new URLRequest(script_name);

   	FileRequest.data = DataVartibles;

	FileRequest.method = URLRequestMethod.POST;
				
    send_element["original"].upload(FileRequest, php_file);
}

function onPOST_progress(e:ProgressEvent){
    //здесь некие действия, например вывод e.bytesLoaded
}

function onPOST_error(e:IOErrorEvent){
    //здесь некие действия проиходящие при ошибке
    if(FileList.length > 0) save(); 
}

function onPOST_complete(e:Event){
    if(FileList.length > 0) save(); 
}

Метод browse вызывает диалог загрузки файлов. Тут есть одна тонкость: метод выполнится только в том случае, если код вызывается в слушателе события EVENT.CLICK какого-нибудь элемента.

Также стоит остановиться на описании класса FileReference. Он нужен для загрузки локального изображения и передачи его на сервер. Метод load нужен для загрузки, upload – для передачи на сервер. Здесь есть ещё одна тонкость: при загрузке на сервер с помощью FileReference куки передаются только из IE(спасибо за информацию Demetros). Поэтому, если вы хотите, например, работать в рамках одной сессии, то придется достать куки из браузера с помощью вызова функции javascript. Лучше это делать из самой флешки. Для этого подойдет ExternalInterface. Далее записываем нужные нам куки в URLVariables.

Итак, базовая функцинальность есть. Но остается вопрос с созданием и отправкой уменьшенных копий изображений. И вот тут FileReference нам уже не поможет, так как он способен отправить только загруженные локальные файлы.

Вопрос создания уменьшенных копий я не буду рассматривать, есть много библиотек, способных это сделать. Однако для отправки на сервер эти данные должны быть представлены в виде ByteArray.
Теперь рассмотрим, как нам организовать отправку этих данных на сервер. С помощью обычной переменной URLVariables с последущем добавлением в URLRequest это сделать не получится. Поэтому придется формировать шапку запроса самим. URLRequest позволяет это сделать. В итоге создаем класс, который занимается отправкой данных на сервер:

package  {
import flash.net.URLRequest;
import flash.net.URLLoader;
import flash.net.URLRequestMethod;
import flash.net.URLLoaderDataFormat;
import flash.utils.ByteArray;
import flash.utils.Endian;
import flash.net.URLRequestHeader;
	
    public class HTTPLoader extends URLLoader
    {
        var HTTPRequest;

	var BOUND:String = "";

	var ENTER:String = "\r\n";

	var ADDB:String = "--";
		
	var index_file = 0;
		
	var PostData:ByteArray;
		
	public function HTTPLoader(script_name: String){
            BOUND = getBoundary();

	    PostData = new ByteArray();

	    PostData.endian = Endian.BIG_ENDIAN;
			
	    HTTPRequest = new URLRequest(script_name);

	    HTTPRequest.requestHeaders.push(new URLRequestHeader('Content-type','multipart/form-data; boundary=' + BOUND));

	    HTTPRequest.method = URLRequestMethod.POST;
	}
		
	public function addVariable(param_name:String, param_value:String){
	    PostData.writeUTFBytes(ADDB + BOUND);
   	    PostData.writeUTFBytes(ENTER);
	    PostData.writeUTFBytes('Content-Disposition: form-data; name="'+param_name+'"');
	    PostData.writeUTFBytes(ENTER);
	    PostData.writeUTFBytes(ENTER);
	    PostData.writeUTFBytes(param_value);
	    PostData.writeUTFBytes(ENTER);
	}
		
	public function addFile(filename:String, filedata:ByteArray){
	    PostData.writeUTFBytes(ADDB + BOUND);
	    PostData.writeUTFBytes(ENTER);
	    PostData.writeUTFBytes('Content-Disposition: form-data; name="Filedata' + index_file + '"; filename="' + filename + '"');
	    PostData.writeUTFBytes(ENTER);
  	    PostData.writeUTFBytes('Content-Type: application/octet-stream');
	    PostData.writeUTFBytes(ENTER);
            PostData.writeUTFBytes(ENTER);		
	    PostData.writeBytes(filedata,0,filedata.length);
	    PostData.writeUTFBytes(ENTER);
            PostData.writeUTFBytes(ENTER);
			
	    index_file++;
	}
		
		
	public function send(){
	    PostData.writeUTFBytes(ADDB+BOUND+ADDB);
	    HTTPRequest.data = PostData;
	    this.load(HTTPRequest);
	}

	public function getBoundary():String {
	    var _boundary:String = "";

	    for (var i:int = 0; i < 0x20; i++) {

               _boundary += String.fromCharCode( int( 97 + Math.random() * 25 ) );

	    }

	    return _boundary;
	}
		
    }
	
}

Привожу класс полностью, так как формирование правильной шапки HTTP-запроса заняло у меня довольно много времени. Надеюсь, кому-то поможет. Пользоваться классом очень просто. Вот пример:

var POSTLoader:HTTPLoader = new HTTPLoader("../../ajax.php");

    POSTLoader.addEventListener(Event.COMPLETE, POSTLoader_complete);

    POSTLoader.addVariable("AJAX_module_name", "pic_loader.php");

    POSTLoader.addFile("pic_100", send_element["pic_100"]);

    POSTLoader.send();

Ну а в слушателе Event.COMPLETE делайте, что вам необходимо. Минус такого способа в том, что нельзя отследить процесс загрузки, поэтому доступно только два состояния — загрузился файл или нет.
В php можно легко пройтись по массиву $_FILES и подцепить каждый файл по имени «Filedata»+index, а потом просто сохранить.


if(isset($_FILES["Filedata0"])){

    for($i = 0; $i < $n; $i++){  
         $file_name = $_FILES["Filedata".$i]["name"];
         move_uploaded_file($_FILES["Filedata".$i]['tmp_name'],$file_path.$file_name);
    }
}

Вот такое решение. При желании можно выводить превью картинок сразу же, или передавать в javascript в виде base64. Ну это уже кому как нравится.

Решение третье. HTML5


Сразу скажу, что с помощью html5 я загрузчик не реализовывал, однако для относительной полноты картины (java-апплеты я брать не стал) об этом решении стоит упомянуть. Скорее всего, в будущем это станет наилучшим способом решения этой задачи. Однако пока ещё в ходу старые браузеры, да и в новых File API ещё не до конца проработан. Также стоит упомянуть про неизменяемый input-file. В современных браузерах можно вызвать программно метод click, а сам input скрыть.

<input id="im" type="file" style="position:absolute; top:-999px; visibility:hidden"/>
<div id="button" style="background-color: blue; width: 100px; height:40px;"></div>

<script type='text/javascript'>
var btn = document.querySelector("#button");
btn.onclick = function(){
    var im = document.querySelector("#im");
    im.click();
}
</script>

Такое решение работает почти во всех современных браузерах. Кроме одного. Угадайте какого…
Не угадали, Opera. В версиях до 11.52 включительно так сделать нельзя. Поэтому наложение грима на input-file до сих пор остается проблемой.

Заключение


Конечно, в реальном проекте лучше использовать комбинацию этих способов, чтобы загрузчик работал у всех. Например, если функции html5 в браузере пользователя не поддерживаются, то можно попытаться использовать flash. Если же и flash отсутствует, то даем простое html-решение. Неказисто, зато работает.
Надеюсь, моя статья кому-нибудь поможет.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 22

    +2
    > Здесь есть ещё одна тонкость: при загрузке на сервер с помощью FileReference почему-то не передаются куки(В URLLoader, кстати, все передается).

    На самом деле куки передаются, но всегда из IE — это известная «особенность» объекта FileReference.
    В терминах сильверлайт FileReference использует ClientHttpStack, а URLRequest использует BrowserHttpStack.

    Также, нелишне было бы упомянуть, что URLRequest не предоставляет событий прогресса загрузки, поэтому загрузка миниатюр будет проходить две унылые стадии: начало и окончание.
      0
      Спасибо за замечание. Сейчас добавлю.
      +1
      А какой версии ActionScript вы использовали? Я ценитель Flash MX 2004, там AS2, сработает ли или пытаться даже не стоит? :)
        +1
        Ого :) А я ценитель Fireworks MX 2004 :)
        Много нас таких осталось?
          +1
          Fireworks MX 2004 тоже шикарен :) Полагаю, что мало. Посмотрим на плюсики… :)
          • UFO just landed and posted this here
          0
          Вам наверно и так известно, поэтому не стоит говорить о том, что AS3 быстрее чем AS2, да и удобнее, в конце концов.
            0
            AS2 я касался, но будет ли работать — не знаю. Я писал на AS3 под 10-й плеер.
              +1
              В AS2 насколько я помню нет метода FileReference.load поэтому не получится делать ресайз на клиенте.
              +2
              Как-то тоже довелось делать загрузку изображений из флеша на сервер с помощью multipart binary.
              Правда заголовок нашаманил наш server-side программер. Для коллекции закину сюда, может кому-то пригодиться:

              public function sendImage(bmpData:BitmapData, url:String):void 
              {
              	//Converting BitmapData into a PNG-encoded ByteArray		
              	var imageBytes: ByteArray = PNGEncoder.encode(bmpData);
              	imageBytes.position = 0;
              	
              	var boundary: String = '---------------------------7d76d1b56035e';
              	var header1: String  = '--'+boundary + '\r\n'
              							+'Content-Disposition: form-data; name="poster"\r\n\r\n'
              							+'picture.jpg\r\n'
              							+'--'+boundary + '\r\n'
              							+'Content-Disposition: form-data; name="poster"; filename="picture.png"\r\n'
              							+'Content-Type: application/octet-stream\r\n\r\n';
              							
              	//In a normal POST header, you'd find the image data here
              	var header2: String =	'--'+boundary + '\r\n'
              							+'Content-Disposition: form-data; name="Upload"\r\n\r\n'
              							+'Submit Query\r\n'
              							+'--' + boundary + '--';
              							
              	//Encoding the two string parts of the header
              	var headerBytes1: ByteArray = new ByteArray();
              	headerBytes1.writeMultiByte(header1, "ascii");
              	
              	var headerBytes2: ByteArray = new ByteArray();
              	headerBytes2.writeMultiByte(header2, "ascii");
              	
              	//Creating one final ByteArray
              	var sendBytes: ByteArray = new ByteArray();
              	sendBytes.writeBytes(headerBytes1, 0, headerBytes1.length);
              	sendBytes.writeBytes(imageBytes, 0, imageBytes.length);
              	sendBytes.writeBytes(headerBytes2, 0, headerBytes2.length);
              	
              	var request: URLRequest = new URLRequest(url);
              	request.data = sendBytes;
              	request.method = URLRequestMethod.POST;
              	request.contentType = "multipart/form-data; boundary=" + boundary;
              	
              	var loader:URLLoader = new URLLoader();
              	loader.addEventListener(Event.COMPLETE, uploadComplete);
              	loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);		
              	
              	try {	
              		loader.load(request);
              	} catch (error: Error) {
              		trace("Unable to load requested document.");
              	}
              }
                –1
                try {
                loader.load(request);
                } catch (error: Error) {
                trace("Unable to load requested document.");
                }
                }

                Этот просто шикарно. Ставить в try-catch то, что никогда не выкинет исключения.
                  +1
                  Читайте доки на UrlLoader.load:
                  Throws
                  ArgumentError — URLRequest.requestHeader objects may not contain certain prohibited HTTP request headers. For more information, see the URLRequestHeader class description.

                  MemoryError — This error can occur for the following reasons: 1) Flash Player or AIR cannot convert the URLRequest.data parameter from UTF8 to MBCS. This error is applicable if the URLRequest object passed to load() is set to perform a GET operation and if System.useCodePage is set to true. 2) Flash Player or AIR cannot allocate memory for the POST data. This error is applicable if the URLRequest object passed to load is set to perform a POST operation.

                  SecurityError — Local untrusted files may not communicate with the Internet. This may be worked around by reclassifying this file as local-with-networking or trusted.

                  SecurityError — You are trying to connect to a commonly reserved port. For a complete list of blocked ports, see «Restricting Networking APIs» in the ActionScript 3.0 Developer's Guide.

                  TypeError — The value of the request parameter or the URLRequest.url property of the URLRequest object passed are null.
                    –2
                    Хм, ну дело в том, что ошибки будут возникать только в том случае, если приложение заведомо не рабочее. То есть либо всегда, либо никогда. Как показывает практика — никогда, т.к выкладывать нерабочее приложение в продакшн нет смысла =)
                0
                есть готовое решение подобного загрузчика: plupload
                использовал для своего проекта — удобно.
                  0
                  Спасибо за ссылку. Только странно как то! Файл не сохраняет на сервере и при этом ошибку не выдает.
                    0
                    я старую версию использовал, давно ведь дело было. Помню только что переписывал загрузчик на php, были ага какие-то проблемы. Точню не помню что, но копать туда надо.
                      0
                      спасибо, я разобрался.
                  0
                  Не надо вызывать Click() для инпута, его просто увеличивают, делают 100% прозрачным и позиционируют поверх красивой кнопки — этакий кликджекинг.

                  Еще подвох — если вы используете мультиинпут для файлов, то Опера (в отличие от FF и Chrome) посылает несколько файлов одним полем в соответствии со стандартом w3c, в результате PHP эти файлы не распознает (надо руками парсить входные данные). Проще запретить мультизагрузку в Опере, чем заморачиваться.
                    0
                    Насчет позиционирования — есть такое решение. Но мне, если честно, больше нравится убирать инпут с глаз долой и не заморачиваться с соответствием размера кнопки и инпута. Как я недавно узнал, в Опере 11.60 можно будет вызвать click.
                    0
                    Есть отличный плагин для jQuery. Достаточно гибко настраивается и работает на всех браузерах
                      0
                      Для загрузки ByteArray рекомендую MultipartURLLoader.as
                        0
                        Мне нравится Plupload

                        Only users with full accounts can post comments. Log in, please.