Table of Contents:
If you want to skip the tutorial and get right into the code, it's listed at the bottom of the page.
- img_ctrl.php - The image handling class.
- img_ctrl_test.php - The public display of saved images.
- update/index.php - When a user clicks "Submit" on our form, data is sent here.
The Problem:
If you've ever tried to build a content management system (CMS) or attempted to allow a user to post information to a site, you've probably experienced a lot of the difficulty involved with user-uploaded images. When I built my first CMS, I simply uploaded photos from a form, stored the image in a folder on the site's server, and saved the path in a database.
This approach had issues, however, when outputting the saved image on a page. First off, I had no idea how large the image was, so if the user had uploaded a wallpaper graphic at 1024x768, it would ruin my CSS layout that allowed 520px for image content.
To try and fix this, I decided to manually set the
<img> tag's
width and
height attributes. This, however, presented even more problems.
Now when the image was displayed, the aspect ratio was messed up in some photos. Additionally, some images (namely larger graphics) were taking a really long time to load, even though they were small on screen. Even worse, some of my images came out really pixelated, and other ones had very strange distortion (the awkward rendering that browsers will perform with a scaled down large image).
And on top of all this, my images weren't clearing validation because I didn't have an
alt attribute specified. I could fake it by determining an
alt attribute to apply to all images, but that doesn't really help when viewing a page in text-only browsers or in image searches.
This was getting
really frustrating...
So how did I fix it? I did some research and found
PHP's GD library. After tinkering around for a bit, I wrote a class that could easily resize, resample, and save user-uploaded images on the server (
View the demo here).
The Solution:
Let's define the problem more clearly:
we need to create a class that will allow us to obtain information about uploaded images, then use that information to resize, resample, and store the new image, creating a format-friendly, good-looking, and quick-to-load image. Additionally, we need to save the path to our new image, as well as a user-defined alt attribute, in a database to make the information easily accessible.
How do we do it?
Necessary Tools and Skills
I'm assuming for the purposes of this tutorial that you have a basic understanding of PHP and MySQL. You'll also need a server that supports PHP and access to a MySQL database.
Collect the Pieces
We will be creating a PHP class to contain all the functions of our new and improved image handler. We'll call this class
img_ctrl and save it in a PHP file of the same name.
Let's define all of the steps that we need to take and break those into individual functions. That allows us to take on the problem in sections, which always helps me keep myself organized.
Our class needs to do the following:
- Create a form that allows the user to upload a photo and a caption
- Process the form to resize and resample the image, save the image on the server, and store the URL and caption in the database
- Display the image on the desired page as valid XHTML
Create the Class
Our first task is to simply initiate the class:
class img_ctrl {
}
That was easy enough. Next, let's add our variables and our constructor class:
class img_ctrl {
var $max_width = 510;
var $max_height = 400;
var $alt;
var $path = 'img/userPics/';
function img_ctrl () {
}
Let's break this down. Our first two variables,
$max_width and
$max_height, are the most vital pieces of this class; without them, we wouldn't be able to resize our photos. These variables contain our maximum values for image dimensions.
Next, we have our
$alt variable. This will contain the photo caption as defined by the user.
$path contains the path to the folder where the photos will be stored.
For the moment, our constructor function is empty. If the need arises, we'll add functionality later on.
Now, on to the fun part!
Create the Form
To allow our users to upload an image, we're going to use a basic HTML form. Let's write the function, then I'll explain it below:
public function display_admin ( $max_width=600, $max_height=400, $path='img/userPics/' ) {
$this->max_width = $max_width;
$this->max_height = $max_height;
$this->path = $path;
$formdisp = <<<______EOD
<!--// Begin Ennui Design's Image Processing HTML //-->
<h2 class="img_upload_head">Upload a Photo</h2>
<form id="img_upload"
action="update/?action=img_upload"
method="post"
enctype="multipart/form-data">
<div class="img_uploader">
<label for="image">Image:</label>
<input type="file"
name="image"
id="image" />
<label for="alt">Description:</label>
<input type="text"
id="alt"
name="alt" />
<input type="hidden"
name="max_w"
value="{$this->max_width}" />
<input type="hidden"
name="max_h"
value="{$this->max_height}" />
<input type="hidden"
name="path"
value="{$this->path}" />
<input type="submit"
value="Upload!" />
</div>
</form>
<!--// End Ennui Design's Image Processing HTML //-->
______EOD;
return $formdisp;
}
Alright, let's take it from the top. In our function declaration, we're including three parameters:
$max_width,
$max_height, and
$path. I made the decision to include default values as a shortcut and a safeguard (if I forget to declare my path, now I can be sure the uploaded file won't end up in my root directory).
Next, we set our class's variables equal to the parameters passed by the function call, using the
$this call.
Our last step for this piece is the form itself. Using one of my favorite PHP features, heredoc syntax, we build the form using valid XHTML and store it in a variable,
$formdisp, to be returned after the function runs.
There are five inputs in our form: one file input to accept the image upload, one text input to accept the caption, and three hidden inputs to pass our height, width, and path parameters for processing.
Feeling good so far? Let's get into processing the image.
Process the Input
Building this piece is a little tricky, so I decided to break it into three pieces. I have my function
upload to take all the variables from my form, create a filename, check that my directories exist (and create them if they don't!), and send the uploaded image off to be processed. It also provides the option to store this information in a database we'll create a little later.
Ready to get coding?
Let's start with the function call: we pass three parameters,
$upload,
$post, and
$store, containing the uploaded image, the caption and other information passed via the
$_POST array, and instructions on whether or not to store the information in the database, respectively.
public function upload ( $upload, $post=NULL, $store=false ) {
Next, we check all the values in
$post, setting the class variable to their values if they are set. We also take the values in
$upload and break them into easy-to-manage variables.
$this->alt = $post['alt'];
if ( isset($post['max_w']) )
$this->max_width = $post['max_w'];
if ( isset($post['max_h']) )
$this->max_height = $post['max_h'];
if ( isset($post['path']) )
$this->path = $post['path'];
$name = $upload['image']['name'];
$tmp = $upload['image']['tmp_name'];
$size = $upload['image']['size'];
$type = $upload['image']['type'];
$err = $upload['image']['error'];
Now we're almost ready to go. First, as a safeguard, we check to make sure that the file passed was, in fact, an image, that the file is less than 2MB in size, and that there were no errors with it.
if ( $type == 'image/jpeg'
|| $type == 'image/pjpeg'
|| $type == 'image/gif'
|| $type == 'image/png'
&& $size <= 1024*1024*2 ) {
if ( $err > 0 ) {
die( "Error: {$err}" );
} else {
Now we're ready to roll! The first task is to create a file name. To make sure I don't accidentally overwrite user input, I opted not to keep the filename supplied by the user. Instead, I use the current timestamp and a random number between 10,000 and 99,9999. The timestamp alone should be enough to avoid duplicate filenames, but it never hurts to add an extra measure of certainty.
$img_name = time() . '_' . rand(10000,99999);
Now that we have our filename, we store the path in our variable
$loc. Something to note is that I chose to use PHP's
imagejpeg() to output final images, so I append the ".jpg" extension to the filename. You have the option, if you choose, to swap out JPG for GIF or PNG (
you can find out more about imagejpeg() and the other image functions in the PHP manual).
$loc = $this->path . $img_name . '.jpg';
Our code now checks to see if the path leads to an existing folder. If not, it uses the function
mkdir() to create the folder.
NOTE: Keep in mind that this will only make one folder at a time, so if neither 'img/' nor 'userPics/' exist on the server, mkdir() will throw a fatal error.
if ( !is_dir('../'.$this->path) && strlen($this->path) > 0 )
mkdir('../'.$this->path) or die("Could not create the directory '{$this->path}'.");
After ensuring that our directory exists, we'll move our uploaded image there. This is so we can find it easily for processing a little later on.
move_uploaded_file( $tmp, '../'.$loc ) or die("Could not move the image.");
Now we send the file off to process. We haven't built this function yet, but I know that it will return a
true or
false output, so we'll set up our code to check for a successful execution. We also pass the location and file type to the
process function, which we'll explain in a moment.
if ( $this->process( '../'.$loc, $type ) === true ) {
After confirming the successful execution of
process, we check to see if the
$store variable is set to save the information to our database.
if ( $store === true ) {
$this->store($loc);
return true;
}
We're pretty much done here, except for finishing a few if...else statements. The first one returns the value of
$loc if the variable
$store is set to
false. I haven't really touched on why you might want to do this, so let's take a moment to explain.
This class is very useful by itself, but it's much more powerful when it's run in tandem with another class or set of classes. For instance, I use this class together with my
blog class to resize and resample all of the images I upload to my blog. However, I have a different database for my blogs, and I store my image path in
that database. In order to avoid redundant storage, I have the
img_ctrl class simply return the image path for storage elsewhere, which is exactly what the function
upload will do if the variable
$store is set to
false.
else return $loc;
If any errors were encountered during the running of our
upload function, we return
false.
Alright, so we know how to name our file and create a directory for it. Let's get down to business and resize this sucker.
Start by declaring our function
process and passing the parameters
$loc and
$type. Also, note that this function is private. That means it can only be called from within the class itself, which is important, since this function will only work if it's called from the function
upload.
private function process ( $loc, $type ) {
Now we run a
switch statement to properly handle our uploaded image. We could simply run
imagecreatefromstring() here, but it has a significant negative effect on performance, so if we can help it, we'll use a targeted function.
switch ( $type ) {
case 'image/gif':
$src_img = imagecreatefromgif($loc);
break;
case 'image/jpeg':
$src_img = imagecreatefromjpeg($loc);
break;
case 'image/pjpeg': // This is to handle an IE quirk.
$src_img = imagecreatefromjpeg($loc);
break;
case 'image/png':
$src_img = imagecreatefrompng($loc);
break;
default:
$src_img = imagecreatefromstring(file_get_contents($loc));
break;
}
Once we've figured out what kind of image we're dealing with and created an editable instance of it, we can start getting our dimensions ready. Our first step is to figure out what the original size of the uploaded image is. We can do this easily using PHP's built-in
getimagesize() function and loading the
height and
width into two variables:
$src_w and
$src_h.
$src_info = getimagesize($loc);
$src_w = $src_info[0];
$src_h = $src_info[1];
Now we need to figure out if the image is bigger than our maximum dimensions. We've all seen what happens when an image gets blown up bigger than it was meant to be, and we don't want our code to be responsible for pixelated images.
if ( $src_w > $this->max_width
|| $src_h > $this->max_height ) {
The next step is to figure out which side of our image is longer. Then we use the long side to figure out the aspect ratio, or the ratio of our maximum length to our original length.
if ( $src_h >= $src_w ) {
$aspect = $this->max_height / $src_h;
} else {
$aspect = $this->max_width / $src_w;
}
Now that we have our aspect ratio stored in the variable
$aspect, we can multiply both the
height and
width of our original image by the aspect ratio, resulting in an image that is at the maximum allowed length for one side, and the proportional length for the other side, so we avoid any distortion in our new image. If the image is smaller than our maximum size, we simple set our maximum values to match the smaller image.
$new_w = intval($src_w * $aspect);
$new_h = intval($src_h * $aspect);
} else {
$new_w = $src_w;
$new_h = $src_h;
}
This next bit of code is where all the work is actually done. This code, in short, creates an image container at our specified dimensions, then takes our source image and resamples it to fit inside the new image, and finally outputs the new image as a JPG file into the location we specified all the way back in our
upload function.
$new_img = imagecreatetruecolor($new_w,$new_h);
imagecopyresampled($new_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, $src_w, $src_h);
imagejpeg($new_img, $loc);
How neat is that?!
We're getting pretty close, here. Let's put the finishing touches on our processing suite of functions by creating the database for storage and writing the code to actually store the image.
The database function, creatively named
buildDB() does exactly what you'd expect. It checks to see if the database exists and, if not, builds it.
private function buildDB() {
$sql = "CREATE TABLE IF NOT EXISTS imgMgr\n";
$sql .= "(pid int(9) PRIMARY KEY auto_increment\n"; // Unique ID for the image.
$sql .= ",url varchar(150)\n"; // Location of image on server.
$sql .= ",alt varchar(150)\n"; // Alt attribute of the image (for text-only browsers).
$sql .= ")";
if ( !mysql_query($sql) )
die(mysql_error());
}
Our function
store is also fairly simple. It takes the the path to our processed image and the
alt text supplied by the user and places them in a database with a unique ID.
private function store($loc) {
$q = "INSERT INTO imgMgr VALUES ("
. "'',"
. "'{$loc}',"
. "'{$this->alt}'"
. ")";
if ( mysql_query($q) )
return true;
else
return false;
}
One last step: our form calls the page
upload/?action=img_upload, so we need to have that page in place. Essentially, it connects to the database, creates a new instance of our class, processes the posted image, stores it to the database, and sends us to a destination based on the success or failure of the process. There's also an
__autoload function at the bottom. This is so you can run multiple classes without needing to specifically include each file. Here's the code:
<?php
include_once('../path/to/your/db/login/code.php');
if ( $_GET['action'] == 'img_upload' ) {
$img = new img_ctrl();
if ( $img->upload($_FILES,$_POST,true) )
$header = 'Location: ../img_ctrl_test.php?error=false';
else
$header = 'Location: ../img_ctrl_test.php?error=true';
header($header);
}
function __autoload($class_name) {
require_once '../_class/' . $class_name . '.php';
}
?>
That's it! We're ready to show these images to the world!
Displaying the Images
All that's left to do, now, is create a few valid lines of XHTML to display our uploaded images publicly. I won't get into anything too advanced here; this is a very basic display of a select number of recent user-uploaded images. The most important thing here is that the code stored in the variable
$img is strict XHTML valid code, which then is wrapped in a div for further formatting control.
public function display_public ( $numrows=1 ) {
// Get the latest images from the database.
$q = "SELECT * FROM imgMgr ORDER BY pid DESC LIMIT {$numrows}";
$r = mysql_query($q) or die(mysql_error());
// Use a while loop to format all selected photos.
while ( $a = mysql_fetch_assoc($r) ) {
$img .= <<<__________EOD
<div class="img_disp">
<img src="{$a['url']}"
alt="{$a['alt']}" />
</div>
__________EOD;
}
$img_disp = <<<________EOD
<!--// Begin Ennui Design's Image Disply HTML //-->
<h2 class="img_disp_head">User-Uploaded Images</h2>
<div class="img_disp_cont">{$img}
</div>
<!--// End Ennui Design's Image Display HTML //-->
________EOD;
return $img_disp;
}
That's it! You've successfully created a module to resize, resample, and display user-uploaded images on the fly!
Check out the demo to see this class in action, then look below to get the full source code and implement this feature on your page! Make sure to leave links in the comments!
img_ctrl.php
<?php
//----------------------------------------------------------
// Image Processor by Ennui Design.
// www.EnnuiDesign.com | answers@ennuidesign.com
//----------------------------------------------------------
class img_ctrl {
var $max_width = 510;
var $max_height = 400;
var $alt;
var $path = 'img/userPics/';
function img_ctrl () {
$this->buildDB();
}
public function display_admin ( $max_width=600, $max_height=400, $path='img/userPics/' ) {
$this->max_width = $max_width;
$this->max_height = $max_height;
$this->path = $path;
$formdisp = <<<______EOD
<!--// Begin Ennui Design's Image Processing HTML //-->
<h2 class="img_upload_head">Upload a Photo</h2>
<form id="img_upload"
action="update/?action=img_upload"
method="post"
enctype="multipart/form-data">
<div class="img_uploader">
<label for="image">Image:</label>
<input type="file"
name="image"
id="image" />
<label for="alt">Description:</label>
<input type="text"
id="alt"
name="alt" />
<input type="hidden"
name="max_w"
value="{$this->max_width}" />
<input type="hidden"
name="max_h"
value="{$this->max_height}" />
<input type="hidden"
name="path"
value="{$this->path}" />
<input type="submit"
value="Upload!" />
</div>
</form>
<!--// End Ennui Design's Image Processing HTML //-->
______EOD;
return $formdisp;
}
public function display_public ( $numrows=1 ) {
// Get the latest images from the database.
$q = "SELECT * FROM imgMgr ORDER BY pid DESC LIMIT {$numrows}";
$r = mysql_query($q) or die(mysql_error());
// Use a while loop to format all selected photos.
while ( $a = mysql_fetch_assoc($r) ) {
$img .= <<<__________EOD
<div class="img_disp">
<img src="{$a['url']}"
alt="{$a['alt']}" />
</div>
__________EOD;
}
$img_disp = <<<________EOD
<!--// Begin Ennui Design's Image Disply HTML //-->
<h2 class="img_disp_head">User-Uploaded Images</h2>
<div class="img_disp_cont">{$img}
</div>
<!--// End Ennui Design's Image Display HTML //-->
________EOD;
return $img_disp;
}
public function upload ( $upload, $post=NULL, $store=false ) {
$this->alt = $post['alt'];
if ( isset($post['max_w']) )
$this->max_width = $post['max_w'];
if ( isset($post['max_h']) )
$this->max_height = $post['max_h'];
if ( isset($post['path']) )
$this->path = $post['path'];
$name = $upload['image']['name'];
$tmp = $upload['image']['tmp_name'];
$size = $upload['image']['size'];
$type = $upload['image']['type'];
$err = $upload['image']['error'];
if ( $type == 'image/jpeg'
|| $type == 'image/pjpeg'
|| $type == 'image/gif'
|| $type == 'image/png'
&& $size <= 1024*1024*2 ) {
if ( $err > 0 ) {
die( "Error: {$err}" );
} else {
// To ensure a unique filename, we'll use the
// current UNIX timestamp plus a random five-
// digit number.
$img_name = time() . '_' . rand(10000,99999);
// We need to determine the path and filename
// for this image to make sure we can find it
// again after processing.
// NOTE: The 'process' function always outputs
// a JPG file, so we'll automatically append
// that file extension.
$loc = $this->path . $img_name . '.jpg';
// Create the directory 'userPics' if it doesn't
// already exist.
if ( !is_dir('../'.$this->path) && strlen($this->path) > 0 )
mkdir('../'.$this->path) or die("Could not create the directory '{$this->path}'.");
// Put the temporary image somewhere easily accessible
move_uploaded_file( $tmp, '../'.$loc ) or die("Could not move the image.");
// Now we process the file!
if ( $this->process( '../'.$loc, $type ) === true ) {
// If the image is successfully processed,
// return the location for handling.
if ( $store === true ) {
$this->store($loc);
return true;
}
else return $loc;
} else return false;
}
} else return false;
}
private function process ( $loc, $type ) {
// For a lighter server load, we run a switch to figure out
// what image file type was uploaded.
switch ( $type ) {
case 'image/gif':
$src_img = imagecreatefromgif($loc);
break;
case 'image/jpeg':
$src_img = imagecreatefromjpeg($loc);
break;
case 'image/pjpeg':
$src_img = imagecreatefromjpeg($loc);
break;
case 'image/png':
$src_img = imagecreatefrompng($loc);
break;
default:
$src_img = imagecreatefromstring(file_get_contents($loc));
break;
}
// We need to figure out the dimensions of our original image.
$src_info = getimagesize($loc);
$src_w = $src_info[0];
$src_h = $src_info[1];
// Now we need to resize the image to fit within our $max_width
// and $max_height parameters while keeping the image proportional.
if ( $src_w > $this->max_width
|| $src_h > $this->max_height ) {
if ( $src_h >= $src_w ) {
$aspect = $this->max_height / $src_h;
} else {
$aspect = $this->max_width / $src_w;
}
$new_w = intval($src_w * $aspect);
$new_h = intval($src_h * $aspect);
} else {
$new_w = $src_w;
$new_h = $src_h;
}
// Now we've figured out the proper dimensions for our image. All
// that's left to do is resample it!
$new_img = imagecreatetruecolor($new_w,$new_h);
imagecopyresampled($new_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, $src_w, $src_h);
imagejpeg($new_img, $loc);
return true;
}
private function store($loc) {
$q = "INSERT INTO imgMgr VALUES ("
. "'',"
. "'{$loc}',"
. "'{$this->alt}'"
. ")";
if ( mysql_query($q) )
return true;
else
return false;
}
private function buildDB() {
// Send a query to the DB that checks for the existence of the
// table, then creates it if it doesn't already exist.
$sql = "CREATE TABLE IF NOT EXISTS imgMgr\n";
$sql .= "(pid int(9) PRIMARY KEY auto_increment\n"; // Unique ID for the image.
$sql .= ",url varchar(150)\n"; // Location of image on server.
$sql .= ",alt varchar(150)\n"; // Alt attribute of the image (for text-only browsers).
$sql .= ")";
// If our query fails, stop the execution and display an error message.
if ( !mysql_query($sql) )
die(mysql_error());
}
}
?>
img_ctrl_test.php
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Tutorial: Handling User-Uploaded Images (demo) | Ennui Design</title>
<style type="text/css">
body {
text-align:center;
background:url('../img/ennui_2.0.1_bg.jpg') top center repeat-y;
font:85%/1.25em 'Century Gothic',sans-serif;
}
h2 {
background-color:#CFCFCF;
border:1px solid #CFCFCF;
padding:10px;
margin:0px auto;
width:510px;
}
div.img_uploader {
text-align:left;
margin:0px auto;
margin-bottom:30px;
padding:10px;
width:510px;
border:1px solid #CFCFCF;
}
div.img_uploader label {
float:left;
width:200px;
text-align:right;
font-weight:bold;
margin:5px 10px 5px 0;
}
div.img_uploader input {
margin:5px 0 5px 210px;
display:block;
}
div.img_disp_cont {
width:510px;
margin:0px auto;
padding:10px;
border:1px solid #CFCFCF;
}
div.img_disp {
margin:10px 0 5px 0;
}
</style>
</head>
<body>
<?php
include_once('../path/to/your/db/login/code.php');
require '_class/img_ctrl.php';
$img = new img_ctrl();
echo $img->display_admin(500,360);
echo $img->display_public(3);
?>
</body>
</html>
update/index.php
<?php
include_once('../path/to/your/db/login/code.php');
if ( $_GET['action'] == 'img_upload' ) {
$img = new img_ctrl();
if ( $img->upload($_FILES,$_POST,true) )
$header = 'Location: ../img_ctrl_test.php?error=false';
else
$header = 'Location: ../img_ctrl_test.php?error=true';
header($header);
}
function __autoload($class_name) {
require_once '../_class/' . $class_name . '.php';
}
?>