PageRenderTime 126ms CodeModel.GetById 61ms app.highlight 32ms RepoModel.GetById 28ms app.codeStats 0ms

/Nette/Image.php

https://github.com/DocX/nette
PHP | 516 lines | 259 code | 103 blank | 154 comment | 40 complexity | 03d7b4c650ca0c57b1a79b38cb415cd0 MD5 | raw file
  1<?php
  2
  3/**
  4 * Nette Framework
  5 *
  6 * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  7 *
  8 * This source file is subject to the "Nette license" that is bundled
  9 * with this package in the file license.txt.
 10 *
 11 * For more information please see http://nettephp.com
 12 *
 13 * @copyright  Copyright (c) 2004, 2009 David Grudl
 14 * @license    http://nettephp.com/license  Nette license
 15 * @link       http://nettephp.com
 16 * @category   Nette
 17 * @package    Nette
 18 */
 19
 20/*namespace Nette;*/
 21
 22
 23
 24require_once dirname(__FILE__) . '/Object.php';
 25
 26
 27
 28/**
 29 * Basic manipulation with images.
 30 *
 31 * <code>
 32 * $image = Image::fromFile('nette.jpg');
 33 * $image->resize(150, 100);
 34 * $image->sharpen();
 35 * $image->send();
 36 * </code>
 37 *
 38 * @author     David Grudl
 39 * @copyright  Copyright (c) 2004, 2009 David Grudl
 40 * @package    Nette
 41 *
 42 * @property-read int $width
 43 * @property-read int $height
 44 * @property-read resource $imageResource
 45 */
 46class Image extends Object
 47{
 48	/**#@+ resizing flags {@link resize()} */
 49	const ENLARGE = 1;
 50	const STRETCH = 2;
 51	/**#@-*/
 52
 53	/**#@+ @int image types {@link send()} */
 54	const JPEG = IMAGETYPE_JPEG;
 55	const PNG = IMAGETYPE_PNG;
 56	const GIF = IMAGETYPE_GIF;
 57	/**#@-*/
 58
 59	const EMPTY_GIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
 60
 61	/** @var bool */
 62	public static $useImageMagick = FALSE;
 63
 64	/** @var resource */
 65	private $image;
 66
 67
 68
 69	/**
 70	 * Returns RGB color.
 71	 * @param  int  red 0..255
 72	 * @param  int  green 0..255
 73	 * @param  int  blue 0..255
 74	 * @param  int  transparency 0..127
 75	 * @return array
 76	 */
 77	public static function rgb($red, $green, $blue, $transparency = 0)
 78	{
 79		return array(
 80			'red' => max(0, min(255, (int) $red)),
 81			'green' => max(0, min(255, (int) $green)),
 82			'blue' => max(0, min(255, (int) $blue)),
 83			'alpha' => max(0, min(127, (int) $transparency)),
 84		);
 85	}
 86
 87
 88
 89	/**
 90	 * Opens image from file.
 91	 * @param  string
 92	 * @param  mixed  detected image format
 93	 * @return Image
 94	 */
 95	public static function fromFile($file, & $format = NULL)
 96	{
 97		if (!extension_loaded('gd')) {
 98			throw new /*\*/Exception("PHP extension GD is not loaded.");
 99		}
100
101		$info = @getimagesize($file); // intentionally @
102		if (self::$useImageMagick && (empty($info) || $info[0] * $info[1] > 2e6)) {
103			return new ImageMagick($file, $format);
104		}
105
106		switch ($format = $info[2]) {
107		case self::JPEG:
108			return new self(imagecreatefromjpeg($file));
109
110		case self::PNG:
111			return new self(imagecreatefrompng($file));
112
113		case self::GIF:
114			return new self(imagecreatefromgif($file));
115
116		default:
117			if (self::$useImageMagick) {
118				return new ImageMagick($file, $format);
119			}
120			throw new /*\*/Exception("Unknown image type or file '$file' not found.");
121		}
122	}
123
124
125
126	/**
127	 * Create a new image from the image stream in the string.
128	 * @param  string
129	 * @param  mixed  detected image format
130	 * @return Image
131	 */
132	public static function fromString($s, & $format = NULL)
133	{
134		if (strncmp($s, "\xff\xd8", 2) === 0) {
135			$format = self::JPEG;
136
137		} elseif (strncmp($s, "\x89PNG", 4) === 0) {
138			$format = self::PNG;
139
140		} elseif (strncmp($s, "GIF", 3) === 0) {
141			$format = self::GIF;
142
143		} else {
144			$format = NULL;
145		}
146		return new self(imagecreatefromstring($s));
147	}
148
149
150
151	/**
152	 * Creates blank image.
153	 * @param  int
154	 * @param  int
155	 * @param  array
156	 * @return Image
157	 */
158	public static function fromBlank($width, $height, $color = NULL)
159	{
160		if (!extension_loaded('gd')) {
161			throw new /*\*/Exception("PHP extension GD is not loaded.");
162		}
163
164		$width = (int) $width;
165		$height = (int) $height;
166		if ($width < 1 || $height < 1) {
167			throw new /*\*/InvalidArgumentException('Image width and height must be greater than zero.');
168		}
169
170		$image = imagecreatetruecolor($width, $height);
171		if (is_array($color)) {
172			$color += array('alpha' => 0);
173			$color = imagecolorallocatealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
174			imagealphablending($image, FALSE);
175			imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
176			imagealphablending($image, TRUE);
177		}
178		return new self($image);
179	}
180
181
182
183	/**
184	 * Wraps GD image.
185	 * @param  resource
186	 */
187	public function __construct($image)
188	{
189		$this->setImageResource($image);
190	}
191
192
193
194	/**
195	 * Returns image width.
196	 * @return int
197	 */
198	public function getWidth()
199	{
200		return imagesx($this->image);
201	}
202
203
204
205	/**
206	 * Returns image height.
207	 * @return int
208	 */
209	public function getHeight()
210	{
211		return imagesy($this->image);
212	}
213
214
215
216	/**
217	 * Sets image resource.
218	 * @param  resource
219	 * @return Image  provides a fluent interface
220	 */
221	protected function setImageResource($image)
222	{
223		if (!is_resource($image) || get_resource_type($image) !== 'gd') {
224			throw new /*\*/InvalidArgumentException('Image is not valid.');
225		}
226		$this->image = $image;
227		return $this;
228	}
229
230
231
232	/**
233	 * Returns image GD resource.
234	 * @return resource
235	 */
236	public function getImageResource()
237	{
238		return $this->image;
239	}
240
241
242
243	/**
244	 * Resizes image.
245	 * @param  mixed  width in pixels or percent
246	 * @param  mixed  height in pixels or percent
247	 * @param  int    flags
248	 * @return Image  provides a fluent interface
249	 */
250	public function resize($newWidth, $newHeight, $flags = 0)
251	{
252		list($newWidth, $newHeight) = $this->calculateSize($newWidth, $newHeight, $flags);
253		$newImage = self::fromBlank($newWidth, $newHeight, self::RGB(0, 0, 0, 127))->getImageResource();
254		imagecopyresampled($newImage, $this->getImageResource(), 0, 0, 0, 0, $newWidth, $newHeight, $this->getWidth(), $this->getHeight());
255		$this->image = $newImage;
256		return $this;
257	}
258
259
260
261	/**
262	 * Calculates dimensions of resized image.
263	 * @param  mixed  width in pixels or percent
264	 * @param  mixed  height in pixels or percent
265	 * @param  int    flags
266	 * @return array
267	 */
268	public function calculateSize($newWidth, $newHeight, $flags = 0)
269	{
270		$width = $this->getWidth();
271		$height = $this->getHeight();
272
273		if (substr($newWidth, -1) === '%') {
274			$newWidth = round($width / 100 * $newWidth);
275			$flags |= self::ENLARGE;
276			$percents = TRUE;
277		} else {
278			$newWidth = (int) $newWidth;
279		}
280
281		if (substr($newHeight, -1) === '%') {
282			$newHeight = round($height / 100 * $newHeight);
283			$flags |= empty($percents) ? self::ENLARGE : self::STRETCH;
284		} else {
285			$newHeight = (int) $newHeight;
286		}
287
288		if ($flags & self::STRETCH) { // non-proportional
289			if ($newWidth < 1 || $newHeight < 1) {
290				throw new /*\*/InvalidArgumentException('For stretching must be both width and height specified.');
291			}
292
293			if (($flags & self::ENLARGE) === 0) {
294				$newWidth = round($width * min(1, $newWidth / $width));
295				$newHeight = round($height * min(1, $newHeight / $height));
296			}
297
298		} else {  // proportional
299			if ($newWidth < 1 && $newHeight < 1) {
300				throw new /*\*/InvalidArgumentException('At least width or height must be specified.');
301			}
302
303			$scale = array();
304			if ($newWidth > 0) { // fit width
305				$scale[] = $newWidth / $width;
306			}
307
308			if ($newHeight > 0) { // fit height
309				$scale[] = $newHeight / $height;
310			}
311
312			if (($flags & self::ENLARGE) === 0) {
313				$scale[] = 1;
314			}
315
316			$scale = min($scale);
317			$newWidth = round($width * $scale);
318			$newHeight = round($height * $scale);
319		}
320
321		return array($newWidth, $newHeight);
322	}
323
324
325
326	/**
327	 * Crops image.
328	 * @param  int    x-coordinate
329	 * @param  int    y-coordinate
330	 * @param  int    width
331	 * @param  int    height
332	 * @return Image  provides a fluent interface
333	 */
334	public function crop($left, $top, $width, $height)
335	{
336		$left = max(0, (int) $left);
337		$top = max(0, (int) $top);
338		$width = min((int) $width, $this->getWidth() - $left);
339		$height = min((int) $height, $this->getHeight() - $top);
340
341		$newImage = self::fromBlank($width, $height, self::RGB(0, 0, 0, 127))->getImageResource();
342		imagecopy($newImage, $this->getImageResource(), 0, 0, $left, $top, $width, $height);
343		$this->image = $newImage;
344		return $this;
345	}
346
347
348
349	/**
350	 * Sharpen image.
351	 * @return Image  provides a fluent interface
352	 */
353	public function sharpen()
354	{
355		imageconvolution($this->getImageResource(), array( // my magic numbers ;)
356			array( -1, -1, -1 ),
357			array( -1, 24, -1 ),
358			array( -1, -1, -1 ),
359		), 16, 0);
360		return $this;
361	}
362
363
364
365	/**
366	 * Puts another image into this image.
367	 * @param  Image
368	 * @param  mixed  x-coordinate in pixels or percent
369	 * @param  mixed  y-coordinate in pixels or percent
370	 * @param  int  opacity 0..100
371	 * @return Image  provides a fluent interface
372	 */
373	public function place(Image $image, $left = 0, $top = 0, $opacity = 100)
374	{
375		$opacity = max(0, min(100, (int) $opacity));
376
377		if (substr($left, -1) === '%') {
378			$left = round(($this->getWidth() - $image->getWidth()) / 100 * $left);
379		}
380
381		if (substr($top, -1) === '%') {
382			$top = round(($this->getHeight() - $image->getHeight()) / 100 * $top);
383		}
384
385		if ($opacity === 100) {
386			imagecopy($this->getImageResource(), $image->getImageResource(), $left, $top, 0, 0, $image->getWidth(), $image->getHeight());
387
388		} elseif ($opacity <> 0) {
389			imagecopymerge($this->getImageResource(), $image->getImageResource(), $left, $top, 0, 0, $image->getWidth(), $image->getHeight(), $opacity);
390		}
391		return $this;
392	}
393
394
395
396	/**
397	 * Saves image to the file.
398	 * @param  string  filename
399	 * @param  int  quality 0..100 (for JPEG and PNG)
400	 * @param  int  optional image type
401	 * @return bool TRUE on success or FALSE on failure.
402	 */
403	public function save($file = NULL, $quality = NULL, $type = NULL)
404	{
405		if ($type === NULL) {
406			switch (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
407			case 'jpg':
408			case 'jpeg':
409				$type = self::JPEG;
410				break;
411			case 'png':
412				$type = self::PNG;
413				break;
414			case 'gif':
415				$type = self::GIF;
416			}
417		}
418
419		switch ($type) {
420		case self::JPEG:
421			$quality = $quality === NULL ? 85 : max(0, min(100, (int) $quality));
422			return imagejpeg($this->getImageResource(), $file, $quality);
423
424		case self::PNG:
425			$quality = $quality === NULL ? 9 : max(0, min(9, (int) $quality));
426			return imagepng($this->getImageResource(), $file, $quality);
427
428		case self::GIF:
429			return imagegif($this->getImageResource(), $file);
430
431		default:
432			throw new /*\*/Exception("Unsupported image type.");
433		}
434	}
435
436
437
438	/**
439	 * Outputs image to string.
440	 * @param  int  image type
441	 * @param  int  quality 0..100 (for JPEG and PNG)
442	 * @return string
443	 */
444	public function toString($type = self::JPEG, $quality = NULL)
445	{
446		ob_start();
447		$this->save(NULL, $quality, $type);
448		return ob_get_clean();
449	}
450
451
452
453	/**
454	 * Outputs image to string.
455	 * @return string
456	 */
457	public function __toString()
458	{
459		try {
460			return $this->toString();
461
462		} catch (/*\*/Exception $e) {
463			trigger_error($e->getMessage(), E_USER_WARNING);
464			return '';
465		}
466	}
467
468
469
470	/**
471	 * Outputs image to browser.
472	 * @param  int  image type
473	 * @param  int  quality 0..100 (for JPEG and PNG)
474	 * @return bool TRUE on success or FALSE on failure.
475	 */
476	public function send($type = self::JPEG, $quality = NULL)
477	{
478		if ($type !== self::GIF && $type !== self::PNG && $type !== self::JPEG) {
479			throw new /*\*/Exception("Unsupported image type.");
480		}
481		header('Content-Type: ' . image_type_to_mime_type($type));
482		return $this->save(NULL, $quality, $type);
483	}
484
485
486
487	/**
488	 * Call to undefined method.
489	 *
490	 * @param  string  method name
491	 * @param  array   arguments
492	 * @return mixed
493	 * @throws \MemberAccessException
494	 */
495	public function __call($name, $args)
496	{
497		$function = 'image' . $name;
498		if (function_exists($function)) {
499			foreach ($args as $key => $value) {
500				if ($value instanceof self) {
501					$args[$key] = $value->getImageResource();
502
503				} elseif (is_array($value) && isset($value['red'])) { // rgb
504					$args[$key] = imagecolorallocatealpha($this->getImageResource(), $value['red'], $value['green'], $value['blue'], $value['alpha']);
505				}
506			}
507			array_unshift($args, $this->getImageResource());
508
509			$res = call_user_func_array($function, $args);
510			return is_resource($res) ? new self($res) : $res;
511		}
512
513		return parent::__call($name, $args);
514	}
515
516}