Gal Ratner
Gal Ratner is a Techie who lives and works in Los Angeles CA and Austin TX. Follow galratner on Twitter Google
Using HTML 5 and the Web API for AJAX file uploads with image preview and a progress bar

AJAX file uploads used to be a client / server process in HTML 4. They required a round trip to a server side module, usually using an IFRAME and constant querying of the number of bytes uploaded, then reloading a progress bar inside the IFRAME with the current progress value.
Luckily HTML 5 has a level 2 XMLHttpRequest object that natively support file uploads. This makes it a breeze to give your users a great experience when uploading files to your application.

In this article we are going to use it to show an end to end process of previewing an image and uploading it to the server while showing a progress bar.
Technologies we will be using include:

  • HTML 5 File API
  • HTML 5 XMLHttpRequest Level 2
  • jQuery
  • jQuery UI Progressbar
  • MVC ApiController
  • Web API’s MultipartFormDataStreamProvider


Let’s start by building our view. At the very least we will need an input file, a preview div and a submit button. In this example I also use a few more parameters that serve as hints for image processing: a caption text, font size and type and color.

 

@{
    ViewBag.Title = "Home Page";
}
@section featured {
    <form name="form1" method="post" enctype="multipart/form-data" action="api/uploads">
    <section class="featured">
        <div class="content-wrapper">
            <hgroup class="title">
                <h1>@ViewBag.Title.</h1>
                <h2>@ViewBag.Message</h2>
            </hgroup>
            <p>
                Upload<input id="imageFile" type="file" />
            </p>
        </div>
    </section>
}
<h3>Write something:</h3>
<ol class="round">
     <div id="imageParams" style="width:100%;height:50%;">
        <div id="imageText" style="width:50%;float:left">
            <textarea name="styled" id="styled" onfocus="this.value=''; setbg('#e5fff3');" onblur="setbg('white')">Enter Text here...</textarea>
        </div>
        <div id="textparams" style="width:50%;float:left">
            Pick a text color: <input id="textcolor" name="textcolor" class="color" value="ffffff" /><br />
            <div id="fontName">Pick a Font:
                            <input type="radio" id="Tahoma" name="fontNameRadio" value="Tahoma" onchange="changeFont(this);" checked /><label for="Tahoma" style="font-family:Tahoma">Tahoma</label>
                            <input type="radio" id="Arial" name="fontNameRadio" value="Arial" onchange="changeFont(this);" /><label for="Arial" style="font-family:Arial">Arial</label>
                            <input type="radio" id="Verdana" name="fontNameRadio" value="Verdana" onchange="changeFont(this);" /><label for="Verdana" style="font-family:Verdana">Verdana</label>
                            <input type="radio" id="Helvetica" name="fontNameRadio" value="Helvetica" onchange="changeFont(this);" /><label for="Helvetica" style="font-familyHelvetica">Helvetica</label>
                        </div><br />
            Pick a Font Size: <input id="fontSize" name="fontSize" value="12" />px<br /><br />
             </div>
        </div>
    <div id ="theOuptout" style="height:50%width:100%float:left">
        <div id="theBackground" style="background-repeat:no-repeat;background-position:left topwidth:700px;height:700px;border:2px solidpositionrelativefloat:leftbackground-sizecontain">
            <div id="caption">Enter Text here...</div>
        </div>
    </div>
</ol>
        <input id="actionButton" type="submit" onclick="return sendForm(this.form);" value="Upload">
        </form>
<div id="progressbar"></div>
@section scripts {
    @Scripts.Render("~/bundles/jqueryui")
    @Scripts.Render("~/bundles/jscolor")
    @Styles.Render("~/Content/themes/base/css")
    @Scripts.Render("~/bundles/index")   
}

 
Once we have our UI in place we need to add the view script logic and validation. In order to use minification, I have bundled the script:

 

bundles.Add(new ScriptBundle("~/bundles/index").Include(
                      "~/Scripts/Views/Index.js"));



And added it to the view:

 

@Scripts.Render("~/bundles/index")


Let’s take a look at Index.js

 

$(function () {
    var container = $('#theBackground')
    $("#caption").draggable({ containment: container }).resizable({ containment: container });
 
    $("#fontName").buttonset();
 
    $("#styled").keypress(function () {
        $("#caption").html($("#styled").val());
    });
 
    $("#textcolor").change(function () {
        $("#caption").css("color""#" + $("#textcolor").val());
    });
 
    $("#fontSize").spinner({
        spin: function (event, ui) {
            $("#caption").css("font-size", ui.value);
        },
        max: 50,
        min: 0,
    });
    // Show an image preview
    document.getElementById('imageFile').addEventListener('change', handleFileSelect, false);
    
});
 
function changeFont(fontName) {
    $("#caption").css("font-family", fontName.value);
}
 
function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object
 
    // Loop through the FileList and render image files as thumbnails.
    for (var i = 0, f; f = files[i]; i++) {
 
        // Only process image files.
        if (!f.type.match('image.*')) {
            continue;
        }
 
        var reader = new FileReader();
 
        // Closure to capture the file information.
        reader.onload = (function (theFile) {
            return function (e) {
                // Render thumbnail.
                $("#theBackground").css("background-image""url('" + e.target.result + "')");
            };
        })(f);
 
        // Read in the image file as a data URL.
        reader.readAsDataURL(f);
    }
}
 
function sendForm(form) {
    if ($("#imageFile").val() == '') {
        alert('Please select an image');
        return false;
    }
    $("#progressbar").progressbar();
    var formData = new FormData(form);
    formData.append("offsetTop", $("#caption").position().top);
    formData.append("offsetLeft", $("#caption").position().left);
    formData.append("imageFile", document.getElementById("imageFile").files[0]);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', form.action, true);
    xhr.upload.onprogress = function (e) {
        if (e.lengthComputable) {
            var percentComplete = (e.loaded / e.total) * 100;
            $("#progressbar").progressbar({ value: percentComplete });
        }
    };
    xhr.onload = function () {
        if (this.status == 200) {
            $("#progressbar").progressbar("destroy");
            location.href = '/Home/ViewImage/' + encodeURIComponent(this.response).replace(/[!'()]/g, escape).replace(/\*/g"%2A");
        };
    };
    xhr.send(formData);
 
    return false// Prevent page from submitting.
}
 
 
 

You can already see that when the DOM is loaded, we use jQuery and jQuery UI to make our image caption div resizable and resizable and, to make a buttonset out of our radio buttons and to initialize the progress bar.


Next we set up the preview. When a user selects an image, we use the File API to show the image to the user before we upload it.
Adding an event listener: document.getElementById('imageFile').addEventListener('change', handleFileSelect, false);

We create a FileReader:

 

var reader = new FileReader();


Use its onload event to set the result as the background of out preview div and then call its readAsDataURL to read the image.


When a user decides to submit the form and upload their image, we override the onSubmit event and use AJAX instead of the default submit action:

 

function sendForm(form) {
    if ($("#imageFile").val() == '') {
        alert('Please select an image');
        return false;
    }
    $("#progressbar").progressbar();
    var formData = new FormData(form);
    formData.append("offsetTop", $("#caption").position().top);
    formData.append("offsetLeft", $("#caption").position().left);
    formData.append("imageFile", document.getElementById("imageFile").files[0]);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', form.action, true);
    xhr.upload.onprogress = function (e) {
        if (e.lengthComputable) {
            var percentComplete = (e.loaded / e.total) * 100;
            $("#progressbar").progressbar({ value: percentComplete });
        }
    };
    xhr.onload = function () {
        if (this.status == 200) {
            $("#progressbar").progressbar("destroy");
            location.href = '/Home/ViewImage/' + encodeURIComponent(this.response).replace(/[!'()]/g, escape).replace(/\*/g"%2A");
        };
    };
    xhr.send(formData);
 
    return false// Prevent page from submitting.
}



Values in the submitted forms include all regular form fields with the addition of our own custom values and the file selected by the user. The onprogress event is used to track and display the overall upload progress.


Now that we have all of the client code in place, we can move over to the server and use some pretty cool Web API features we are going to use an ApiController:

 

public class UploadsController : ApiController
    {
        public async Task<HttpResponseMessage> PostFormData()
        {
            // Check if the request contains multipart/form-data.
            if (!Request.Content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }
            string root = HttpContext.Current.Server.MapPath("~/Images/Uploads/");
            var provider = new MultipartFormDataStreamProvider(root);
 
            try
            {
                await Request.Content.ReadAsMultipartAsync(provider);
                foreach (MultipartFileData file in provider.FileData)
                {
                    //Add the original extention to the file
                    string newFileName = String.Format("{0}{1}", file.LocalFileName, Path.GetExtension(file.Headers.ContentDisposition.FileName.Replace("\"""")));
                    File.Move(file.LocalFileName, newFileName);
                    // Do the image processing here and return a URL to view the image
                    ProcessImage(newFileName,
                        provider.FormData["styled"],
                        int.Parse(provider.FormData["offsetTop"]),
                        int.Parse(provider.FormData["offsetLeft"]),
                        provider.FormData["textcolor"],
                        provider.FormData["fontNameRadio"],
                        int.Parse(provider.FormData["fontSize"]));
                    return new HttpResponseMessage()
                    {
                        Content = new StringContent(Path.GetFileName(newFileName).Replace(".""dot")),
                        StatusCode = HttpStatusCode.OK
                    };
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Error processing image");
            }
            catch (System.Exception e)
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
            }
        }
 
        private void ProcessImage(string fileName, string caption, int captionTop, int captionLeft, string fontColor, string fontFamily, int fontSize)
        {
            if (string.IsNullOrWhiteSpace(caption))
                caption = "Something clever should have been here.";
            if (string.IsNullOrWhiteSpace(fontColor))
                fontColor = "#ffffff";
            Color color = System.Drawing.ColorTranslator.FromHtml(fontColor.StartsWith("#") ? fontColor : "#" + fontColor);
            PointF textPosition = new PointF((float)captionTop, (float)captionLeft);
            using (Bitmap bitmap = (Bitmap)Image.FromFile(fileName))
            {
                SolidBrush drawBrush = new SolidBrush(color);
                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    using (Font drawFont = new Font(fontFamily, (float)fontSize))
                    {
                        graphics.DrawString(caption, drawFont, drawBrush, textPosition);
                    }
                }
 
                bitmap.Save(fileName + "tmp");//save the image file
            }
            File.Replace(fileName + "tmp", fileName, null);
        }
    }


System.Net.Http includes a MultipartFormDataStreamProvider. This provider is used with HTML file uploads for writing file content to a FileStream. The stream provider looks at the <b>Content-Disposition</b> header field and determines an output Stream based on the presence of a <b>filename</b> parameter.

string root = HttpContext.Current.Server.MapPath("~/Images/Uploads/");
var provider = new MultipartFormDataStreamProvider(root);

At this point our provider is set to save the files in our Uploads directory

await Request.Content.ReadAsMultipartAsync(provider);


Reads the content of the submitted form and saves the file uploaded to disk asynchronously. As a default, the files are renamed to a random name and contain no extension.

We can now use the rest of the form’s parameters to help us process the image as we like. In our case, we simply write a caption into it. When we are done, we compose an HttpResponseMessage containing he location of the new image to be returned to the client.
The client can then use the message and redirect the user to view the uploaded image. This time, served from the server.

 xhr.onload = function () {
        if (this.status == 200) {
            $("#progressbar").progressbar("destroy");
            location.href = '/Home/ViewImage/' + encodeURIComponent(this.response).replace(/[!'()]/g, escape).replace(/\*/g"%2A");
        };
    };


Conclusion
As you can see, HTML 5 has made some significant improvements that are much needed in the web 2.0 world. The ability to access a local file and AJAX submit multipart forms are a much needed addition and are sure to make your code simpler and your users happier.

Shout it


Posted 22 Mar 2013 10:52 PM by Gal Ratner

Powered by Community Server (Non-Commercial Edition), by Telligent Systems