Image Manipulation With jQuery & PHP GD

Advertisement

One of the numerous advantages brought about by the explosion of jQuery and other JavaScript libraries is the ease with which you can create interactive tools for your site. When combined with server-side technologies such as PHP, this puts a serious amount of power at your finger tips.

In this article, I’ll be looking at how to combine JavaScript/jQuery with PHP and, particularly, PHP’s GD library to create an image manipulation tool to upload an image, then crop it and finally save the revised version to the server. Sure, there are plugins out there that you can use to do this; but this article aims to show you what’s behind the process. You can download the source files51 (updated) for reference.

We’ve all seen this sort of Web application before — Facebook, Flickr, t-shirt-printing sites. The advantages are obvious; by including a functionality like this, you alleviate the need to edit pictures manually from your visitors, which has obvious drawbacks. They may not have access to or have the necessary skills to use Photoshop, and in any case why would you want to make the experience of your visitors more difficult?

Before You Start

For this article, you would ideally have had at least some experience working with PHP. Not necessarily GD — I’ll run you through that part, and GD is very friendly anyway. You should also be at least intermediate level in JavaScript, though if you’re a fast learning beginner, you should be fine as well.

A quick word about the technologies you’ll need to work through this article. You’ll need a PHP test server running the GD library, either on your hosting or, if working locally, through something like XAMPP2. GD has come bundled with PHP as standard for some time, but you can confirm this by running the phpinfo() function and verifying that it’s available on your server. Client-side-wise you’ll need a text editor, some pictures and a copy of jQuery.

Setting Up The Files

And off we go, then. Set up a working folder and create four files in it: index.php, js.js, image_manipulation.php and css.css. index.php is the actual webpage, js.js and css.css should be obvious, while image_manipulation.php will store the code that handles the uploaded image and then, later, saves the manipulated version.

In index.php, first let’s add a line of PHP to start a PHP session and call in our image_manipulation.php file:

After that, add in the DOCTYPE and skeleton-structure of the page (header, body areas etc) and call in jQuery and the CSS sheet via script and link tags respectively.

Add a directory to your folder, called imgs, which will receive the uploaded files. If you’re working on a remote server, ensure you set the permissions on the directory such that the script will be able to save image files in it.

First, let’s set up and apply some basic styling to the upload facility.

The Upload Functionality

Now to some basic HTML. Let’s add a heading and a simple form to our page that will allow the user to upload an image and assign that image a name:

<h1>Image uploader and manipulator</h1>

Please note that we specify enctype=’multipart/form-data’ which is necessary whenever your form contains file upload fields.

As you can see, the form is pretty basic. It contains 3 fields: an upload field for the image itself, a text field, so the user can give it a name and a submit button. The submit button has a name so it can act as an identifier for our PHP handler script which will know that the form was submitted.

Let’s add a smattering of CSS to our stylesheet:

/* -----------------
| UPLOAD FORM
----------------- */
#imgForm { border: solid 4px #ddd; background: #eee; padding: 10px; margin: 30px; width: 600px; overflow:hidden;}
	#imgForm label { float: left; width: 200px; font-weight: bold; color: #666; clear:both; padding-bottom:10px; }
	#imgForm input { float: left; }
	#imgForm input[type="submit"] {clear: both; }
	#img_upload { width: 400px; }
	#img_name { width: 200px; }

Now we have the basic page set up and styled. Next we need to nip into image_manipulation.php and prepare it to receive the submitted form. Which leads nicely on to validation…

Validating The Form

Open up image_manipulation.php. Since we made a point above of including it into our HTML page, we can rest assured that when it’s called into action, it will be present in the environment.

Let’s set up a condition, so the PHP knows what task it is being asked to do. Remember we named our submit button upload_form_submitted? PHP can now check its existence, since the script knows that it should start handling the form.

This is important because, as I said above, the PHP script has two jobs to do: to handle the uploaded form and to save the manipulated image later on. It therefore needs a technique such as this to know which role it should be doing at any given time.

/* -----------------
| UPLOAD FORM - validate form and handle submission
----------------- */

if (isset($_POST['upload_form_submitted'])) {
	//code to validate and handle upload form submission here
}

So if the form was submitted, the condition resolves to true and whatever code we put inside, it will execute. That code will be validation code. Knowing that the form was submitted, there are now five possible obstacles to successfully saving the file: 1) the upload field was left blank; 2) the file name field was left blank; 3) both these fields were filled in, but the file being uploaded isn’t a valid image file; 4) an image with the desired name already exists; 5) everything is fine, but for some reason, the server fails to save the image, perhaps due to file permission issues. Let’s look at the code behind picking up each of these scenarios, should any occur, then we’ll put it all together to build our validation script.

Combined into a single validation script, the whole code looks as follows.

/* -----------------
| UPLOAD FORM - validate form and handle submission
----------------- */
 
if (isset($_POST['upload_form_submitted'])) {

	//error scenario 1
	if (!isset($_FILES['img_upload']) || empty($_FILES['img_upload']['name'])) {
		$error = "Error: You didn't upload a file";

	//error scenario 2
	} else if (!isset($_POST['img_name']) || empty($_FILES['img_upload'])) {
		$error = "Error: You didn't specify a file name";
	} else {

		$allowedMIMEs = array('image/jpeg', 'image/gif', 'image/png');
		foreach($allowedMIMEs as $mime) {
			if ($mime == $_FILES['img_upload']['type']) {
				$mimeSplitter = explode('/', $mime);
				$fileExt = $mimeSplitter[1];
				$newPath = 'imgs/'.$_POST['img_name'].'.'.$fileExt;
				break;
			}
		}

		//error scenario 3
		if (file_exists($newPath)) {
			$error = "Error: A file with that name already exists";

		//error scenario 4
		} else if (!isset($newPath)) {
			$error = 'Error: Invalid file format - please upload a picture file';

		//error scenario 5
		} else if (!copy($_FILES['img_upload']['tmp_name'], $newPath)) {
			$error = 'Error: Could not save file to server';

		//...all OK!
		} else {
			$_SESSION['newPath'] = $newPath;
			$_SESSION['fileExt'] = $fileExt;
		}
	}
}

There are a couple of things to note here.

$error & $_SESSION[‘newPath’]

Firstly, note that I’m using a variable, $error, to log whether we hit any of the hurdles. If no error occurs and the image is saved, we set a session variable, $_SESSION['new_path'], to store the path to the saved image. This will be helpful in the next step where we need to display the image and, therefore, need to know its SRC.

I’m using a session variable rather than a simple variable, so when the time comes for our PHP script to crop the image, we don’t have to pass it a variable informing the script which image to use — the script will already know the context, because it will remember this session variable. Whilst this article doesn’t concern itself deeply with security, this is a simple precaution. Doing this means that the user can affect only the image he uploaded, rather than, potentially, someone else’s previously-saved image — the user is locked into manipulating only the image referenced in $error and has no ability to enforce the PHP script to affect another image.

The $_FILES superglobal

Note that even though the form was sent via POST, we access the file upload not via the $_POST superglobal (i.e. variables in PHP which are available in all scopes throughout a script), but via the special $_FILES superglobal. PHP automatically assigns file fields to that, provided the form was sent with the required enctype='multipart/form-data' attribute. Unlike the $_POST and $_GET superglobals, the $_FILES superglobal goes a little “deeper” and is actually a multi-dimensional array. Through this, you can access not only the file itself but also a variety of meta data related to it. You’ll see how we can use this information shortly. We use this meta data in the third stage of validation above, namely checking that the file was a valid image file. Let’s look at this code in a little more detail.

Confirming the upload is an image

Any time you’re allowing users to upload files to your server, you obviously want to assume full control of precisely what sort of files you allow to be uploaded. It should be blindingly obvious, but you don’t want people able to upload just any file to you server – this needs to be something you control, and tightly.

We could check by file extension – only this would be insecure. Just because something has a .jpg extension, doesn’t mean its inner code is that of a picture. Instead, we check by MIME-type, which is more secure (though still not totally perfect).

To this end we check the uploaded file’s MIME-type – which lives in the ‘type’ property of its array – against a white list of allowed MIME-types.

$allowedMIMEs = array('image/jpeg', 'image/gif', 'image/png');
foreach($allowedMIMEs as $mime) {
	if ($mime == $_FILES['img_upload']['type']) {
		$mimeSplitter = explode('/', $mime);
		$fileExt = $mimeSplitter[1];
		$newPath = 'imgs/'.$_POST['img_name'].'.'.$fileExt;
		break;
	}
}

If a match is found, we extract its extension and use that to build the name we’ll use to save the file.

To extract the extension we exploit the fact that MIME-types are always in the format something/something – i.e. we can rely on the forward slash. We therefore ‘explode’ the string based on that delimited. Explode returns an array of parts – in our case, two parts, the part of the MIME-type either side of the slash. We know, therefore, that the second part of the array ([1]) is the extension associated with the MIME-type.

Note that, if a matching MIME-type is found, we set two variables: $newPath and $fileExt. Both of these will be important later to the PHP that actually saves the file, but the former is also used, as you can see, by error scenario 4 as a means of detecting whether MIME look-up was successful.

Saving the file

All uploaded files are assigned a temporary home by the server until such time as the session expires or they are moved. So saving the file means moving the file from its temporary location to a permanent home. This is done via the copy() function, which needs to know two rather obvious things: what’s the path to the temporary file, and what’s the path to where we want to put it.

The answer to the first question is read from the tmp_name part of the $_FILES superglobal. The answer to the second is the full path, including new filename, to where you want it to live. So it is formed of the name of the directory we set up to store images (/imgs), plus the new file name (i.e. the value entered into the img_name field) and the extension. Let’s assign it to its own variable, $newPath and then save the file:

$newPath = 'imgs/'.$_POST['img_name'].'.'.$fileExt;
...
copy($_FILES['img_upload']['tmp_name'],$newPath);

Reporting Back and Moving On

What happens next depends entirely on whether an error occurred, and we can find it out by looking up whether $error is set. If it is, we need to communicate this error back to the user. If it’s not set, it’s time to move on and show the image and let the user manipulate it. Add the following above your form:

<?php if (isset($error)) echo '<p id="error">'.$error.'</p>'; ?>

If there’s an error, we’d want to show the form again. But the form is currently set to show regardless of the situation. This needs to change, so that it shows only if no image has been uploaded yet, i.e. if the form hasn’t been submitted yet, or if it has but there was an error. We can check whether an uploaded image has been saved by interrogating the $_SESSION['newPath'] variable. Wrap your form HTML in the following two lines of code:

<?php if (!isset($_SESSION['newPath']) || isset($_GET['true'])) { ?>

<?php } else echo '<img src="'.$_SESSION['newPath'].'" />'; ?>

Now the form appears only if an uploaded image isn’t registered — i.e. $_SESSION['newPath'] isn’t set — or if new=true is found in the URL. (This latter part provides us with a means of letting the user start over with a new image upload should they wish so; we’ll add a link for this in a moment). Otherwise, the uploaded image displays (we know where it lives because we saved its path in $_SESSION['newPath']).

This is a good time to take stock of where we are, so try it out. Upload an image, and verify that that it displays. Assuming it does, it’s time for our JavaScript to provide some interactivity for image manipulation.

Adding Interactivity

First, let’s extend the line we just added so that we a) give the image an ID to reference it later on; b) call the JavaScript itself (along with jQuery); and c) we provide a “start again” link, so the user can start over with a new upload (if necessary). Here is the code snippet:

<?php } else { ?>
	<img id="uploaded_image" src="" />
	<p>start over with new image3

<script src="http://www.google.com/jsapi"></script> <script>google.load("jquery", "1.5");</script> <script src="js.js"></script>

Note that I defined an ID for the image, not a class, because it’s a unique element, and not one of the many (this sounds obvious, but many people fail to observe this distinction when assigning IDs and classes). Note also, in the image’s SRC, I’m appending a random string. This is done to force the browser not to cache the image once we’ve cropped it (since the SRC doesn’t change).

Open js.js and let’s add the obligatory document ready handler (DRH), required any time you’re using freestanding jQuery (i.e. not inside a custom function) to reference or manipulate the DOM. Put the following JavaScript inside this DRH:

$(function() {
	// all our JS code will go here
});

We’re providing the functionality to a user to crop the image, and it of course means allowing him to drag a box area on the image, denoting the part he wishes to keep. Therefore, the first step is to listen for a mousedown event on the image, the first of three events involved in a drag action (mouse down, mouse move and then, when the box is drawn, mouse up).

var dragInProgress = false;

$("#uploaded_image").mousedown(function(evt) {
	dragInProgress = true;
});

And in similar fashion, let’s listen to the final mouseup event.

$(window).mouseup(function() {
	dragInProgress = false;
});

Note that our mouseup event runs on window, not the image itself, since it’s possible that the user could release the mouse button anywhere on the page, not necessarily on the image.

Note also that the mousedown event handler is prepped to receive the event object. This object holds data about the event, and jQuery always passes it to your event handler, whether or not it’s set up to receive it. That object will be crucial later on in ascertaining where the mouse was when the event fired. The mouseup event doesn’t need this, because all we care about if is that the drag action is over and it doesn’t really matter where the mouse is.

We’re tracking whether or not the mouse button is currently depressed in a variable, <cpde?dragInProgress. Why? Because, in a drag action, the middle event of the three (see above) only applies if the first happened. That is, in a drag action, you move the mouse whilst the mouse is down. If it’s not, our mousemove event handler should exit. And here it is:

$("#uploaded_image").mousemove(function(evt) {
	if (!dragInProgress) return;
});

So now our three event handlers are set up. As you can see, the mousemove event handler exits if it discovers that the mouse button is not currently down, as we decided above it should be.

Now let’s extend these event handlers.

This is a good time to explain how our JavaScript will be simulating the drag action being done by the user. The trick is to create a DIV on mousedown, and position it at the mouse cursor. Then, as the mouse moves, i.e. the user is drawing his box, that element should resize consistently to mimic that.

Let’s add, position and style our DIV. Before we add it, though, let’s remove any previous such DIV, i.e. from a previous drag attempt. This ensures there’s only ever one drag box, not several. Also, we want to log the mouse coordinates at the time of mouse down, as we’ll need to reference these later when it comes to drawing and resizing ourDIV. Extend the mousedown event handler to become:

$("#uploaded_image").mousedown(function(evt) {
	dragInProgress = true;
	$("#drag_box").remove();
	$("<div>").appendTo("body").attr("id", "drag_box").css({left: evt.clientX, top: evt.clientY});
	mouseDown_left = evt.clientX;
	mouseDown_top = evt.clientY;
});

Notice that we don’t prefix the three variables there with the 'var' keyword. That would make them accessible only within the mousedown handler, but we need to reference them later in our mousemove handler. Ideally, we’d avoid global variables (using a namespace would be better) but for the purpose of keeping the code in this tutorial concise, they’ll do for now.

Notice that we obtain the coordinates of where the event took place — i.e. where the mouse was when the mouse button was depressed — by reading the clientX and clientY properties of the event object, and it’s those we use to position our DIV.

Let’s style the DIV by adding the following CSS to your stylesheet.

#drag_box { position: absolute; border: solid 1px #333; background: #fff; opacity: .5; filter: alpha(opacity=50); z-index: 10; }

Now, if you upload an image and then click it, the DIV will be inserted at your mouse position. You won’t see it yet, as it’s got width and height zero; only when we start dragging should it become visible, but if you use Firebug or Dragonfly to inspect it, you will see it in the DOM.

So far, so good. Our drag box functionality is almost complete. Now we just need to make it respond to the user’s mouse movement. What’s involved here is very much what we did in the mousedown event handler when we referenced the mouse coordinates.

The key to this part is working out what properties should be updated, and with what values. We’ll need to change the box’s left, top, width and height.

Sounds pretty obvious. However, it’s not as simple as it sounds. Imagine that the box was created at coordinates 40×40 and then the user drags the mouse to coordinates 30×30. By updating the box’s left and top properties to 30 and 30, the position of the top left corner of the box would be correct, but the position of its bottom right corner would not be where the mousedown event happened. The bottom corner would be 10 pixels north west of where it should be!

To get around this, we need to compare the mousedown coordinates with the current mouse coordinates. That’s why in our mousedown handler, we logged the mouse coordinates at the time of mouse down. The box’s new CSS values will be as follows:

  • left: the lower of the two clientX coordinates
  • width: the difference between the two clientX coordinates
  • top: the lower of the two clientY coordinates
  • height: the difference between the two clientY coordinates

So let’s extend the mousemove event handler to become:

$("#uploaded_image").mousemove(function(evt) {
	if (!dragInProgress) return;
	var newLeft = mouseDown_left < evt.clientX ? mouseDown_left : evt.clientX;
	var newWidth = Math.abs(mouseDown_left - evt.clientX);
	var newTop = mouseDown_top < evt.clientY ? mouseDown_top : evt.clientY;
	var newHeight = Math.abs(mouseDown_top - evt.clientY);
	$('#drag_box').css({left: newLeft, top: newTop, width: newWidth, height: newHeight});
});

Notice also that, to establish the new width and height, we didn’t have to do any comparison. Although we don’t know, for example, which is lower out of the mousedown left and the current mouse left, we can subtract either from the other and counter any negative result by forcing the resultant number to be positive via Math.abs(), i.e.

result = 50 – 20; //30
result = Math.abs(20 – 50); //30 (-30 made positive)

One final, small but important thing. When Firefox and Internet Explorer detect drag attempts on images they assume the user is trying to drag out the image onto their desktop, or into Photoshop, or wherever. This has the potential to interfere with our creation. The solution is to stop the event from doing its default action. The easiest way is to return false. What’s interesting, though, is that Firefox interprets drag attempts as beginning on mouse down, whilst IE interprets them as beginning on mouse move. So we need to append the following, simple line to the ends of both of these functions:

return false;

Try your application out now. You should have full drag box functionality.

Saving the Cropped Image

And so to the last part, saving the modified image. The plan here is simple: we need to grab the coordinates and dimensions of the drag box, and pass them to our PHP script which will use them to crop the image and save a new version.

Grabbing the drag box data

It makes sense to grab the drag box’s coordinates and dimensions in our mouseup handler, since it denotes the end of the drag action. We could do that with the following:

var db = $("#drag_box");
var db_data = {left: db.offset().left, top: db.offset().top, width: db.width(), height: db.height()};

There’s a problem, though, and it has to do with the drag box’s coordinates. The coordinates we grab above are relative to the body, not the uploaded image. So to correct this, we need to subtract the position, relative to the body, of the image itself, from them. So let’s add this instead:

var db = $("#drag_box");
if (db.width() == 0 || db.height() == 0 || db.length == 0) return;
var img_pos = $('#uploaded_image').offset();
var db_data = {
	left: db.offset().left – img_pos.left,
	top: db.offset().top - img_pos.top,
	width: db.width(),
	height: db.height()
};

What’s happening there? We’re first referencing the drag box in a local shortcut variable, db, and then store the four pieces of data we need to know about it, its left, top, width and height, in an object db_data. The object isn’t essential: we could use separate variables, but this approach groups the data together under one roof and might be considered tidier.

Note the condition on the second line, which guards against simple, dragless clicks to the image being interpreted as crop attempts. In these cases, we return, i.e. do nothing.

Note also that we get the left and top coordinates via jQuery’s offset() method. This returns the dimensions of an object relative to the document, rather than relative to any parent or ancestor with relative positioning, which is what position() or css('top/right/bottom/left') would return. However, since we appended our drag box directly to the body, all of these three techniques would work the same in our case. Equally, we get the width and height via the width() and height() methods, rather than via css('width/height'), as the former omits ‘px’ from the returned values. Since our PHP script will be using these coordinates in a mathematical fashion, this is the more suitable option.

For more information on the distinction between all these methods, see my previous article on SmashingMag, Commonly Confused Bits of jQuery4.

Let’s now throw out a confirm dialogue box to check that the user wishes to proceed in cropping the image using the drag box they’ve drawn. If so, time to pass the data to our PHP script. Add a bit more to your mouseup handler:

if (confirm("Crop the image using this drag box?")) {
	location.href = "index.php?crop_attempt=true&crop_l="+db_data.left+"&crop_t="+
db_data.top+"&crop_w="+db_data.width+"&crop_h="+db_data.height;
} else {
	db.remove();
}

So if the user clicks ‘OK’ on the dialogue box that pops up, we redirect to the same page we’re on, but passing on the four pieces of data we need to give to our PHP script. We also pass it a flag crop_attempt, which our PHP script can detect, so it knows what action we’d like it to do. If the user clicks ‘Cancel’, we remove the drag box (since it’s clearly unsuitable). Onto the PHP…

PHP: saving the modified file

Remember we said that our image_manipulation.php had two tasks — one to first save the uploaded image and another to save the cropped version of the image? It’s time to extend the script to handle the latter request. Append the following to image_manipulation.php:

/* -----------------
| CROP saved image
----------------- */

if (isset($_GET["crop_attempt"])) {
	//cropping code here
}

So just like before, we condition-off the code area and make sure a flag is present before executing the code. As for the code itself, we need to go back into the land of GD. We need to create two image handles. Into one, we import the uploaded image; the second one will be where we paste the cropped portion of the uploaded image into, so we can essentially think of these two as source and destination. We copy from the source onto the destination canvas via the GD function imagecopy(). This needs to know 8 pieces of information:

  • destination, the destination image handle
  • source, the source image handle
  • destination X, the left position to paste TO on the destination image handle
  • destination Y, the top position “ “ “ “
  • source X, the left position to grab FROM on the source image handle
  • source Y, the top position “ “ “ “
  • source W, the width (counting from source X) of the portion to be copied over from the source image handle
  • source H, the height (counting from source Y) “ “ “ “

Fortunately, we already have the data necessary to pass to the final 6 arguments in the form of the JavaScript data we collected and passed back to the page in our mouseup event handler a few moments ago.

Let’s create our first handle. As I said, we’ll import the uploaded image into it. That means we need to know its file extension, and that’s why we saved it as a session variable earlier.

switch($_SESSION["fileExt"][1]) {
	case "jpg": case "jpeg":
		var source_img = imagecreatefromjpeg($_SESSION["newPath"]);
		break;
	case "gif":
		var source_img = imagecreatefromgif($_SESSION["newPath"]); 
		break;
	case "png":
		var source_img = imagecreatefrompng($_SESSION["newPath"]); 
		break;
}

As you can see, the file type of the image determines which function we use to open it into an image handle. Now let’s extend this switch statement to create the second image handle, the destination canvas. Just as the function for opening an existing image depends on image type, so too does the function used to create a blank image. Hence, let’s extend our switch statement:

switch($_SESSION["fileExt"][1]) {
	case "jpg": case "jpeg":
		$source_img = imagecreatefromjpeg($_SESSION["newPath"]);
		$dest_ing = imagecreatetruecolor($_GET["crop_w"], $_GET["crop_h"]);
		break;
	case "gif":
		$source_img = imagecreatefromgif($_SESSION["newPath"]);
		$dest_ing = imagecreate($_GET["crop_w"], $_GET["crop_h"]);
		break;
	case "png":
		$source_img = imagecreatefrompng($_SESSION["newPath"]);
		$dest_ing = imagecreate($_GET["crop_w"], $_GET["crop_h"]);
		break;
}

You’ll notice that the difference between opening a blank image and opening one from an existing or uploaded file is that, for the former, you must specify the dimensions. In our case, that’s the width and height of the drag box, which we passed into the page via the $_GET['crop_w'] and $_GET['crop_h'] vars respectively.

So now we have our two canvases, it’s time to do the copying. The following is one function call, but since it takes 8 arguments, I’m breaking it onto several lines to make it readable. Add it after your switch statement:

imagecopy(
	$dest_img,
	$source_img,
	0,
	0,
	$_GET["crop_l"],
	$_GET["crop_t"],
	$_GET["crop_w"],
	$_GET["crop_h"]
);

The final part is to save the cropped image. For this tutorial, we’ll overwrite the original file, but you might like to extend this application, so the user has the option of saving the cropped image as a separate file, rather than losing the original.

Saving the image is easy. We just call a particular function based on (yes, you guessed it) the image’s type. We pass in two arguments: the image handle we’re saving, and the file name we want to save it as. So let’s do that:

switch($_SESSION["fileExt"][1]) {
	case "jpg": case "jpeg":
		imagejpeg($dest_img, $_SESSION["newPath"]); break;
	case "gif":
		imagegif($dest_img, $_SESSION["newPath"]); break;
	case "png":
		imagepng($dest_img, $_SESSION["newPath"]); break;
}

It’s always good to clean up after ourselves – in PHP terms that means freeing up memory, so let’s destroy our image handlers now that we don’t need them anymore.

imagedestroy($dest_img);
imagedestroy($source_img);

Lastly, we want to redirect to the index page. You might wonder why we’d do this, since we’re on it already (and have been the whole time). The trick is that by redirecting, we can lose the arguments we passed in the URL. We don’t want these hanging around because, if the user refreshes the page, he’ll invoke the PHP crop script again (since it will detect the arguments). The arguments have done their job, so now they have to go, so we redirect to the index page without these arguments. Add the following line to force the redirect:

header("Location: index.php"); //bye bye arguments

Final Touches

So that’s it. We now have a fully-working facility to first upload then crop an image, and save it to the server. Don’t forget you can download the source files51 (updated) for your reference.

There’s plenty of ways you could extend this simple application. Explore GD (and perhaps other image libraries for PHP); you can do wonders with images, resizing them, distorting them, changing them to greyscale and much more. Another thing to think about would be security; this tutorial does not aim to cover that here, but if you were working in a user control panel environment, you’d want to make sure the facility was secure and that the user could not edit other user’s files.

With this in mind, you might make the saved file’s path more complex, e.g. if the user named it pic.jpg, you might actually name it on the server 34iweshfjdshkj4r_pic.jpg. You could then hide this image path, e.g. by specifying the SRC attribute as ‘getPic.php‘ instead of referencing the image directly inside an image’s SRC attribute. That PHP script would then open and display the saved file (by reading its path in the session variable), and the user would never be aware of its path.

The possibilities are endless, but hopefully this tutorial has given you a starting point.

(vf)

Footnotes

  1. 1 http://coding.smashingmagazine.com/wp-content/uploads/2011/04/manipulation_img_crop.zip
  2. 2 http://www.apachefriends.org
  3. 3 index.php?new=true
  4. 4 'http://www.smashingmagazine.com/2010/08/04/commonly-confused-bits-of-jquery/'
  5. 5 http://coding.smashingmagazine.com/wp-content/uploads/2011/04/manipulation_img_crop.zip

↑ Back to topShare on Twitter

Andy Croxall is a Web developer from Wandsworth, London, England. He is a Javascript specialist and is an active member of the jQuery community, posting plugins and extensions. He has worked for clients ranging from the London Stock Exchange to Durex. You can keep up with him and his projects and creations on his website, mitya.co.uk.

Advertising

Note: Our rating-system has caused errors, so it's disabled at the moment. It will be back the moment the problem has been resolved. We're very sorry. Happy Holidays!

  1. 1

    Small bug in the download, first line in index.php “require_once ‘php.php';” should be “require_once ‘image_manipulation.php';”

    Otherwise good job, thanks for this…

    • 2

      Vitaly Friedman (editor-in-chief of Smashing Magazine)

      April 5, 2011 2:36 am

      Thanks for heads up, Robin. The link was wrong; it’s fixed now.

    • 3

      Thanks for telling people how to make an insecure file upload

      img_name=evil.php%00a.png

      Also most of the code is completely invalid:
      <!–?php } else echo "”; ?>

      • 4

        A little vitriolic, George, but I take your point. I have already conceded the extension check could be better. The article was more to show potential for interactivity rather than being a master class in PHP security :)

        • 5

          “show potential for interactivity rather than being a master class in PHP security”?

          Please don’t make it as an excuse. You endanger the PHP noobs reading your tutorials.

        • 6

          I with you Andy. It is pointless trying to address every conceivable issue in a tutorial. George and Opaw are elitist snobs who clearly cannot understand that the point of a tutorial is teach the basics. I think every single tutorial I have ever seen has heaps of holes which you would not put into production. If you were to create production level code for a tutorial it would be way too long.

  2. 7

    nice article, let’s see it in action ;)

    • 8

      No kidding. I suppose he didn’t have a directory available that he wanted everyone uploading images to.

  3. 9

    Thanks, the Javascript part was interesting. However, the PHP is not good.

    – The method of determining the file extension is slow, insecure and unreliable.
    – Use move_uploaded_file() instead of copy()
    – Use imagedestroy() to free associated memory after the image has been created

    • 10

      Same here.

      I’d use something like this:


      function is_image($strPath) {
      $return = false;

      $strFileName = basename($strPath);

      if(strpos($strFileName, '.') !== false && $strFileName[0] != '.') { // is probably a file and NOT hidden or a fucked-up try of sending a file w/o name but extension
      $arrFileParts = explode('.', $strFileName);

      if(sizeof($arrFileParts) > 1) {
      if( in_array(strtolower($arrFileParts[sizeof($arrFileParts)-1]), $this->arrKnownImageExtensions ) != false ) {
      $return = true;
      }
      }
      }

      return $return;
      }

      Could probably improved a bit by combining one if-clause and maybe add some trim().

      cu, w0lf.

    • 11

      Yes – move_uploaded_file, in hindsight, would have been a better choice, however both do the job. Not destroying the image was an oversight – I’ve now added that in, thanks.

  4. 12

    Better off using $_FILES[‘image’][‘type’] to find image extension.

  5. 13

    This solution is cool, but I really suggest a more OOP and aggressive style. GD has helped me a lot of times, but it is now time to use more powerful tools, newer software. GD is from another era.
    It is a nice tutorial for GD newbs, but please consider trying to use stuff like Imagick, faster and more powerful.
    Also consider avoiding validation by file extension. It is an old habit of the Windows enviroment, which should be avoided in favour of mime type validation.
    I just point you to Zend_Validate_File_IsImage, which works quite well.

    Nice tutorial, good for newbs, but really disappointing from the perspective of “clean and reusable” :(

    Thank you for sharing anyway :)

    • 14

      Thanks, Marco – some good points there. With regards to OOP, I specifically avoided that as the article was aimed at beginner-to-intermediate users and OOP would have diverted their attention away from the main point, i.e. interactivity between JS and PHP, and more to OOP, which is more complex if you’re new to it.

  6. 15

    Awesome.

  7. 16

    Thanks for the positive feedback so far, guys. Bright Cherry – there was a reason I didn’t use this approach for getting file extension, but it eludes me now :)

    This tutorial is more about showing potential than being genuinely useful on a real site. I just wanted to show how jQuery and PHP GD are natural bed fellows for image interaction like this.

    • 17

      Philip Schweiger

      April 7, 2011 7:23 am

      A few months back I ran into this – I wanted to use mime checking, but ended up having to use extension checking instead. If I recall correctly, it was because the production server’s PHP didn’t support mime-checking – it was a point or two behind my development server. I can dig up the code in question if there’s interest in the details.

  8. 18

    Great tutorial, thanks you, I also like the comments which help me to see and work around the other options available.

  9. 19

    Is it just me or is the code in the section “Setting Up The Files” blank?

  10. 21

    You can also just use Transloadit.com, much easier :)

    • 22

      Bart, you’ve made my evening :) I came across the service before, but now it’s a timely reminder.

  11. 23

    A bug here: When you scroll down on a big image (too big for a, maybe small, screen) and try to make a selection, the selection start point is too high (not beneath the cursor).

    • 24

      any feedback on this issue?

      • 25

        Hi Jeroen – yes this is an issue. As I say, it’s not a fully-fledged Photoshop, it’s an example of what is possible. Ideally, images that are too big for the facility would be auto-zoomed out, but that was well beyond the scope of this tutorial.

  12. 26

    Not load CPU?

  13. 27

    Hm, the GD-Lib is very old and needs A LOT OF Memory. A 5MB jpeg will need ~50MB of Memory – this is to much for most shared hosters.

    I would recommend the Imagick functions instead. To be honest, on smashingmag are a lot of tutorials which teach to use very old functions, such as the mysql_-Function (PDO or mysqli are the better ways!).

    The PHP-Tuts here are not very useful when deprecated functions and methods where used.
    Sorry for my bad english :)

    • 28

      Daniel – GD is less preferable than Imagick, sure, but the latter is not as widely installed and is a more extensive (some might say complex) library. As I keep saying, the point of the article was to showcase how JS and PHP go well together to create interactivity. For simple cropping, GD is fine. For more ambitious image manipulation, sure, Imagick is certainly the way to go.

  14. 29

    Nice concept, thanks for sharing. Since this is such a long tutorial, I would suggest adding a wrap-up paragraph that summarized the process for the sake of clarity.

    Kind regards,
    VirtualFlavius

  15. 30

    As other people have already pointed out, I could upload any type of file with a .gif, .jpg, or .png extension, it doesn’t have to be an image.

    While checking the extension does limit the possibility of uploading a malicious file due to the fact that these files are served with image mime-types, on a shared hosting environment a security mistake like only checking the file extension could be the last one you’ll make, if you know what I mean.

    The only fool proof method of checking to see if the uploaded file is an image – use getimagesize(). It should return an array of two numeric values. If it returns false instead, then it’s not an image that’s being uploaded, regardless of the file extension.

    The mime-type-checking method is tricky as some raw digital camera jpg’s have uncommon mime-types and sometimes they don’t exist as you would think at all.

  16. 31

    Atleast test the script and re-read it before you post it. Theres alot of weird thing and cases of bad practise.
    HTML expects doublequotes
    If you want to teach people, atleast teach them the right things.

  17. 37

    Ugh. That PHP code is scary. Please, nobody actually put that on a live server.

    • 38

      Seconded.
      PHP security is an absolute must. And this code completely lacks it. Nonetheless, if somebody needs a tutorial to show them that:
      a) this is possible
      and
      b) how to do this
      Then they’re probably not skilled enough in PHP yet and risk making their site or someone’s site vulnerable to attacks–particularly if they don’t realize this code is insecure (which if the person is a novice in PHP, they won’t) and attempts to use it.

  18. 39

    A similar article on http://www.webmotionuk.co.uk/php-jquery-image-upload-and-crop-v11/ although a bit dated but covers more of the security issues using mime types that this article doesn’t.

  19. 40

    Hi, it was a good read at first, but it has too many flaws.
    To name a few:
    * If you draw the crop div and want to shrink the are you cannot.
    * If the image is long, left and top no longer match cursor position when starting drag.
    * Most of the code is unusable, but this might be because of the way Smashing Magazine site deals with code snippets.
    * PHP is really bad in a lot of ways. If you do show something to people show it all. Don’t just say you will only show a part of it.
    * Just checking extension is not enough, please DO check the MIME-Type that also comes with the $_FILES super-global.
    * Place this outside the switch statement as it’s the same regardless of case: $dest_ing=imagecreate($_GET[“crop_w”], $_GET[“crop_h”]);

    Good effort, but should have been more.

    • 41

      Hi Mr Mouse

      Thanks for your feedback. In response:

      1) Not sure what you mean here. Do remember it’s an introductory, demo tutorial, though, and not meant as an entire image manipulation suite.

      2) See 1.

      3) Yes – I’ve noticed this. The code was fine – some friends of mine did the tutorial before I posted it into the Smashing WP system, and everything worked fine. Please consult the source code files instead.

      4) Not sure what you mean here. What didn’t I show? I concede the file type checking should be better (perhaps this is why I’m a JS specialist!).

      5) See 4.

      6) Not quite – in the case of JPG, I use imagecreatetruecolor. For other GIF and PNG I use imagecreate.

  20. 42

    Hey Smashing Magazine !
    Can I please re-invent the concept of the wheel, the concept of hot water and the concept of the machin that curves bananas and post it in your blog as well ??

  21. 43

    Thank you Andy for exposing usage of jquery and PHP GD for image cropping. Thanks others who also pointed out potential flaws in code.

  22. 44

    Thanks for the tutorial. One question. Under “Adding Interactivity”, You write, “First, let’s extend the line we just added…” It’s not clear to me what the line you just added was i.e. what line we’re extending, so I don’t know where to put the Adding Interactivity code.

    Please explain if you can. Thanks

    • 45

      Hi fMike – sorry, I was referring to the code snippet immediately before that sentence, specifically the ‘else’ clause.

  23. 46

    Great resource !

  24. 47

    I would be pleased to see a summary of what we are going to do and how – only a list of key concepts of selecting the area to be cropped and cropping it. The article has tons of boilerplate code that’s trivial for semi-experienced developers.

    • 48

      Bence – as you are surely aware, a site like Smashing is read by people of varying proficiencices. So what you find boilerplate, others might find necessary. If I left out that code, I would equally be criticised for omitting it.

  25. 49

    Alexander Filatov

    April 10, 2011 11:14 pm

    @Andy
    I think it’s a great tutorial despite its lack of server-side security. Kudos for doing your best!

    @Everyone else who complains
    (and I’m not talking about constructive criticism)
    Why does it have to be in that form? Why not remove the negativity and instead help the author make the tutorial better by explaining what has been done wrong and how to fix it?
    All good authors around here are able to accept that they are wrong and modify the tutorial and you’ll be put in the honorary mention for helping!

    • 50

      Alexaneder – spasibo for your comments. Coders as a whole are not known for their social or communication skills. Criticism is therefore offered up in a sarcastic or otherwise vitriolic way, rather than keeping things civil! This character trait is the subject of countless comedies :)

  26. 51

    Of recent I used JCrop and PHP for image cropping and resampling and it was a breeze. All you need is 4 hidden fields for the w,h,x&y co-ordinates. And this js library updates those co-ordinates based on your selection of the cropped area. on submit send to server side and PHP resamples using the GD Library.

    If you need to work with PNGs with transparency set using the GD Library remember to set
    [code]

    //(where $canvas is the truecolor object)
    imagealphablending($canvas, false);
    imagesavealpha($canvas, true);


    [/code]

    to add the alpha channel to the image or the resampled image will have a black background

    Also on the options settings for JCrop set bgColor: ‘transparent’ to avoid showing a black background on the transparent PNG on display.

  27. 52

    Most of SM’s design related articles are really good, but for back-end programming, seems like you need a much more experienced writer.
    The comments on this article point many flaws that you should think about next time you post a server side code example. So, for the sake of your readers, please do.

  28. 53

    Nice work Andy! I am sure there are better ways for people to point out the flaws in the code other than the unnecessary negativity. Kudos to you Andy!

  29. 54

    Very nice solution, got it working and implemented it together with existing javascript framework and code. Got 100 lines of image manipulation for 3 fixed image sizes and working image of maximum 500 pixels after user upload. Try using the dragbox on camera uploads with 3000×2000 pixels…

    Pretty boring, these comments on secure image upload. Boy, are these guys clever, but can’t see the intention and environment of this excellent code.

    On HTML display of the images I use: “newimage.jpg?refresh=”.rand(0,999) wich will force the browser to get the newest image and not use it’s cache.

    One problem remains for me: the dragbox is not visible to the user, not ever, not in IE, Moz, or Chrome. I tried to tweak the CSS but no result. Could anybody share some insight or experience on this?

  30. 55

    How would you go about saving the image as just a png the end from whatever image type was initially uploaded?

  31. 56

    this is a beginner’s tutorial guys get over yourselves! go start a pro php blog if your so damn proficient. thanks ANDY! great work!

  32. 57

    With regard to security – yes it’s boring, but we know that noobs and the uninformed will just go ahead and use this code ‘as is’. Maybe something like
    image_type_to_mime_type(exif_imagetype($file);
    could give the mime type as it checks the actual file itself and not the extension.
    I think the tute is great for showing the concept +1.

  33. 58

    Hello there,
    I want to integrate an image editor in my site. In this feature, user should be able to upload images, put text over it, make sharp, blur or colourize it (like Photoshop).

    Is there any plugin available for it?

↑ Back to top