Image Processing For Web Site Load Optimization

Many times, we develop a website that has simple features; at first, everything looks solid, including the speed of loading pages. Over time, after additional features are added, this changes, and the site's loading speed gets completely bogged down.

We encountered this problem. Although we were using caching and CDN strategies to increase loading speed, users were still experiencing issues. As a result, we reduced the count of requests. Some of these are web-service calls that can be aggregated to one service call. Others are media that can be streamed or images that can be spirited with CSS sprites. In this article, we focus on reducing request calls by using CSS sprites.

Two Types of Images

In a website, there are two types of images

Static images : These images are constant and cannot be changed by users of the website.

: These images are constant and cannot be changed by users of the website. Dynamic images: These images can be changed by users, such as images created by a CMS.

For static images the approach is very simple, you create CSS sprites once and use them in multiple places. However, when it comes to dynamic images, we cannot use a constant CSS sprite, as every time a user updates an image, ad, or remove it, we must update our CSS sprite. This is why we use image processing.

The Algorithm

Our approach to this issue is:

Detect change event of images.

Load related objects from the dataset.

Merge images.

Compress the resulting image.

Create CSS attribute for each related object to each image.

Implementation

Every content that has an image implements the interface ISpriteable. The FileName property shows the name of the generated CSS sprite image file. When we load images, we just want to load all related images together with just one request.

public interface ISpriteable { string FileName { get; } int PosX { get; set; } int PosY { get; set; } int Width { get; set; } int Height { get; set; } }





public class SpriteCss : ISpriteable { public string FileName { get; } public int PosX { get; set; } public int PosY { get; set; } public int Width { get; set; } public int Height { get; set; } public int BackgroundWidth { get; set; } public int BackgroundHeight { get; set; } }





public class News : SpriteCss { public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } public string ImageFileName { get; set; } public bool IsActive { get; set; } new public string FileName => ImageFileName; }





In the image processor class, we are working with Ispritable interface as follows:

using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using Emgu.CV; using Emgu.CV.Structure; namespace Service.Areas.Panel.Models.Sprite { public enum ImageCodec { Jpg, Png } public static class ImageProcessor { public static void GenerateSprites<T>(T objects, string folderPath, string fileName, int quality, ImageCodec imageCodec) where T : IEnumerable<ISpriteable> { var path = ""; switch (imageCodec) { case ImageCodec.Jpg: var images = GetImages(objects, folderPath); var mergedImage = MergeImages(images); var image = mergedImage.ToBitmap(); path = Path.Combine(folderPath, $"{fileName}.jpg"); SaveJpeg(path, image, quality); break; case ImageCodec.Png: var bitmapImages = GetImagesAsBitmap(objects, folderPath); var mergedBitmap = MergeImages(bitmapImages); path = Path.Combine(folderPath, $"{fileName}.png"); SavePng(path, mergedBitmap, quality); break; default: throw new ArgumentOutOfRangeException(nameof(imageCodec), imageCodec, null); } } public static List<ISpriteable> ComputeCss<T>(T objects, string folderPath) where T : IEnumerable<ISpriteable> { try { var cssList = new List<ISpriteable>(); var y = 0; foreach (var obj in objects) { var path = Path.Combine(folderPath, obj.FileName); var image = Image.FromFile(path); var css = new Sprite { Height = image.Height, Width = image.Width, PosX = 0, PosY = y }; cssList.Add(css); y -= image.Height; } return cssList; } catch (FileNotFoundException exception) { throw new Exception("Related file not found.", exception); } } private static List<Image<Rgba, byte>> GetImages<T>(T objects, string folderPath) where T : IEnumerable<ISpriteable> { var images = new List<Image<Rgba, byte>>(); foreach (var obj in objects) { var path = Path.Combine(folderPath, obj.FileName); var mat = CvInvoke.Imread(path, Emgu.CV.CvEnum.ImreadModes.AnyColor); var img = mat.ToImage<Rgba, byte>(); images.Add(img); } return images; } private static List<Bitmap> GetImagesAsBitmap<T>(T objects, string folderPath) where T : IEnumerable<ISpriteable> { return objects .Select(obj => Path.Combine(folderPath, obj.FileName)) .Select(path => new Bitmap(path)) .ToList(); } private static void SaveJpeg(string path, Image img, long quality) { var qualityParam = new EncoderParameter(Encoder.Quality, quality); var jpegCodec = GetEncoderInfo("image/jpeg"); if (jpegCodec != null) { var encoderParams = new EncoderParameters(1) {Param = {[0] = qualityParam}}; img.Save(path, jpegCodec, encoderParams); } } private static void SavePng(string path, Image img, long quality) { var qualityParam = new EncoderParameter(Encoder.Quality, quality); var pngCodec = GetEncoderInfo("image/png"); if (pngCodec != null) { var encoderParams = new EncoderParameters(1) { Param = { [0] = qualityParam } }; img.Save(path, pngCodec, encoderParams); } } private static ImageCodecInfo GetEncoderInfo(string mimeType) { var codecs = ImageCodecInfo.GetImageEncoders(); return codecs.FirstOrDefault(t => t.MimeType == mimeType); } private static Image<Rgba, byte> MergeImages(IReadOnlyList<Image<Rgba, byte>> images) { var mergedImage = MergeTwoImageVertically(images[0], images[1]); for (var i = 2; i < images.Count; i++) { mergedImage = MergeTwoImageVertically(mergedImage, images[i]); } return mergedImage; } private static Bitmap MergeImages(IReadOnlyList<Bitmap> images) { var mergedImage = MergeTwoImageVertically(images[0], images[1]); for (var i = 2; i < images.Count; i++) { mergedImage = MergeTwoImageVertically(mergedImage, images[i]); } return mergedImage; } private static Image<Rgba, byte> MergeTwoImageHorizontally(CvArray<byte> image1, CvArray<byte> image2) { var height = image1.Height > image2.Height ? image1.Height : image2.Height; var width = image1.Width + image2.Width; var imageResult = new Image<Rgba, byte>(width, height) { ROI = new Rectangle(0, 0, image1.Width, image1.Height) }; image1.CopyTo(imageResult); imageResult.ROI = new Rectangle(image1.Width, 0, image2.Width, image2.Height); image2.CopyTo(imageResult); imageResult.ROI = Rectangle.Empty; return imageResult; } private static Image<Rgba, byte> MergeTwoImageVertically(CvArray<byte> image1, CvArray<byte> image2) { var imageWidth = image1.Width > image2.Width ? image1.Width : image2.Width; var imageHeight = image1.Height + image2.Height; var imageResult = new Image<Rgba, byte>(imageWidth, imageHeight) { ROI = new Rectangle(0, 0, image1.Width, image1.Height) }; image1.CopyTo(imageResult); imageResult.ROI = new Rectangle(0, image1.Height, image2.Width, image2.Height); image2.CopyTo(imageResult); imageResult.ROI = Rectangle.Empty; return imageResult; } private static Bitmap MergeTwoImageVertically(Image image1, Image image2) { var imageWidth = Math.Max(image1.Width, image2.Width); var imageHeight = image1.Height + image2.Height; var imageResult = new Bitmap(imageWidth, imageHeight); using (var graphics = Graphics.FromImage(imageResult)) { graphics.DrawImage(image1, new Rectangle(new Point(), image1.Size), new Rectangle(new Point(), image1.Size), GraphicsUnit.Pixel); graphics.DrawImage(image2, new Rectangle(new Point(0, image1.Height), image2.Size), new Rectangle(new Point(), image2.Size), GraphicsUnit.Pixel); } return imageResult; } } }





Here, we use the EmguCV library for merge and compressing images. EmguCV is a C# port of the OpenCV, one of the most popular image processing libraries in C++.

Sample Usage of Image Processor Class

In this example, we are using an image processor in an ASP.NET MVC controller. When an action is called, the CSS sprite image is generated automatically.

using System; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; using System.Web; using System.Web.Mvc; using Service.Areas.Panel.Models; using Service.Areas.Panel.Models.Sprite; using Service.Classes; namespace Service.Areas.Panel.Controllers { public class NewsController : BaseController { [System.Web.Mvc.Authorize(Roles = "News")] public ActionResult News() { GenerateSprites(Server.MapPath("~/UploadPic/News")); RajaWebSiteEntities db = new RajaWebSiteEntities(); return View(db.News.ToList().OrderByDescending(r => r.ID)); } public static void GenerateSprites(string folderPath) { var applicationName = ConfigurationManager.AppSettings["ApplicationName"] ?? ""; if (applicationName.Equals("WebSite", StringComparison.InvariantCultureIgnoreCase)) { ImageProcessor.GenerateSprites( new NewsRepository().GetSpecialNews(), folderPath, "NewsSprites", 50, ImageCodec.Jpg ); } } } }





The Result

Using this approach, we decrease total requests to the server by 30. As a result, the page loaded about 40 percent faster than before.