Загрузка изображения на сервер с предварительным просмотром при помощи HTML5 Drag and Drop

Здравствуйте, уважаемые читатели XoZbloga! За последнее время я получил несколько предложений сделать урок, на тему загрузки изображений на сервер с предварительным просмотром. В этом уроке я попытаюсь разобрать данную тему, чтобы у Вас не осталось вопросов. Загрузка будет осуществляться с помощью HTML5 Drag and Drop (перетащил и бросил).

HTML5 Drag and Drop загрузчик изображений

Для создания данного загрузчика будем использовать следующие:

  • HTML5 File API (FileReader);
  • Drag and Drop API;
  • PHP сценарий для загрузки на сервер.

Сразу напомню, что исходники работают только на сервере.

HTML разметка

Страница состоит из области для перетаскивания, а также области предварительного просмотра. Идея заключается в том, чтобы захватить изображения на рабочем столе (или в папке) и перетащить в область указанную пунктиром на странице, или если удобно нажать на кнопку «Выбрать файлы» по «старинке». После чего выбранные картинки отобразятся в области предварительного просмотра.

Для работы загрузчика нам понадобится библиотека jQuery, а также созданный нами скрипт и стили:

1
2
3
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
<script src="javascript.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />

Как всегда начинаем с HTML разметки, здесь ничего сложного нет:

1
2
3
4
5
6
7
   <!-- Область для перетаскивания -->
   <div id="drop-files" ondragover="return false">
      <p>Перетащите изображение сюда</p>
        <form id="frm">
           <input type="file" id="uploadbtn" multiple />
        </form>
   </div>

В самом верху страницы находится область для перетаскивания изображений, отмеченная пунктирной линией. А также кнопка выбора файлов, кому как удобнее. Событие ondragover работает пока перемещаемый файл над областью для перетаскивая.

Далее формируется область для предварительного просмотра изображений, а также кнопки для манипуляции и прогресс бар загрузки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    <!-- Область предварительного просмотра -->
   <div id="uploaded-holder">
      <div id="dropped-files">
         <!-- Кнопки загрузить и удалить, а также количество файлов -->
         <div id="upload-button">
               <center>
                  <span>0 Файлов</span>
               <a href="#" class="upload">Загрузить</a>
               <a href="#" class="delete">Удалить</a>
                    <!-- Прогресс бар загрузки -->
                  <div id="loading">
                  <div id="loading-bar">
                     <div class="loading-color"></div>
                  </div>
                  <div id="loading-content"></div>
               </div>
                </center>
         </div>  
        </div>
   </div>

После того как выбранные изображения будут загружены появляется список-оповещение о выполненных загрузках:

1
2
3
4
5
6
   <!-- Список загруженных файлов -->
   <div id="file-name-holder">
      <ul id="uploaded-files">
         <h1>Загруженные файлы</h1>
      </ul>
   </div>

Вот так выглядит HTML разметка. Переходим к обработке событий загрузчика изображений.

Обработка событий

Начинается все с функции $(document).ready(). Каждая функция внутри нее будет выполняться как только DOM дерево документа будет готово:

1
2
3
$(document).ready(function() {
   // Весь последующий код javascript здесь
});

Начнем с объявления глобальных переменных нашего обработчика:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// В dataTransfer помещаются изображения которые перетащили в область div
jQuery.event.props.push('dataTransfer');
   
// Максимальное количество загружаемых изображений за одни раз
var maxFiles = 6;
   
// Оповещение по умолчанию
var errMessage = 0;
   
// Кнопка выбора файлов
var defaultUploadBtn = $('#uploadbtn');
   
// Массив для всех изображений
var dataArray = [];
   
// Область информер о загруженных изображениях - скрыта
$('#uploaded-files').hide();

Когда необходимые переменный объявлены, можно переходить к обработке происходящих событий на странице. А именно когда пользователь перетаскивает или выбирает изображение. Сначала разберем перетаскивание файла:

1
2
3
4
5
6
7
8
9
10
11
12
13
   // Метод при падении файла в зону загрузки
   $('#drop-files').on('drop', function(e) {
      // Передаем в files все полученные изображения
      var files = e.dataTransfer.files;
      // Проверяем на максимальное количество файлов
      if (files.length <= maxFiles) {
         // Передаем массив с файлами в функцию загрузки на предпросмотр
         loadInView(files);
      } else {
         alert('Вы не можете загружать больше '+maxFiles+' изображений!');
         files.length = 0; return;
      }
   });

Метод .on присоединяет обработчик события drop — перетаскивания, к блоку div (#drop-files). Что касается функции loadInView(files), о ней чуть ниже. Если количество выбранных файлов больше максимально установленного, появляется предупреждение и массив с изображениями очищается.

Теперь событие при выборе файлов с помощью кнопки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// При нажатии на кнопку выбора файлов
   defaultUploadBtn.on('change', function() {
         // Заполняем массив выбранными изображениями
         var files = $(this)[0].files;
         // Проверяем на максимальное количество файлов
      if (files.length <= maxFiles) {
         // Передаем массив с файлами в функцию загрузки на предпросмотр
         loadInView(files);
              // Очищаем инпут файл путем сброса формы
              // Или вот так $("#uploadbtn").replaceWith( $("#uploadbtn").val('').clone( true ) );
              $('#frm').each(function(){
                this.reset();
              });

      } else {
         alert('Вы не можете загружать больше '+maxFiles+' изображений!');
         files.length = 0;
      }
   });

Он практически идентичен вышеописанному, но событие change.

Теперь можно перейти к описанию функции loadInView(files). Формирование области предварительного просмотра изображений:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Функция загрузки изображений на предпросмотр
   function loadInView(files) {
      // Показываем обасть предпросмотра
      $('#uploaded-holder').show();
     
      // Для каждого файла
      $.each(files, function(index, file) {
                 
         // Несколько оповещений при попытке загрузить не изображение
         if (!files[index].type.match('image.*')) {
           
            if(errMessage == 0) {
               $('#drop-files p').html('Эй! только изображения!');
               ++errMessage
            }
            else if(errMessage == 1) {
               $('#drop-files p').html('Стоп! Загружаются только изображения!');
               ++errMessage
            }
            else if(errMessage == 2) {
               $('#drop-files p').html("Не умеешь читать? Только изображения!");
               ++errMessage
            }
            else if(errMessage == 3) {
               $('#drop-files p').html("Хорошо! Продолжай в том же духе");
               errMessage = 0;
            }

         } else {
         
         // Проверяем количество загружаемых элементов
         if((dataArray.length+files.length) <= maxFiles) {
            // показываем область с кнопками
            $('#upload-button').css({'display' : 'block'});
         }
         else { alert('Вы не можете загружать больше '+maxFiles+' изображений!'); return; }
         
         // Создаем новый экземпляра FileReader
         var fileReader = new FileReader();
            // Инициируем функцию FileReader
            fileReader.onload = (function(file) {
               
               return function(e) {
                  // Помещаем URI изображения в массив
                  dataArray.push({name : file.name, value : this.result});
                  addImage((dataArray.length-1));
               };
                 
            })(files[index]);
         // Производим чтение картинки по URI
         fileReader.readAsDataURL(file);
                     }
      });
      return false;
   }

С каждым файлом в переданном массиве files выполняем следующие действия. Сначала проверяем изображение это или нет. Если нет то выводим предупреждение. Если полученный файл является изображением, то снова проверяем не является ли загружаемый файл лишним. Для это уже к существующему количеству изображений в массиве прибавляем этот файл и проверяем. Если все удачно, то в дело вступает HTML5 file API. С помощью интерфейса FileReader производим асинхронное чтение файла. Который в последствии помещается в массив dataArray.

Для отображения изображений которые уже помещены в массив, используем функцию addImage() с параметром текущий индекс изображения в массиве.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Процедура добавления эскизов на страницу
   function addImage(ind) {
      // Если индекс отрицательный значит выводим весь массив изображений
      if (ind < 0 ) {
      start = 0; end = dataArray.length;
      } else {
      // иначе только определенное изображение
      start = ind; end = ind+1; }
      // Оповещения о загруженных файлах
      if(dataArray.length == 0) {
         // Если пустой массив скрываем кнопки и всю область
         $('#upload-button').hide();
         $('#uploaded-holder').hide();
      } else if (dataArray.length == 1) {
         $('#upload-button span').html("Был выбран 1 файл");
      } else {
         $('#upload-button span').html(dataArray.length+" файлов были выбраны");
      }
      // Цикл для каждого элемента массива
      for (i = start; i < end; i++) {
         // размещаем загруженные изображения
         if($('#dropped-files > .image').length <= maxFiles) {
            $('#dropped-files').append('<div id="img-'+i+'" class="image" style="background: url('+dataArray[i].value+'); background-size: cover;"> <a href="#" id="drop-'+i+'" class="drop-button">Удалить изображение</a></div>');
         }
      }
      return false;
   }

Ключевым здесь является метод .append, который позволяет вставить указанное содержимое в соответствующий элемент. В нашем случае вставляем эскиз загруженного изображения в div (dropped-files). Эскиз это блок div, где id равен img-индекс изображения в массиве и в качестве фона используется загруженное изображение. А также ссылка на удаление только этого изображения из массива, ее id равен drop-индекс изображения в массиве.

Раз уж дошли до удаления определенного изображения из массива, стоит разобрать соответствующую функцию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Удаление только выбранного изображения
   $("#dropped-files").on("click","a[id^='drop']", function(e) {
        // Предотвращаем стандартное поведение
        e.preventDefault();
      // получаем название id
      var elid = $(this).attr('id');
      // создаем массив для разделенных строк
      var temp = new Array();
      // делим строку id на 2 части
      temp = elid.split('-');
      // получаем значение после тире тоесть индекс изображения в массиве
      dataArray.splice(temp[1],1);
      // Удаляем старые эскизы
      $('#dropped-files > .image').remove();
      // Обновляем эскизи в соответсвии с обновленным массивом
      addImage(-1);    
   });

C помощью селектора выбираем все элементы id которых начинается на drop. После чего получаем значение id полностью и разбиваем эту строку (метод split) на две части от символа тире. Вторая (т.е. правая от тире) часть является индексом изображения в массиве. Далее методом splice очищаем элемент (изображение) в массиве по указанному индексу, и методом remove удаляем все элементы эскизы в div (dropped-files). После чего, вызываем функцию addImage(-1) с отрицательным числом, это сделано для того чтобы из массива в область эскизов были снова выведены все оставшиеся изображения.

Теперь функция restartFiles, удаляет сразу все изображения.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Функция удаления всех изображений
   function restartFiles() {
   
      // Установим бар загрузки в значение по умолчанию
      $('#loading-bar .loading-color').css({'width' : '0%'});
      $('#loading').css({'display' : 'none'});
      $('#loading-content').html(' ');
     
      // Удаляем все изображения на странице и скрываем кнопки
      $('#upload-button').hide();
      $('#dropped-files > .image').remove();
      $('#uploaded-holder').hide();
   
      // Очищаем массив
      dataArray.length = 0;
     
      return false;
   }

Здесь все просто длину массива dataArray приравниваем к 0, тем самым очищая его.

А вызывается она дважды один раз по нажатию на кнопку Удалить, а второй раз после загрузки изображений на сервер. Первый вариант:

1
$('#dropped-files #upload-button .delete').click(restartFiles);

И последнее событие это загрузка изображений на сервер.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Загрузка изображений на сервер
   $('#upload-button .upload').click(function() {
     
      // Показываем прогресс бар
      $("#loading").show();
      // переменные для работы прогресс бара
      var totalPercent = 100 / dataArray.length;
      var x = 0;
     
      $('#loading-content').html('Загружен '+dataArray[0].name);
      // Для каждого файла
      $.each(dataArray, function(index, file) {
         // загружаем страницу и передаем значения, используя HTTP POST запрос
         $.post('upload.php', dataArray[index], function(data) {
         
            var fileName = dataArray[index].name;
            ++x;
           
            // Изменение бара загрузки
            $('#loading-bar .loading-color').css({'width' : totalPercent*(x)+'%'});
            // Если загрузка закончилась
            if(totalPercent*(x) == 100) {
               // Загрузка завершена
               $('#loading-content').html('Загрузка завершена!');
               
               // Вызываем функцию удаления всех изображений после задержки 1 секунда
               setTimeout(restartFiles, 1000);
            // если еще продолжается загрузка   
            } else if(totalPercent*(x) < 100) {
               // Какой файл загружается
               $('#loading-content').html('Загружается '+fileName);
            }
           
            // Формируем в виде списка все загруженные изображения
            // data формируется в upload.php
            var dataSplit = data.split(':');
            if(dataSplit[1] == 'загружен успешно') {
               $('#uploaded-files').append('<li><a href="images/'+dataSplit[0]+'">'+fileName+'</a> загружен успешно</li>');
                       
            } else {
               $('#uploaded-files').append('<li><a href="images/'+data+'. Имя файла: '+dataArray[index].name+'</li>');
            }
           
         });
      });
      // Показываем список загруженных файлов
      $('#uploaded-files').show();
      return false;
   });

По нажатию на кнопку Загрузить отображается прогресс бар, чтобы следить за ходом загрузки изображений. Данные по каждому изображению из массива dataArray передаются в PHP сценарий (файл upload.php), который загружается с помощью HTTP POST запроса ($.post). Это упрощенная версия HTTP (Ajax) запроса $.ajax. Данный файл после выполнения возвращает некоторые данные. C помощью уже известного метода split, разбиваем полученную строку на части. И если во второй части нам встречается строка «загружен успешно», значит формируем элемент списка загруженных файлов (имя файла в виде ссылки на него). После того как массив закончился и элементы списка готовы показываем весь список.

PHP сценарий загрузки

Последним шагом является загрузка изображения в папку на сервере. Приведенный PHP сценарий достаточно простой, при желании к нему можно «прикрутить» еще что-нибудь.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
// Все загруженные файлы помещаются в эту папку
$uploaddir = 'images/';

// Вытаскиваем необходимые данные
$file = $_POST['value'];
$name = $_POST['name'];

// Получаем расширение файла
$getMime = explode('.', $name);
$mime = end($getMime);

// Выделим данные
$data = explode(',', $file);

// Декодируем данные, закодированные алгоритмом MIME base64
$encodedData = str_replace(' ','+',$data[1]);
$decodedData = base64_decode($encodedData);

// Вы можете использовать данное имя файла, или создать произвольное имя.
// Мы будем создавать произвольное имя!
$randomName = substr_replace(sha1(microtime(true)), '', 12).'.'.$mime;

// Создаем изображение на сервере
if(file_put_contents($uploaddir.$randomName, $decodedData)) {
   echo $randomName.":загружен успешно";
}
else {
   // Показать сообщение об ошибке, если что-то пойдет не так.
   echo "Что-то пошло не так. Убедитесь, что файл не поврежден!";
}
?>

Из массива POST извлекаются URI данные по изображению (имя и символьная строка идентифицирующая изображение). После чего получаем из имени изображения его расширении и формируем произвольное имя для создаваемого на сервере изображения. А символьную строка идентифицирующую изображение декодируем. Далее функцией file_put_contents (она последовательна успешным вызовам функций fopen(), fwrite() и fclose()) создаем изображение на сервере в указанной папке.

Этот сценарий возвращает строку с новым именем изображения и пояснением «загружен успешно» при успешном выполнении. Как раз над этой строкой (переменная data) мы и производили манипуляции в последнем событии.

Не стоит забывать, что папку куда помещаются изображения нужно создать вручную.

Схема файлов

Стили CSS разбирать не будем, так как они особой роли не играют, но без них все равно нельзя 🙂 Поэтому лучше скачать ИСХОДНИКИ и разобраться со стилями самостоятельно (файл style.css).

На этом все. Загрузчик изображений на сервер с предпросмотром готов. Если у Вас остались какие-либо вопросы, оставляйте комментарии.

Дополнение

В этом дополнение напишу о том как можно реализовать хранение картинок в БД MySQL, точнее хранить будем только название каталога и имя файла. Это наверно самый распространенный метод, т.к. в любой момент можно изменить расположение картинок. Для хранения в БД следует создать отдельную таблицу, назовем ее images и добавим 4 поля:


Создадим новую таблицу для хранения картинок

И обеспечим следующую структуру таблицы, где id первичный ключ с автозаполнением:


Структура таблицы images

Решил также добавить дату загрузки файла, может понадобится при сортировке и тд.

Таблица готова, подготовим сценарий занесения данных по картинкам. Разместим его в том же файле, что и загрузка на сервер upload.php. В начало файла добавим строку подключения к серверу и выбор БД:

1
2
$db = mysql_connect ("servername","user","password");
mysql_select_db ("dbname",$db);

C подключением думаю сложностей не должно возникнуть 🙂

Теперь осталось произвести запись данных в таблицу images:

1
mysql_query ("INSERT INTO images (date,catalog,filename) VALUES (NOW(),'$uploaddir','$randomName')");

А добавляем эту строку сразу после загрузки файла на сервер. после этих небольших манипуляций файл upload.php примет вот такой вид:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Создаем подключение к серверу
$db = mysql_connect ("servername","user","password");
// Выбираем БД
mysql_select_db ("dbname",$db);

// Все загруженные файлы помещаются в эту папку
$uploaddir = 'images/';

// Вытаскиваем необходимые данные
$file = $_POST['value'];
$name = $_POST['name'];

// Получаем расширение файла
$getMime = explode('.', $name);
$mime = end($getMime);

// Выделим данные
$data = explode(',', $file);

// Декодируем данные, закодированные алгоритмом MIME base64
$encodedData = str_replace(' ','+',$data[1]);
$decodedData = base64_decode($encodedData);

// Вы можете использовать данное имя файла, или создать произвольное имя.
// Мы будем создавать произвольное имя!
$randomName = substr_replace(sha1(microtime(true)), '', 12).'.'.$mime;

// Создаем изображение на сервере
if(file_put_contents($uploaddir.$randomName, $decodedData)) {
   // Записываем данные изображения в БД
   mysql_query ("INSERT INTO images (date,catalog,filename) VALUES (NOW(),'$uploaddir','$randomName')");
   echo $randomName.":загружен успешно";
}
else {
   // Показать сообщение об ошибке, если что-то пойдет не так.
   echo "Что-то пошло не так. Убедитесь, что файл не поврежден!";
}

А теперь то, ради чего затевалась БД. Страница показа изображений, так сказать наипростейшая галерея. Создадим файлик gallery.php и расположим его там же где и upload.php. В начале файла также производим подключение к серверу и выбор БД, а в теле документа добавляем следующий код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="main">
<?
$result = mysql_query ("SELECT * FROM images",$db);
if(!$result) { echo "Произошла ошибка подключения к серверу и БД, проверьте параметры подключения"; }
// Если количество записей больше нуля
if (mysql_num_rows($result) > 0)
{
   // Записываем полученные данные в массив
   $myrow = mysql_fetch_array ($result);
   // В цикле размещаем изображения на странице
   do {
                // В атрибуту src присваиваем путь до файла картинки
      echo "<img src='".$myrow['catalog'].$myrow['filename']."' />";
   }
   while ($myrow = mysql_fetch_array($result));
}
else
{
   // Сообщение о пустой таблице
   echo "<p>Информация по запросу не может быть извлечена, в таблице нет записей.</p>";
   exit();
}
?>
</div>

Чтобы изображения выводились в строку по три штуки, цикл вывода расположим в блоке div с шириной 900px, а изображениям присвоим относительное позиционирование и выравнивание по левому краю:

1
2
3
4
5
6
7
8
9
10
11
12
.main {
   width:900px;
   margin:0 auto;
   height:100%
}
.main img {
   position:relative;
   float:left;
   width:280px;
   height:180px;
   margin:10px;
}

Примечания:
1. При загрузке файлов используется передача данных методом post, поэтому стоит обратить внимание на директиву php.ini — post_max_size, отвечающий за максимальный размер передаваемых данных.
2. Примечание от Фидана Газимова: при использовании патча безопасности — Suhosin, необходимо отключить проверку длинны get, post запросов в файле suhosin.ini (в Debian файл расположен /etc/php5/conf.d/suhosin.ini):

  • suhosin.get.max_value_length = off
  • suhosin.post.max_value_length = off
  • suhosin.request.max_value_length = off

Чтобы оставаться в курсе свежих статей и уроков подписывайтесь на страницу ВКонтакте или добавляйте в круги на Google+. Спасибо!

Чтобы оставаться в курсе свежих статей и уроков подписывайтесь на еженедельную почтовую рассылку или на новостную ленту RSS. Спасибо!