Compress/Resize images before and after upload in Asp.net

Introduction

Today we will cover scenarios to compress and resize images before and after upload in Asp.net. Specifically, sometimes users are in low bandwidth area where cellular network coverage is low and they are trying to upload images for their business flows. We’ll also resize images on the server side and also compress images further by reducing the quality to optimize server side storage.

Goal

  1. Resize large images to smaller sizes before upload to improve upload speed.
  2. Resize images on the server side if still required.
  3. Reduce the quality of JPG images on server side to further optimize storage space on the server.
  4. One important focus area was to integrate this functionality into legacy asp.net sites which may or may not use AjaxControlToolkit.

Pre-requisites

I’m targeting legacy Asp.net Web forms application environment. But you can easily modify the code to suit Asp.net MVC code and use jQuery to upload images.

  1. Any Visual Studio environment which supports Web Forms
  2. Install AjaxControlToolkit using NuGet package manager or from DevExpress as an installer. This toolkit since we will be using AsyncFileUpload control.
  3. jQuery latest version

Code: Asp.net Web Forms way

  1. On an empty Web form, drop the AsycFileUpload control
  2. Open the .aspx file and the control details should look like this
<cc1:AsyncFileUpload OnClientUploadError="uploadError" Width="200px"
	OnClientUploadComplete="uploadComplete" runat="server"
	ID="AsyncFileUpload1"
	OnClientUploadStarted="uploadStart"
	accept="image/*"></cc1:AsyncFileUpload>

3. Focus on the OnClientUploadStarted event. We will be calling uploadStart JS function to resize the image here. In case of your Asp.net MVC code, you can drop a regular File control and assign a JS function in the onchange event.

function uploadStart(sender, args) {
        var file = sender._inputFile.files[0];
        resizeAndUpload(file);
}

Javascript Section

function resizeAndUpload(file) {
	var reader = new FileReader();
	reader.onloadend = function () {

	    var tempImg = new Image();
	    tempImg.src = reader.result;
	    tempImg.onload = function () {
		var MAX_WIDTH = 1440; // you can keep any width you want
		var MAX_HEIGHT = 960;  // you can keep any height you want

		var tempW = tempImg.width;
		var tempH = tempImg.height;
                
                //calculating the ratio of width that needs to be resized.
                //resizing the height in the same ratio as width.  
		if (tempW > MAX_WIDTH) {
		        var ratio = tempW / MAX_WIDTH;
			tempW = parseInt(tempW / ratio);
			tempH = parseInt(tempH / ratio);
		}
		else {
		        if (tempH > MAX_HEIGHT) {
			        var ratio = tempW / MAX_HEIGHT;
				tempW = parseInt(tempW / ratio);
				tempH = parseInt(tempH / ratio);
			}
		}

		var canvas = document.createElement('canvas');
		canvas.width = tempW;
		canvas.height = tempH;
		var ctx = canvas.getContext("2d");
		ctx.drawImage(this, 0, 0, tempW, tempH);
		var dataURL = canvas.toDataURL("image/jpeg");

		$.ajax({
			url: "http://<<Server address>>/api/images",
			type: "POST",
			data: 'image=' + dataURL,
			contentType: "application/x-www-form-urlencoded",
			success: function (data, textStatus, jqXHR) {
				console.log('posted image');
			},
			error: function (jqXHR, textStatus, errorThrown) {
				console.log('error posting image');
			}
		});
		
	    }
	}
	reader.readAsDataURL(file);	
}

If you console.log your Base64 string [var dataURL], it will look like (extremely large):

data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDA .... VmaK0v//Z

Web API Code – Refer this section to check how to read the Base64 string in Web API code and save it as an image.

Fun fact: Another important parameter for the method in the line also controls the quality of the image. It is the encoderOptions parameter. Refer this line – canvas.toDataURL("image/jpeg");.

You can write canvas.toDataURL("image/jpeg", 0.5); You can write a value anywhere between 0.1 and 1.0. So 0.5 will give you medium quality, 0.1 will give you lowest quality, 0.7 will give you higher quality and 1.0 will give best quality.

Implementing this parameter will be like a double whammy for the image. Resizing will reduce the size and the second parameter will reduce the quality which will again reduce the image size before upload. Even if you put 0.5, the loss in image quality is generally not clearly visible.

Server side code – Independent code

Note: This server side code is not related to AsyncFileUpload control and it just a server side explanation of resizing and compression on server side.

..
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
...
///
.
code
.
///

public void FileUploadComplete()
{
    // to reduce image quality to further reduce the image size
	// set the compression parameters via EncoderParameter
	// https://docs.microsoft.com/en-us/dotnet/framework/winforms/advanced/how-to-set-jpeg-compression-level
	ImageCodecInfo jpgEncoder = GetEncoder(ImageFormat.Jpeg);

	System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality;
	Int64 compressionPercentage = 50L;

	EncoderParameters myEncoderParameters = new EncoderParameters(1);
	EncoderParameter myEncoderParameter = new EncoderParameter(myEncoder, compressionPercentage);
	myEncoderParameters.Param[0] = myEncoderParameter;

	int width = 1440;
	int height = 960;
        string filename = HttpContext.Current.Server.MapPath("any server physical path") + System.IO.Path.GetFileName("<<Full Path of file name>>");

	using (FileStream fs = new System.IO.FileStream(fileName, System.IO.FileMode.Create))
	{
		System.Drawing.Image image = System.Drawing.Image.FromStream(fs);
		Bitmap bmp = new Bitmap(image);
		Bitmap resultImage = Resize(image, width, height);            
		string imagePath = "<<New Path>>";
		resultImage.Save(imagePath, jpgEncoder, myEncoderParameters);
	}
}

//http://stackoverflow.com/questions/11137979/image-resizing-using-c-sharp
public static Bitmap Resize(Image image, int width, int height)
{
	var destRect = new Rectangle(0, 0, width, height);
	var destImage = new Bitmap(width, height);

	destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);

	using (var graphics = Graphics.FromImage(destImage))
	{
		graphics.CompositingMode = CompositingMode.SourceCopy;
		graphics.CompositingQuality = CompositingQuality.HighQuality;
		graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
		graphics.SmoothingMode = SmoothingMode.HighQuality;
		graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

		using (var wrapMode = new ImageAttributes())
		{
			wrapMode.SetWrapMode(WrapMode.TileFlipXY);
			graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
		}
	}

	return destImage;
}

private ImageCodecInfo GetEncoder(ImageFormat format)
{
	ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
	foreach (ImageCodecInfo codec in codecs)
	{
		if (codec.FormatID == format.Guid)
		{
			return codec;
		}
	}
	return null;
}

Points for Server side code

  1. Check the line – Int64 compressionPercentage = 50L;. This reduces the quality of the image by 50 percentage without changing its dimensions. Uptil 50%, there is no visible degradation in the image quality. If you reduce this number to 40 or 25, the quality loss will be clearly visible. So 50% appears to be a sweet spot!. Increasing the number will improve the image quality and vice versa.
  2. The encoder properties are applied while saving the image – resultImage.Save(imagePath, jpgEncoder, myEncoderParameters)
  3. Dimensions can further be changed by Resize method.
  4. The Resize method shows what all image parameters you can change for an image. Please keep in mind that JPG has most of the options supported. It may happen that PNG, TIFF and other formats may not support all the options. E.g. PNG format does not support CompressionPercentage EncoderParameters. Please thoroughly test for other image formats.
  5. PNG format has bits per pixel as parameter of image quality. By default it stores 32 bits per pixel (pixel depth of 32) which is very high quality. You can compress a PNG image by reducing its pixel depth to 24 or 16 or 8 bits per pixel. You can also implement interlacing to compress your PNG. But reducing pixel depth is a completely different ball game. Instead I would suggest code that you covert a PNG to JPG and then follow the code given here. The !faint_hearted can proceed here to compress PNG files.
  6. The System.Drawing library supports reducing the pixel depth for TIFF image formats.

Alternate code: jQuery and Server side code

If you are into jQuery/MVC pages, you can upload the Base64 data via a simple Ajax call.

// Simple HTML File input control
<input type="file" id="fileUpload" name="fileUpload" onchange="onChange()"  accept="image/*" />

// change event of the control
function onChange() {
            debugger;
            var file = $('#fileUpload').prop('files');
            resizeAndUpload(file[0]);
            console.log("called fileUpload");
        }

// You can call the same resizeAndUpload. It also has code that will upload the file data asynchronously.

Note: There is no need to convert the Base64 string to blob data to upload it on to a Web API. Just the correct contentType is sufficient.

The Web API project

In Visual Studio, create a new Web API project. Ensure that you add Microsoft.Aspnet.Cors Nuget package to your Web Api project. In my case the version was 5.2.7. This step will enable your Web API to accept cross-domain calls since generally your Application and Web API will be deployed on different machines or port numbers.

Secondly, in your WebApiConfig.cs file, add this line config.EnableCors(); at the bottom of the Register method.

Observe how I have enabled CORS on the controller class. [EnableCors(origins: "*", headers: "*", methods: "*")].

Important note: Please be very careful when you enable ‘*’ for all origins, headers and method. This is the most insecure method and will allow your Web API to accepts calls from any origin/method/header. Just doing it here for the sake of demo to make things easier for the Ajax call to upload Base64 data via Web API call.

[EnableCors(origins: "*", headers: "*", methods: "*")]
    public class ImagesController : ApiController
    {
        // POST api/values
        public string Post()
        {            
            string result = Request.Content.ReadAsStringAsync().Result;
            byte[] bytes = Convert.FromBase64String(result.Split(new string[] { "base64," }, StringSplitOptions.RemoveEmptyEntries)[1]);

            Image image;
            using (MemoryStream ms = new MemoryStream(bytes))
            {
                image = Image.FromStream(ms);
                Bitmap bmp = new Bitmap(image);
                bmp.Save(HostingEnvironment.MapPath("~/uploads/wow.jpg"), ImageFormat.Jpeg);
            }

            string ServerBaseUrl = HttpContext.Current.Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Authority + HttpContext.Current.Request.ApplicationPath.TrimEnd('/');
            return ServerBaseUrl + "/uploads/wow.jpg";
        }

    }

Check how the Base64 data is converted to a byte array. In the using section, check how the byte array is loaded in to a MemoryStream, converted to a Bitmap image and finally saving it in a JPG format. You can further resize and compress the image by referring the code in the Server side section.

Important Note: If you do not load the image into a Bitmap and directly try to save the Image to a drive path, you will get a A generic error occurred in GDI+. Loading the image in a Bitmap helped me get rid of this “highly friendly” error message. There are several reasons for this error message but my case was different. I haven’t “Disposed” off objects for the sake of brevity.

The final line returns the full path of the image including server and port name. This can be used at the client side to show the uploaded image. You will need to add a reference to System.Web to make this line work.

Acknowledgements

Many blogs helped write code or gave valuable information. Here they are:

  1. A code sample
  2. A dev from Mozilla.
  3. Also this and this.

Conclusion

Well, we covered scenarios to compress and resize images before and after upload in Asp.net. We also learnt the server side image resize and compression logic. Finally we also saw how to apply the same code in jQuery and Web API scenarios. Hope this helps you. If you have questions, fire a mail to code@onezeroeight.co

4 Responses

  1. Like!! Great article post.Really thank you! Really Cool.

  2. When I originally left a comment I appear to have clicked on the -Notify me when new comments are
    added- checkbox and now each time a comment is added I get four
    emails with the exact same comment. There has to be an easy method you can remove me from that
    service? Thanks!

    • himanshu says:

      If you have selected ‘subscribe to emails’ for comments by mistake, then in the email notification, you will get a link to unsubscribe. Hope this helps.

Leave a Reply

Your email address will not be published. Required fields are marked *