PageRenderTime 74ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/class.image.php

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