Pull to refresh

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

Reading time7 min
Views8.4K

Предисловие


Всем привет. Я хочу рассказать о создании загрузчика изображений для своего первого 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-решение. Неказисто, зато работает.
Надеюсь, моя статья кому-нибудь поможет.
Tags:
Hubs:
Total votes 17: ↑13 and ↓4+9
Comments22

Articles