SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Drivers/Image/GD.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\Drivers\Image;
  17.  
  18. use Kohana;
  19. use Kohana_Exception;
  20.  
  21. use Sprout\Helpers\Image;
  22. use Sprout\Helpers\Drivers\ImageDriver;
  23.  
  24.  
  25. /**
  26.  * GD Image Driver.
  27.  */
  28. class GD extends ImageDriver
  29. {
  30.  
  31. // A transparent PNG as a string
  32. protected static $blank_png;
  33. protected static $blank_png_width;
  34. protected static $blank_png_height;
  35.  
  36. public function __construct()
  37. {
  38. // Make sure that GD2 is available
  39. if ( ! function_exists('gd_info'))
  40. throw new Kohana_Exception('image.gd.requires_v2');
  41.  
  42. // Get the GD information
  43. $info = gd_info();
  44.  
  45. // Make sure that the GD2 is installed
  46. if (strpos($info['GD Version'], '2.') === FALSE)
  47. throw new Kohana_Exception('image.gd.requires_v2');
  48. }
  49.  
  50. public function process($image, $actions, $dir, $file, $render = FALSE)
  51. {
  52. // Set the "create" function
  53. switch ($image['type'])
  54. {
  55. case IMAGETYPE_JPEG:
  56. $create = 'imagecreatefromjpeg';
  57. break;
  58. case IMAGETYPE_GIF:
  59. $create = 'imagecreatefromgif';
  60. break;
  61. case IMAGETYPE_PNG:
  62. $create = 'imagecreatefrompng';
  63. break;
  64. }
  65.  
  66. // Set the "save" function
  67. switch (strtolower(substr(strrchr($file, '.'), 1)))
  68. {
  69. case 'jpg':
  70. case 'jpeg':
  71. $save = 'imagejpeg';
  72. break;
  73. case 'gif':
  74. $save = 'imagegif';
  75. break;
  76. case 'png':
  77. $save = 'imagepng';
  78. break;
  79. }
  80.  
  81. // Make sure the image type is supported for import
  82. if (empty($create) OR ! function_exists($create))
  83. throw new Kohana_Exception('image.type_not_allowed', $image['file']);
  84.  
  85. // Make sure the image type is supported for saving
  86. if (empty($save) OR ! function_exists($save))
  87. throw new Kohana_Exception('image.type_not_allowed', $dir.$file);
  88.  
  89. // Load the image
  90. $this->image = $image;
  91.  
  92. // Create the GD image resource
  93. $this->tmp_image = $create($image['file']);
  94.  
  95. // Get the quality setting from the actions
  96. if (isset($actions['quality'])) {
  97. $quality = (int) $actions['quality'];
  98. unset($actions['quality']);
  99. } else {
  100. $quality = null;
  101. }
  102.  
  103. if ($status = $this->execute($actions))
  104. {
  105. // Prevent the alpha from being lost
  106. imagealphablending($this->tmp_image, TRUE);
  107. imagesavealpha($this->tmp_image, TRUE);
  108.  
  109. switch ($save)
  110. {
  111. case 'imagejpeg':
  112. ($quality === NULL) and $quality = (int)Kohana::config('sprout.jpeg_quality');
  113. imageinterlace($this->tmp_image, Kohana::config('sprout.jpeg_progressive'));
  114. break;
  115. case 'imagegif':
  116. // Remove the quality setting, GIF doesn't use it
  117. unset($quality);
  118. break;
  119. case 'imagepng':
  120. // Always use a compression level of 9 for PNGs. This does not
  121. // affect quality, it only increases the level of compression!
  122. $quality = 9;
  123. break;
  124. }
  125.  
  126. if ($render === FALSE)
  127. {
  128. // Set the status to the save return value, saving with the quality requested
  129. $status = isset($quality) ? $save($this->tmp_image, $dir.$file, $quality) : $save($this->tmp_image, $dir.$file);
  130. }
  131. else
  132. {
  133. // Output the image directly to the browser
  134. switch ($save)
  135. {
  136. case 'imagejpeg':
  137. header('Content-Type: image/jpeg');
  138. break;
  139. case 'imagegif':
  140. header('Content-Type: image/gif');
  141. break;
  142. case 'imagepng':
  143. header('Content-Type: image/png');
  144. break;
  145. }
  146.  
  147. $status = isset($quality) ? $save($this->tmp_image, NULL, $quality) : $save($this->tmp_image);
  148. }
  149.  
  150. // Destroy the temporary image
  151. imagedestroy($this->tmp_image);
  152. }
  153.  
  154. return $status;
  155. }
  156.  
  157. public function flip($direction)
  158. {
  159. // Get the current width and height
  160. $width = imagesx($this->tmp_image);
  161. $height = imagesy($this->tmp_image);
  162.  
  163. // Create the flipped image
  164. $flipped = $this->imagecreatetransparent($width, $height);
  165.  
  166. if ($direction === Image::HORIZONTAL)
  167. {
  168. for ($x = 0; $x < $width; $x++)
  169. {
  170. $status = imagecopy($flipped, $this->tmp_image, $x, 0, $width - $x - 1, 0, 1, $height);
  171. }
  172. }
  173. elseif ($direction === Image::VERTICAL)
  174. {
  175. for ($y = 0; $y < $height; $y++)
  176. {
  177. $status = imagecopy($flipped, $this->tmp_image, 0, $y, 0, $height - $y - 1, $width, 1);
  178. }
  179. }
  180. else
  181. {
  182. // Do nothing
  183. return TRUE;
  184. }
  185.  
  186. if ($status === TRUE)
  187. {
  188. // Swap the new image for the old one
  189. imagedestroy($this->tmp_image);
  190. $this->tmp_image = $flipped;
  191. }
  192.  
  193. return $status;
  194. }
  195.  
  196. public function crop($properties)
  197. {
  198. // Sanitize the cropping settings
  199. $this->sanitizeGeometry($properties);
  200.  
  201. // Get the current width and height
  202. $width = imagesx($this->tmp_image);
  203. $height = imagesy($this->tmp_image);
  204.  
  205. // Create the temporary image to copy to
  206. $img = $this->imagecreatetransparent($properties['width'], $properties['height']);
  207.  
  208. // Execute the crop
  209. if ($status = imagecopyresampled($img, $this->tmp_image, 0, 0, $properties['left'], $properties['top'], $width, $height, $width, $height))
  210. {
  211. // Swap the new image for the old one
  212. imagedestroy($this->tmp_image);
  213. $this->tmp_image = $img;
  214. }
  215.  
  216. return $status;
  217. }
  218.  
  219. public function resize($properties)
  220. {
  221. // Get the current width and height
  222. $width = imagesx($this->tmp_image);
  223. $height = imagesy($this->tmp_image);
  224.  
  225. if (substr($properties['width'], -1) === '%')
  226. {
  227. // Recalculate the percentage to a pixel size
  228. $properties['width'] = round($width * (substr($properties['width'], 0, -1) / 100));
  229. }
  230.  
  231. if (substr($properties['height'], -1) === '%')
  232. {
  233. // Recalculate the percentage to a pixel size
  234. $properties['height'] = round($height * (substr($properties['height'], 0, -1) / 100));
  235. }
  236.  
  237. // Recalculate the width and height, if they are missing
  238. empty($properties['width']) and $properties['width'] = round($width * $properties['height'] / $height);
  239. empty($properties['height']) and $properties['height'] = round($height * $properties['width'] / $width);
  240.  
  241. if ($properties['master'] === Image::AUTO)
  242. {
  243. // Change an automatic master dim to the correct type
  244. $properties['master'] = (($width / $properties['width']) > ($height / $properties['height'])) ? Image::WIDTH : Image::HEIGHT;
  245. }
  246.  
  247. if (empty($properties['height']) OR $properties['master'] === Image::WIDTH)
  248. {
  249. // Recalculate the height based on the width
  250. $properties['height'] = round($height * $properties['width'] / $width);
  251. }
  252.  
  253. if (empty($properties['width']) OR $properties['master'] === Image::HEIGHT)
  254. {
  255. // Recalculate the width based on the height
  256. $properties['width'] = round($width * $properties['height'] / $height);
  257. }
  258.  
  259. // Test if we can do a resize without resampling to speed up the final resize
  260. if ($properties['width'] > $width / 2 AND $properties['height'] > $height / 2)
  261. {
  262. // Presize width and height
  263. $pre_width = $width;
  264. $pre_height = $height;
  265.  
  266. // The maximum reduction is 10% greater than the final size
  267. $max_reduction_width = round($properties['width'] * 1.1);
  268. $max_reduction_height = round($properties['height'] * 1.1);
  269.  
  270. // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction
  271. while ($pre_width / 2 > $max_reduction_width AND $pre_height / 2 > $max_reduction_height)
  272. {
  273. $pre_width /= 2;
  274. $pre_height /= 2;
  275. }
  276.  
  277. // Create the temporary image to copy to
  278. $img = $this->imagecreatetransparent($pre_width, $pre_height);
  279.  
  280. if ($status = imagecopyresized($img, $this->tmp_image, 0, 0, 0, 0, $pre_width, $pre_height, $width, $height))
  281. {
  282. // Swap the new image for the old one
  283. imagedestroy($this->tmp_image);
  284. $this->tmp_image = $img;
  285. }
  286.  
  287. // Set the width and height to the presize
  288. $width = $pre_width;
  289. $height = $pre_height;
  290. }
  291.  
  292. // Create the temporary image to copy to
  293. $img = $this->imagecreatetransparent($properties['width'], $properties['height']);
  294.  
  295. // Execute the resize
  296. if ($status = imagecopyresampled($img, $this->tmp_image, 0, 0, 0, 0, $properties['width'], $properties['height'], $width, $height))
  297. {
  298. // Swap the new image for the old one
  299. imagedestroy($this->tmp_image);
  300. $this->tmp_image = $img;
  301. }
  302.  
  303. return $status;
  304. }
  305.  
  306. public function rotate($amount)
  307. {
  308. // Use current image to rotate
  309. $img = $this->tmp_image;
  310.  
  311. // White, with an alpha of 0
  312. $transparent = imagecolorallocatealpha($img, 255, 255, 255, 127);
  313.  
  314. // Rotate, setting the transparent color
  315. $img = imagerotate($img, 360 - $amount, $transparent, -1);
  316.  
  317. // Fill the background with the transparent "color"
  318. imagecolortransparent($img, $transparent);
  319.  
  320. // Merge the images
  321. if ($status = imagecopymerge($this->tmp_image, $img, 0, 0, 0, 0, imagesx($this->tmp_image), imagesy($this->tmp_image), 100))
  322. {
  323. // Prevent the alpha from being lost
  324. imagealphablending($img, TRUE);
  325. imagesavealpha($img, TRUE);
  326.  
  327. // Swap the new image for the old one
  328. imagedestroy($this->tmp_image);
  329. $this->tmp_image = $img;
  330. }
  331.  
  332. return $status;
  333. }
  334.  
  335. public function sharpen($amount)
  336. {
  337. // Make sure that the sharpening function is available
  338. if ( ! function_exists('imageconvolution'))
  339. throw new Kohana_Exception('image.unsupported_method', __FUNCTION__);
  340.  
  341. // Amount should be in the range of 18-10
  342. $amount = round(abs(-18 + ($amount * 0.08)), 2);
  343.  
  344. // Gaussian blur matrix
  345. $matrix = array
  346. (
  347. array(-1, -1, -1),
  348. array(-1, $amount, -1),
  349. array(-1, -1, -1),
  350. );
  351.  
  352. // Perform the sharpen
  353. return imageconvolution($this->tmp_image, $matrix, $amount - 8, 0);
  354. }
  355.  
  356. protected function properties()
  357. {
  358. return array(imagesx($this->tmp_image), imagesy($this->tmp_image));
  359. }
  360.  
  361. /**
  362.   * Returns an image with a transparent background. Used for rotating to
  363.   * prevent unfilled backgrounds.
  364.   *
  365.   * @param integer image width
  366.   * @param integer image height
  367.   * @return resource
  368.   */
  369. protected function imagecreatetransparent($width, $height)
  370. {
  371. if (self::$blank_png === NULL)
  372. {
  373. // Decode the blank PNG if it has not been done already
  374. (
  375. 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29'.
  376. 'mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADqSURBVHjaYvz//z/DYAYAAcTEMMgBQAANegcCBN'.
  377. 'CgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQ'.
  378. 'AANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoH'.
  379. 'AgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB'.
  380. '3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAgAEAMpcDTTQWJVEAAAAASUVORK5CYII='
  381. ));
  382.  
  383. // Set the blank PNG width and height
  384. self::$blank_png_width = imagesx(self::$blank_png);
  385. self::$blank_png_height = imagesy(self::$blank_png);
  386. }
  387.  
  388. $img = imagecreatetruecolor($width, $height);
  389.  
  390. // Resize the blank image
  391. imagecopyresized($img, self::$blank_png, 0, 0, 0, 0, $width, $height, self::$blank_png_width, self::$blank_png_height);
  392.  
  393. // Prevent the alpha from being lost
  394. imagealphablending($img, FALSE);
  395. imagesavealpha($img, TRUE);
  396.  
  397. return $img;
  398. }
  399.  
  400.  
  401. public function addText($text)
  402. {
  403. /** Text colour */
  404. $colour = imagecolorallocate($this->tmp_image, 0xFF, 0xFF, 0xFF);
  405.  
  406. /** Fill colour for the transparent rectangle which will surround the text */
  407. $fill_colour = imagecolorallocate($this->tmp_image, 0x20, 0x20, 0x20);
  408.  
  409. /** Width of image which will have text added */
  410. $w = imagesx($this->tmp_image);
  411.  
  412. /** Height of image which will have text added */
  413. $h = imagesy($this->tmp_image);
  414.  
  415. /** Max font size (N.B. in points, not pixels) */
  416. $max_font_size = 14;
  417.  
  418. /** Min font size (N.B. in points, not pixels) */
  419. $min_font_size = 8;
  420.  
  421. /** Text angle, N.B. 0 represents normal left-right text */
  422. $angle = 0;
  423.  
  424. /** Path to .ttf font */
  425. $font_file = DOCROOT . 'media/fonts/DejaVuSans.ttf';
  426.  
  427. /** Num pixels surrounding text in transparent rectangle */
  428. $border = 5;
  429.  
  430. /** Opacity between 0 (retain underlying image) and 100 (completely obscure underlying image) */
  431. $opacity = 60;
  432.  
  433. /**
  434.   * Y-point at which text should be rendered inside image.
  435.   *
  436.   * N.B. One would expect the text to start at (image height - text height - border), but the
  437.   * position for imagettftext is based on the font's baseline, which is font dependent but assumed
  438.   * to be about 2/3 of the overall height of the text
  439.   */
  440. $y = $h;
  441.  
  442. // Calculate the width and height of a box to contain the rendered text
  443. $text_w = PHP_INT_MAX;
  444. $font_size = $max_font_size + 1;
  445. do {
  446. --$font_size;
  447. $bounds = imagettfbbox($font_size, $angle, $font_file, $text);
  448. $rect_w = $bounds[2] - $bounds[6] + (2 * $border);
  449. $rect_h = $bounds[3] - $bounds[7] + (2 * $border);
  450. } while ($rect_w > $w and $font_size >= $min_font_size);
  451.  
  452. // Don't embed text if it can't fit into the image
  453. if ($font_size < $min_font_size) return true;
  454.  
  455. // Draw a rectangle which will contain the text
  456. $rect = imagecreatetruecolor($rect_w, $rect_h);
  457. imagefill($rect, 0, 0, $fill_colour);
  458.  
  459. // Merge the rectangle into the original image, with some transparency
  460. imagealphablending($this->tmp_image, TRUE);
  461. imagecopymerge($this->tmp_image, $rect, 0, $h - $rect_h, 0, 0, $rect_w, $rect_h, $opacity);
  462.  
  463. // Add the text inside the transparent rectangle
  464. $y = $h - ($font_size / 3) - $border;
  465. imagettftext($this->tmp_image, $font_size, $angle, $border, $y, $colour, $font_file, $text);
  466.  
  467. return true;
  468. }
  469.  
  470. } // End Image GD Driver
  471.