SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Image.php

Copyright (C) 2017 Karmabunny Pty Ltd.

This file is a part of SproutCMS.

SproutCMS is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

For more information, visit <http://getsproutcms.com>.

This class was originally from Kohana 2.3.4
Copyright 2007-2008 Kohana Team
  1. <?php
  2. /**
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  *
  13.  * This class was originally from Kohana 2.3.4
  14.  * Copyright 2007-2008 Kohana Team
  15.  */
  16. namespace Sprout\Helpers;
  17.  
  18. use InvalidArgumentException;
  19.  
  20. use Kohana;
  21. use Kohana_Exception;
  22.  
  23. use Sprout\Helpers\Drivers\ImageDriver;
  24.  
  25.  
  26. /**
  27.  * Manipulate images using standard methods such as resize, crop, rotate, etc.
  28.  * This class must be re-initialized for every image you wish to manipulate.
  29.  */
  30. class Image
  31. {
  32.  
  33. // Master Dimension
  34. const NONE = 1;
  35. const AUTO = 2;
  36. const HEIGHT = 3;
  37. const WIDTH = 4;
  38. // Flip Directions
  39. const HORIZONTAL = 5;
  40. const VERTICAL = 6;
  41.  
  42. // Allowed image types
  43. public static $allowed_types = array
  44. (
  45. IMAGETYPE_GIF => 'gif',
  46. IMAGETYPE_JPEG => 'jpg',
  47. IMAGETYPE_PNG => 'png',
  48. IMAGETYPE_TIFF_II => 'tiff',
  49. IMAGETYPE_TIFF_MM => 'tiff',
  50. );
  51.  
  52. // Driver instance
  53. protected $driver;
  54.  
  55. // Driver actions
  56. protected $actions = array();
  57.  
  58. // Reference to the current image filename
  59. protected $image = '';
  60.  
  61. /**
  62.   * Creates a new Image instance and returns it.
  63.   *
  64.   * @param string filename of image
  65.   * @param array non-default configurations
  66.   * @return object
  67.   */
  68. public static function factory($image, $config = NULL)
  69. {
  70. return new Image($image, $config);
  71. }
  72.  
  73. /**
  74.   * Creates a new image editor instance.
  75.   *
  76.   * @throws Kohana_Exception
  77.   * @param string filename of image
  78.   * @param array non-default configurations
  79.   * @return void
  80.   */
  81. public function __construct($image, $config = NULL)
  82. {
  83. static $check;
  84.  
  85. // Make the check exactly once
  86. ($check === NULL) and $check = function_exists('getimagesize');
  87.  
  88. if ($check === FALSE)
  89. throw new Kohana_Exception('image.getimagesize_missing');
  90.  
  91. // Check to make sure the image exists
  92. if ( ! is_file($image))
  93. throw new Kohana_Exception('image.file_not_found', $image);
  94.  
  95. // Disable error reporting, to prevent PHP warnings
  96. $ER = error_reporting(0);
  97.  
  98. // Fetch the image size and mime type
  99. $image_info = getimagesize($image);
  100.  
  101. // Turn on error reporting again
  102.  
  103. // Make sure that the image is readable and valid
  104. if ( ! is_array($image_info) OR count($image_info) < 3)
  105. throw new Kohana_Exception('image.file_unreadable', $image);
  106.  
  107. // Check to make sure the image type is allowed
  108. if ( ! isset(Image::$allowed_types[$image_info[2]]))
  109. throw new Kohana_Exception('image.type_not_allowed', $image);
  110.  
  111. // Image has been validated, load it
  112. $this->image = array
  113. (
  114. 'file' => str_replace('\\', '/', realpath($image)),
  115. 'width' => $image_info[0],
  116. 'height' => $image_info[1],
  117. 'type' => $image_info[2],
  118. 'ext' => Image::$allowed_types[$image_info[2]],
  119. 'mime' => $image_info['mime']
  120. );
  121.  
  122. // Load configuration
  123. $this->config = (array) $config + Kohana::config('image');
  124.  
  125. // Set driver class name
  126. $driver = 'Sprout\\Helpers\\Drivers\\Image\\' . ucfirst($this->config['driver']);
  127.  
  128. // Load the driver
  129. if (!class_exists($driver))
  130. throw new Kohana_Exception('core.driver_not_found', $this->config['driver'], get_class($this));
  131.  
  132. // Initialize the driver
  133. $this->driver = new $driver($this->config['params']);
  134.  
  135. // Validate the driver
  136. if ( ! ($this->driver instanceof ImageDriver))
  137. throw new Kohana_Exception('core.driver_implements', $this->config['driver'], get_class($this), 'ImageDriver');
  138. }
  139.  
  140. /**
  141.   * Handles retrieval of pre-save image properties
  142.   *
  143.   * @param string property name
  144.   * @return mixed
  145.   */
  146. public function __get($property)
  147. {
  148. if (isset($this->image[$property]))
  149. {
  150. return $this->image[$property];
  151. }
  152. else
  153. {
  154. throw new Kohana_Exception('core.invalid_property', $property, get_class($this));
  155. }
  156. }
  157.  
  158. /**
  159.   * Resize an image to a specific width and height. By default, Kohana will
  160.   * maintain the aspect ratio using the width as the master dimension. If you
  161.   * wish to use height as master dim, set $image->master_dim = Image::HEIGHT
  162.   * This method is chainable.
  163.   *
  164.   * @throws Kohana_Exception
  165.   * @param integer width
  166.   * @param integer height
  167.   * @param integer one of: Image::NONE, Image::AUTO, Image::WIDTH, Image::HEIGHT
  168.   * @return object
  169.   */
  170. public function resize($width, $height, $master = NULL)
  171. {
  172. if ( ! $this->validSize('width', $width))
  173. throw new Kohana_Exception('image.invalid_width', $width);
  174.  
  175. if ( ! $this->validSize('height', $height))
  176. throw new Kohana_Exception('image.invalid_height', $height);
  177.  
  178. if (empty($width) AND empty($height))
  179. throw new Kohana_Exception('image.invalid_dimensions', __FUNCTION__);
  180.  
  181. if ($master === NULL)
  182. {
  183. // Maintain the aspect ratio by default
  184. $master = Image::AUTO;
  185. }
  186. elseif ( ! $this->validSize('master', $master))
  187. throw new Kohana_Exception('image.invalid_master');
  188.  
  189. $this->actions['resize'] = array
  190. (
  191. 'width' => $width,
  192. 'height' => $height,
  193. 'master' => $master,
  194. );
  195.  
  196. return $this;
  197. }
  198.  
  199. /**
  200.   * Crop an image to a specific width and height. You may also set the top
  201.   * and left offset.
  202.   * This method is chainable.
  203.   *
  204.   * @throws Kohana_Exception
  205.   * @param integer width
  206.   * @param integer height
  207.   * @param integer top offset, pixel value or one of: top, center, bottom
  208.   * @param integer left offset, pixel value or one of: left, center, right
  209.   * @return object
  210.   */
  211. public function crop($width, $height, $top = 'center', $left = 'center')
  212. {
  213. if ( ! $this->validSize('width', $width))
  214. throw new Kohana_Exception('image.invalid_width', $width);
  215.  
  216. if ( ! $this->validSize('height', $height))
  217. throw new Kohana_Exception('image.invalid_height', $height);
  218.  
  219. if ( ! $this->validSize('top', $top))
  220. throw new Kohana_Exception('image.invalid_top', $top);
  221.  
  222. if ( ! $this->validSize('left', $left))
  223. throw new Kohana_Exception('image.invalid_left', $left);
  224.  
  225. if (empty($width) AND empty($height))
  226. throw new Kohana_Exception('image.invalid_dimensions', __FUNCTION__);
  227.  
  228. $this->actions['crop'] = array
  229. (
  230. 'width' => $width,
  231. 'height' => $height,
  232. 'top' => $top,
  233. 'left' => $left,
  234. );
  235.  
  236. return $this;
  237. }
  238.  
  239. /**
  240.   * Allows rotation of an image by 180 degrees clockwise or counter clockwise.
  241.   *
  242.   * @param integer degrees
  243.   * @return object
  244.   */
  245. public function rotate($degrees)
  246. {
  247. $degrees = (int) $degrees;
  248.  
  249. if ($degrees > 180)
  250. {
  251. do
  252. {
  253. // Keep subtracting full circles until the degrees have normalized
  254. $degrees -= 360;
  255. }
  256. while($degrees > 180);
  257. }
  258.  
  259. if ($degrees < -180)
  260. {
  261. do
  262. {
  263. // Keep adding full circles until the degrees have normalized
  264. $degrees += 360;
  265. }
  266. while($degrees < -180);
  267. }
  268.  
  269. $this->actions['rotate'] = $degrees;
  270.  
  271. return $this;
  272. }
  273.  
  274. /**
  275.   * Flip an image horizontally or vertically.
  276.   *
  277.   * @throws Kohana_Exception
  278.   * @param integer direction
  279.   * @return object
  280.   */
  281. public function flip($direction)
  282. {
  283. if ($direction !== Image::HORIZONTAL AND $direction !== Image::VERTICAL)
  284. throw new Kohana_Exception('image.invalid_flip');
  285.  
  286. $this->actions['flip'] = $direction;
  287.  
  288. return $this;
  289. }
  290.  
  291. /**
  292.   * Change the quality of an image.
  293.   *
  294.   * @param integer quality as a percentage
  295.   * @return object
  296.   */
  297. public function quality($amount)
  298. {
  299. $this->actions['quality'] = max(1, min($amount, 100));
  300.  
  301. return $this;
  302. }
  303.  
  304. /**
  305.   * Sharpen an image.
  306.   *
  307.   * @param integer amount to sharpen, usually ~20 is ideal
  308.   * @return object
  309.   */
  310. public function sharpen($amount)
  311. {
  312. $this->actions['sharpen'] = max(1, min($amount, 100));
  313.  
  314. return $this;
  315. }
  316.  
  317. /**
  318.   * Save the image to a new image or overwrite this image.
  319.   *
  320.   * @throws Kohana_Exception
  321.   * @param string new image filename
  322.   * @param integer permissions for new image
  323.   * @param boolean keep or discard image process actions
  324.   * @return object
  325.   */
  326. public function save($new_image = FALSE, $chmod = 0644, $keep_actions = FALSE)
  327. {
  328. // If no new image is defined, use the current image
  329. empty($new_image) and $new_image = $this->image['file'];
  330.  
  331. // Separate the directory and filename
  332. $dir = pathinfo($new_image, PATHINFO_DIRNAME);
  333. $file = pathinfo($new_image, PATHINFO_BASENAME);
  334.  
  335. // Normalize the path
  336. $dir = str_replace('\\', '/', realpath($dir)).'/';
  337.  
  338. if ( ! is_writable($dir))
  339. throw new Kohana_Exception('image.directory_unwritable', $dir);
  340.  
  341. if ($status = $this->driver->process($this->image, $this->actions, $dir, $file))
  342. {
  343. if ($chmod !== FALSE)
  344. {
  345. // Set permissions
  346. @chmod($new_image, $chmod);
  347. }
  348. }
  349.  
  350. // Reset actions. Subsequent save() or render() will not apply previous actions.
  351. if ($keep_actions === FALSE)
  352. $this->actions = array();
  353.  
  354. return $status;
  355. }
  356.  
  357. /**
  358.   * Output the image to the browser.
  359.   *
  360.   * @param boolean keep or discard image process actions
  361.   * @return object
  362.   */
  363. public function render($keep_actions = FALSE)
  364. {
  365. $new_image = $this->image['file'];
  366.  
  367. // Separate the directory and filename
  368. $dir = pathinfo($new_image, PATHINFO_DIRNAME);
  369. $file = pathinfo($new_image, PATHINFO_BASENAME);
  370.  
  371. // Normalize the path
  372. $dir = str_replace('\\', '/', realpath($dir)).'/';
  373.  
  374. // Process the image with the driver
  375. $status = $this->driver->process($this->image, $this->actions, $dir, $file, $render = TRUE);
  376.  
  377. // Reset actions. Subsequent save() or render() will not apply previous actions.
  378. if ($keep_actions === FALSE)
  379. $this->actions = array();
  380.  
  381. return $status;
  382. }
  383.  
  384. /**
  385.   * Sanitize a given value type.
  386.   *
  387.   * @param string type of property
  388.   * @param mixed property value
  389.   * @return boolean
  390.   */
  391. protected function validSize($type, & $value)
  392. {
  393. if (is_null($value))
  394. return TRUE;
  395.  
  396. if ( ! is_scalar($value))
  397. return FALSE;
  398.  
  399. switch ($type)
  400. {
  401. case 'width':
  402. case 'height':
  403. if (is_string($value) AND ! ctype_digit($value))
  404. {
  405. // Only numbers and percent signs
  406. if ( ! preg_match('/^[0-9]++%$/D', $value))
  407. return FALSE;
  408. }
  409. else
  410. {
  411. $value = (int) $value;
  412. }
  413. break;
  414. case 'top':
  415. if (is_string($value) AND ! ctype_digit($value))
  416. {
  417. if ( ! in_array($value, array('top', 'bottom', 'center')))
  418. return FALSE;
  419. }
  420. else
  421. {
  422. $value = (int) $value;
  423. }
  424. break;
  425. case 'left':
  426. if (is_string($value) AND ! ctype_digit($value))
  427. {
  428. if ( ! in_array($value, array('left', 'right', 'center')))
  429. return FALSE;
  430. }
  431. else
  432. {
  433. $value = (int) $value;
  434. }
  435. break;
  436. case 'master':
  437. if ($value !== Image::NONE AND
  438. $value !== Image::AUTO AND
  439. $value !== Image::WIDTH AND
  440. $value !== Image::HEIGHT)
  441. return FALSE;
  442. break;
  443. }
  444.  
  445. return TRUE;
  446. }
  447.  
  448.  
  449. /**
  450.   * Adds text to the base of an image, e.g. for copyright credit
  451.   * @param string $text The text to add to the image
  452.   * @return Image self, for chaining
  453.   */
  454. public function addText($text)
  455. {
  456. $this->actions['addText'] = (string) $text;
  457. return $this;
  458. }
  459.  
  460.  
  461. /**
  462.   * Calculates dimensions to be generated by a resize
  463.   * @param int $width Width in pixels
  464.   * @param int $height Height in pixels
  465.   * @param int $master Master dimension, e.g. Image::HEIGHT for a portrait image,
  466.   * Image::AUTO for any kind of image
  467.   * @return array [0 => width, 1 => height]
  468.   */
  469. public function calcResizeDims($width, $height, $master)
  470. {
  471. if ($master == Image::NONE) return [$width, $height];
  472.  
  473. $img_width = $this->image['width'];
  474. $img_height = $this->image['height'];
  475.  
  476. if ($width == 0) $master = Image::HEIGHT;
  477. if ($height == 0) $master = Image::WIDTH;
  478.  
  479. // Determine automatic master dimension
  480. if ($master == Image::AUTO) {
  481. if ($img_width / $width > $img_height / $height) {
  482. $master = Image::WIDTH;
  483. } else {
  484. $master = Image::HEIGHT;
  485. }
  486. }
  487.  
  488. // Calculate appropriate width or height for resize
  489. if ($master == Image::WIDTH) {
  490. $height = round($height * $width / $img_width);
  491. } elseif ($master == Image::HEIGHT) {
  492. $width = round($width * $height / $img_height);
  493. } else {
  494. $err = 'Invalid master dimension; see Image constants';
  495. throw new InvalidArgumentException($err);
  496. }
  497.  
  498. return [$width, $height];
  499. }
  500.  
  501.  
  502. /**
  503.   * Generate a base64-encoded PNG image, e.g. for an <img src="data:image/png;base64,..."> tag
  504.   * @param $file_path Path to the original file
  505.   */
  506. public static function base64($file_path, ImageTransform $transform = null)
  507. {
  508. $img = new Image($file_path);
  509. if ($transform) $transform->transform($img);
  510. $temp_file = APPPATH . 'temp/shrunk_' . date('ymdHis') . '_' . Sprout::randStr(8) . '.png';
  511. $img->save($temp_file);
  512.  
  513. $base64_img = base64_encode(file_get_contents($temp_file));
  514. unlink($temp_file);
  515.  
  516. return $base64_img;
  517. }
  518.  
  519. } // End Image
  520.