Lazy Loading Images? Don’t Rely On JavaScript!

So much of the internet is now made up of pages containing loads of images; just visit your favourite shopping site and scroll through a product listing page for an example of this.

As you can probably imagine, bringing in all of these images when the page loads can add unnecessary bloat, causing the user to download lots of data they may not see. It can also make the page slow to interact with, due to the page layout constantly changing as new images load in, causing the browser to reprocess the page.

One popular method to deal with this is to “Lazy Load” the images; that is, to only load the images just before the user will need to see them.

If this technique is applied to the “above the fold” content – i.e., the first average viewport-sized section of the page – then the user can get a significantly faster first view experience.

So everyone should always do this, right?

Before we get on to that, let’s look at how this is usually achieved. It’s so easy to find a suitable jQuery plugin or angularjs module that a simple install command later and you’re almost done; just add a new attribute to image tags or JavaScript method to process the images you want to delay loading for.

So surely this is a no-brainer?

Continue reading

#velocityconf notes part 2

Yoav Weiss did a great session on responsive images and techniques; this scared me a little as he’s covering a lot of content that could contradict the talk I’m doing later in the week!

image

He’s mentioned some great pertinent points that I’ll reference back to.

Other things he talked about:

LQIP, which sounds like a reintroduction of the ancient lowsrc attribute.

He’s really having a pop at Mobify and their image loading hack script, which he calls Bat-Shit-Loco-Insane ™. Nice. Explaining how it works feels like the whole room has just facepalmed.

He also talks about compressive images. Luckily nothing that completely ruins my stuff.. whew. I’ll just have to update my notes a little bit.

Each session I’m going to gives me ideas on changes I should make to our Friday session!

Azure Image Proxy

The previous couple of articles configured an image resizing Azure Web Role, plopped those resized images on an Azure Service Bus, picked them up with a Worker Role and saved them into Blob Storage.

This one will click in the last missing piece; the proxy at the front to initially attempt to get the pregenerated image from blob storage and failover to requesting a dynamically resized image.

New Web Role

Add a new web role to your cloud project – I’ve called mine “ImagesProxy” – and make it an empty MVC4 Web API project. This is the easiest of the projects, so you can just crack right on and create a new controller – I called mine “Image” (not the best name, but it’ll do).

Retrieve

This whole project will consist of one controller with one action – Retrieve – which does three things;

  1. attempt to retrieve the resized image directly from blob storage
  2. if that fails, go and have it dynamically resized
  3. if that fails, send a 404 image and the correct http header

Your main method/action should look something like this:

[HttpGet]
public HttpResponseMessage Retrieve(int height, int width, string source)
{
    try
    {
        var resizedFilename = BuildResizedFilenameFromParams(height, width, source);
        var imageBytes = GetFromCdn("resized", resizedFilename);
        return BuildImageResponse(imageBytes, "CDN", false);
    }
    catch (StorageException)
    {
        try
        {
            var imageBytes = RequestResizedImage(height, width, source);
            return BuildImageResponse(imageBytes, "Resizer", false);
        }
        catch (WebException)
        {
            var imageBytes = GetFromCdn("origin", "404.jpg");
            return BuildImageResponse(imageBytes, "CDN-Error", true);
        }
    }
}

Feel free to alt-enter and clean up the red squiggles by creating stubs and referencing the necessary assemblies.

You should be able to see the three sections mentioned above within the nested try-catch blocks.

  1. attempt to retrieve the resized image directly from blob storage

    var resizedFilename = BuildResizedFilenameFromParams(height, width, source);
    var imageBytes = GetFromCdn("resized", resizedFilename);
    return BuildImageResponse(imageBytes, "CDN", false);
    
  2. if that fails, go and have it dynamically resized

    var imageBytes = RequestResizedImage(height, width, source);
    return BuildImageResponse(imageBytes, "Resizer", false)
    
  3. if that fails, send a 404 image and the correct http header

    var imageBytes = GetFromCdn("origin", "404.jpg");
    return BuildImageResponse(imageBytes, "CDN-Error", true);
    

So let’s build up those stubs.

BuildResizedFilenameFromParams

Just a little duplication of code to get the common name of the resized image (yes, yes, this logic should have been abstracted out into a common library for all projects to reference, I know, I know..)

private static string BuildResizedFilenameFromParams(int height, int width, string source)
{
    return string.Format("{0}_{1}-{2}", height, width, source.Replace("/", string.Empty));
}

GetFromCDN

We’ve seen this one before too; just connecting into blob storage (within these projects blob storage is synonymous with CDN) to pull out the pregenerated/pre-reseized image:

private static byte[] GetFromCdn(string path, string filename)
{
    var connectionString = CloudConfigurationManager.GetSetting("Microsoft.Storage.ConnectionString");
    var account = CloudStorageAccount.Parse(connectionString);
    var cloudBlobClient = account.CreateCloudBlobClient();
    var cloudBlobContainer = cloudBlobClient.GetContainerReference(path);
    var blob = cloudBlobContainer.GetBlockBlobReference(filename);

    var m = new MemoryStream();
    blob.DownloadToStream(m);

    return m.ToArray();
}

BuildImageResponse

Yes, yes, I know – more duplication.. almost. The method to create an HTTP response message from before, but this time with extras params to set a header saying where the image came from, and allow to set the HTTP status code correctly. We’re just taking the image bytes and putting them in the message content, whilst setting the headers and status code appropriately.

private static HttpResponseMessage BuildImageResponse(byte[] imageBytes, string whereFrom, bool error)
{
    var httpResponseMessage = new HttpResponseMessage { Content = new ByteArrayContent(imageBytes) };
    httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
    httpResponseMessage.Content.Headers.Add("WhereFrom", whereFrom);
    httpResponseMessage.StatusCode = error ? HttpStatusCode.NotFound : HttpStatusCode.OK;

    return httpResponseMessage;
}

RequestResizedImage

Build up a request to our pre-existing image resizing service via a cloud config setting and the necessary dimensions and filename, and return the response:

private static byte[] RequestResizedImage(int height, int width, string source)
{
    byte[] imageBytes;
    using (var wc = new WebClient())
    {
        imageBytes = wc.DownloadData(
            string.Format("{0}?height={1}&width={2}&source={3}",
                          CloudConfigurationManager.GetSetting("Resizer_Endpoint"), 
                          height, width, source));
    }
    return imageBytes;
}

And that’s all there is to it! A couple of other changes to make within your project in order to allow pretty URLs:

  1. Create the necessary route:

    config.Routes.MapHttpRoute(
        name: "Retrieve",
        routeTemplate: "{height}/{width}/{source}",
        defaults: new { controller = "Image", action = "Retrieve" }
    );
    
  2. Be a moron:

      <system.webServer>
        <modules runAllManagedModulesForAllRequests="true" />
      </system.webServer>
    

That last one is dangerous; I’m using it here as a quick hack to ensure that URLs ending with known file extensions (e.g., /600/200/image1.jpg) are still processed by the MVC app instead of assuming they’re static files on the filesystem. However, this setting is not advised since it means that every request will be picked up by your .Net app; don’t use it in regular web apps which also host images, js, css, etc!

If you don’t use this setting then you’ll go crazy trying to debug your routes, wondering why nothing is being hit even after you install Glimpse..

In action

First request

Hit your proxy with a request for an image that exists within your blob storage “origin” folder; this will raise a storage exception when attempting to retrieve from blob storage and drop into the resizer code chunk e.g.:
image proxy, calling the resizer
Notice the new HTTP header that tells us the request was fulfilled via the Resizer service, and we got an HTTP 200 status code. The resizer web role will have also added a message to the service bus awaiting pick up.

Second request

By the time you refresh that page (if you’re not too trigger happy) the uploader worker role should have picked up the message from the service bus and saved the image data into blob storage, such that subsequent requests should end up with a response similar to:
image proxy, getting it from cdn
Notice the HTTP header tells us the request was fulfilled straight from blob storage (CDN), and the request was successful (HTTP 200 response code).

Failed request

If we request an image that doesn’t exist within the “origin” folder, then execution drops into the final code chunk where we return a default image and set an error status code:
image proxy, failed request

So..

This is the last bit of the original plan:

Azure Image Resizing - Conceptual Architecture

Please grab the source from github, add in your own settings to the cloud config files, and have a go. It’s pretty cool being able to just upload one image and have other dimension images autogenerated upon demand!

Automated Image Resizing and Hosting in Azure #2

Saving the resized images

Last article concluded with us creating a web role that will retrieve an image from blob storage, resize it, raise an event, and stream the result back.

This article is about the worker role to handle those raised events.

Simply enough, all we’ll be doing is creating a worker role, hooking into the same azure service bus queue, picking up each message, pulling out the relevant data within, and uploading that to blob storage.

Overall Process

A reminder of the overall process:
Azure Image Resizing Conceptual Architecture

The Worker Role

The section of that which the worker role is responsible for is as below:

Azure-Image-Resizing-Uploader-Achitecture

Add a new worker role to the Cloud project within the solution from last time (or a new one if you like). This one consists of four little methods; Run, OnStart, and OnEnd, where Run will call an UploadBlob method.

Run

This method will pick up any messages appearing on the queue, deserialize the contents of the message to a known structure, and pass them to an uploading method.

Kick off by pasting over the Run method with this one, including the definitions at the top – set the QueueName to the same queue you configured for the resize notification from the last post:

    const string QueueName = "azureimages";
    QueueClient _client;
    readonly ManualResetEvent _completedEvent = new ManualResetEvent(false);

    public override void Run()
    {
        _client.OnMessage(receivedMessage =>
        {
            try
            {
                // Process the message
                var receivedImage = receivedMessage.GetBody<ImageData>();
                UploadBlob("resized", receivedImage);
            }
            catch (Exception e)
            {
                Trace.WriteLine("Exception:" + e.Message);
            }
        }, new OnMessageOptions
        {
            AutoComplete = true,
            MaxConcurrentCalls = 1
        });

        _completedEvent.WaitOne();
    }

Yes, I’m not doing anything with exceptions; that’s an exercise for the reader.. ahem… (Me? Lazy? Never..happypathhappypathhappypath)

Naturally you’ll get a few squiggles and highlights to fix; Install-Package Microsoft.ServiceBus.NamespaceManager will help with some, as will creating the stub UploadBlob.

Now, to tidy up the reference to ImageData you could do a few things:

  1. Copy the ImageData.cs over from the previous project into this one
  2. Create a reference to the previous project and add in a using to this file
  3. Extract ImageData from the previous project into a common referenced project for them both to share.

I can live with my own conscience, so am just whacking in a reference to the previous project. Don’t hate me.

OnStart and OnStop

   public override bool OnStart()
    {
        // Set the maximum number of concurrent connections 
        ServicePointManager.DefaultConnectionLimit = 2;

        // Create the queue if it does not exist already
        var connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
        var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
        if (!namespaceManager.QueueExists(QueueName))
        {
            namespaceManager.CreateQueue(QueueName);
        }

        // Initialize the connection to Service Bus Queue
        _client = QueueClient.CreateFromConnectionString(connectionString, QueueName);
        return base.OnStart();
    }

    public override void OnStop()
    {
        // Close the connection to Service Bus Queue
        _client.Close();
        _completedEvent.Set();
        base.OnStop();
    }

OnStart gets a connection to the service bus, creates the named queue if necessary, and creates a queue client referencing that queue within that service bus.

OnStop kills everything off.

So, off you pop and add the requisite service connection string details; right click the role within the cloud project, properties:

Cloud-Service-Role-Properties

Click settings, add setting “Microsoft.ServiceBus.ConnectionString” with the value you used previously.

Role-Settings

Lastly:

UploadBlob

    public void UploadBlob(string path, ImageData image)
    {
        var connectionString = CloudConfigurationManager.GetSetting("Microsoft.Storage.ConnectionString");
        var account = CloudStorageAccount.Parse(connectionString);
        var cloudBlobClient = account.CreateCloudBlobClient();
        var cloudBlobContainer = cloudBlobClient.GetContainerReference(path);

        cloudBlobContainer.CreateIfNotExists();

        var blockref = image.FormattedName ?? Guid.NewGuid().ToString();
        var blob = cloudBlobContainer.GetBlockBlobReference(blockref);

        if (!blob.Exists())
            blob.UploadFromStream(new MemoryStream(image.Data));
    }

Pretty self explanatory, isn’t it? Get a reference to an area of blob storage within a container associated with an account, and stream some data to it if it doesn’t already exist (you might actually want to overwrite it so could remove that check). Bosch. Done. Handsome.

Notice we’re using the FormattedName property on ImageData to get a blob name which includes the requested dimensions; this will be used in the next article where we create the image proxy.

This means that for a request like:

http://127.0.0.1/api/Image/Resize?height=600&width=400&source=image1.jpg

The formatted name will be set to:

600_400-image1.jpg

You shouldn’t get any compile errors here but you’ll need to add in the setting for your storage account (“Microsoft.Storage.ConnectionString”).

Kick it off

To run that you’ll need VS to be running as admin (right click VS, run as admin):

run-as-admin

After you’ve got it running, fire off a request within the resizing web api (if it’s not the same solution/cloud service) for something like:

http://127.0.0.1/api/Image/Resize?height=600&width=400&source=image1.jpg

Resulting in:
Resized-Image

Then open up your Azure storage explorer to see something similar to the below within the “resized” blob container:

Resized-Blob

What happened?

  1. The ImageController on your Resizer Web API web role did the hard work and popped a message on an Azure Service Bus queue containing the image data
  2. The new Uploader worker role is subscribed to the same Azure Service Bus queue
    1. it picks up the message
    2. pulls out the image data
    3. generates an image name based on the image dimensions and origin
    4. streams the image data into a blob block with the generated name

Cool, huh?

The code for this series is up on GitHub

Next up

One more web role to act as a proxy for checking blob storage first before firing off the resize request. Another easy one. Azure is easy. Everyone should be doing this. You should wait and see what else I’ll write about Azure.. it’s awesome.. and easy..!

Hackathon #1: Composite Image

My company recently had a Hackathon day, where the IT dept. was split into teams and had one day to develop something amazing to present to a panel of judges.

Over the next couple of posts I’d like to just show some things that my team hacked together, as I found them quite interesting, if not amazingly applicable to a real-world app!

First up is a very quick hack I created to generate a composite image on the fly given several other images. Apologies that this is in vb.net and not C#, but I was forced to work within our legacy environment! Assumption: all images to composite are of the same dimensions

Here is a function to take an arraylist of image URLs, merge them, and return the byte array:

Public Function BuildMergedImage(ByVal imgsToMerge As ArrayList) As Byte()

    'build image array, loading each image from a stream into the image array
    Dim imagesToMerge As ArrayList = New ArrayList
    Dim webClient As System.Net.WebClient = New System.Net.WebClient()
    Dim imgStream As Stream

    For Each imgToMerge As String In imgsToMerge
        imgStream = webClient.OpenRead(imgToMerge.ToString())
        imagesToMerge.Add(System.Drawing.Image.FromStream(imgStream))
    Next

    'get composite dimensions
    Dim height As Integer = CType(imagesToMerge(0), System.Drawing.Image).Height
    Dim width As Integer = (CType(imagesToMerge(0), System.Drawing.Image).Width * imagesToMerge.Count())

    'init composite image
    Dim mergedImage As Bitmap = New Bitmap(width, height)
    Dim g As Graphics = Graphics.FromImage(mergedImage)

    'create composite image
    For i As Integer = 0 To (imagesToMerge.Count() - 1)
        g.DrawImage(imagesToMerge(i), (CType(imagesToMerge(i), System.Drawing.Image).Width * i), 0)
    Next

    'save merged image to mem stream
    Dim imgMemStream As New System.IO.MemoryStream
    mergedImage.Save(imgMemStream, ImageFormat.Jpeg)

    'convert stream to byte array
    Dim bytes(imgMemStream.Length() - 1) As Byte
    imgMemStream.Position = 0
    imgMemStream.Read(bytes, 0, bytes.Length)
    imgMemStream.Close()

    'return byte array
    Return bytes
End Function

Then to use this method, build up a string arraylist of image urls, pass it to a new function, and output the returned byte array to the response buffer (I used mine in the Page Load, hence the obligatory postback check):

If Not IsPostBack Then
    Response.Buffer = True
    Response.ContentType = "image/jpeg" 'match to the imageformat in the composite image function
    Response.BinaryWrite(BuildMergedImage(taggedImg))
End If

You’ll need these at the top of your file to get it working though. I realise the “option strict off” is a horrid hack, but it was for a hackathon..

Option Strict Off

Imports System.IO
Imports System.Drawing.Imaging

Bit random, I know, but I found it quite interesting. Have a go with something similar to (but hopefully cleaner than) this:

Dim imageArrayList As ArrayList = new ArrayList

For Each imageUrl As String In Request.QueryString("i").Split(CChar("-"))
    If Not IsNothing(imageUrl) AndAlso Not String.IsNullOrEmpty(imageUrl) Then
        imageArrayList.Add(imageUrl)
    End If
Next

If Not IsPostBack Then
    Response.Buffer = True
    Response.ContentType = "image/jpeg" 'match to the imageformat in the composite image function
    Response.BinaryWrite(BuildMergedImage(imageArrayList))
End If

Example usage (although in this example it’s not pulling the correct dimensions through, which is odd):

http://localhost/combinedimages.aspx?i=http://www.docdatastorage.co.uk/Live/flyfiftythree/ProductImages/savage_white_1_medium.jpg-http://www.docdatastorage.co.uk/Live/flyfiftythree/ProductImages/checkit_green_1_medium.jpg-http://www.docdatastorage.co.uk/Live/flyfiftythree/ProductImages/telemaniacs_navy_1_medium.jpg