Image manipulation in Laravel 4 with Imagine...

Laravel
written by Boris Strahija on July 15, 2013 in Blog with 63 comments

Laravel 4 already uses many 3rd party packages provided by the community, so there’s really no reason to rewrite things if they are already good enough. I’ve been using the Imagine library for quite some time now, and it served me very well. So in this short tutorial I wanted to share the way I like to upload and manipulate images on sites I build using the Imagine library.

First of all I’m going to assume you know how to install and setup Laravel 4, and also add 3rd party packages via Composer. So go ahead and install Laravel 4:

composer create-project laravel/laravel

Now edit your composer.json file, add the imagine library and continue on running:

composer update

Next we will create a “wrapper” for our image manipulation class. I like to put classes like these into a “services” directory:

app/services/Image.php

<?php namespace App\Services;

class Image {

}

Now if we want to use the class in a “Laravel” way, we need a facade, so let’s create that also:

app/facades/ImageFacade.php

<?php namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class ImageFacade extends Facade {

	protected static function getFacadeAccessor()
	{
		return new \App\Services\Image;
	}

}

Now we also need an alias to use the class in our view easily.

app/config/app.php

'Image' => 'App\Facades\ImageFacade',

For this to actually work, we need to autoload the classes. This is done by adding the directories “app/facades” and “app/services” to our autoload classmap inside the composer.json file.

"app/services",
"app/facades"

The last thing to do is running “composer dump-autoload”.

I know this seems a lot of work for setting up a helper class like that, but once do this setup, you can add more services really easy.

Ok, so now we have are classes setup and ready to use, so lets go into creating the helper methods that we’ll use in our app. I personally use the helper mostly for resizing the images, so let’s create the resize() method. This method will take the path to the original image as a parameter, and also a couple of other parameters like the width and height, the compression quality, if the image should be cropped, etc. But first let’s setup and initialize our class:

app/services/Image.php

<?php namespace App\Services;

use Config, File, Log;

class Image {

	/**
	 * Instance of the Imagine package
	 * @var Imagine\Gd\Imagine
	 */
	protected $imagine;

	/**
	 * Type of library used by the service
	 * @var string
	 */
	protected $library;

	/**
	 * Initialize the image service
	 * @return void
	 */
	public function __construct()
	{
		if ( ! $this->imagine)
		{
			$this->library = Config::get('image.library', 'gd');

			// Now create the instance
			if     ($this->library == 'imagick') $this->imagine = new \Imagine\Imagick\Imagine();
			elseif ($this->library == 'gmagick') $this->imagine = new \Imagine\Gmagick\Imagine();
			elseif ($this->library == 'gd')      $this->imagine = new \Imagine\Gd\Imagine();
			else                                 $this->imagine = new \Imagine\Gd\Imagine();
		}
	}

}

As you can see the contructor simply creates an instance of the Imagine library based on a setting from our configuration. For this to actually work, we need to create the configuration file that’s gonna hold some defaults for the type of library we’re gonna use (GD/ImageMagick…), some paths, etc.

app/config/image.php

return array(
	'library'     => 'imagick',
	'upload_dir'  => 'uploads',
	'upload_path' => public_path() . '/uploads/',
	'quality'     => 85,
);

And now the actual resizing :) We also want to cache/save the resized images somewhere so well put that also inside our resize method.

The method will look like this:

/**
 * Resize an image
 * @param  string  $url
 * @param  integer $width
 * @param  integer $height
 * @param  boolean $crop
 * @return string
 */
public function resize($url, $width = 100, $height = null, $crop = false, $quality = 90)
{
	if ($url)
	{
		// URL info
		$info = pathinfo($url);

		// The size
		if ( ! $height) $height = $width;

		// Quality
		$quality = Config::get('image.quality', $quality);

		// Directories and file names
		$fileName       = $info['basename'];
		$sourceDirPath  = public_path() . '/' . $info['dirname'];
		$sourceFilePath = $sourceDirPath . '/' . $fileName;
		$targetDirName  = $width . 'x' . $height . ($crop ? '_crop' : '');
		$targetDirPath  = $sourceDirPath . '/' . $targetDirName . '/';
		$targetFilePath = $targetDirPath . $fileName;
		$targetUrl      = asset($info['dirname'] . '/' . $targetDirName . '/' . $fileName);

		// Create directory if missing
		try
		{
			// Create dir if missing
			if ( ! File::isDirectory($targetDirPath) and $targetDirPath) @File::makeDirectory($targetDirPath);

			// Set the size
			$size = new \Imagine\Image\Box($width, $height);

			// Now the mode
			$mode = $crop ? \Imagine\Image\ImageInterface::THUMBNAIL_OUTBOUND : \Imagine\Image\ImageInterface::THUMBNAIL_INSET;

			if ( ! File::exists($targetFilePath) or (File::lastModified($targetFilePath) < File::lastModified($sourceFilePath)))
			{
				$this->imagine->open($sourceFilePath)
				              ->thumbnail($size, $mode)
				              ->save($targetFilePath, array('quality' => $quality));
			}
		}
		catch (\Exception $e)
		{
			Log::error('[IMAGE SERVICE] Failed to resize image "' . $url . '" [' . $e->getMessage() . ']');
		}

		return $targetUrl;
	}
}

A little explanation what’s going on here. First we take the URL of the image that needs to be resized and some info on the URL with the pathinfo() method. Then we create a directory based on the width, height and crop flag. So if our image resides in “public/uploads/article/1/chuck.jpg” and we call the resize method like this:

Image::resize('uploads/article/1/chuck.jpg', 200, 200);

The image will be resized and saved to “public/uploads/article/1/200×200/chuck.jpg”. And if we pass the “crop” parameter, a “_crop” sufix will be added: “public/uploads/article/1/200x200_crop/chuck.jpg”. I hope this all makes sense until know. If you need to know how exactly the Imagine library works to make your own changes in the helper, consults their docs.

One nice thing built is that the $height value defaults to the $width value, so if you want a rectangular resized image, just don’t specify the $height:

Image::resize('uploads/article/1/chuck.jpg', 200);

And since the resize method doesn’t crop the images by default, and most of the times when creating thumbnails we need cropped images, we’ll create a simple helper method:

/**
* Helper for creating thumbs
* @param string $url
* @param integer $width
* @param integer $height
* @return string
*/
public function thumb($url, $width, $height = null)
{
	return $this->resize($url, $width, $height, true);
}

This method simply sets the crop parameter to true, so if we want a rectangular thumbnail 80×80 px, we just call:

Image::thumb('uploads/article/1/chuck.jpg', 80);

And the image “public/uploads/article/1/80x80_crop/chuck.jpg” will be created. Also note that the image wont be created every time, but only if it doesn’t exist or if the original image is newer that the resized image. You could also add a flag to the configuration to always overwrite and create the images if you want, shouldn’t be to difficult.

Uploading

One more thing that almost all apps need is image uploading, and our helper could also handle that. But consider also that resizing images when the user accesses the site isn’t ideal, it could take quite long if your page has a lot of images that haven’t been created yet. So we’ll need to create those copies in different sizes when uploading the images. This could also take some time, so you can consider using queues here, which would really be the best solution, but since I want to keep it simple we’ll just do it right after the image has been uploaded.

I wont go into the details of uploading files, there are a lot of tutorials on this topic. Also one of mine over at Codeforest ;) but basically you do something like this (after validation) in your controller:

Image::upload(Input::file('image'), 'articles/' . $article->id, true);

As you can see we’re calling the upload() method from our image helper, but since we didn’t create it, let’s do it now.

/**
 * Upload an image to the public storage
 * @param  File $file
 * @return string
 */
public function upload($file, $dir = null, $createDimensions = false)
{
	if ($file)
	{
		// Generate random dir
		if ( ! $dir) $dir = str_random(8);

		// Get file info and try to move
		$destination = Config::get('image.upload_path') . $dir;
		$filename    = $file-&gt;getClientOriginalName();
		$path        = Config::get('image.upload_dir') . '/' . $dir . '/' . $filename;
		$uploaded    = $file-&gt;move($destination, $filename);

		if ($uploaded)
		{
			if ($createDimensions) $this->createDimensions($path);

			return $path;
		}
	}
}

So when the image has been successfully uploaded, we call the createDimensions() method to resize and cache the image to all the dimensions. The thing is we need to define those dimensions somewhere, so we’ll do that inside our config file.

app/config/image.php

return array(
	'library'     => 'imagick',
	'upload_path' => public_path() . '/uploads/',
	'quality'     => 85,

	'dimensions' => array(
		'thumb'  => array(100, 100, true,  80),
		'medium' => array(600, 400, false, 90),
	),
);

Simple as that. Now we just need to create the missing createDimensions() method that will take those dimensions from the config file, loop through each dimension and create the resized versions of the image.

/**
 * Creates image dimensions based on a configuration
 * @param  string $url
 * @param  array  $dimensions
 * @return void
 */
public function createDimensions($url, $dimensions = array())
{
	// Get default dimensions
	$defaultDimensions = Config::get('image.dimensions');

	if (is_array($defaultDimensions)) $dimensions = array_merge($defaultDimensions, $dimensions);

	foreach ($dimensions as $dimension)
	{
		// Get dimmensions and quality
		$width   = (int) $dimension[0];
		$height  = isset($dimension[1]) ?  (int) $dimension[1] : $width;
		$crop    = isset($dimension[2]) ? (bool) $dimension[2] : false;
		$quality = isset($dimension[3]) ?  (int) $dimension[3] : Config::get('image.quality');

		// Run resizer
		$img = $this->resize($url, $width, $height, $crop, $quality);
	}
}

You can pass an array of custom dimensions to this method, and it will merge those with the default dimensions from the config file, loop through them, and resize the image.

This is it, let me know if you like or dislike the article, what can be done better, how you do it, etc.

Cheers!