sviluppiamo comunicazione con weLaika Advertising

Javascript client resized image upload in Rails

Tag: rails, ruby, javascript, canvas, html5
Filippo Gangi Dino -

Recently I had to refactor an existing multipart form which allowed users to submit an image and a small description. The newer version must be submitted in AJAX and the image must be resized on the client side to save user bandwidth, instead of doing it on the server side like the older version.

This post talks about this refactoring.

Before

I have this “simple” form in app/views/review/_form.html.slim:

1
2
3
4
  = simple_form_for review, html: { multipart: true } do |f|
    = f.input :image
    = f.input :content
    = f.button :submit

And this is the Review model (I’m using paperclip gem to manage uploads):

1
2
3
4
  class Review < ActiveRecord::Base
    has_attached_file :image
    validates :content, presence: true
  end

After

First things first. Let’s add remote: true to our form, because I want to submit it asynchronously with ajax.

1
2
3
4
  = simple_form_for review, remote: true, html: { multipart: true } do |f|
    = f.input :image
    = f.input :content
    = f.button :submit

Add a .js view (in app/views/review/_create.js.erb or _update.js.erb) called from controller responding to js:

1
2
var $container = $("#<%= dom_id(@review); %>")
$container.html("<%= escape_javascript(render 'reviews/form', review: @review) %>");

This solution is fast and simple but won’t work with multipart forms.

Now we have to rewrite our form using a little bit of javascript to fix this problem. The goal is to send a resized image via form as Base64 string using HTML canvas.

Luckily, HTML5’ Canvas is present in the major browsers today!

This is an useful ImageResize script that accepts some settings, returns a Base64 string and have a callback function. Source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
$.fn.ImageResize = function (options) {
    var defaults = {
        maxWidth: Number.MAX_VALUE,
        maxHeigth: Number.MAX_VALUE,
        onImageResized: null
    }
    var settings = $.extend({}, defaults, options);
    var selector = $(this);

    selector.each(function (index) {
        var control = selector.get(index);
        if ($(control).prop("tagName").toLowerCase() == "input" && $(control).attr("type").toLowerCase() == "file") {
            $(control).attr("accept", "image/*");

            control.addEventListener('change', handleFileSelect, false);
        }
        else {
            cosole.log("Invalid file input field");
        }
    });

    function handleFileSelect(event) {
        //Check File API support
        if (window.File && window.FileList && window.FileReader) {
            var count = 0;
            var files = event.target.files;

            for (var i = 0; i < files.length; i++) {
                var file = files[i];
                //Only pics
                if (!file.type.match('image')) continue;

                var picReader = new FileReader();
                picReader.addEventListener("load", function (event) {
                    var picFile = event.target;
                    var imageData = picFile.result;
                    var img = new Image();
                    img.src = imageData;
                    img.onload = function () {
                        if (img.width > settings.maxWidth || img.height > settings.maxHeigth) {
                            var width = settings.maxWidth;
                            var height = settings.maxHeigth;

                            if (img.width > settings.maxWidth) {
                                width = settings.maxWidth;
                                var ration = settings.maxWidth / img.width;
                                height = Math.round(img.height * ration);
                            }

                            if (height > settings.maxHeigth) {
                                height = settings.maxHeigth;
                                var ration = settings.maxHeigth / img.height;
                                width = Math.round(img.width * ration);
                            }

                            var canvas = $("<canvas/>").get(0);
                            canvas.width = width;
                            canvas.height = height;
                            var context = canvas.getContext('2d');
                            context.drawImage(img, 0, 0, width, height);
                            imageData = canvas.toDataURL();

                            if (settings.onImageResized != null && typeof (settings.onImageResized) == "function") {
                                settings.onImageResized(imageData);
                            }
                        }

                    }
                    img.onerror = function () {

                    }
                });
                //Read the image
                picReader.readAsDataURL(file);
            }
        } else {
            console.log("Your browser does not support File API");
        }
    }


}

Now the _form refactoring:

1
2
3
4
5
6
  input type="file" data-uploader=true

  = simple_form_for review, remote: true, html: { multipart: true } do |f|
    = f.input :image, as: :hidden # simple_form automatically sets the id to 'review_image'
    = f.input :content
    = f.button :submit

In this example I’ve hidden the image input and put an input file out of form with a data attribute to indentify it.

And now we can connect ImageResize.js and the Rails with:

1
2
3
4
5
6
  $("[data-uploader]").ImageResize({
    maxWidth: 800,
    onImageResized: function (imageData) {
      $("#review_image").val(imageData);
    }
   });

In this Javascript snippet we select [data-uploader] input file and we call the ImageResize function that takes as input a max width value and a callback. The callback is called after the original image has been resized, so we fill in our hidden input with the Base64 string of the image.

Paperclip works out of the box with Base64 strings.

That’s all!

welaika