Pull to refresh

Загрузчик изображений. Закрывая тему

Reading time 8 min
Views 3.4K

Предисловие


Всем привет. О создании загрузчика изображений я уже писал. Сначала — загрузчик на flash, затем — на html5. По большому счёту, этих двух вариантов достаточно. И если вы поклонник рациональности, а сама тема особенного интереса не вызывает, то можете дальше не читать.
Рабочий пример загрузчика на Silverlight 4 привожу здесь же: да вот он.

Напомню задачу. Необходимо реализовать пакетную загрузку изображений, ресайз на клиенте, а также удобное получение файлов на сервере(например, в массив $_FILES в php). Ну и удобный интерфейс, разумеется.
Теперь перейдем к инструментарию. В данном случае мы используем Silverlight 4, а значит у нас есть мощь .Net, статическая типизация(да-да, я знаю, dynamic рулит), классный бесплатный редактор(Microsoft Visual Web Developer 2010 Express).

Работа с изображениями


Итак, C#. Язык мне нравится(даже диплом я на нём писал), однако после javascript, actionscript и php не очень привычно. Впрочем, это быстро проходит.
Чтобы получить список файлов, нам потребуется OpenFileDialog. Загрузить файлы тоже не проблема: через FileStream это легко сделать. Сами данные в Silverlight у нас есть, теперь нужно представить их виде изображений и потом ресайзить.
Удивительно, но представить данные в Bitmap мы можем(WriteableBitmap), но нет встроенных методов для ресайза и тем паче для кодирования обратно в png или jpeg. Всё это, естественно, можно сделать вручную. Но это тема для отдельной статьи, тем более подобные вопросы много раз пережевывались, и на Хабре в том числе. Поэтому возьмем библиотеку, которая даст больше возможностей. Я использовал ImageTools. Для своих нужд написал класс MyImage, который реализует требующийся мне функционал:

using System;
using System.Net;
using System.Windows;
using System.IO;
using System.Windows.Media.Imaging;
using ImageTools;
using ImageTools.IO.Bmp;
using ImageTools.IO.Jpeg;
using ImageTools.IO.Png;
using ImageTools.Helpers;
using ImageTools.Filtering;
using ImageTools.IO;

namespace Uploader.Libs
{
    public class MyImage
    {
        private ExtendedImage im; //базовый класс из библиотеки ImageTools
        public string name { get; set; }
        public string extension { get; set; }
        public FileInfo origin { get; set; }

        public int originSize
        {
            get
            {
                return origin != null ?  Utils.ByteToKB((int)origin.Length) : 0; //Utils - мой класс для всяких доп.функций
            }
        }

        public MyImage(FileInfo fileinfo)
        {
            name = fileinfo.Name;
            extension = fileinfo.Extension;
            origin = fileinfo;

            WriteableBitmap bmp = new WriteableBitmap(1, 1);
            bmp.SetSource(origin.OpenRead());
            
            im = bmp.ToImage();
        }

        public MyImage(ExtendedImage im, string name, string extension)
        {
            this.name = name;
            this.extension = extension;

            this.im = im;
        }

        public MyImage resize(int width, int height)
        {
            string prefix = width.ToString() + "_" + height.ToString() + "_";

            ExtendedImage rImage = ExtendedImage.Resize(im, width, height, new ImageTools.Filtering.NearestNeighborResizer()); //пользуемся возможностями библиотеки для ресайза

            return new MyImage(rImage, prefix + name, extension);
        }

        //ресайз, сохраняя пропорции
        public MyImage scale(int value)
        {
            double width = im.PixelWidth;
            double height = im.PixelHeight;

            double max = width > height ? width : height;

            double sc = max > value ? value / max : 1;

            int nWidth = (int)Math.Round(sc * width);
            int nHeight = (int)Math.Round(sc * height);

            return resize(nWidth, nHeight); 
        }

        //метод для получения оригинала изображения
        public byte[] getOrigin()
        {
            byte[] buffer;
            if (origin != null)
            {
                FileStream fStream = origin.OpenRead();
                buffer = new byte[fStream.Length];
                fStream.Read(buffer, 0, buffer.Length);
            }
            else buffer = null;

            return buffer;

        }

        //метод для получения конечного byte-контейнера, именно он нужен для загрузки
        public byte[] toByte(string extension = "")
        {
            MemoryStream mStream = new MemoryStream();

            string ext = extension != String.Empty ? extension : this.extension;

            dynamic encoder;  //кодируем изображение в тот формат, который имеет оригинал, поэтому воспользуемся dynamic
            
            switch (ext)
            {
                case ".png":
                    encoder = new PngEncoder();
                    break;
                default:
                    encoder = new JpegEncoder();
                    break;
            }

            encoder.Quality = 100; //кодируем с максимальным качеством
            encoder.Encode(im, mStream);

            return mStream.ToArray();
        }

        //base64 нам потребуется для вывода в html превью изображения
        public string toBase64(byte[] data = null)
        {
            byte[] iData = data != null ? data : toByte();

            return "data:image/" + extension.Substring(1) + ";base64," + Convert.ToBase64String(iData);
        }


    }
}


Итак, у нас есть изображения, есть функционал для работы с ними, в итоге есть массив byte[] для отправки на сервер.

Шапка


Вообще взаимодействие с сервером на silverlight тема для отдельной статьи(разберусь побольше и напишу, скорее всего). Достаточно сказать, что просто передать переменные на сервер(без использования веб-сервиса, soap) не так-то просто. В итоге, как и для flash придется формировать шапку запроса самим, эмулируя, таким образом, отправку формы. В итоге получилась очередная реинкарнация моего класса для формирования шапки:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Browser;

namespace Uploader.Libs
{
    public class FormBuilder
    {
        private string BOUND;
        private string ENTER = "\r\n";
        private string ADDB = "--";

        UTF8Encoding encoding;

        private List<byte> Data;

        public string bound
        {
            get { return BOUND; }
        }
            

        public FormBuilder()
        {
            BOUND = getBoundary();

            Data = new List<byte>();

            encoding = new UTF8Encoding();
        }

        public void addFile(string name, byte[] buffer)
        {
            string encode_name = HttpUtility.UrlEncode(name);
            StringBuilder header = new StringBuilder();
            
            header.Append(ADDB + BOUND);
            header.Append(ENTER);
            header.Append("Content-Disposition: form-data; name='" + encode_name  + "'; filename='" + encode_name + "'");
            header.Append(ENTER);
            header.Append("Content-Type: application/octet-stream");
            header.Append(ENTER);
            header.Append(ENTER);

            Data.AddRange(encoding.GetBytes(header.ToString()));
            Data.AddRange(buffer);
            Data.AddRange(encoding.GetBytes(ENTER));
        }

        public void addParam(string name, string value)
        {
            StringBuilder header = new StringBuilder();

            header.Append(ADDB + BOUND);
            header.Append(ENTER);
            header.Append("Content-Disposition: form-data; name='" + name + "'");
            header.Append(ENTER);
            header.Append(ENTER);
            header.Append(value);
            header.Append(ENTER);

            Data.AddRange(encoding.GetBytes(header.ToString()));
        }

        public byte[] getForm()
        {
            StringBuilder header = new StringBuilder();
            
            header.Append(ENTER);
            header.Append(ENTER);
            header.Append(ADDB + BOUND + ADDB);

            Data.AddRange(encoding.GetBytes(header.ToString()));

            byte[] formData = new byte[Data.Count];
            Data.CopyTo(formData);

            return formData;
        }

        private string getBoundary()
        {
            string _boundary = "";
            Random rnd = new Random();

            for (int i = 0; i < 0x20; i++)
            {
                _boundary += (char)(97 + rnd.NextDouble() * 25);
            }


            return _boundary;
        }

    }
}


Асинхронность


Вот мы и перешли к самому интересному. Собственно, к механизмам загрузки данных на сервер и получению ответа. В нашем случае понадобится HttpWebRequest. Вообще идеология работы с этим классом и с запросами в silverlight не столь очевидна, поэтому имеет смысл расписать последовательность действий:

1) Создание экземпляра HttpWebRequest, указание url назначения, метода отправки(Post,Get), заголовка(Content-type).

2) Далее вызов метода BeginGetRequestStream. У этого метода 2 параметра. Первый — это делегат функции, которая и будет вызвана. Второй — параметры переданные этой функции. Вообще, зачем нам эта функция? Она нужна для того, чтобы получить доступ к потоку(Stream), записывающему данные на сервер. Поток можно получить методом EndGetRequestStream.

Стоит отметить, что функция вызывается асинхронно и в отдельном потоке(не путать со Stream), то есть доступа к пользовательскому интерфейсу у нас нет. С этим можно побороться двумя путями: через вызов Dispatcher.BeginInvoke(вызов функции в потоке пользовательского интерфейса) или же через SynchronizationContext сначала сохранить контекст синхронизации для потока пользовательского интерфейса, а потом в требуемом месте вызвать функцию, которая асинхронно выполнится в этом потоке.

3) Далее просто записываем через Write данные(тут же можно и отслеживать процент загрузки).

4) Далее вызов метода BeginGetResponse, который запрашивает ответ от сервера. Параметры у метода такие же как и для BeginGetRequestStream, и функция тоже вызывается асинхронно.

5) В этой функции получаем экземпляр HttpWebResponse(методом EndGetResponse объекта HttpWebRequest).

6) Далее получаем поток(Stream) с ответом(вызвав метода GetResponseStream объекта HttpWebResponse). А из потока уже получаем сам ответ(через StreamReader.ReadToEnd).

Я понимаю, что объяснение корявое, но по-другому я не сумел. Надеюсь, пример кода будет наглядней(ну и некоторые комментарии в коде тоже присутствуют):
        SynchronizationContext sync; //контекст синхронизации для потока пользовательского интерфейса

        Dictionary<string, MyImage> images; //собственно коллекция изображений

        HtmlView mainView; //это мой класс для вывода в представление html

        string script = "../upload.php"; //php-скрипт, обрабатывающий полученные файлы

        private void upload()
        {
            if (images.Count > 0)
            {
                MyImage im = (MyImage)(images.First().Value);

                MyImage mini = im.scale(300); //ресайзим

                FormBuilder builder = new FormBuilder(); //класс формирования шапки

                builder.addFile(im.name, im.getOrigin()); 
                builder.addFile(mini.name, mini.toByte());

                byte[] formData = builder.getForm();

                Uri uri = new Uri(script, UriKind.Relative);

                HttpWebRequest request = (HttpWebRequest)WebRequestCreator.BrowserHttp.Create(uri); // здесь мы используем Browser Http Stack для того, чтобы можно было отправлять куки
                request.Method = "POST";
                request.ContentType = "multipart/form-data; boundary=" + builder.bound;
                request.ContentLength = formData.Length;

                List<object> uploadState = new List<object>(); //собственно объект, который мы передаем в функцию
                uploadState.Add(request);
                uploadState.Add(formData);

                request.BeginGetRequestStream(new AsyncCallback(GetRequestStream), uploadState);
            }
        }

        private void GetRequestStream(IAsyncResult result)
        {
            List<object> state = (List<object>)result.AsyncState;

            HttpWebRequest request = (HttpWebRequest)state[0];
            byte[] data = (byte[])state[1];

            int k = 0;
            int h = data.Length / 100;
            int ost = data.Length % 100;
            int dLength = data.Length - ost;

            Stream writeStream = request.EndGetRequestStream(result); //получаем поток

            for (int i = 0; i < dLength; i += h)
            {
                writeStream.Write(data, i, h);

                k++;

                Dispatcher.BeginInvoke(() =>
                {
                    mainView.setPercent(k); //отслеживаем загрузку
                });
            }

            if (ost > 0) writeStream.Write(data, dLength, ost);
     
            writeStream.Close();

            request.BeginGetResponse(new AsyncCallback(GetResponse), request);

        }

        private void GetResponse(IAsyncResult result)
        {
            HttpWebRequest request = (HttpWebRequest)result.AsyncState;
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);

            sync.Post(onComplete, response); //вызываем функцию в потоке пользовательского интерфейса
        }


        private void onComplete(object state)
        {
            HttpWebResponse response = (HttpWebResponse)state;

            if (response.StatusCode == HttpStatusCode.OK)
            {

                StreamReader reader = new StreamReader(response.GetResponseStream());

                string responseText = reader.ReadToEnd();

                reader.Close();
            }

            images.Remove(images.First().Key); //удаляем загруженное изображение из коллекции

            mainView.setRowComplete();

            upload(); //переходим к следующему изображению

        }

Такая вот загрузка. Зато на сервере ничто не мешает нам воспользоваться банальным и знакомым скриптом:
foreach($_FILES as $key => $value){
	$filename = substr_replace($key, '.', -4, 1);
	move_uploaded_file($value['tmp_name'], "upload/". urldecode($filename));	
}
echo 'complete'; 


Представление


Остался вопрос, как должен выглядеть интерфейс загрузсика(xaml или html). Тут уж каждый решает, как ему больше нравится. Я сделал в виде html. То есть кнопка вызова диалога выбора файлов, конечно, на silverlight(в силу ограничения безопасности). Зато всё остальное на html(включая и превью изображений, их я выводил в виде base64). В этом мне помогли замечательные классы Html Bridge, позволяющие работать с DOM-деревом прямо из silverlight.

Вот так выглядит демка загрузчика:
imageimage

За скобками


Хочется сказать спасибо хабраюзеру Demetros за отличные статьи на эту же тему, а также хабраюзерам, кто в комментариях к моим предыдущим статьям давал ссылки на готовые решения. Те, кто заинтересуются этой темой, могут найти что-то для своих задач.

Поздравляю всех с Новым годом и желаю, чтобы ваша работа была продолжением ваших увлечений!
Tags:
Hubs:
+9
Comments 16
Comments Comments 16

Articles