PageRenderTime 79ms CodeModel.GetById 37ms app.highlight 18ms RepoModel.GetById 20ms app.codeStats 0ms

/lib/class.image.php

https://github.com/symphonycms/jit_image_manipulation
PHP | 427 lines | 193 code | 60 blank | 174 comment | 28 complexity | 807714dd24c11c7c47bed5193fa3f44c MD5 | raw file
  1<?php
  2
  3	require_once(TOOLKIT . '/class.gateway.php');
  4
  5	Class Image {
  6		private $_resource;
  7		private $_meta;
  8		private $_image;
  9		static $_result;
 10
 11		const DEFAULT_QUALITY = 80;
 12		const DEFAULT_INTERLACE = true;
 13		const CURL_MAXREDIRS = 6;
 14
 15		private function __construct($resource, stdClass $meta){
 16			$this->_resource = $resource;
 17			$this->_meta = $meta;
 18		}
 19
 20		public function __destruct(){
 21			if(is_resource($this->_resource)) {
 22				imagedestroy($this->_resource);
 23			}
 24		}
 25
 26		public function Resource(){
 27			return $this->_resource;
 28		}
 29
 30		public function Meta(){
 31			return $this->_meta;
 32		}
 33
 34		/**
 35		 * This function will attempt to load an image from a remote URL using
 36		 * CURL. If CURL is not available, `file_get_contents` will attempt to resolve
 37		 * the given `$uri`. The remote image will be saved into PHP's temporary directory
 38		 * as determined by `sys_get_temp_dir`. Once the remote image has been
 39		 * saved to the temp directory, it's path will be passed to `Image::load`
 40		 * to return an instance of this `Image` class. If the file cannot be found
 41		 * an Exception is thrown.
 42		 *
 43		 * @param string $uri
 44		 *  The URL of the external image to load.
 45		 * @return Image
 46		 */
 47		public static function loadExternal($uri){
 48			// create the Gateway object
 49			$gateway = new Gateway();
 50			// set our url
 51			$gateway->init($uri);
 52			// set some options
 53			$gateway->setopt(CURLOPT_HEADER, false);
 54			$gateway->setopt(CURLOPT_RETURNTRANSFER, true);
 55			$gateway->setopt(CURLOPT_FOLLOWLOCATION, true);
 56			$gateway->setopt(CURLOPT_MAXREDIRS, Image::CURL_MAXREDIRS);
 57			// get the raw body response, ignore errors
 58			$response = @$gateway->exec();
 59
 60			if($response === false){
 61				throw new Exception(sprintf('Error reading external image <code>%s</code>. Please check the URI.', $uri));
 62			}
 63
 64			// clean up
 65			$gateway->flush();
 66
 67			// Symphony 2.4 enhances the TMP constant so it can be relied upon
 68			$dest = tempnam(TMP, 'IMAGE');
 69
 70			if(!file_put_contents($dest, $response)) {
 71				throw new Exception(sprintf('Error writing to temporary file <code>%s</code>.', $dest));
 72			}
 73
 74			return self::load($dest);
 75		}
 76
 77		/**
 78		 * Given a path to an image, `$image`, this function will verify it's
 79		 * existence, and generate a resource for use with PHP's image functions
 80		 * based off the file's type (.gif, .jpg, .png).
 81		 * Images must be RGB, CMYK jpg's are not supported due to GD limitations.
 82		 *
 83		 * @param string $image
 84		 *  The path to the file
 85		 * @return Image
 86		 */
 87		public static function load($image){
 88			if(!is_file($image) || !is_readable($image)){
 89				throw new Exception(sprintf('Error loading image <code>%s</code>. Check it exists and is readable.', str_replace(DOCROOT, '', $image)));
 90			}
 91
 92			$meta = self::getMetaInformation($image);
 93
 94			switch($meta->type) {
 95				// GIF
 96				case IMAGETYPE_GIF:
 97					$resource = imagecreatefromgif($image);
 98					break;
 99
100				// JPEG
101				case IMAGETYPE_JPEG:
102					// GD 2.0.22 supports basic CMYK to RGB conversion.
103					// RE: https://github.com/symphonycms/jit_image_manipulation/issues/47
104					$gdSupportsCMYK = version_compare(GD_VERSION, '2.0.22', '>=');
105
106					// Can't handle CMYK JPEG files
107					if ($meta->channels > 3 && $gdSupportsCMYK === false) {
108						throw new Exception('Cannot load CMYK JPG images');
109
110					// Can handle CMYK, or image has less than 3 channels.
111					} else{
112						$resource = imagecreatefromjpeg($image);
113					}
114					break;
115
116				// PNG
117				case IMAGETYPE_PNG:
118					$resource = imagecreatefrompng($image);
119					break;
120
121				default:
122					throw new Exception('Unsupported image type. Supported types: GIF, JPEG and PNG');
123					break;
124			}
125
126			if(!is_resource($resource)){
127				throw new Exception(sprintf('Error creating image <code>%s</code>. Check it exists and is readable.', str_replace(DOCROOT, '', $image)));
128			}
129
130			$obj = new self($resource, $meta);
131
132			return $obj;
133		}
134
135		/**
136		 * Given a path to a file, this function will attempt to find out the
137		 * dimensions, type and channel information using `getimagesize`.
138		 *
139		 * @link http://www.php.net/manual/en/function.getimagesize.php
140		 * @param string $file
141		 *  The path to the image.
142		 */
143		public static function getMetaInformation($file){
144			if(!$array = @getimagesize($file)) return false;
145
146			$meta = array();
147
148			$meta['width'] = $array[0];
149			$meta['height'] = $array[1];
150			$meta['type'] = $array[2];
151			$meta['channels'] = isset($array['channels']) ? $array['channels'] : false;
152
153			return (object)$meta;
154		}
155
156		/**
157		 * Given string representing the type return by `getMetaInformation`,
158		 * this function will generate the correct Content-Type header using
159		 * `image_type_to_mime_type` function.
160		 *
161		 * @see getMetaInformation()
162		 * @link http://php.net/manual/en/function.image-type-to-mime-type.php
163		 * @link http://www.php.net/manual/en/image.constants.php
164		 * @param integer $type
165		 *  One of the IMAGETYPE constants
166		 * @param string $destination
167		 *  The destination of the image. This defaults to null, if provided,
168		 *  this function will prompt the user to download the image rather
169		 *  than display it inline
170		 */
171		public static function renderOutputHeaders($type, $destination=NULL){
172			header('Content-Type: ' . image_type_to_mime_type($type));
173
174			if(is_null($destination)) return;
175
176			// Try to remove old extension
177			$ext = strrchr($destination, '.');
178			if($ext !== false){
179				$destination = substr($destination, 0, -strlen($ext));
180			}
181
182			header('Expires: ' . gmdate('D, d M Y H:i:s') . ' GMT');
183			header("Content-Disposition: inline; filename=$destination" . image_type_to_extension($type));
184			header('Pragma: no-cache');
185		}
186
187		/**
188		 * Get the HTTP response code of a resource
189		 *
190		 * @param string $url
191		 * @return integer - HTTP response code
192		 */
193		public static function getHttpResponseCode($url){
194			$head = self:: getHttpHead($url);
195			return $head['info']['http_code'];
196		}
197
198		/**
199		 * Get the value of a named HTTP response header field
200		 *
201		 * @param string $url
202		 * @param string $field - name of the header field
203		 * @return string - value of the header field
204		 */
205		public static function getHttpHeaderFieldValue($url, $field){
206			$headers = self::getHttpHeaders($url);
207			return $headers[$field];
208		}
209
210		/**
211		 * Get all HTTP response headers as an array
212		 *
213		 * @param string $url
214		 * @return array - response headers
215		 */
216		public static function getHttpHeaders($url){
217			$head = self:: getHttpHead($url);
218			return $head['headers'];
219		}
220
221		/**
222		 * Get all HTTP response headers and info as an array of arrays.
223		 *
224		 * @since 1.17
225		 *
226		 * @param string $url
227		 * @return array
228		 *  Contains the headers and the infos
229		 */
230		public static function getHttpHead($url){
231			// Check if we have a cached result
232			if(isset(self::$_result[$url])) {
233				return self::$_result[$url];
234			}
235
236			// create the Gateway object
237			$gateway = new Gateway();
238			// set our url
239			$gateway->init($url);
240			// set some options
241			$gateway->setopt(CURLOPT_URL, $url);
242			$gateway->setopt(CURLOPT_RETURNTRANSFER, true);
243			$gateway->setopt(CURLOPT_HEADER, true);
244			$gateway->setopt(CURLOPT_NOBODY, true);
245			$gateway->setopt(CURLOPT_FOLLOWLOCATION, true);
246			$gateway->setopt(CURLOPT_MAXREDIRS, Image::CURL_MAXREDIRS);
247			// get the raw head response, ignore errors
248			$head = @$gateway->exec();
249			// Get all info
250			$result = array(
251				'headers' => array(),
252				'info' => $gateway->getInfoLast()
253			);
254			// Clean up
255			$gateway->flush();
256
257			if ($head !== false) {
258				$result['headers'] = self::parseHttpHeaderFields($head);
259			}
260
261			// Save for subsequent requests
262			self::$_result[$url] = $result;
263
264			return $result;
265		}
266
267		/**
268		 * Parse HTTP response headers
269		 *
270		 * @param string $header - response header
271		 * @return array - header fields; name => value
272		 */
273		public static function parseHttpHeaderFields($header){
274			$retVal = array();
275			$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
276			foreach( $fields as $field ){
277				if( preg_match('/([^:]+): (.+)/m', $field, $match) ){
278					$match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
279					if( isset($retVal[$match[1]]) ){
280						$retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
281					}
282					else{
283						$retVal[$match[1]] = trim($match[2]);
284					}
285				}
286			}
287			return $retVal;
288		}
289
290		/**
291		 * Accessor to return of the height of the given `$res`
292		 *
293		 * @param resource $res
294		 * @return integer
295		 *  The height of the image in pixels, false otherwise
296		 */
297		public static function height($res){
298			return imagesy($res);
299		}
300
301		/**
302		 * Accessor to return of the width of the given `$res`
303		 *
304		 * @param resource $res
305		 * @return integer
306		 *  The width of the image in pixels, false otherwise
307		 */
308		public static function width($res){
309			return imagesx($res);
310		}
311
312		/**
313		 * Given a filter name, this function will attempt to load the filter
314		 * from the `/filters` folder and call it's `run` method. The JIT core
315		 * provides filters for 'crop', 'resize' and 'scale'.
316		 *
317		 * @param string $filter
318		 *  The filter name
319		 * @param array $args
320		 *  The arguments to pass to the filter's `run()` method
321		 * @return boolean
322		 */
323		public function applyFilter($filter = null, array $args = array()) {
324			if(is_null($filter) || !is_file(EXTENSIONS . "/jit_image_manipulation/lib/filters/filter.{$filter}.php")) return false;
325
326			require_once("filters/filter.{$filter}.php");
327
328			array_unshift($args, $this->_resource);
329
330			$this->_resource = call_user_func_array(array(sprintf('Filter%s', ucfirst($filter)), 'run'), $args);
331
332			return true;
333		}
334
335		/**
336		 * The function takes optional `$quality`, `$interlacing` and `$output`
337		 * parameters to return an image resource after it has been processed
338		 * using `applyFilter`.
339		 *
340		 * @param integer $quality
341		 *  Range of 1-100, if not provided, uses `Image::DEFAULT_QUALITY`
342		 * @param boolean $interlacing
343		 *  If true, the resulting image will be interlaced, otherwise it won't.
344		 *  By default uses the value of `Image::DEFAULT_INTERLACE`
345		 * @param IMAGETYPE_xxx $output
346		 *  A IMAGETYPE constant of the image's type, by default this is null
347		 *  which will attempt to get the constant using the `$this->Meta` accessor
348		 * @return resource
349		 */
350		public function display($quality = Image::DEFAULT_QUALITY, $interlacing = Image::DEFAULT_INTERLACE, $output = null) {
351			if(!$output) $output = $this->Meta()->type; //DEFAULT_OUTPUT_TYPE;
352
353			self::renderOutputHeaders($output);
354
355			if(isset($this->_image) && is_resource($this->_image)) {
356				return $this->_image;
357			}
358			else return self::__render(NULL, $quality, $interlacing, $output);
359		}
360
361		/**
362		 * This function will attempt to save an image at a desired destination.
363		 *
364		 * @param string $dest
365		 *  The path to save the image at
366		 * @param integer $quality
367		 *  Range of 1-100, if not provided, uses `Image::DEFAULT_QUALITY`
368		 * @param boolean $interlacing
369		 *  If true, the resulting image will be interlaced, otherwise it won't.
370		 *  By default uses the value of `Image::DEFAULT_INTERLACE`
371		 * @param IMAGETYPE_xxx $output
372		 *  A IMAGETYPE constant of the image's type, by default this is null
373		 *  which will attempt to get the constant using the `$this->Meta` accessor
374		 * @return boolean
375		 */
376		public function save($dest, $quality = Image::DEFAULT_QUALITY, $interlacing = Image::DEFAULT_INTERLACE, $output = null) {
377			if(!$output) $output = $this->Meta()->type; //DEFAULT_OUTPUT_TYPE;
378
379			$this->_image = self::__render($dest, $quality, $interlacing, $output);
380			return $this->_image;
381		}
382
383		/**
384		 * Renders the `$this->_resource` using the PHP image functions to save the
385		 * image at a location specified by `$dest`.
386		 *
387		 * @param string $dest
388		 *  The path to save the image at
389		 * @param integer $quality
390		 *  Range of 1-100, if not provided, uses `Image::DEFAULT_QUALITY`
391		 * @param boolean $interlacing
392		 *  If true, the resulting image will be interlaced, otherwise it won't.
393		 *  By default uses the value of `Image::DEFAULT_INTERLACE`
394		 * @param IMAGETYPE_xxx $output
395		 *  A IMAGETYPE constant of the image's type, by default this is null
396		 *  which will attempt to get the constant using the `$this->Meta` accessor
397		 * @return boolean
398		 */
399		private function __render($dest, $quality = Image::DEFAULT_QUALITY, $interlacing = Image::DEFAULT_INTERLACE, $output = null){
400			if(!is_resource($this->_resource)) {
401				throw new Exception('Invalid image resource supplied');
402			}
403
404			// Turn interlacing on for JPEG or PNG only
405			if($interlacing && ($output == IMAGETYPE_JPEG || $output == IMAGETYPE_PNG)) {
406				imageinterlace($this->_resource);
407			}
408
409			switch($output) {
410				case IMAGETYPE_GIF:
411					return imagegif($this->_resource, $dest);
412					break;
413
414				case IMAGETYPE_PNG:
415					return imagepng($this->_resource, $dest, round(9 * ($quality * 0.01)));
416					break;
417
418				case IMAGETYPE_JPEG:
419				default:
420					return imagejpeg($this->_resource, $dest, $quality);
421					break;
422			}
423
424			return false;
425		}
426
427	}