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

    Предисловие


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

    Поздравляю всех с Новым годом и желаю, чтобы ваша работа была продолжением ваших увлечений!
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 16

      0
      А у вас вызовы setPercent происходят равномерно даже без вызова writeStream.Flush? Наверно, в 4-ом сильверлайте починили проблемы с буферизацией тела запроса.
        0
        Наверное, всё-таки стоит использовать Flush, но я проблем не заметил, что не доказывает, что их нет
        0
        >>Шапка

        OMG, WTF? С помощью WCF это делается в две с половиной строчки, без рукопашных формирований хедеров и всего прочего.
          0
          Я хочу, чтобы от представления(чем в моем случае и является silverlight) не зависела серверная архитектура
            +1
            Да она и так не будет зависеть. Или я не совсем вас понимаю.
          0
          Я неправильно выразился. Просто мне при работе с php это не очень удобно.
            0
            Это удобно, поверьте. :) На РНР точно-так же можно писать сервисы.
            Это конечно жутчайшее ИМХО, но то, чо вы написал выглдядит дикостью.
            SL специально заточен на «правильную» архитектуру, есть сервис, у него есть синтерфес, к нему ходит клиент, он генерит красивые прокси, асинхронность, наматывается инфраструктура, программисту остаётся только дёрнуть класс…
            И тут ВНЕЗАПНО StringBuilder.Appentd(header), рукопашный post… Вы же можете покалечить разум начинающего разработчика! :D
              +1
              А теперь представьте, что клиентом является не только silverlight, но и flash, и html5.
                +1
                Все они поддерживают RESTful web services.
                0
                Хорошо, вы будете пользоваться «правильной» архитектурой. А я, пожалуй, воспользуюсь неправильной)
              0
              как обстоят дела с памятью, сколько реально изображений может вместить контейнер? Я когда то писал тоже загрузчик с ресайзом. у столкнулся с проблемой массового поглощения памяти силверлайтом при отображении картинок на стороне клиента.
                0
                С памятью проблем не возникало. Но имейте ввиду, что если вы держите в памяти несколько битмапов, то это всё равно скажется.
                  0
                  я о том же)
                    0
                    Просто нужно, конечно, не держать все битмапы в памяти. Последовательно загружать, и удалять уже загруженные на сервер
                      0
                      у меня была чуть другая задача, я сначала должен все показать, после юзер имеет возможность удалить перевернуть и кропнуть и уже только после этого идет отправка
                        0
                        В принципе, если это так критично, то можно по одному обрабатывать(выводить превью), затем удалять, а затем снова грузить при окончательной отправке на сервер. Не очень хороший, наверное подход, но расход памяти будет гораздо меньшим

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое