Advertisement
  1. Code
  2. Web Development

How to Create a jQuery Image Cropping Plugin from Scratch - Part II

Scroll to top
Read Time: 25 min

Web applications need to provide easy-to-use solutions for uploading and manipulating rich content. This process can create difficulties for some users who have minimal photo editing skills. Cropping is one of the most used photo manipulation techniques, and this step-by-step tutorial will cover the entire development process of an image cropping plug-in for the jQuery JavaScript library.


A Quick Recap

In the previous tutorial, we reviewed:

  • how to extend jQuery
  • how to make a plug-in more flexible by using custom options
  • how to create basic image cropping application

Today, we'll take things further and wrap up our plug-in: we'll define more custom option, add callbacks, make the selection draggable and resizable, build a preview pane and a size hint and write some server-side code to crop the image.


Step 1: Adding More Options

Open your jquery.imagecrop.js file located at /resources/js/imageCrop/and add the following code:

1
2
var defaultOptions = {
3
    allowMove : true,
4
    allowResize : true,
5
    allowSelect : true,
6
    aspectRatio : 0,
7
    displayPreview : false,
8
    displaySizeHint : false,
9
    minSelect : [0, 0],
10
    minSize : [0, 0],
11
    maxSize : [0, 0],
12
    outlineOpacity : 0.5,
13
    overlayOpacity : 0.5,
14
    previewBoundary : 90,
15
    previewFadeOnBlur : 1,
16
    previewFadeOnFocus : 0.35,
17
    selectionPosition : [0, 0],
18
    selectionWidth : 0,
19
    selectionHeight : 0,
20
21
    // Plug-in's event handlers

22
    onChange : function() {},
23
    onSelect : function() {}
24
};

We've added more options and two callbacks, onChange and onSelect. These two can be quite useful in retrieving the state of the plug-in.

The Options

Here is a quick rundown of the options we're adding:

  • aspectRatio - Specifies the aspect ratio of the selection (default value is 0).
  • displayPreview - Specifies whether the preview pane is visible or not (default value is false)
  • displaySizeHint - Specifies whether the size hint is visible or not (default value is false)
  • minSize - Specifies the minimum size of the selection (default value is [0, 0])
  • maxSize - Specifies the maximum size of the selection (default value is [0, 0])
  • previewBoundary - Specifies the size of the preview pane (default value is 90)
  • previewFadeOnBlur - Specifies the opacity of the preview pane on blur (default value is 1)
  • previewFadeOnFocus - Specifies the opacity of the preview pane on focus (default value is 0.35)
  • onCahnge - Returns the plug-in's state when the selection is changed
  • onSelect - Returns the plug-in's state when the selection is made

Step 2: Adding More Layers

In this step, we're going to add more layers. Let's begin with the size hint.

1
2
...
3
4
// Initialize a background layer of size hint and place it above the

5
// selection layer

6
var $sizeHintBackground = $('<div id="image-crop-size-hint-background" />')
7
        .css({
8
            opacity : 0.35,
9
            position : 'absolute'
10
        })
11
        .insertAfter($selection);
12
13
// Initialize a foreground layer of size hint and place it above the

14
// background layer

15
    var $sizeHintForeground = $('<span id="image-crop-size-hint-foreground" />')
16
            .css({
17
                position : 'absolute'
18
            })
19
            .insertAfter($sizeHintBackground);

We've added two separate layers because we don't want the foreground to be affected by the background opacity.

Now we'll add nine more layers: the resize handlers.

1
2
...
3
4
// Initialize a north/west resize handler and place it above the

5
// selection layer

6
var $nwResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-nw-resize-handler" />')
7
        .css({
8
            opacity : 0.5,
9
            position : 'absolute'
10
        })
11
        .insertAfter($selection);
12
13
// Initialize a north resize handler and place it above the selection

14
// layer

15
var $nResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-n-resize-handler" />')
16
        .css({
17
            opacity : 0.5,
18
            position : 'absolute'
19
        })
20
        .insertAfter($selection);
21
22
// Initialize a north/east resize handler and place it above the

23
// selection layer

24
var $neResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-ne-resize-handler" />')
25
        .css({
26
            opacity : 0.5,
27
            position : 'absolute'
28
        })
29
        .insertAfter($selection);
30
31
// Initialize an west resize handler and place it above the selection

32
// layer

33
var $wResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-w-resize-handler" />')
34
        .css({
35
            opacity : 0.5,
36
            position : 'absolute'
37
        })
38
        .insertAfter($selection);
39
40
// Initialize an east resize handler and place it above the selection

41
// layer

42
var $eResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-e-resize-handler" />')
43
        .css({
44
            opacity : 0.5,
45
            position : 'absolute'
46
        })
47
        .insertAfter($selection);
48
49
// Initialize a south/west resize handler and place it above the

50
// selection layer

51
var $swResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-sw-resize-handler" />')
52
        .css({
53
            opacity : 0.5,
54
            position : 'absolute'
55
        })
56
        .insertAfter($selection);
57
58
// Initialize a south resize handler and place it above the selection

59
// layer

60
var $sResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-s-resize-handler" />')
61
        .css({
62
            opacity : 0.5,
63
            position : 'absolute'
64
        })
65
        .insertAfter($selection);
66
67
// Initialize a south/east resize handler and place it above the

68
// selection layer

69
var $seResizeHandler = $('<div class="image-crop-resize-handler" id="image-crop-se-resize-handler" />')
70
        .css({
71
            opacity : 0.5,
72
            position : 'absolute'
73
        })
74
        .insertAfter($selection);

We've initialized a resize handler for each corner and the middle side.

And finally, the preview pane.

1
2
...
3
4
// Initialize a preview holder and place it after the outline layer

5
var $previewHolder = $('<div id="image-crop-preview-holder" />')
6
        .css({
7
            opacity : options.previewFadeOnBlur,
8
            overflow : 'hidden',
9
            position : 'absolute'
10
        })
11
        .insertAfter($outline);
12
13
// Initialize a preview image and append it to the preview holder

14
var $preview = $('<img alt="Crop preview" id="image-crop-preview" />')
15
        .css({
16
            position : 'absolute'
17
        })
18
        .attr('src', $image.attr('src'))
19
        .appendTo($previewHolder);

We've initialized two layers:

  • the holder, which works as a mask and
  • the preview image, which has the same src as the original image.
Directory tree

We've used the .appendTo() method to insert the preview image at the end of the holder.


Step 3: Enhancing the Interface

First, we'll add two new global variables.

1
2
...
3
4
// Initialize global variables

5
var resizeHorizontally = true,
6
    resizeVertically = true,
7
    selectionExists,
8
    selectionOffset = [0, 0],
9
    selectionOrigin = [0, 0];

We'll need these variables later, when we update the resizeSelection() function.

In the first part, we only took care of the allowSelect option. Let's handle allowMove and allowResize too.

1
2
...
3
4
if (options.allowMove)
5
    // Bind an event handler to the 'mousedown' event of the selection layer

6
    $selection.mousedown(pickSelection);
7
8
if (options.allowResize)
9
    // Bind an event handler to the 'mousedown' event of the resize handlers

10
    $('div.image-crop-resize-handler').mousedown(pickResizeHandler);

We've attached the mousedown event to the selection and all resize handlers.

Now we need to write a little more code to update the new layers we've added before.

1
2
...
3
4
// Update the size hint

5
function updateSizeHint(action) {
6
    switch (action) {
7
        case 'fade-out' :
8
            // Fade out the size hint

9
            $sizeHintBackground.fadeOut('slow');
10
            $sizeHintForeground.fadeOut('slow');
11
12
            break;
13
        default :
14
            var display = (selectionExists && options.displaySize) ? 'block' : 'none';
15
16
            // Update the foreground layer

17
            $sizeHintForeground.css({
18
                    cursor : 'default',
19
                    display : display,
20
                    left : options.selectionPosition[0] + 4,
21
                    top : options.selectionPosition[1] + 4
22
                })
23
                .html(options.selectionWidth + 'x' + options.selectionHeight);
24
25
            // Update the background layer

26
            $sizeHintBackground.css({
27
                    cursor : 'default',
28
                    display : display,
29
                    left : options.selectionPosition[0] + 1,
30
                    top : options.selectionPosition[1] + 1
31
                })
32
                .width($sizeHintForeground.width() + 6)
33
                .height($sizeHintForeground.height() + 6);
34
    }
35
};

The updateSizeHint() function treats two cases depending on the specified parameter.

  • If none is specified, the default behavior is to display and update the size hint (if the selection exists).
  • The second behavior is to fade out the hint. This will be used when the user is done with resizing the selection.

On the previous step, we've only initialized the resize handlers. Now we'll place them in the right position.

1
2
...
3
4
// Update the resize handlers

5
function updateResizeHandlers(action) {
6
    switch (action) {
7
        case 'hide-all' :
8
            $('.image-crop-resize-handler').each(function() {
9
                $(this).css({
10
                        display : 'none'
11
                    });
12
            });
13
14
            break;
15
        default :
16
            var display = (selectionExists && options.allowResize) ? 'block' : 'none';
17
18
            $nwResizeHandler.css({
19
                    cursor : 'nw-resize',
20
                    display : display,
21
                    left : options.selectionPosition[0] - Math.round($nwResizeHandler.width() / 2),
22
                    top : options.selectionPosition[1] - Math.round($nwResizeHandler.height() / 2)
23
                });
24
25
            $nResizeHandler.css({
26
                    cursor : 'n-resize',
27
                    display : display,
28
                    left : options.selectionPosition[0] + Math.round(options.selectionWidth / 2 - $neResizeHandler.width() / 2) - 1,
29
                    top : options.selectionPosition[1] - Math.round($neResizeHandler.height() / 2)
30
                });
31
32
            $neResizeHandler.css({
33
                    cursor : 'ne-resize',
34
                    display : display,
35
                    left : options.selectionPosition[0] + options.selectionWidth - Math.round($neResizeHandler.width() / 2) - 1,
36
                    top : options.selectionPosition[1] - Math.round($neResizeHandler.height() / 2)
37
                });
38
39
            $wResizeHandler.css({
40
                    cursor : 'w-resize',
41
                    display : display,
42
                    left : options.selectionPosition[0] - Math.round($neResizeHandler.width() / 2),
43
                    top : options.selectionPosition[1] + Math.round(options.selectionHeight / 2 - $neResizeHandler.height() / 2) - 1
44
                });
45
46
            $eResizeHandler.css({
47
                    cursor : 'e-resize',
48
                    display : display,
49
                    left : options.selectionPosition[0] + options.selectionWidth - Math.round($neResizeHandler.width() / 2) - 1,
50
                    top : options.selectionPosition[1] + Math.round(options.selectionHeight / 2 - $neResizeHandler.height() / 2) - 1
51
                });
52
53
            $swResizeHandler.css({
54
                    cursor : 'sw-resize',
55
                    display : display,
56
                    left : options.selectionPosition[0] - Math.round($swResizeHandler.width() / 2),
57
                    top : options.selectionPosition[1] + options.selectionHeight - Math.round($swResizeHandler.height() / 2) - 1
58
                });
59
60
            $sResizeHandler.css({
61
                    cursor : 's-resize',
62
                    display : display,
63
                    left : options.selectionPosition[0] + Math.round(options.selectionWidth / 2 - $seResizeHandler.width() / 2) - 1,
64
                    top : options.selectionPosition[1] + options.selectionHeight - Math.round($seResizeHandler.height() / 2) - 1
65
                });
66
67
            $seResizeHandler.css({
68
                    cursor : 'se-resize',
69
                    display : display,
70
                    left : options.selectionPosition[0] + options.selectionWidth - Math.round($seResizeHandler.width() / 2) - 1,
71
                    top : options.selectionPosition[1] + options.selectionHeight - Math.round($seResizeHandler.height() / 2) - 1
72
                });
73
    }
74
};

Similar to the last function, the updateResizeHandlers() tests two cases: hide-all and default. In the first case, we call the .each() method to iterate over the matched elements.

Let's create the updatePreview() function.

1
2
...
3
4
// Update the preview

5
function updatePreview(action) {
6
    switch (action) {
7
        case 'focus' :
8
            // Fade in the preview holder layer

9
            $previewHolder.stop()
10
                .animate({
11
                    opacity : options.previewFadeOnFocus
12
                });
13
14
            break;
15
        case 'blur' :
16
            // Fade out the preview holder layer

17
            $previewHolder.stop()
18
                .animate({
19
                    opacity : options.previewFadeOnBlur
20
                });
21
22
            break;
23
        case 'hide' :
24
            // Hide the preview holder layer

25
            $previewHolder.css({
26
                display : 'none'
27
            });
28
29
            break;
30
        default :
31
            var display = (selectionExists && options.displayPreview) ? 'block' : 'none';
32
33
            // Update the preview holder layer

34
            $previewHolder.css({
35
                    display : display,
36
                    left : options.selectionPosition[0],
37
                    top : options.selectionPosition[1] + options.selectionHeight + 10
38
                });
39
40
            // Update the preview size

41
            if (options.selectionWidth > options.selectionHeight) {
42
                if (options.selectionWidth && options.selectionHeight) {
43
                    // Update the preview image size

44
                    $preview.width(Math.round($image.width() * options.previewBoundary / options.selectionWidth));
45
                    $preview.height(Math.round($image.height() * $preview.width() / $image.width()));
46
47
                    // Update the preview holder layer size

48
                    $previewHolder.width(options.previewBoundary)
49
                    .height(Math.round(options.selectionHeight * $preview.height() / $image.height()));
50
                }
51
            } else {
52
                if (options.selectionWidth && options.selectionHeight) {
53
                    // Update the preview image size

54
                    $preview.height(Math.round($image.height() * options.previewBoundary / options.selectionHeight));
55
                    $preview.width(Math.round($image.width() * $preview.height() / $image.height()));
56
57
                    // Update the preview holder layer size

58
                    $previewHolder.width(Math.round(options.selectionWidth * $preview.width() / $image.width()))
59
                        .height(options.previewBoundary);
60
                }
61
            }
62
63
            // Update the preview image position

64
            $preview.css({
65
                left : - Math.round(options.selectionPosition[0] * $preview.width() / $image.width()),
66
                top : - Math.round(options.selectionPosition[1] * $preview.height() / $image.height())
67
            });
68
    }
69
};

The code for the first three cases should be self explanatory. We call the .animate() method to perform a custom animation of a set off CSS properties. Next, we decide the display value and set the position of the preview holder. Then, we scale the preview image to fit the previewBoundary option and calculate its new position.

We need to update the updateCursor() function too.

1
2
...
3
4
// Update the cursor type

5
function updateCursor(cursorType) {
6
    $trigger.css({
7
            cursor : cursorType
8
        });
9
10
    $outline.css({
11
            cursor : cursorType
12
        });
13
14
    $selection.css({
15
            cursor : cursorType
16
        });
17
18
    $sizeHintBackground.css({
19
            cursor : cursorType
20
        });
21
22
    $sizeHintForeground.css({
23
            cursor : cursorType
24
        });
25
};

And now, the last function of this step.

1
2
...
3
4
// Update the plug-in interface

5
function updateInterface(sender) {
6
    switch (sender) {
7
        case 'setSelection' :
8
            updateOverlayLayer();
9
            updateSelection();
10
            updateResizeHandlers('hide-all');
11
            updatePreview('hide');
12
13
            break;
14
        case 'pickSelection' :
15
            updateResizeHandlers('hide-all');
16
17
            break;
18
        case 'pickResizeHandler' :
19
            updateSizeHint();
20
            updateResizeHandlers('hide-all');
21
22
            break;
23
        case 'resizeSelection' :
24
            updateSelection();
25
            updateSizeHint();
26
            updateResizeHandlers('hide-all');
27
            updatePreview();
28
            updateCursor('crosshair');
29
30
            break;
31
        case 'moveSelection' :
32
            updateSelection();
33
            updateResizeHandlers('hide-all');
34
            updatePreview();
35
            updateCursor('move');
36
37
            break;
38
        case 'releaseSelection' :
39
            updateTriggerLayer();
40
            updateOverlayLayer();
41
            updateSelection();
42
            updateSizeHint('fade-out');
43
            updateResizeHandlers();
44
            updatePreview();
45
46
            break;
47
        default :
48
            updateTriggerLayer();
49
            updateOverlayLayer();
50
            updateSelection();
51
            updateResizeHandlers();
52
            updatePreview();
53
    }
54
};

Step 4: Enhancing setSelection()

We'll add just one thing here: support for the preview pane.

1
2
...
3
4
// Set a new selection

5
function setSelection(event) {
6
    // Prevent the default action of the event

7
    event.preventDefault();
8
9
    // Prevent the event from being notified

10
    event.stopPropagation();
11
12
    // Bind an event handler to the 'mousemove' event

13
    $(document).mousemove(resizeSelection);
14
15
    // Bind an event handler to the 'mouseup' event

16
    $(document).mouseup(releaseSelection);
17
18
    // If display preview option is enabled

19
    if (options.displayPreview) {
20
        // Bind an event handler to the 'mouseenter' event of the preview

21
        // holder

22
        $previewHolder.mouseenter(function() {
23
            updatePreview('focus');
24
         });
25
26
         // Bind an event handler to the 'mouseleave' event of the preview

27
         // holder

28
         $previewHolder.mouseleave(function() {
29
             updatePreview('blur');
30
         });
31
    }
32
33
    // Notify that a selection exists

34
    selectionExists = true;
35
36
    // Reset the selection size

37
    options.selectionWidth = 0;
38
    options.selectionHeight = 0;
39
40
    // Get the selection origin

41
    selectionOrigin = getMousePosition(event);
42
43
    // And set its position

44
    options.selectionPosition[0] = selectionOrigin[0];
45
    options.selectionPosition[1] = selectionOrigin[1];
46
47
    // Update only the needed elements of the plug-in interface

48
    // by specifying the sender of the current call

49
    updateInterface('setSelection');
50
};

We've tested the displayPreview option and used the .mouseenter() and .mouseleave() functions to attach event handlers to the preview holder.


Step 5: Picking the Selection

To make the selection draggable, we need to deduce when the user moves and releases the mouse button.

1
2
...
3
4
// Pick the current selection

5
function pickSelection(event) {
6
    // Prevent the default action of the event

7
    event.preventDefault();
8
9
    // Prevent the event from being notified

10
    event.stopPropagation();
11
12
    // Bind an event handler to the 'mousemove' event

13
    $(document).mousemove(moveSelection);
14
15
    // Bind an event handler to the 'mouseup' event

16
    $(document).mouseup(releaseSelection);
17
18
    var mousePosition = getMousePosition(event);
19
20
    // Get the selection offset relative to the mouse position

21
    selectionOffset[0] = mousePosition[0] - options.selectionPosition[0];
22
    selectionOffset[1] = mousePosition[1] - options.selectionPosition[1];
23
24
    // Update only the needed elements of the plug-in interface

25
    // by specifying the sender of the current call

26
    updateInterface('pickSelection');
27
};

Also, we've got the selection offset relative to the mouse position. We'll need it later, in the moveSelection() function.


Step 6: Picking the Resize Handlers

The user will be able to resize the selection by picking and dragging one of the resize handlers. And this can be done in two ways: on both axis - if the user chooses to drag a handler from a corner - or on one axis - if the user chooses to drag a handler from the middle of a side.

1
2
...
3
4
// Pick one of the resize handlers

5
function pickResizeHandler(event) {
6
// Prevent the default action of the event

7
    event.preventDefault();
8
9
    // Prevent the event from being notified

10
    event.stopPropagation();
11
12
    switch (event.target.id) {
13
        case 'image-crop-nw-resize-handler' :
14
            selectionOrigin[0] += options.selectionWidth;
15
            selectionOrigin[1] += options.selectionHeight;
16
            options.selectionPosition[0] = selectionOrigin[0] - options.selectionWidth;
17
            options.selectionPosition[1] = selectionOrigin[1] - options.selectionHeight;
18
19
            break;
20
        case 'image-crop-n-resize-handler' :
21
            selectionOrigin[1] += options.selectionHeight;
22
            options.selectionPosition[1] = selectionOrigin[1] - options.selectionHeight;
23
24
            resizeHorizontally = false;
25
26
            break;
27
        case 'image-crop-ne-resize-handler' :
28
            selectionOrigin[1] += options.selectionHeight;
29
            options.selectionPosition[1] = selectionOrigin[1] - options.selectionHeight;
30
31
            break;
32
        case 'image-crop-w-resize-handler' :
33
            selectionOrigin[0] += options.selectionWidth;
34
            options.selectionPosition[0] = selectionOrigin[0] - options.selectionWidth;
35
36
            resizeVertically = false;
37
38
            break;
39
        case 'image-crop-e-resize-handler' :
40
            resizeVertically = false;
41
42
            break;
43
        case 'image-crop-sw-resize-handler' :
44
            selectionOrigin[0] += options.selectionWidth;
45
            options.selectionPosition[0] = selectionOrigin[0] - options.selectionWidth;
46
47
            break;
48
        case 'image-crop-s-resize-handler' :
49
            resizeHorizontally = false;
50
51
            break;
52
    }
53
54
    // Bind an event handler to the 'mousemove' event

55
    $(document).mousemove(resizeSelection);
56
57
    // Bind an event handler to the 'mouseup' event

58
    $(document).mouseup(releaseSelection);
59
60
    // Update only the needed elements of the plug-in interface

61
    // by specifying the sender of the current call

62
    updateInterface('pickResizeHandler');
63
};

We've written a case for each resize handler, because each one needs specific settings.


Step 7: Enhancing resizeSelection()

Different from the first version, the resizeSelection() function will be able to test the minimum/maximum size and lock the aspect ratio of the selection.

1
2
...
3
4
// Resize the current selection

5
function resizeSelection(event) {
6
    // Prevent the default action of the event

7
    event.preventDefault();
8
9
    // Prevent the event from being notified

10
    event.stopPropagation();
11
12
    var mousePosition = getMousePosition(event);
13
14
    // Get the selection size

15
    var height = mousePosition[1] - selectionOrigin[1],
16
        width = mousePosition[0] - selectionOrigin[0];
17
18
    // If the selection size is smaller than the minimum size set it

19
    // accordingly

20
    if (Math.abs(width) < options.minSize[0])
21
        width = (width >= 0) ? options.minSize[0] : - options.minSize[0];
22
23
    if (Math.abs(height) < options.minSize[1])
24
        height = (height >= 0) ? options.minSize[1] : - options.minSize[1];
25
26
    // Test if the selection size exceeds the image bounds

27
    if (selectionOrigin[0] + width < 0 || selectionOrigin[0] + width > $image.width())
28
        width = - width;
29
30
    if (selectionOrigin[1] + height < 0 || selectionOrigin[1] + height > $image.height())
31
        height = - height;
32
33
    if (options.maxSize[0] > options.minSize[0] &&
34
        options.maxSize[1] > options.minSize[1]) {
35
        // Test if the selection size is bigger than the maximum size

36
        if (Math.abs(width) > options.maxSize[0])
37
            width = (width >= 0) ? options.maxSize[0] : - options.maxSize[0];
38
39
        if (Math.abs(height) > options.maxSize[1])
40
            height = (height >= 0) ? options.maxSize[1] : - options.maxSize[1];
41
    }
42
43
    // Set the selection size

44
    if (resizeHorizontally)
45
        options.selectionWidth = width;
46
47
    if (resizeVertically)
48
        options.selectionHeight = height;
49
50
    // If any aspect ratio is specified

51
    if (options.aspectRatio) {
52
        // Calculate the new width and height

53
        if ((width > 0 && height > 0) || (width < 0 && height < 0))
54
            if (resizeHorizontally)
55
                height = Math.round(width / options.aspectRatio);
56
            else
57
                width = Math.round(height * options.aspectRatio);
58
        else
59
            if (resizeHorizontally)
60
                height = - Math.round(width / options.aspectRatio);
61
            else
62
                width = - Math.round(height * options.aspectRatio);
63
64
        // Test if the new size exceeds the image bounds

65
        if (selectionOrigin[0] + width > $image.width()) {
66
            width = $image.width() - selectionOrigin[0];
67
            height = (height > 0) ? Math.round(width / options.aspectRatio) : - Math.round(width / options.aspectRatio);
68
        }
69
70
        if (selectionOrigin[1] + height < 0) {
71
            height = - selectionOrigin[1];
72
            width = (width > 0) ? - Math.round(height * options.aspectRatio) : Math.round(height * options.aspectRatio);
73
        }
74
75
        if (selectionOrigin[1] + height > $image.height()) {
76
            height = $image.height() - selectionOrigin[1];
77
            width = (width > 0) ? Math.round(height * options.aspectRatio) : - Math.round(height * options.aspectRatio);
78
        }
79
80
        // Set the selection size

81
        options.selectionWidth = width;
82
        options.selectionHeight = height;
83
    }
84
85
    if (options.selectionWidth < 0) {
86
        options.selectionWidth = Math.abs(options.selectionWidth);
87
        options.selectionPosition[0] = selectionOrigin[0] - options.selectionWidth;
88
    } else
89
        options.selectionPosition[0] = selectionOrigin[0];
90
91
    if (options.selectionHeight < 0) {
92
        options.selectionHeight = Math.abs(options.selectionHeight);
93
        options.selectionPosition[1] = selectionOrigin[1] - options.selectionHeight;
94
    } else
95
        options.selectionPosition[1] = selectionOrigin[1];
96
97
    // Trigger the 'onChange' event when the selection is changed

98
    options.onChange(getCropData());
99
100
    // Update only the needed elements of the plug-in interface

101
    // by specifying the sender of the current call

102
    updateInterface('resizeSelection');
103
};

Additionally, we've invoked the onChange() callback at the end of the function. The getCropData() function returns the current state of the plug-in. We'll write its body a few steps later.


Step 8: Moving the Selection

Now we'll write the moveSelection() function.

1
2
...
3
4
// Move the current selection

5
function moveSelection(event) {
6
    // Prevent the default action of the event

7
    event.preventDefault();
8
9
    // Prevent the event from being notified

10
    event.stopPropagation();
11
12
    var mousePosition = getMousePosition(event);
13
14
    // Set the selection position on the x-axis relative to the bounds

15
    // of the image

16
    if (mousePosition[0] - selectionOffset[0] > 0)
17
        if (mousePosition[0] - selectionOffset[0] + options.selectionWidth < $image.width())
18
            options.selectionPosition[0] = mousePosition[0] - selectionOffset[0];
19
        else
20
            options.selectionPosition[0] = $image.width() - options.selectionWidth;
21
    else
22
        options.selectionPosition[0] = 0;
23
24
    // Set the selection position on the y-axis relative to the bounds

25
    // of the image

26
    if (mousePosition[1] - selectionOffset[1] > 0)
27
        if (mousePosition[1] - selectionOffset[1] + options.selectionHeight < $image.height())
28
            options.selectionPosition[1] = mousePosition[1] - selectionOffset[1];
29
        else
30
            options.selectionPosition[1] = $image.height() - options.selectionHeight;
31
        else
32
            options.selectionPosition[1] = 0;
33
34
    // Trigger the 'onChange' event when the selection is changed

35
    options.onChange(getCropData());
36
37
    // Update only the needed elements of the plug-in interface

38
    // by specifying the sender of the current call

39
    updateInterface('moveSelection');
40
};

Just like before, we've invoked the onChange() callback at the end of the function.


Step 9: Enhancing releaseSelection()

We need to edit the releaseSelection() function too.

1
2
...
3
4
// Release the current selection

5
function releaseSelection(event) {
6
    // Prevent the default action of the event

7
    event.preventDefault();
8
9
    // Prevent the event from being notified

10
    event.stopPropagation();
11
12
    // Unbind the event handler to the 'mousemove' event

13
    $(document).unbind('mousemove');
14
15
    // Unbind the event handler to the 'mouseup' event

16
    $(document).unbind('mouseup');
17
18
    // Update the selection origin

19
    selectionOrigin[0] = options.selectionPosition[0];
20
    selectionOrigin[1] = options.selectionPosition[1];
21
22
    // Reset the resize constraints

23
    resizeHorizontally = true;
24
    resizeVertically = true;
25
26
    // Verify if the selection size is bigger than the minimum accepted

27
    // and set the selection existence accordingly

28
    if (options.selectionWidth > options.minSelect[0] &&
29
        options.selectionHeight > options.minSelect[1])
30
        selectionExists = true;
31
    else
32
        selectionExists = false;
33
34
    // Trigger the 'onSelect' event when the selection is made

35
    options.onSelect(getCropData());
36
37
    // If the selection doesn't exist

38
    if (!selectionExists) {
39
        // Unbind the event handler to the 'mouseenter' event of the

40
        // preview

41
        $previewHolder.unbind('mouseenter');
42
43
        // Unbind the event handler to the 'mouseleave' event of the

44
        // preview

45
        $previewHolder.unbind('mouseleave');
46
    }
47
48
    // Update only the needed elements of the plug-in interface

49
    // by specifying the sender of the current call

50
    updateInterface('releaseSelection');
51
};

We've reset the resize constraints and added support for the preview pane. Also, we've invoked the onSelect() callback in the same manner as we did before with the onChange() function.


Step 10: Getting the Current State

Now, we are almost ready. Let's write the getCropData() function.

1
2
...
3
4
// Return an object containing information about the plug-in state

5
function getCropData() {
6
    return {
7
        selectionX : options.selectionPosition[0],
8
        selectionY : options.selectionPosition[1],
9
        selectionWidth : options.selectionWidth,
10
        selectionHeight : options.selectionHeight,
11
12
        selectionExists : function() {
13
            return selectionExists;
14
        }
15
    };
16
};

We've just written the last function of this file. Save it and prepare for the next step.


Step 11: Minifying the Code

"Minifying the code reduces its size and improves loading time."

In this step, we'll minify the code of our plug-in to reduce its size and improve the loading time. This practice consists in removing unnecessary characters like comments, spaces, newlines and tabs. Two popular tools for minifying JavaScript code are YUI Compressor (which can also minify CSS) and JSMin. We'll use the first one. Also, it is open-source, so you can take a look at the code to understand exactly how it works.

Using the YUI Compressor

YUI Compressor is written in Java, so it doesn't matter which operating system you use. The only requirement is Java >= 1.4. Download the YUI Compressor and extract it in the /resources/js/imageCrop/ folder. Open the command line and change the current working directory to the same path.

If you're using it for the first time you should start by executing the following line in the command line and read the usage instructions.

1
2
$ java -jar yuicompressor-x.y.z.jar

Now let's minify our code.

1
2
$ java -jar yuicompressor-x.y.z.jar jquery.imagecrop.js -o jquery.imagecrop.js --preserve-semi

Don't forget to replace x.y.z with the YUI Compressor version that you're using. And that's it; wait for it to finish and then close the command line window.


Step 12: Styling the New Elements

Open up /resources/js/imageCrop/jquery.imagecrop.css and add the following lines to it:

1
2
...
3
4
div#image-crop-size-hint-background {
5
    background-color : #000000;
6
}
7
8
span#image-crop-size-hint-foreground {
9
    color : #ffffff;
10
    font-family : 'Verdana', 'Geneva', sans-serif;
11
    font-size : 12px;
12
    text-shadow : 0 -1px 0 #000000;
13
}
14
15
div#image-crop-preview-holder {
16
    -moz-box-shadow : 0 0 5px #000000;
17
    -webkit-box-shadow : 0 0 5px #000000;
18
    border : 3px #ef2929 solid;
19
    box-shadow : 0 0 5px #000000;
20
}
21
22
img#image-crop-preview {
23
    border : none;
24
}
25
26
div.image-crop-resize-handler {
27
    background-color : #000000;
28
    border : 1px #ffffff solid;
29
    height : 7px;
30
    overflow : hidden;
31
    width : 7px;
32
}

We've added some styling for the size hint, preview pane and resize handlers.


Step 13: Testing the Final Result

First, let's load the minified plug-in.

1
2
<script src="resources/js/imageCrop/jquery.imagecrop.min.js" type="text/javascript"></script>

To be able to test the plug-in, we need to somehow get the selection size and position. That's why we'll use onSelect callback; it returns an object with the current state of the plug-in.

1
2
$(document).ready(function() {
3
    $('img#example').imageCrop({
4
        displayPreview : true,
5
        displaySize : true,
6
        overlayOpacity : 0.25,
7
8
        onSelect : updateForm
9
    });
10
});
11
12
var selectionExists;
13
14
// Update form inputs

15
function updateForm(crop) {
16
    $('input#x').val(crop.selectionX);
17
    $('input#y').val(crop.selectionY);
18
    $('input#width').val(crop.selectionWidth);
19
    $('input#height').val(crop.selectionHeight);
20
21
    selectionExists = crop.selectionExists();
22
};
23
24
// Validate form data

25
function validateForm() {
26
    if (selectionExists)
27
        return true;
28
29
    alert('Please make a selection first!');
30
31
    return false;
32
};

The updateForm() function sets the input values and retains it if the selection exists. Next, the validateForm() function tests if the selection exists and displays an alert pop-up if it's needed.

Let's add the form.

1
2
...
3
4
<br /><br />
5
6
<form action="crop.php" method="post" onsubmit="return validateForm();">
7
    <input id="x" name="x" type="hidden" />
8
    <input id="y" name="y" type="hidden" />
9
    <input id="width" name="width" type="hidden" />
10
    <input id="height" name="height" type="hidden" />
11
    <input type="submit" value="Crop Image" />
12
</form>

We've added a few hidden inputs and a submit button.

The PHP

In this example, we'll use PHP with the gd library but you can use any other server-side scripting language that supports a graphic library.

Create an empty file, name it crop.php and fire up your editor.

1
2
<?php
3
    if ($_SERVER['REQUEST_METHOD'] == 'POST')
4
    {
5
        // Initialize the size of the output image

6
        $boundary = 150;
7
        $dst_w = $_POST['width'];
8
        $dst_h = $_POST['height'];
9
10
        if ($dst_w > $dst_h)
11
        {
12
            $dst_h = $dst_h * $boundary / $dst_w;
13
            $dst_w = $boundary;
14
        }
15
        else
16
        {
17
            $dst_w = $dst_w * $boundary / $dst_h;
18
            $dst_h = $boundary;
19
        }
20
21
        // Initialize the quality of the output image

22
        $quality = 80;
23
24
        // Set the source image path

25
        $src_path = 'resources/images/example.jpg';
26
27
        // Create a new image from the source image path

28
        $src_image = imagecreatefromjpeg($src_path);
29
30
        // Create the output image as a true color image at the specified size

31
        $dst_image = imagecreatetruecolor($dst_w, $dst_h);
32
33
        // Copy and resize part of the source image with resampling to the

34
        // output image

35
        imagecopyresampled($dst_image, $src_image, 0, 0, $_POST['x'],
36
                           $_POST['y'], $dst_w, $dst_h, $_POST['width'],
37
                           $_POST['height']);
38
39
        // Destroy the source image

40
        imagedestroy($src_image);
41
42
        // Send a raw HTTP header

43
        header('Content-type: image/jpeg');
44
45
        // Output the image to browser

46
        imagejpeg($dst_image, null, $quality);
47
48
        // Destroy the output image

49
        imagedestroy($dst_image);
50
51
        // Terminate the current script

52
        exit();
53
    }
54
?>

We've used the imagecreatefromjpeg() method to create a new image from the source path and imagecreatetruecolor() to create the output as a true color image. Next, we've called imagecopyresampled() to copy and resize a part of the image with resampling. The current document type is not what we need, so we call the header() function to change it to image/jpeg. The images that aren't needed anymore are destroyed with the imagedestroy() function. With exit(), we stop the execution of the current script.


That's All

We now have a fully customizable jQuery image cropping plug-in that allows the user to make, drag and resize a selection and displays a size hint and a preview pane. And yes, it looks the same even in Internet Explorer 6! So that completes are two-part tutorial! Thanks for reading!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.