1<?php defined('SYSPATH') OR die('No direct script access.');
2/**
3 * Support for image manipulation using [Imagick](http://php.net/Imagick).
4 *
5 * @package    Kohana/Image
6 * @category   Drivers
7 * @author     Tamas Mihalik tamas.mihalik@gmail.com
8 * @copyright  (c) 2009-2012 Kohana Team
9 * @license    http://kohanaphp.com/license.html
10 */
11class Kohana_Image_Imagick extends Image {
12
13	/**
14	 * @var  Imagick  image magick object
15	 */
16	protected $im;
17
18	/**
19	 * Checks if ImageMagick is enabled.
20	 *
21	 * @throws  Kohana_Exception
22	 * @return  boolean
23	 */
24	public static function check()
25	{
26		if ( ! extension_loaded('imagick'))
27		{
28			throw new Kohana_Exception('Imagick is not installed, or the extension is not loaded');
29		}
30
31		return Image_Imagick::$_checked = TRUE;
32	}
33
34	/**
35	 * Runs [Image_Imagick::check] and loads the image.
36	 *
37	 * @return  void
38	 * @throws  Kohana_Exception
39	 */
40	public function __construct($file)
41	{
42		if ( ! Image_Imagick::$_checked)
43		{
44			// Run the install check
45			Image_Imagick::check();
46		}
47
48		parent::__construct($file);
49
50		$this->im = new Imagick;
51		$this->im->readImage($file);
52
53		if ( ! $this->im->getImageAlphaChannel())
54		{
55			// Force the image to have an alpha channel
56			$this->im->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
57		}
58	}
59
60	/**
61	 * Destroys the loaded image to free up resources.
62	 *
63	 * @return  void
64	 */
65	public function __destruct()
66	{
67		$this->im->clear();
68		$this->im->destroy();
69	}
70
71	protected function _do_resize($width, $height)
72	{
73		if ($this->im->scaleImage($width, $height))
74		{
75			// Reset the width and height
76			$this->width = $this->im->getImageWidth();
77			$this->height = $this->im->getImageHeight();
78
79			return TRUE;
80		}
81
82		return FALSE;
83	}
84
85	protected function _do_crop($width, $height, $offset_x, $offset_y)
86	{
87		if ($this->im->cropImage($width, $height, $offset_x, $offset_y))
88		{
89			// Reset the width and height
90			$this->width = $this->im->getImageWidth();
91			$this->height = $this->im->getImageHeight();
92
93			// Trim off hidden areas
94			$this->im->setImagePage($this->width, $this->height, 0, 0);
95
96			return TRUE;
97		}
98
99		return FALSE;
100	}
101
102	protected function _do_rotate($degrees)
103	{
104		if ($this->im->rotateImage(new ImagickPixel('transparent'), $degrees))
105		{
106			// Reset the width and height
107			$this->width = $this->im->getImageWidth();
108			$this->height = $this->im->getImageHeight();
109
110			// Trim off hidden areas
111			$this->im->setImagePage($this->width, $this->height, 0, 0);
112
113			return TRUE;
114		}
115
116		return FALSE;
117	}
118
119	protected function _do_flip($direction)
120	{
121		if ($direction === Image::HORIZONTAL)
122		{
123			return $this->im->flopImage();
124		}
125		else
126		{
127			return $this->im->flipImage();
128		}
129	}
130
131	protected function _do_sharpen($amount)
132	{
133		// IM not support $amount under 5 (0.15)
134		$amount = ($amount < 5) ? 5 : $amount;
135
136		// Amount should be in the range of 0.0 to 3.0
137		$amount = ($amount * 3.0) / 100;
138
139		return $this->im->sharpenImage(0, $amount);
140	}
141
142	protected function _do_reflection($height, $opacity, $fade_in)
143	{
144		// Clone the current image and flip it for reflection
145		$reflection = $this->im->clone();
146		$reflection->flipImage();
147
148		// Crop the reflection to the selected height
149		$reflection->cropImage($this->width, $height, 0, 0);
150		$reflection->setImagePage($this->width, $height, 0, 0);
151
152		// Select the fade direction
153		$direction = array('transparent', 'black');
154
155		if ($fade_in)
156		{
157			// Change the direction of the fade
158			$direction = array_reverse($direction);
159		}
160
161		// Create a gradient for fading
162		$fade = new Imagick;
163		$fade->newPseudoImage($reflection->getImageWidth(), $reflection->getImageHeight(), vsprintf('gradient:%s-%s', $direction));
164
165		// Apply the fade alpha channel to the reflection
166		$reflection->compositeImage($fade, Imagick::COMPOSITE_DSTOUT, 0, 0);
167
168		// NOTE: Using setImageOpacity will destroy alpha channels!
169		$reflection->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA);
170
171		// Create a new container to hold the image and reflection
172		$image = new Imagick;
173		$image->newImage($this->width, $this->height + $height, new ImagickPixel);
174
175		// Force the image to have an alpha channel
176		$image->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
177
178		// Force the background color to be transparent
179		// $image->setImageBackgroundColor(new ImagickPixel('transparent'));
180
181		// Match the colorspace between the two images before compositing
182		$image->setColorspace($this->im->getColorspace());
183
184		// Place the image and reflection into the container
185		if ($image->compositeImage($this->im, Imagick::COMPOSITE_SRC, 0, 0)
186		AND $image->compositeImage($reflection, Imagick::COMPOSITE_OVER, 0, $this->height))
187		{
188			// Replace the current image with the reflected image
189			$this->im = $image;
190
191			// Reset the width and height
192			$this->width = $this->im->getImageWidth();
193			$this->height = $this->im->getImageHeight();
194
195			return TRUE;
196		}
197
198		return FALSE;
199	}
200
201	protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity)
202	{
203		// Convert the Image intance into an Imagick instance
204		$watermark = new Imagick;
205		$watermark->readImageBlob($image->render(), $image->file);
206
207		if ($watermark->getImageAlphaChannel() !== Imagick::ALPHACHANNEL_ACTIVATE)
208		{
209			// Force the image to have an alpha channel
210			$watermark->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE);
211		}
212
213		if ($opacity < 100)
214		{
215			// NOTE: Using setImageOpacity will destroy current alpha channels!
216			$watermark->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA);
217		}
218
219		// Match the colorspace between the two images before compositing
220		// $watermark->setColorspace($this->im->getColorspace());
221
222		// Apply the watermark to the image
223		return $this->im->compositeImage($watermark, Imagick::COMPOSITE_DISSOLVE, $offset_x, $offset_y);
224	}
225
226	protected function _do_background($r, $g, $b, $opacity)
227	{
228		// Create a RGB color for the background
229		$color = sprintf('rgb(%d, %d, %d)', $r, $g, $b);
230
231		// Create a new image for the background
232		$background = new Imagick;
233		$background->newImage($this->width, $this->height, new ImagickPixel($color));
234
235		if ( ! $background->getImageAlphaChannel())
236		{
237			// Force the image to have an alpha channel
238			$background->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
239		}
240
241		// Clear the background image
242		$background->setImageBackgroundColor(new ImagickPixel('transparent'));
243
244		// NOTE: Using setImageOpacity will destroy current alpha channels!
245		$background->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA);
246
247		// Match the colorspace between the two images before compositing
248		$background->setColorspace($this->im->getColorspace());
249
250		if ($background->compositeImage($this->im, Imagick::COMPOSITE_DISSOLVE, 0, 0))
251		{
252			// Replace the current image with the new image
253			$this->im = $background;
254
255			return TRUE;
256		}
257
258		return FALSE;
259	}
260
261	protected function _do_save($file, $quality)
262	{
263		// Get the image format and type
264		list($format, $type) = $this->_get_imagetype(pathinfo($file, PATHINFO_EXTENSION));
265
266		// Set the output image type
267		$this->im->setFormat($format);
268
269		// Set the output quality
270		$this->im->setImageCompressionQuality($quality);
271
272		if ($this->im->writeImage($file))
273		{
274			// Reset the image type and mime type
275			$this->type = $type;
276			$this->mime = image_type_to_mime_type($type);
277
278			return TRUE;
279		}
280
281		return FALSE;
282	}
283
284	protected function _do_render($type, $quality)
285	{
286		// Get the image format and type
287		list($format, $type) = $this->_get_imagetype($type);
288
289		// Set the output image type
290		$this->im->setFormat($format);
291
292		// Set the output quality
293		$this->im->setImageCompressionQuality($quality);
294
295		// Reset the image type and mime type
296		$this->type = $type;
297		$this->mime = image_type_to_mime_type($type);
298
299		return (string) $this->im;
300	}
301
302	/**
303	 * Get the image type and format for an extension.
304	 *
305	 * @param   string  $extension  image extension: png, jpg, etc
306	 * @return  string  IMAGETYPE_* constant
307	 * @throws  Kohana_Exception
308	 */
309	protected function _get_imagetype($extension)
310	{
311		// Normalize the extension to a format
312		$format = strtolower($extension);
313
314		switch ($format)
315		{
316			case 'jpg':
317			case 'jpe':
318			case 'jpeg':
319				$type = IMAGETYPE_JPEG;
320			break;
321			case 'gif':
322				$type = IMAGETYPE_GIF;
323			break;
324			case 'png':
325				$type = IMAGETYPE_PNG;
326			break;
327			default:
328				throw new Kohana_Exception('Installed ImageMagick does not support :type images',
329					array(':type' => $extension));
330			break;
331		}
332
333		return array($format, $type);
334	}
335} // End Kohana_Image_Imagick