GD.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. <?php
  2. /**
  3. * Support for image manipulation using [GD](http://php.net/GD).
  4. *
  5. * @package Kohana/Image
  6. * @category Drivers
  7. * @author Kohana Team
  8. * @copyright (c) Kohana Team
  9. * @license https://koseven.ga/LICENSE.md
  10. */
  11. class Kohana_Image_GD extends Image {
  12. // Which GD functions are available?
  13. const IMAGEROTATE = 'imagerotate';
  14. const IMAGECONVOLUTION = 'imageconvolution';
  15. const IMAGEFILTER = 'imagefilter';
  16. const IMAGELAYEREFFECT = 'imagelayereffect';
  17. protected static $_available_functions = [];
  18. /**
  19. * Checks if GD is enabled and verify that key methods exist, some of which require GD to
  20. * be bundled with PHP. Exceptions will be thrown from those methods when GD is not
  21. * bundled.
  22. *
  23. * @return boolean
  24. */
  25. public static function check()
  26. {
  27. if ( ! function_exists('gd_info'))
  28. {
  29. throw new Kohana_Exception('GD is either not installed or not enabled, check your configuration');
  30. }
  31. $functions = [
  32. Image_GD::IMAGEROTATE,
  33. Image_GD::IMAGECONVOLUTION,
  34. Image_GD::IMAGEFILTER,
  35. Image_GD::IMAGELAYEREFFECT
  36. ];
  37. foreach ($functions as $function)
  38. {
  39. Image_GD::$_available_functions[$function] = function_exists($function);
  40. }
  41. if (defined('GD_VERSION'))
  42. {
  43. // Get the version via a constant, available in PHP 5.2.4+
  44. $version = GD_VERSION;
  45. }
  46. else
  47. {
  48. // Get the version information
  49. $info = gd_info();
  50. // Extract the version number
  51. preg_match('/\d+\.\d+(?:\.\d+)?/', $info['GD Version'], $matches);
  52. // Get the major version
  53. $version = $matches[0];
  54. }
  55. if ( ! version_compare($version, '2.0.1', '>='))
  56. {
  57. throw new Kohana_Exception('Image_GD requires GD version :required or greater, you have :version',
  58. ['required' => '2.0.1', ':version' => $version]);
  59. }
  60. return Image_GD::$_checked = TRUE;
  61. }
  62. // Temporary image resource
  63. protected $_image;
  64. // Function name to open Image
  65. protected $_create_function;
  66. /**
  67. * Runs [Image_GD::check] and loads the image.
  68. *
  69. * @param string $file image file path
  70. * @return void
  71. * @throws Kohana_Exception
  72. */
  73. public function __construct($file)
  74. {
  75. if ( ! Image_GD::$_checked)
  76. {
  77. // Run the install check
  78. Image_GD::check();
  79. }
  80. parent::__construct($file);
  81. // Set the image creation function name
  82. switch ($this->type)
  83. {
  84. case IMAGETYPE_JPEG:
  85. $create = 'imagecreatefromjpeg';
  86. break;
  87. case IMAGETYPE_GIF:
  88. $create = 'imagecreatefromgif';
  89. break;
  90. case IMAGETYPE_PNG:
  91. $create = 'imagecreatefrompng';
  92. break;
  93. case self::IMAGETYPE_WEBP:
  94. $create = 'imagecreatefromwebp';
  95. break;
  96. }
  97. if ( ! isset($create) OR ! function_exists($create))
  98. {
  99. throw new Kohana_Exception('Installed GD does not support :type images',
  100. [':type' => image_type_to_extension($this->type, FALSE)]);
  101. }
  102. // Save function for future use
  103. $this->_create_function = $create;
  104. // Save filename for lazy loading
  105. $this->_image = $this->file;
  106. }
  107. /**
  108. * Destroys the loaded image to free up resources.
  109. *
  110. * @return void
  111. */
  112. public function __destruct()
  113. {
  114. if ( $this->_has_image() )
  115. {
  116. // Free all resources
  117. imagedestroy($this->_image);
  118. }
  119. }
  120. /**
  121. * Loads an image into GD.
  122. *
  123. * @return void
  124. */
  125. protected function _load_image()
  126. {
  127. if ( ! $this->_has_image() )
  128. {
  129. // Gets create function
  130. $create = $this->_create_function;
  131. // Open the temporary image
  132. $this->_image = $create($this->file);
  133. // Preserve transparency when saving
  134. imagesavealpha($this->_image, TRUE);
  135. }
  136. }
  137. /**
  138. * checks if an image has been set
  139. *
  140. * @return boolean
  141. */
  142. protected function _has_image()
  143. {
  144. if ( version_compare(PHP_VERSION, '8', '>=') )
  145. {
  146. return is_object($this->_image);
  147. }
  148. return is_resource($this->_image);
  149. }
  150. /**
  151. * Execute a resize.
  152. *
  153. * @param integer $width new width
  154. * @param integer $height new height
  155. * @return void
  156. */
  157. protected function _do_resize($width, $height)
  158. {
  159. // Presize width and height
  160. $pre_width = $this->width;
  161. $pre_height = $this->height;
  162. // Loads image if not yet loaded
  163. $this->_load_image();
  164. // Test if we can do a resize without resampling to speed up the final resize
  165. if ($width > ($this->width / 2) AND $height > ($this->height / 2))
  166. {
  167. // The maximum reduction is 10% greater than the final size
  168. $reduction_width = round($width * 1.1);
  169. $reduction_height = round($height * 1.1);
  170. while ($pre_width / 2 > $reduction_width AND $pre_height / 2 > $reduction_height)
  171. {
  172. // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction
  173. $pre_width /= 2;
  174. $pre_height /= 2;
  175. }
  176. // Create the temporary image to copy to
  177. $image = $this->_create($pre_width, $pre_height);
  178. if (imagecopyresized($image, $this->_image, 0, 0, 0, 0, $pre_width, $pre_height, $this->width, $this->height))
  179. {
  180. // Swap the new image for the old one
  181. imagedestroy($this->_image);
  182. $this->_image = $image;
  183. }
  184. }
  185. // Create the temporary image to copy to
  186. $image = $this->_create($width, $height);
  187. // Execute the resize
  188. if (imagecopyresampled($image, $this->_image, 0, 0, 0, 0, $width, $height, $pre_width, $pre_height))
  189. {
  190. // Swap the new image for the old one
  191. imagedestroy($this->_image);
  192. $this->_image = $image;
  193. // Reset the width and height
  194. $this->width = imagesx($image);
  195. $this->height = imagesy($image);
  196. }
  197. }
  198. /**
  199. * Execute a crop.
  200. *
  201. * @param integer $width new width
  202. * @param integer $height new height
  203. * @param integer $offset_x offset from the left
  204. * @param integer $offset_y offset from the top
  205. * @return void
  206. */
  207. protected function _do_crop($width, $height, $offset_x, $offset_y)
  208. {
  209. // Create the temporary image to copy to
  210. $image = $this->_create($width, $height);
  211. // Loads image if not yet loaded
  212. $this->_load_image();
  213. // Execute the crop
  214. if (imagecopyresampled($image, $this->_image, 0, 0, $offset_x, $offset_y, $width, $height, $width, $height))
  215. {
  216. // Swap the new image for the old one
  217. imagedestroy($this->_image);
  218. $this->_image = $image;
  219. // Reset the width and height
  220. $this->width = imagesx($image);
  221. $this->height = imagesy($image);
  222. }
  223. }
  224. /**
  225. * Execute a rotation.
  226. *
  227. * @param integer $degrees degrees to rotate
  228. * @return void
  229. */
  230. protected function _do_rotate($degrees)
  231. {
  232. if (empty(Image_GD::$_available_functions[Image_GD::IMAGEROTATE]))
  233. {
  234. throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
  235. [':function' => 'imagerotate']);
  236. }
  237. // Loads image if not yet loaded
  238. $this->_load_image();
  239. // Transparent black will be used as the background for the uncovered region
  240. $transparent = imagecolorallocatealpha($this->_image, 0, 0, 0, 127);
  241. // Rotate, setting the transparent color
  242. $image = imagerotate($this->_image, 360 - $degrees, $transparent, 1);
  243. // Save the alpha of the rotated image
  244. imagesavealpha($image, TRUE);
  245. // Get the width and height of the rotated image
  246. $width = imagesx($image);
  247. $height = imagesy($image);
  248. if (imagecopymerge($this->_image, $image, 0, 0, 0, 0, $width, $height, 100))
  249. {
  250. // Swap the new image for the old one
  251. imagedestroy($this->_image);
  252. $this->_image = $image;
  253. // Reset the width and height
  254. $this->width = $width;
  255. $this->height = $height;
  256. }
  257. }
  258. /**
  259. * Execute a flip.
  260. *
  261. * @param integer $direction direction to flip
  262. * @return void
  263. */
  264. protected function _do_flip($direction)
  265. {
  266. // Create the flipped image
  267. $flipped = $this->_create($this->width, $this->height);
  268. // Loads image if not yet loaded
  269. $this->_load_image();
  270. if ($direction === Image::HORIZONTAL)
  271. {
  272. for ($x = 0; $x < $this->width; $x++)
  273. {
  274. // Flip each row from top to bottom
  275. imagecopy($flipped, $this->_image, $x, 0, $this->width - $x - 1, 0, 1, $this->height);
  276. }
  277. }
  278. else
  279. {
  280. for ($y = 0; $y < $this->height; $y++)
  281. {
  282. // Flip each column from left to right
  283. imagecopy($flipped, $this->_image, 0, $y, 0, $this->height - $y - 1, $this->width, 1);
  284. }
  285. }
  286. // Swap the new image for the old one
  287. imagedestroy($this->_image);
  288. $this->_image = $flipped;
  289. // Reset the width and height
  290. $this->width = imagesx($flipped);
  291. $this->height = imagesy($flipped);
  292. }
  293. /**
  294. * Execute a sharpen.
  295. *
  296. * @param integer $amount amount to sharpen
  297. * @return void
  298. */
  299. protected function _do_sharpen($amount)
  300. {
  301. if (empty(Image_GD::$_available_functions[Image_GD::IMAGECONVOLUTION]))
  302. {
  303. throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
  304. [':function' => 'imageconvolution']);
  305. }
  306. // Loads image if not yet loaded
  307. $this->_load_image();
  308. // Amount should be in the range of 18-10
  309. $amount = round(abs(-18 + ($amount * 0.08)), 2);
  310. // Gaussian blur matrix
  311. $matrix = [
  312. [-1, -1, -1],
  313. [-1, $amount, -1],
  314. [-1, -1, -1],
  315. ];
  316. // Perform the sharpen
  317. if (imageconvolution($this->_image, $matrix, $amount - 8, 0))
  318. {
  319. // Reset the width and height
  320. $this->width = imagesx($this->_image);
  321. $this->height = imagesy($this->_image);
  322. }
  323. }
  324. /**
  325. * Execute a reflection.
  326. *
  327. * @param integer $height reflection height
  328. * @param integer $opacity reflection opacity
  329. * @param boolean $fade_in TRUE to fade out, FALSE to fade in
  330. * @return void
  331. */
  332. protected function _do_reflection($height, $opacity, $fade_in)
  333. {
  334. if (empty(Image_GD::$_available_functions[Image_GD::IMAGEFILTER]))
  335. {
  336. throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
  337. [':function' => 'imagefilter']);
  338. }
  339. // Loads image if not yet loaded
  340. $this->_load_image();
  341. // Convert an opacity range of 0-100 to 127-0
  342. $opacity = round(abs(($opacity * 127 / 100) - 127));
  343. if ($opacity < 127)
  344. {
  345. // Calculate the opacity stepping
  346. $stepping = (127 - $opacity) / $height;
  347. }
  348. else
  349. {
  350. // Avoid a "divide by zero" error
  351. $stepping = 127 / $height;
  352. }
  353. // Create the reflection image
  354. $reflection = $this->_create($this->width, $this->height + $height);
  355. // Copy the image to the reflection
  356. imagecopy($reflection, $this->_image, 0, 0, 0, 0, $this->width, $this->height);
  357. for ($offset = 0; $height >= $offset; $offset++)
  358. {
  359. // Read the next line down
  360. $src_y = $this->height - $offset - 1;
  361. // Place the line at the bottom of the reflection
  362. $dst_y = $this->height + $offset;
  363. if ($fade_in === TRUE)
  364. {
  365. // Start with the most transparent line first
  366. $dst_opacity = round($opacity + ($stepping * ($height - $offset)));
  367. }
  368. else
  369. {
  370. // Start with the most opaque line first
  371. $dst_opacity = round($opacity + ($stepping * $offset));
  372. }
  373. // Create a single line of the image
  374. $line = $this->_create($this->width, 1);
  375. // Copy a single line from the current image into the line
  376. imagecopy($line, $this->_image, 0, 0, 0, $src_y, $this->width, 1);
  377. // Colorize the line to add the correct alpha level
  378. imagefilter($line, IMG_FILTER_COLORIZE, 0, 0, 0, $dst_opacity);
  379. // Copy a the line into the reflection
  380. imagecopy($reflection, $line, 0, $dst_y, 0, 0, $this->width, 1);
  381. }
  382. // Swap the new image for the old one
  383. imagedestroy($this->_image);
  384. $this->_image = $reflection;
  385. // Reset the width and height
  386. $this->width = imagesx($reflection);
  387. $this->height = imagesy($reflection);
  388. }
  389. /**
  390. * Execute a watermarking.
  391. *
  392. * @param Image $image watermarking Image
  393. * @param integer $offset_x offset from the left
  394. * @param integer $offset_y offset from the top
  395. * @param integer $opacity opacity of watermark
  396. * @return void
  397. */
  398. protected function _do_watermark(Image $watermark, $offset_x, $offset_y, $opacity)
  399. {
  400. if (empty(Image_GD::$_available_functions[Image_GD::IMAGELAYEREFFECT]))
  401. {
  402. throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD',
  403. [':function' => 'imagelayereffect']);
  404. }
  405. // Loads image if not yet loaded
  406. $this->_load_image();
  407. // Create the watermark image resource
  408. $overlay = imagecreatefromstring($watermark->render());
  409. imagesavealpha($overlay, TRUE);
  410. // Get the width and height of the watermark
  411. $width = imagesx($overlay);
  412. $height = imagesy($overlay);
  413. if ($opacity < 100)
  414. {
  415. // Convert an opacity range of 0-100 to 127-0
  416. $opacity = round(abs(($opacity * 127 / 100) - 127));
  417. // Allocate transparent gray
  418. $color = imagecolorallocatealpha($overlay, 127, 127, 127, $opacity);
  419. // The transparent image will overlay the watermark
  420. imagelayereffect($overlay, IMG_EFFECT_OVERLAY);
  421. // Fill the background with the transparent color
  422. imagefilledrectangle($overlay, 0, 0, $width, $height, $color);
  423. }
  424. // Alpha blending must be enabled on the background!
  425. imagealphablending($this->_image, TRUE);
  426. if (imagecopy($this->_image, $overlay, $offset_x, $offset_y, 0, 0, $width, $height))
  427. {
  428. // Destroy the overlay image
  429. imagedestroy($overlay);
  430. }
  431. }
  432. /**
  433. * Execute a background.
  434. *
  435. * @param integer $r red
  436. * @param integer $g green
  437. * @param integer $b blue
  438. * @param integer $opacity opacity
  439. * @return void
  440. */
  441. protected function _do_background($r, $g, $b, $opacity)
  442. {
  443. // Loads image if not yet loaded
  444. $this->_load_image();
  445. // Convert an opacity range of 0-100 to 127-0
  446. $opacity = round(abs(($opacity * 127 / 100) - 127));
  447. // Create a new background
  448. $background = $this->_create($this->width, $this->height);
  449. // Allocate the color
  450. $color = imagecolorallocatealpha($background, $r, $g, $b, $opacity);
  451. // Fill the image with white
  452. imagefilledrectangle($background, 0, 0, $this->width, $this->height, $color);
  453. // Alpha blending must be enabled on the background!
  454. imagealphablending($background, TRUE);
  455. // Copy the image onto a white background to remove all transparency
  456. if (imagecopy($background, $this->_image, 0, 0, 0, 0, $this->width, $this->height))
  457. {
  458. // Swap the new image for the old one
  459. imagedestroy($this->_image);
  460. $this->_image = $background;
  461. }
  462. }
  463. /**
  464. * Execute a save.
  465. *
  466. * @param string $file new image filename
  467. * @param integer $quality quality
  468. * @return boolean
  469. */
  470. protected function _do_save($file, $quality)
  471. {
  472. // Loads image if not yet loaded
  473. $this->_load_image();
  474. // Get the extension of the file
  475. $extension = pathinfo($file, PATHINFO_EXTENSION);
  476. // Get the save function and IMAGETYPE
  477. list($save, $type) = $this->_save_function($extension, $quality);
  478. // Save the image to a file
  479. $status = isset($quality) ? $save($this->_image, $file, $quality) : $save($this->_image, $file);
  480. if ($status === TRUE AND $type !== $this->type)
  481. {
  482. // Reset the image type and mime type
  483. $this->type = $type;
  484. $this->mime = $this->image_type_to_mime_type($type);
  485. }
  486. return TRUE;
  487. }
  488. /**
  489. * Execute a render.
  490. *
  491. * @param string $type image type: png, jpg, gif, etc
  492. * @param integer $quality quality
  493. * @return string
  494. */
  495. protected function _do_render($type, $quality)
  496. {
  497. // Loads image if not yet loaded
  498. $this->_load_image();
  499. // Get the save function and IMAGETYPE
  500. list($save, $type) = $this->_save_function($type, $quality);
  501. // Capture the output
  502. ob_start();
  503. // Render the image
  504. $status = isset($quality) ? $save($this->_image, NULL, $quality) : $save($this->_image, NULL);
  505. if ($status === TRUE AND $type !== $this->type)
  506. {
  507. // Reset the image type and mime type
  508. $this->type = $type;
  509. $this->mime = $this->image_type_to_mime_type($type);
  510. }
  511. return ob_get_clean();
  512. }
  513. /**
  514. * Get the GD saving function and image type for this extension.
  515. * Also normalizes the quality setting
  516. *
  517. * @param string $extension image type: png, jpg, etc
  518. * @param integer $quality image quality
  519. * @return array save function, IMAGETYPE_* constant
  520. * @throws Kohana_Exception
  521. */
  522. protected function _save_function($extension, & $quality)
  523. {
  524. if ( ! $extension)
  525. {
  526. // Use the current image type
  527. $extension = image_type_to_extension($this->type, FALSE);
  528. }
  529. switch (strtolower($extension))
  530. {
  531. case 'jpg':
  532. case 'jpe':
  533. case 'jpeg':
  534. // Save a JPG file
  535. $save = 'imagejpeg';
  536. $type = IMAGETYPE_JPEG;
  537. break;
  538. case 'gif':
  539. // Save a GIF file
  540. $save = 'imagegif';
  541. $type = IMAGETYPE_GIF;
  542. // GIFs do not a quality setting
  543. $quality = NULL;
  544. break;
  545. case 'png':
  546. // Save a PNG file
  547. $save = 'imagepng';
  548. $type = IMAGETYPE_PNG;
  549. // Use a compression level of 9 (does not affect quality!)
  550. $quality = 9;
  551. break;
  552. case 'webp':
  553. // Save a WEBP file
  554. $save = 'imagewebp';
  555. $type = self::IMAGETYPE_WEBP;
  556. $quality = 80;
  557. break;
  558. default:
  559. throw new Kohana_Exception('Installed GD does not support :type images',
  560. [':type' => $extension]);
  561. break;
  562. }
  563. return [$save, $type];
  564. }
  565. /**
  566. * Create an empty image with the given width and height.
  567. *
  568. * @param integer $width image width
  569. * @param integer $height image height
  570. * @return resource
  571. */
  572. protected function _create($width, $height)
  573. {
  574. // Create an empty image
  575. $image = imagecreatetruecolor($width, $height);
  576. // Do not apply alpha blending
  577. imagealphablending($image, FALSE);
  578. // Save alpha levels
  579. imagesavealpha($image, TRUE);
  580. return $image;
  581. }
  582. }