Масштабируем изображения в веб приложениях с примерами для ASP.Net MVC – часть 2

В первой части статьи я описал способы изменений размеров изображений в веб приложениях на клиентской стороне. В части 2 я покажу, как можно масштабировать изображения на серверной стороне с использованием средств ASP.Net MVC и DDD.

Прежде всего подготовим необходимую инфраструктуру для примера. Будем предполагать что изображения хранятся в базе данных содержимого (не путать с content database из Sharepoint-а. Здесь я говорю о кастомной базе данных. Изображения хранятся в таблице ProductImage с внешним ключом на таблицу товаров Product):

CREATE TABLE [Product](
 [ProductId] [int] IDENTITY(1,1) NOT NULL,
 [Name] [nvarchar](1024) NOT NULL,
 ... )
CREATE TABLE [ProductImage](
 [ImageId] [int] IDENTITY(1,1) NOT NULL,
 [Image] [image] NOT NULL,
 [FileName] [nvarchar](1024) NOT NULL,
 [ProductId] [int] NOT NULL,
 ...)

В качестве ORM используем Fluent NHibernate, с помощью которой записи из базы данных отображаются на сущность ProductImage нашей модели (в терминах DDD):

public class ProductImage : PersistentObject<int>
{
 public virtual byte[] Image { get; set; }
 public virtual string FileName { get; set; }
 public virtual Product Product { get; set; }
}

Здесь PersistentObject<T> – это суперкласс слоя (layer super class), который содержит свойство T Id (идентификатор сущности) и переопределяет метод Equal для сравнения объектов по их идентификаторам. Используя DDD подход, определим репозиторий для наших изображений:

public interface IProductImageRepository : IRepository<ProductImage, int>
{
}

где IRepository<TEntity, TKey> базовый интерфейс для репозиториев, который специализируется типом модели и идентификатора. Этот интерфейс содержит базовые CRUD операции над моделью:

public interface IRepository<TEntity, TKey>
{
 TEntity GetById(TKey id);
 void Save(TEntity entity);
 IEnumerable<TEntity> GetAll();
 void Delete(TEntity entity);
 bool Exist(TKey key);
 ...
}

Для реализации репозитория я использовал Linq 2 NHibernate, но это не важно для примера.

Хорошо, у нас есть хранилище данных и модель. Теперь нам нужен ImageResult для того, чтобы мы могли возращать изображения из контроллеров (см. для примера http://blog.maartenballiauw.be/post/2008/05/ASPNET-MVC-custom-ActionResult.aspx):

public class ImageResult : ActionResult
{
    public byte[] Image { get; set; }
    public string FileName { get; set; }
    public override void ExecuteResult(ControllerContext context)
    {
        if (Image == null)
        {
            throw new ArgumentNullException("Image");
        }
        if (string.IsNullOrEmpty(this.FileName))
        {
            throw new ArgumentNullException("FileName");
        }
        // output
        context.HttpContext.Response.Clear();
        var imageFormat = getContentType(this.FileName);
        context.HttpContext.Response.ContentType = imageFormat;
        using (var ms = new MemoryStream(this.Image))
        {
            ms.WriteTo(context.HttpContext.Response.OutputStream);
        }
        context.HttpContext.Response.End();
    }
    protected string getContentType(string name)
    {
        var defaultType = "image/gif";
        if (string.IsNullOrEmpty(name) || !Path.HasExtension(name))
        {
            return defaultType;
        }
        string ext = Path.GetExtension(name);
        if (string.IsNullOrEmpty(ext))
        {
            return defaultType;
        }
        if (ext.Equals(".bmp", StringComparison.InvariantCultureIgnoreCase))
            return "image/bmp";
        if (ext.Equals(".gif", StringComparison.InvariantCultureIgnoreCase))
            return "image/gif";
        if (ext.Equals(".vnd.microsoft.icon", StringComparison.InvariantCultureIgnoreCase))
            return "image/vnd.microsoft.icon";
        if (ext.Equals(".jpeg", StringComparison.InvariantCultureIgnoreCase))
            return "image/jpeg";
        if (ext.Equals(".jpg", StringComparison.InvariantCultureIgnoreCase))
            return "image/jpeg";
        if (ext.Equals(".png", StringComparison.InvariantCultureIgnoreCase))
            return "image/png";
        if (ext.Equals(".tiff", StringComparison.InvariantCultureIgnoreCase))
            return "image/tiff";
        if (ext.Equals(".wmf", StringComparison.InvariantCultureIgnoreCase))
            return "image/wmf";
        return defaultType;
    }
}

Как видите в классе ImageResult есть 2 свойства: Image и FileName. ImageResult записывает массив байт изображения в response при вызове метода ExecuteResult(). Для того чтобы отобразить ImageResult на наших views мы можем использовать ImageExtensions из MVC futures, но они используют не строго-типизированный подход. Использование магических строк не очень надежный подход. Code-first уже давно доказал свою эффективность, поэтому для нас было бы хорошо написать что-нибудь подобное:

<%= Html.Image<ProductImageController>(c => c.ViewResized(this.Model[i].Id, width, height)) %>

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

public static class ImageResultHelper
{
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, int? width, int? height)
        where T : Controller
    { ... }
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, int? width, int? height, string alt)
        where T : Controller
    { ... }
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, string id, string alt, string @class)
        where T : Controller
    { ... }
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, int? width, int? height, string alt, string @class)
        where T : Controller
    { ... }
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, string alt, string @class)
        where T : Controller
    { ... }
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, string id, int? width, int? height,
        string alt, string @class)
        where T : Controller
    { ... }
    public static string Image<T>(this HtmlHelper helper,
        Expression<Action<T>> action, string id, int? width, int? height,
        string alt, string @class, object attrs)
        where T : Controller
    {
        string url = helper.BuildUrlFromExpression<T>(action);
        var sb = new StringBuilder();
        if (!string.IsNullOrEmpty(url))
        {
            sb.AppendFormat("src=\"{0}\" ", url);
        }
        if (!string.IsNullOrEmpty(id))
        {
            sb.AppendFormat("id=\"{0}\" ", id);
        }
        if (width != null)
        {
            sb.AppendFormat("width=\"{0}\" ", width.Value);
        }
        if (height != null)
        {
            sb.AppendFormat("height=\"{0}\" ", height.Value);
        }
        if (!string.IsNullOrEmpty(alt))
        {
            sb.AppendFormat("alt=\"{0}\" ", alt);
        }
        if (!string.IsNullOrEmpty(@class))
        {
            sb.AppendFormat("class=\"{0}\" ", @class);
        }
        addAttributes(sb, attrs);
        return string.Format("<img {0} />", sb);
    }
    private static void addAttributes(StringBuilder sb, object attrs)
    {
        var routeValues = new RouteValueDictionary(attrs);
        foreach (var kv in routeValues)
        {
            sb.AppendFormat("{0}=\"{1}\" ", kv.Key, kv.Value);
        }
    }
}

Чтобы облегчить себе жизнь, мы могли бы использовать TagBuilder из MVC Futures. Это позволило бы нам избежать ручного форматирования атрибутов. Но для нашего примера я решил использовать простой StringBuilder для того, чтобы не зависеть от внешних библиотек.

Теперь у нас есть необходимая инфраструктура. Давайте определим контроллер ProductImageController, который может быть использован для отображения нашей модели:

public class ProductImageController : Controller
{
    private IProductImageRepository productImageRepository;
    public ProductImageController(IProductImageRepository productImageRepository)
    {
        this.productImageRepository = productImageRepository;
    }
    [HttpGet]
    public ActionResult View(int id)
    {
        var productImage = this.productImageRepository.GetById(id);
        if (productImage == null)
        {
            return new EmptyResult();
        }
        return new ImageResult { FileName = productImage.FileName, Image = productImage.Image };
    }
    [HttpGet]
    public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
    {
        ...
    }
}

Контроллер принимает репозиторий IProductImageRepository в конструкторе (используется инъекция зависимостей через конструктор в терминах IoC. Я использовал StructureMap в качестве IoC контейнера, но для примера это не важно. Можно выбрать любой из существующих). Также контроллер имеет 2 метода: View и ViewResized. Первый метод просто читает изображение из базы данных по переданному идентификатору и возвращает ImageResult (см. выше). Второй метод ViewResized возвращает масштабированное изображение, используя переданные параметры maxWidth и maxHeight.

Для того, чтобы реализовать метод ViewResized можно использовать подход, описанный здесь:

[HttpGet]
public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
{
    var productImage = this.productImageRepository.GetById(id);
    if (productImage == null)
    {
        return new EmptyResult();
    }
    return this.getResizedImageResult(productImage.FileName,
        productImage.Image, maxWidth, maxHeight);
}
private ActionResult getResizedImageResult(string fileName, byte[] image,
    int maxWidth, int maxHeight)
{
    using (var ms = new MemoryStream(image))
    {
        using (var img = Image.FromStream(ms))
        {
            var originalSize = img.Size;
            if (originalSize.Width <= maxWidth && originalSize.Height <= maxHeight)
            {
                return new ImageResult { FileName = fileName, Image = image };
            }
            var widthtRatio = (double)maxWidth / originalSize.Width;
            var heightRatio = (double)maxHeight/originalSize.Height;
            var ratio = Math.Min(widthtRatio, heightRatio);
            var newSize = new Size((int)(originalSize.Width*ratio),
                (int)(originalSize.Height*ratio));
            using (var btm = new Bitmap(newSize.Width, newSize.Height))
            {
                btm.MakeTransparent(Color.White);
                using (var gr = Graphics.FromImage(btm))
                {
                    gr.Clear(Color.White);
                    gr.CompositingQuality = CompositingQuality.HighSpeed;
                    gr.InterpolationMode = InterpolationMode.Default;
                    gr.DrawImage(img, 0, 0, newSize.Width, newSize.Height);
                    gr.Flush();
                    using (var outputMs = new MemoryStream())
                    {
                        btm.Save(outputMs, img.RawFormat);
                        var result = new ImageResult { FileName = fileName,
                            Image = outputMs.ToArray() };
                        return result;
                    }
                }
            }
        }
    }
}

Как я упоминал в первой части, нас интересует только уменьшение размера изображений. Поэтому я добавил проверку того, что переданные параметры maxWeight и maxHeight меньше, чем оригинальные ширина и высота, и если нет – возвращаем оригинальное изображение. Это также позволит предотвратить запросы злоумышленников к действиям контроллера с большими шириной и высотой для того, чтобы занять большое количество памяти на веб сервере.

Со всей описанной инфраструктурой мы можем добавлять изображения во view с использованием следующего кода:

<%= Html.Image<ProductImageController>(c => c.ViewResized(this.Model.Id, 110, 120), "", "")%>

В реальных приложениях нужно также рассмотреть возможность добавления кэширования в ваш код для того чтобы не делать запросы к базе данных каждый раз когда приходит запрос на действие контроллера. Также если вы используете http модули не забудьте, что они инициализируются и используются в пределах одного веб приложения. Т.е. по умолчанию ваши модули будут исполнятся каждый раз, когда поступает запрос к любым ресурсам (включая изображения) в вашем приложении. Для того чтобы избежать снижения производительности рассмотрите использование отдельного веб сервера или хотя бы отдельного веб приложения (последнее можно сделать конвертацией папки приложения в IIS Manager-е с вашими изображениями в виртуальную папку с отдельным web.config-ом).

Реклама

Об авторе sadomovalex

Старший инженер, team lead, консультант. Работаю в стеке .Net. Последние несколько лет занимаюсь разработкой enterprise приложений под Sharepoint, чему и будет в основном посвящена тематика этого блога. Также активно использую и интересуюсь ASP.Net MVC, DDD, TDD, Agile. Активно участвую в жизни многих профессиональных сообществ, SPb .Net UG, SPb ALT.Net, rsdn, Finland SP UG и др.
Запись опубликована в рубрике ASP.Net MVC, DDD, IoC, UI. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s