Category Archives: user experience

Crop image before upload

Let’s talk about cropping and resizing today. This quite common task on any web app. And as a rule there are 2 issues with image size.

original image is too big and it doesn’t make sense to return original to client. We need to make it smaller.
image size is not proportional i.e. width is much bigger than height or vice versa.
too_wide

Dynamically resize images.

Let’s think how we can solve issue #1. The easiest solution is resize image on upload. And then return to client resized images. This will works fine until you need to get same image with bigger or smaller size. We can use browser resizing but you know it sucks. This lead us to decision that allow us to return image with different sizes on fly.

For example we can use such URLs:
/uploads/users/1.png
/uploads/cache/users/1_200_x_200.png
/uploads/cache/users/1_500_x_500.png

You got the idea, right? We will check do we have image with required size in cache if yes- just return it, otherwise create and return. We keep the originals and can easily clean cache folder.

With php we can use https://github.com/avalanche123/Imagine nice library that allows you to make various actions with your images.

Cropping images.

We can use suggested logic but still we have an issue with proportions. For example I’m uploading my another selfie. And I’m not the best photographer. What do we get using logic described below.

bad_cropping

Resizing libraries don’t know how to crop your image and will use center and provided proportions to crop your image. As a result you’ll get image like this. But where is my mouth? 🙂

In such examples when user uploads his avatar or image is quite important and you need to get best of it you can combine first approach with manual cropping.

There’s a dozen of cropping plugins http://www.jqueryrain.com/demo/jquery-crop-image-plugin/ but they work in pretty similar way. They allow use to crop image and then will provide width, height and x,y  points.  Personally I used this one http://fengyuanchen.github.io/cropper/

Finally I provide a piece of javascript code that I used. Ok so you click “Update avatar” we ask user to provide image. After that show popup with cropping dialog. Ask user to crop image and send received data – image, and cropping params to server (x, y, width, height). On server crop the image – save original and then using original cropped image return copies of different sizes.

<!-- our trigger -->
<div class="fileinput-wrapper">
    <button class="block-btn btn text-uppercase btn-primary browse-files fileinput-button">
        Change Icon
        <input type="hidden" name="Profile[avatar_url]" value="">
        <input type="file" id="profile-avatar_url" class="change_profile_avatar" name="Profile[avatar_url]" data-id="70">
        <input type="hidden" id="profile-crop_params" name="Profile[crop_params]" value="">
    </button>
</div>

<!-- cropper dialog -->
<div class="modal fade" style="" id="crop-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal">
                    <span aria-hidden="true">&times;</span>
                    <span class="sr-only">Close</span>
                </button>
                <h4 class="modal-title" id="myModalLabel">Crop Image</h4>
            </div>
            <div class="modal-body">
                <div class="eg-main">
                    <div class="eg-wrapper" id="cropper_original_wrapper">

                    </div>
                    <div class="eg-preview clearfix">
                        <h3>Large Preview</h3>
                        <div class="preview preview-md"></div>
                    </div>
                    <div class="eg-preview clearfix">
                        <h3>Small Preview</h3>
                        <div class="preview preview-xs"></div>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-primary" id="save_profile_avatar">CROP & FINISH</button>
                <button type="button" class="btn btn-inverse" data-dismiss="modal">Cancel</button>
            </div>
        </div>
    </div>
</div>
 function initAvatarUpdate() {
        $('.change_profile_avatar').fileupload({
            dataType: 'json',
            add: function (e, data) {
                var $element = $(this);
                data.url = "/profile/update?id=" + $element.data("id");
                $("#cropper_original_wrapper").empty();
                if (data.files && data.files[0]) {
                    var reader = new FileReader();
                    reader.onload = function(e) {
                        var newImg = $("<img>", {"class": "cropper", "src": e.target.result});
                        $("#cropper_original_wrapper").append(newImg);
                        initCropper($element);
                    }
                    reader.readAsDataURL(data.files[0]);
                    $("#crop-modal").modal("show");

                    $("#save_profile_avatar").off('click').on('click', function () {
                        data.submit();
                        return false;
                    });
                }
            },
            done: function (e, data) {
                var newSrc = data._response.result.model.fileUrl;
                $(this).closest('.profile-avatar').find("img").attr("src", newSrc);
                $("#crop-modal").modal("hide");
                $("#profile_avatar").trigger("profile_changed", [ $(this).data("id"), newSrc])
            }
        });

        function initCropper($element) {
            var $cropper = $(".cropper")
            $cropper.cropper({
                aspectRatio: 1,
                data:
                {
                    x: 1,
                    y: 1,
                    width: 500,
                    height: 500
                },
                preview: ".preview",
                // autoCrop: false,
                // dragCrop: false,
                // modal: false,
                // moveable: false,
                // resizeable: false,
                // maxWidth: 480,
                // maxHeight: 270,
                // minWidth: 160,
                // minHeight: 90,
                done: function(data) {
                    $element.next().val(JSON.stringify({
                        "x": data.x,
                        "y": data.y,
                        "width": data.width,
                        "height":data.height
                    }));
                }
            });
        }
    }

Finally we got

good_cropping