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