find a location for property in a new city

Thursday 19 May 2011

Image resizing, cropping and compression using .NET

If you need to resize an image or crop an image or compress an image or even do all three you will find you can do all of those things using the .NET framework's System.Drawing classes. In this tutorial I will talk you through the steps required to do all three to transform an large image to an image fit for your website.

This is the example scenario. You have been asked to make a page that enables a user to upload any image they want so that it can be manipulated to ensure it is 200px x 200px and compressed to a smaller file size. I won't talk you though how to make the uploading part as there are a plethora of blogs discussing that already. What I will discuss is how to resize crop and compress that image.

Say you have an image that is 1000px x 800px. There are 3 main steps to converting it to a compressed 200px x 200px image:

Resizing an image

To resize it the key is that it needs to keep the same aspect ratio whilst being a lot smaller.

So the expected outcome of this stage will be that the shortest of the two dimensions will be 200px, the other will be larger and the aspect ratio will remain the same. Begin code:
private byte[] GetCroppedImage(byte[] originalBytes, Size size, ImageFormat format)
{
    using (var streamOriginal = new MemoryStream(originalBytes))
    using (var imgOriginal = Image.FromStream(streamOriginal))
    {
        //get original width and height of the incoming image
        var originalWidth = imgOriginal.Width; // 1000
        var originalHeight = imgOriginal.Height; // 800

        //get the percentage difference in size of the dimension that will change the least
        var percWidth = ((float)size.Width / (float)originalWidth); // 0.2
        var percHeight = ((float)size.Height / (float)originalHeight); // 0.25
        var percentage = Math.Max(percHeight, percWidth); // 0.25

        //get the ideal width and height for the resize (to the next whole number)
        var width = (int)Math.Max(originalWidth * percentage, size.Width); // 250
        var height = (int)Math.Max(originalHeight * percentage, size.Height); // 200

        //actually resize it
        using (var resizedBmp = new Bitmap(width, height))
        {
            using (var graphics = Graphics.FromImage((Image)resizedBmp))
            {
                graphics.InterpolationMode = InterpolationMode.Default;
                graphics.DrawImage(imgOriginal, 0, 0, width, height);
            }
        }
    }
}

Cropping an image

After the last step you are left with an image that is 250px x 200px. As you will notice that is still an aspect ratio of 5:4 so it does not look squashed. However this still isn't the right size so you will now need to crop it.

You are now intending to cut off the excess from both sides to reduce the width whilst leaving the height the same. This will leave you with a 200px x 200px image. Code on:
private byte[] GetCroppedImage(byte[] originalBytes, Size size, ImageFormat format)
{
    using (var streamOriginal = new MemoryStream(originalBytes))
    using (var imgOriginal = Image.FromStream(streamOriginal))
    {
        //get original width and height of the incoming image
        var originalWidth = imgOriginal.Width; // 1000
        var originalHeight = imgOriginal.Height; // 800

        //get the percentage difference in size of the dimension that will change the least
        var percWidth = ((float)size.Width / (float)originalWidth); // 0.2
        var percHeight = ((float)size.Height / (float)originalHeight); // 0.25
        var percentage = Math.Max(percHeight, percWidth); // 0.25

        //get the ideal width and height for the resize (to the next whole number)
        var width = (int)Math.Max(originalWidth * percentage, size.Width); // 250
        var height = (int)Math.Max(originalHeight * percentage, size.Height); // 200

        //actually resize it
        using (var resizedBmp = new Bitmap(width, height))
        {
            using (var graphics = Graphics.FromImage((Image)resizedBmp))
            {
                graphics.InterpolationMode = InterpolationMode.Default;
                graphics.DrawImage(imgOriginal, 0, 0, width, height);
            }

            //work out the coordinates of the top left pixel for cropping
            var x = (width - size.Width) / 2; // 25
            var y = (height - size.Height) / 2; // 0

            //create the cropping rectangle
            var rectangle = new Rectangle(x, y, size.Width, size.Height); // 25, 0, 200, 200

            //crop
            using (var croppedBmp = resizedBmp.Clone(rectangle, resizedBmp.PixelFormat))
            {
            }
        }
    }
}

Compressing the image

You now have the image to the correct size but for the web it is not really optimised.

You now want to compress the image down from the current 4 bytes per pixel (32bit) image, which would be ~156KB (for a 200x200 image!). Time to compress:
private byte[] GetCroppedImage(byte[] originalBytes, Size size, ImageFormat format)
{
    using (var streamOriginal = new MemoryStream(originalBytes))
    using (var imgOriginal = Image.FromStream(streamOriginal))
    {
        //get original width and height of the incoming image
        var originalWidth = imgOriginal.Width; // 1000
        var originalHeight = imgOriginal.Height; // 800

        //get the percentage difference in size of the dimension that will change the least
        var percWidth = ((float)size.Width / (float)originalWidth); // 0.2
        var percHeight = ((float)size.Height / (float)originalHeight); // 0.25
        var percentage = Math.Max(percHeight, percWidth); // 0.25

        //get the ideal width and height for the resize (to the next whole number)
        var width = (int)Math.Max(originalWidth * percentage, size.Width); // 250
        var height = (int)Math.Max(originalHeight * percentage, size.Height); // 200

        //actually resize it
        using (var resizedBmp = new Bitmap(width, height))
        {
            using (var graphics = Graphics.FromImage((Image)resizedBmp))
            {
                graphics.InterpolationMode = InterpolationMode.Default;
                graphics.DrawImage(imgOriginal, 0, 0, width, height);
            }

            //work out the coordinates of the top left pixel for cropping
            var x = (width - size.Width) / 2; // 25
            var y = (height - size.Height) / 2; // 0

            //create the cropping rectangle
            var rectangle = new Rectangle(x, y, size.Width, size.Height); // 25, 0, 200, 200

            //crop
            using (var croppedBmp = resizedBmp.Clone(rectangle, resizedBmp.PixelFormat))
            using (var ms = new MemoryStream())
            {
                //get the codec needed
                var imgCodec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == format.Guid);

                //make a paramater to adjust quality
                var codecParams = new EncoderParameters(1);

                //reduce to quality of 80 (from range of 0 (max compression) to 100 (no compression))
                codecParams.Param[0] = new EncoderParameter(Encoder.Quality, 80L);

                //save to the memorystream - convert it to an array and send it back as a byte[]
                croppedBmp.Save(ms, imgCodec, codecParams);
                return ms.ToArray();
            }
        }
    }
}

There you go. Your massive image is looking good to the correct size whilst being a much leaner version of former self. Congratulations!

Update

In line 24, you can see I have set the InterpoltionMode of the graphics object to InterpolationMode.HighQualityBicubic before doing the initial resize. I previously had this set to Default but I noticed that straight diagonal lines in resized images were coming out jagged and they didn't look pretty. HighQualityBicubic is meant to be "the best" form of interpolation according to Microsoft so I sided with that. I can see a big improvement in the quality so I would recommend it too.

Follow britishdev on Twitter

Wednesday 11 May 2011

Why the ICO's new cookie directive is nonsense

As of the 26th May 2011 the law which applies to how you use cookies is changing and quite dramatically too as directed by the ICO. You are now being (asked / advised / ordered) to request your users' permission before storing any "non-essential" cookies on their machine. The given example of an "essential" cookie is one which remembers what is in their shopping cart. So, my thoughts follow.

Chances are you’re here because you heard about the changes to the rules on using cookies and similar technologies for storing information and are now trawling through Google trying to find the second part of the directive saying, "Just kidding!!!!1! lolllzzzzzzzzz, luv ICO xx" but getting progressively concerned as you can't find it anywhere.

I'm also guessing that you can't quite believe what you are hearing. Cookies? The cookies that have been around since early 90s? The ones that have evolved over almost 20 years? The ones that are now integral to your users' experience, your analytics, your business intelligence and possibly tracking click-throughs necessary for your revenue? Yes, those things. Oh PS you have about a week to change how you use them.

Development cycle

Does the ICO have any idea how development cycles work? You can't expect every website available in the EU to suddenly change something as integral as cookies that quickly whilst putting all other development work on hold. Also, since this will be such a degradation of user experience there will surely be a "well, you first" effect where no one will comply until their competitors also comply.

Example of why this isn't fair

Website1 has a link promoting Website2. Website2 sells several products. Website2 has promised that if a user who was referred from Website1 purchases a product it will give a proportion of its revenue to Website1.

This system could be implemented by checking the users' referrer on entry to Website2 and then storing it in a cookie. After clicking around the site a user purchases a product, a cookie is checked to see that they came from Website1 to determine whether or not to share revenue for that purchase.

In this example you can see how the use of this cookie is fundamental to the monetisation of Website1 and the relationship => traffic => monetisation of Website2. You can also see how that cookie is of absolutely no interest of the user and therefore "non-essential". So, when asked, would that user accept that cookie? Probably not.

Hypocrisy

So it hasn't come into effect yet, sure, but even still wouldn't you lead by example if you thought it was such a great idea? Find out by visiting the ICO homepage and viewing your cookies. Looks like analytics cookies tracking my behaviour to me. Update: almost a year later they have updated their site to not send cookies without asking first. Wonder how happy their analysts are with the lack of traffic statistics? Regardless I should concede this point.

Communication

Can you seriously change the law that significantly which such poor communication? I only found out about this through word of mouth (i.e Twitter) and then reading the "guidance" made me surprised to discover developers are now expected to be law graduates. I was expecting to read a clear directive not a rambling 10 page pdf of legal speak.

You can't stop tracking of information

Developers are skilled in the art of workarounds. We'll find a way of storing information about users regardless of this directive. For example if we're allowed a session cookie, we'll use that cookie to reference a plethora of information about the user in a database. Oh look, workaround is already there. So what are you achieving now other than making my DBA less happy?

Conclusion

If this is actually taken seriously you can get used to clicking a lot more often as websites ask you if you'd like to allow cookies or not. Possibly as some websites suffer from losing money streams from both essential tracking and poor user experience they may struggle to exist.

That or we will all be arrested (including the ICO).

Follow britishdev on Twitter

Friday 6 May 2011

MVC3 deploy - Could not load file or assembly 'System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies

Whilst deploying my newly upgraded ASP.NET MVC 3 web application to the production environment I started receiving a FileNotFoundException with the error message "Could not load file or assembly 'System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file specified." I have encountered this before so I know it is about bin-deploying that assembly. But where is the System.Web.WebPages.Razor assembly?

Since you have installed ASP.NET MVC 3 on your development machine you have all these MVC 3 assemblies installed in your GAC. Your production machine does not!

So you need to find this file and make it bin deployable however this cheeky assembly isn't in you solution so how can you do that? Well... add it. Go to Add Reference on your root web project (e.g. Web.csproj) and find System.Web.WebPages.Razor and add.

Now right click this assembly in your references directory and click properties.

Now you can make it bin deployable by setting Copy Local to true like so:

Note this is how to deploy the System.Web.WebPages.Razor assembly withe your ASP.NET MVC 3 web application. To deploy a typical setup of an ASP.NET MVC 3 app you will need to do the same for all of the following assemblies:
  • System.Web.Mvc
  • System.Web.Helpers
  • System.Web.Razor
  • System.Web.WebPages
  • System.Web.WebPages.Deployment
  • System.Web.WebPages.Razor as detailed above
  • Microsoft.Web.Infrastructure this will also need a reference added as above
  • WebMatrix.Data this also needs the reference

Of course you only need to add the reference to these assemblies if they are not already there - in most cases it will be. This was mainly a blog post for the slightly more complicated System.Web.WebPages.Razor.dll

Update

There is an easier way to do this if you are using Visual Studio 2010 Service Pack 1 and using Web Deploy as your publish action when publishing. You can add deployable dependencies to your ASP.NET MVC project that will choose the necessary assemblies for you. Even easier!

Follow britishdev on Twitter

How to convert an ASP.NET Web Forms web application into ASP.NET MVC 3

So, you have a traditional web forms web application and you are inspired by the new ASP.NET MVC 3 are you? Well this is definitely doable. In this how to guide I will tell you step by step exactly how you can add your new ASP.NET MVC 3 work into your existing ASP.NET web forms project.

1. Add references

Right click on your web root project in Visual Studio (2010 in my case) and add the following references:
  • System.Web.Routing
  • System.Web.Abstractions
  • System.Web.Mvc
  • System.WebPages
  • System.Web.Helpers

2. Configuration

Make your web.config look like this. Obviously not exactly like this, don't just paste this over the top of your glorious web.config. I'm just highlighting the necessary points that need to be fitted into your existing web.config:
<appSettings>
  <add key="webpages:Version" value="1.0.0.0"/>
  <add key="ClientValidationEnabled" value="true"/>
  <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>

<system.web>
  <compilation debug="true" targetFramework="4.0">
    <assemblies>
      <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </assemblies>
  </compilation>
  <pages>
    <namespaces>
      <add namespace="System.Web.Helpers" />
      <add namespace="System.Web.Mvc" />
      <add namespace="System.Web.Mvc.Ajax" />
      <add namespace="System.Web.Mvc.Html" />
      <add namespace="System.Web.Routing" />
      <add namespace="System.Web.WebPages"/>
    </namespaces>
  </pages>
</system.web>
<system.webServer>
  <modules>
    <remove name="UrlRoutingModule-4.0" />
    <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="" />
  </modules>
</system.webServer>
<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
      <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
    </dependentAssembly>
  </assemblyBinding>
</runtime>

Update: I have changed my recommendation against using <modules runAllManagedModulesForAllRequests="true"> in favour of adding the UrlRoutingModule-4.0 module with a blank precondition as I explain in my article Don't use runAllManagedModulesForAllRequests="true" for MVC routing.

However, if you disagree with me then you can make the modules section simply look like this:
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>

But be warned with that setting (or at least read the above blog post link to understand what you are doing)!

3. Routing

You will need to add a global.asax file to your web application if you haven't already got one. Right click > Add > Add New Item > Global Application Class:


Now in your global.asax add these usings:
using System.Web.Mvc;
using System.Web.Routing;

Now add these lines:
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    //ignore aspx pages (web forms take care of these)
    routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");

    routes.MapRoute(
        // Route name
        "Default",
        // URL with parameters
        "{controller}/{action}/{id}",
        // Parameter defaults
        new { controller = "home", action = "index", id = "" }
        );
}

protected void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes(RouteTable.Routes);
}

4. Add some standard folders to the solution

Add a folder named 'Controllers' to your web project.
Add a folder named 'Views' to your web project.
Add a folder named 'Shared' to that Views folder.
Add a web configuration file to the Views folder (web.config).
Open this web.config in the Views folder and ensure make its contents as follows:
<?xml version="1.0"?>

<configuration>
  <configSections>
    <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
      <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
    </sectionGroup>
  </configSections>

  <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

  <appSettings>
    <add key="webpages:Enabled" value="false" />
  </appSettings>

  <system.web>
    <httpHandlers>
      <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
    </httpHandlers>

    <!--
        Enabling request validation in view pages would cause validation to occur
        after the input has already been processed by the controller. By default
        MVC performs request validation before a controller processes the input.
        To change this behavior apply the ValidateInputAttribute to a
        controller or action.
    -->
    <pages
        validateRequest="false"
        pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
        pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
        userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      <controls>
        <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
      </controls>
    </pages>
  </system.web>

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />

    <handlers>
      <remove name="BlockViewHandler"/>
      <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
    </handlers>
  </system.webServer>
</configuration>


5. Get Visual Studio 2010 to recognise your MVC skills

If you right click the Controllers folder and select Add, you'll notice there is no Controller class to add. You need to make a change to your web project file.

Using Windows Explorer find the web project file (Web.csproj in my case) and open it in a text editor. You will need to add "{E53F8FEA-EAE0-44A6-8774-FFD645390401};" to the ProjectTypeGuids element.

E.g. mine now looks like this:
<ProjectTypeGuids>{E53F8FEA-EAE0-44A6-8774-FFD645390401};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

6. Optional: Add a Controller (for a laugh)

Back to Visual Studio 2010 and right click the Controllers folder > Add > Controller... call it HomeController.

using System.Web.Mvc;

namespace Web.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "This is MVC";
            return View();
        }
    }
}

7. Optional: Add a View (just to show off)

Right click where it says return View(); and select Add View... then click Add.

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Index</title>
</head>
<body>
    <div>
        <%: ViewBag.Message %>
    </div>
</body>
</html>

8. Optional: Try it (just to self-indulge your skills)

Now run you site and go to http://{localhost}/home

I hope this saved you the hours it took me! Please comment if you have any feedback. It would be much appreciated

More reading here if you would like to use an existing WebForms MasterPage in your new MVC project.

Follow britishdev on Twitter