Image.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. <?php
  2. /**
  3. * Image manipulation support. Allows images to be resized, cropped, etc.
  4. *
  5. * @package Kohana/Image
  6. * @category Base
  7. * @author Kohana Team
  8. * @copyright (c) Kohana Team
  9. * @license https://koseven.ga/LICENSE.md
  10. */
  11. abstract class Kohana_Image {
  12. // Resizing constraints
  13. const NONE = 0x01;
  14. const WIDTH = 0x02;
  15. const HEIGHT = 0x03;
  16. const AUTO = 0x04;
  17. const INVERSE = 0x05;
  18. const PRECISE = 0x06;
  19. // Flipping directions
  20. const HORIZONTAL = 0x11;
  21. const VERTICAL = 0x12;
  22. // PHP image_type_to_mime_type doesn't know WEBP yet
  23. const IMAGETYPE_WEBP = -1;
  24. /**
  25. * @deprecated - provide an image.default_driver value in your configuration instead
  26. * @var string default driver: GD, ImageMagick, etc
  27. */
  28. public static $default_driver = 'GD';
  29. // Status of the driver check
  30. protected static $_checked = FALSE;
  31. /**
  32. * Loads an image and prepares it for manipulation.
  33. *
  34. * $image = Image::factory('upload/test.jpg');
  35. *
  36. * @param string $file image file path
  37. * @param string $driver driver type: GD, ImageMagick, etc
  38. * @return Image
  39. * @uses Image::$default_driver
  40. */
  41. public static function factory($file, $driver = NULL)
  42. {
  43. if ($driver === NULL)
  44. {
  45. // Use the driver from configuration file or default one
  46. $configured_driver = Kohana::$config->load('image.default_driver');
  47. $driver = ($configured_driver) ? $configured_driver : Image::$default_driver;
  48. }
  49. // Set the class name
  50. $class = 'Image_'.$driver;
  51. return new $class($file);
  52. }
  53. /**
  54. * @var string image file path
  55. */
  56. public $file;
  57. /**
  58. * @var integer image width
  59. */
  60. public $width;
  61. /**
  62. * @var integer image height
  63. */
  64. public $height;
  65. /**
  66. * @var integer one of the IMAGETYPE_* constants
  67. */
  68. public $type;
  69. /**
  70. * @var string mime type of the image
  71. */
  72. public $mime;
  73. /**
  74. * Loads information about the image. Will throw an exception if the image
  75. * does not exist or is not an image.
  76. *
  77. * @param string $file image file path
  78. * @return void
  79. * @throws Kohana_Exception
  80. */
  81. public function __construct($file)
  82. {
  83. try
  84. {
  85. // Get the real path to the file
  86. $file = realpath($file);
  87. // Get the image information
  88. $info = getimagesize($file);
  89. }
  90. catch (Exception $e)
  91. {
  92. // Ignore all errors while reading the image
  93. }
  94. if (empty($file) OR empty($info))
  95. {
  96. throw new Kohana_Exception('Not an image or invalid image: :file',
  97. [':file' => Debug::path($file)]);
  98. }
  99. // Store the image information
  100. $this->file = $file;
  101. $this->width = $info[0];
  102. $this->height = $info[1];
  103. $this->type = $info[2];
  104. $this->mime = image_type_to_mime_type($this->type);
  105. }
  106. /**
  107. * Render the current image.
  108. *
  109. * echo $image;
  110. *
  111. * [!!] The output of this function is binary and must be rendered with the
  112. * appropriate Content-Type header or it will not be displayed correctly!
  113. *
  114. * @return string
  115. */
  116. public function __toString()
  117. {
  118. try
  119. {
  120. // Render the current image
  121. return $this->render();
  122. }
  123. catch (Exception $e)
  124. {
  125. if (is_object(Kohana::$log))
  126. {
  127. // Get the text of the exception
  128. $error = Kohana_Exception::text($e);
  129. // Add this exception to the log
  130. Kohana::$log->add(Log::ERROR, $error);
  131. }
  132. // Showing any kind of error will be "inside" image data
  133. return '';
  134. }
  135. }
  136. /**
  137. * Resize the image to the given size. Either the width or the height can
  138. * be omitted and the image will be resized proportionally.
  139. *
  140. * // Resize to 200 pixels on the shortest side
  141. * $image->resize(200, 200);
  142. *
  143. * // Resize to 200x200 pixels, keeping aspect ratio
  144. * $image->resize(200, 200, Image::INVERSE);
  145. *
  146. * // Resize to 500 pixel width, keeping aspect ratio
  147. * $image->resize(500, NULL);
  148. *
  149. * // Resize to 500 pixel height, keeping aspect ratio
  150. * $image->resize(NULL, 500);
  151. *
  152. * // Resize to 200x500 pixels, ignoring aspect ratio
  153. * $image->resize(200, 500, Image::NONE);
  154. *
  155. * @param integer $width new width
  156. * @param integer $height new height
  157. * @param integer $master master dimension
  158. * @return $this
  159. * @uses Image::_do_resize
  160. */
  161. public function resize($width = NULL, $height = NULL, $master = NULL)
  162. {
  163. if ($master === NULL)
  164. {
  165. // Choose the master dimension automatically
  166. $master = Image::AUTO;
  167. }
  168. // Image::WIDTH and Image::HEIGHT deprecated. You can use it in old projects,
  169. // but in new you must pass empty value for non-master dimension
  170. elseif ($master == Image::WIDTH AND ! empty($width))
  171. {
  172. $master = Image::AUTO;
  173. // Set empty height for backward compatibility
  174. $height = NULL;
  175. }
  176. elseif ($master == Image::HEIGHT AND ! empty($height))
  177. {
  178. $master = Image::AUTO;
  179. // Set empty width for backward compatibility
  180. $width = NULL;
  181. }
  182. if (empty($width))
  183. {
  184. if ($master === Image::NONE)
  185. {
  186. // Use the current width
  187. $width = $this->width;
  188. }
  189. else
  190. {
  191. // If width not set, master will be height
  192. $master = Image::HEIGHT;
  193. }
  194. }
  195. if (empty($height))
  196. {
  197. if ($master === Image::NONE)
  198. {
  199. // Use the current height
  200. $height = $this->height;
  201. }
  202. else
  203. {
  204. // If height not set, master will be width
  205. $master = Image::WIDTH;
  206. }
  207. }
  208. switch ($master)
  209. {
  210. case Image::AUTO:
  211. // Choose direction with the greatest reduction ratio
  212. $master = ($this->width / $width) > ($this->height / $height) ? Image::WIDTH : Image::HEIGHT;
  213. break;
  214. case Image::INVERSE:
  215. // Choose direction with the minimum reduction ratio
  216. $master = ($this->width / $width) > ($this->height / $height) ? Image::HEIGHT : Image::WIDTH;
  217. break;
  218. }
  219. switch ($master)
  220. {
  221. case Image::WIDTH:
  222. // Recalculate the height based on the width proportions
  223. $height = $this->height * $width / $this->width;
  224. break;
  225. case Image::HEIGHT:
  226. // Recalculate the width based on the height proportions
  227. $width = $this->width * $height / $this->height;
  228. break;
  229. case Image::PRECISE:
  230. // Resize to precise size
  231. $ratio = $this->width / $this->height;
  232. if ($width / $height > $ratio)
  233. {
  234. $height = $this->height * $width / $this->width;
  235. }
  236. else
  237. {
  238. $width = $this->width * $height / $this->height;
  239. }
  240. break;
  241. }
  242. // Convert the width and height to integers, minimum value is 1px
  243. $width = max(round($width), 1);
  244. $height = max(round($height), 1);
  245. $this->_do_resize($width, $height);
  246. return $this;
  247. }
  248. /**
  249. * Crop an image to the given size. Either the width or the height can be
  250. * omitted and the current width or height will be used.
  251. *
  252. * If no offset is specified, the center of the axis will be used.
  253. * If an offset of TRUE is specified, the bottom of the axis will be used.
  254. *
  255. * // Crop the image to 200x200 pixels, from the center
  256. * $image->crop(200, 200);
  257. *
  258. * @param integer $width new width
  259. * @param integer $height new height
  260. * @param mixed $offset_x offset from the left
  261. * @param mixed $offset_y offset from the top
  262. * @return $this
  263. * @uses Image::_do_crop
  264. */
  265. public function crop($width, $height, $offset_x = NULL, $offset_y = NULL)
  266. {
  267. if ($width > $this->width)
  268. {
  269. // Use the current width
  270. $width = $this->width;
  271. }
  272. if ($height > $this->height)
  273. {
  274. // Use the current height
  275. $height = $this->height;
  276. }
  277. if ($offset_x === NULL)
  278. {
  279. // Center the X offset
  280. $offset_x = round(($this->width - $width) / 2);
  281. }
  282. elseif ($offset_x === TRUE)
  283. {
  284. // Bottom the X offset
  285. $offset_x = $this->width - $width;
  286. }
  287. elseif ($offset_x < 0)
  288. {
  289. // Set the X offset from the right
  290. $offset_x = $this->width - $width + $offset_x;
  291. }
  292. if ($offset_y === NULL)
  293. {
  294. // Center the Y offset
  295. $offset_y = round(($this->height - $height) / 2);
  296. }
  297. elseif ($offset_y === TRUE)
  298. {
  299. // Bottom the Y offset
  300. $offset_y = $this->height - $height;
  301. }
  302. elseif ($offset_y < 0)
  303. {
  304. // Set the Y offset from the bottom
  305. $offset_y = $this->height - $height + $offset_y;
  306. }
  307. // Determine the maximum possible width and height
  308. $max_width = $this->width - $offset_x;
  309. $max_height = $this->height - $offset_y;
  310. if ($width > $max_width)
  311. {
  312. // Use the maximum available width
  313. $width = $max_width;
  314. }
  315. if ($height > $max_height)
  316. {
  317. // Use the maximum available height
  318. $height = $max_height;
  319. }
  320. $this->_do_crop($width, $height, $offset_x, $offset_y);
  321. return $this;
  322. }
  323. /**
  324. * Rotate the image by a given amount.
  325. *
  326. * // Rotate 45 degrees clockwise
  327. * $image->rotate(45);
  328. *
  329. * // Rotate 90% counter-clockwise
  330. * $image->rotate(-90);
  331. *
  332. * @param integer $degrees degrees to rotate: -360-360
  333. * @return $this
  334. * @uses Image::_do_rotate
  335. */
  336. public function rotate($degrees)
  337. {
  338. // Make the degrees an integer
  339. $degrees = (int) $degrees;
  340. if ($degrees > 180)
  341. {
  342. do
  343. {
  344. // Keep subtracting full circles until the degrees have normalized
  345. $degrees -= 360;
  346. }
  347. while ($degrees > 180);
  348. }
  349. if ($degrees < -180)
  350. {
  351. do
  352. {
  353. // Keep adding full circles until the degrees have normalized
  354. $degrees += 360;
  355. }
  356. while ($degrees < -180);
  357. }
  358. $this->_do_rotate($degrees);
  359. return $this;
  360. }
  361. /**
  362. * Flip the image along the horizontal or vertical axis.
  363. *
  364. * // Flip the image from top to bottom
  365. * $image->flip(Image::HORIZONTAL);
  366. *
  367. * // Flip the image from left to right
  368. * $image->flip(Image::VERTICAL);
  369. *
  370. * @param integer $direction direction: Image::HORIZONTAL, Image::VERTICAL
  371. * @return $this
  372. * @uses Image::_do_flip
  373. */
  374. public function flip($direction)
  375. {
  376. if ($direction !== Image::HORIZONTAL)
  377. {
  378. // Flip vertically
  379. $direction = Image::VERTICAL;
  380. }
  381. $this->_do_flip($direction);
  382. return $this;
  383. }
  384. /**
  385. * Sharpen the image by a given amount.
  386. *
  387. * // Sharpen the image by 20%
  388. * $image->sharpen(20);
  389. *
  390. * @param integer $amount amount to sharpen: 1-100
  391. * @return $this
  392. * @uses Image::_do_sharpen
  393. */
  394. public function sharpen($amount)
  395. {
  396. // The amount must be in the range of 1 to 100
  397. $amount = min(max($amount, 1), 100);
  398. $this->_do_sharpen($amount);
  399. return $this;
  400. }
  401. /**
  402. * Add a reflection to an image. The most opaque part of the reflection
  403. * will be equal to the opacity setting and fade out to full transparent.
  404. * Alpha transparency is preserved.
  405. *
  406. * // Create a 50 pixel reflection that fades from 0-100% opacity
  407. * $image->reflection(50);
  408. *
  409. * // Create a 50 pixel reflection that fades from 100-0% opacity
  410. * $image->reflection(50, 100, TRUE);
  411. *
  412. * // Create a 50 pixel reflection that fades from 0-60% opacity
  413. * $image->reflection(50, 60, TRUE);
  414. *
  415. * [!!] By default, the reflection will be go from transparent at the top
  416. * to opaque at the bottom.
  417. *
  418. * @param integer $height reflection height
  419. * @param integer $opacity reflection opacity: 0-100
  420. * @param boolean $fade_in TRUE to fade in, FALSE to fade out
  421. * @return $this
  422. * @uses Image::_do_reflection
  423. */
  424. public function reflection($height = NULL, $opacity = 100, $fade_in = FALSE)
  425. {
  426. if ($height === NULL OR $height > $this->height)
  427. {
  428. // Use the current height
  429. $height = $this->height;
  430. }
  431. // The opacity must be in the range of 0 to 100
  432. $opacity = min(max($opacity, 0), 100);
  433. $this->_do_reflection($height, $opacity, $fade_in);
  434. return $this;
  435. }
  436. /**
  437. * Add a watermark to an image with a specified opacity. Alpha transparency
  438. * will be preserved.
  439. *
  440. * If no offset is specified, the center of the axis will be used.
  441. * If an offset of TRUE is specified, the bottom of the axis will be used.
  442. *
  443. * // Add a watermark to the bottom right of the image
  444. * $mark = Image::factory('upload/watermark.png');
  445. * $image->watermark($mark, TRUE, TRUE);
  446. *
  447. * @param Image $watermark watermark Image instance
  448. * @param integer $offset_x offset from the left
  449. * @param integer $offset_y offset from the top
  450. * @param integer $opacity opacity of watermark: 1-100
  451. * @return $this
  452. * @uses Image::_do_watermark
  453. */
  454. public function watermark(Image $watermark, $offset_x = NULL, $offset_y = NULL, $opacity = 100)
  455. {
  456. if ($offset_x === NULL)
  457. {
  458. // Center the X offset
  459. $offset_x = round(($this->width - $watermark->width) / 2);
  460. }
  461. elseif ($offset_x === TRUE)
  462. {
  463. // Bottom the X offset
  464. $offset_x = $this->width - $watermark->width;
  465. }
  466. elseif ($offset_x < 0)
  467. {
  468. // Set the X offset from the right
  469. $offset_x = $this->width - $watermark->width + $offset_x;
  470. }
  471. if ($offset_y === NULL)
  472. {
  473. // Center the Y offset
  474. $offset_y = round(($this->height - $watermark->height) / 2);
  475. }
  476. elseif ($offset_y === TRUE)
  477. {
  478. // Bottom the Y offset
  479. $offset_y = $this->height - $watermark->height;
  480. }
  481. elseif ($offset_y < 0)
  482. {
  483. // Set the Y offset from the bottom
  484. $offset_y = $this->height - $watermark->height + $offset_y;
  485. }
  486. // The opacity must be in the range of 1 to 100
  487. $opacity = min(max($opacity, 1), 100);
  488. $this->_do_watermark($watermark, $offset_x, $offset_y, $opacity);
  489. return $this;
  490. }
  491. /**
  492. * Set the background color of an image. This is only useful for images
  493. * with alpha transparency.
  494. *
  495. * // Make the image background black
  496. * $image->background('#000');
  497. *
  498. * // Make the image background black with 50% opacity
  499. * $image->background('#000', 50);
  500. *
  501. * @param string $color hexadecimal color value
  502. * @param integer $opacity background opacity: 0-100
  503. * @return $this
  504. * @uses Image::_do_background
  505. */
  506. public function background($color, $opacity = 100)
  507. {
  508. if ($color[0] === '#')
  509. {
  510. // Remove the pound
  511. $color = substr($color, 1);
  512. }
  513. if (strlen($color) === 3)
  514. {
  515. // Convert shorthand into longhand hex notation
  516. $color = preg_replace('/./', '$0$0', $color);
  517. }
  518. // Convert the hex into RGB values
  519. list ($r, $g, $b) = array_map('hexdec', str_split($color, 2));
  520. // The opacity must be in the range of 0 to 100
  521. $opacity = min(max($opacity, 0), 100);
  522. $this->_do_background($r, $g, $b, $opacity);
  523. return $this;
  524. }
  525. /**
  526. * Save the image. If the filename is omitted, the original image will
  527. * be overwritten.
  528. *
  529. * // Save the image as a PNG
  530. * $image->save('saved/cool.png');
  531. *
  532. * // Overwrite the original image
  533. * $image->save();
  534. *
  535. * [!!] If the file exists, but is not writable, an exception will be thrown.
  536. *
  537. * [!!] If the file does not exist, and the directory is not writable, an
  538. * exception will be thrown.
  539. *
  540. * @param string $file new image path
  541. * @param integer $quality quality of image: 1-100
  542. * @return boolean
  543. * @uses Image::_save
  544. * @throws Kohana_Exception
  545. */
  546. public function save($file = NULL, $quality = 100)
  547. {
  548. if ($file === NULL)
  549. {
  550. // Overwrite the file
  551. $file = $this->file;
  552. }
  553. if (is_file($file))
  554. {
  555. if ( ! is_writable($file))
  556. {
  557. throw new Kohana_Exception('File must be writable: :file',
  558. [':file' => Debug::path($file)]);
  559. }
  560. }
  561. else
  562. {
  563. // Get the directory of the file
  564. $directory = realpath(pathinfo($file, PATHINFO_DIRNAME));
  565. if ( ! is_dir($directory) OR ! is_writable($directory))
  566. {
  567. throw new Kohana_Exception('Directory must be writable: :directory',
  568. [':directory' => Debug::path($directory)]);
  569. }
  570. }
  571. // The quality must be in the range of 1 to 100
  572. $quality = min(max($quality, 1), 100);
  573. return $this->_do_save($file, $quality);
  574. }
  575. /**
  576. * Render the image and return the binary string.
  577. *
  578. * // Render the image at 50% quality
  579. * $data = $image->render(NULL, 50);
  580. *
  581. * // Render the image as a PNG
  582. * $data = $image->render('png');
  583. *
  584. * @param string $type image type to return: png, jpg, gif, etc
  585. * @param integer $quality quality of image: 1-100
  586. * @return string
  587. * @uses Image::_do_render
  588. */
  589. public function render($type = NULL, $quality = 100)
  590. {
  591. if ($type === NULL)
  592. {
  593. // Use the current image type
  594. $type = image_type_to_extension($this->type, FALSE);
  595. }
  596. return $this->_do_render($type, $quality);
  597. }
  598. /**
  599. * Returns the image mime type
  600. * Adds support for webp image type, which is not known by php
  601. *
  602. * @param string $type image type: png, jpg, gif, etc
  603. * @return string
  604. */
  605. protected function image_type_to_mime_type($type)
  606. {
  607. if ($type === self::IMAGETYPE_WEBP)
  608. return 'image/webp';
  609. return image_type_to_mime_type($type);
  610. }
  611. /**
  612. * Execute a resize.
  613. *
  614. * @param integer $width new width
  615. * @param integer $height new height
  616. * @return void
  617. */
  618. abstract protected function _do_resize($width, $height);
  619. /**
  620. * Execute a crop.
  621. *
  622. * @param integer $width new width
  623. * @param integer $height new height
  624. * @param integer $offset_x offset from the left
  625. * @param integer $offset_y offset from the top
  626. * @return void
  627. */
  628. abstract protected function _do_crop($width, $height, $offset_x, $offset_y);
  629. /**
  630. * Execute a rotation.
  631. *
  632. * @param integer $degrees degrees to rotate
  633. * @return void
  634. */
  635. abstract protected function _do_rotate($degrees);
  636. /**
  637. * Execute a flip.
  638. *
  639. * @param integer $direction direction to flip
  640. * @return void
  641. */
  642. abstract protected function _do_flip($direction);
  643. /**
  644. * Execute a sharpen.
  645. *
  646. * @param integer $amount amount to sharpen
  647. * @return void
  648. */
  649. abstract protected function _do_sharpen($amount);
  650. /**
  651. * Execute a reflection.
  652. *
  653. * @param integer $height reflection height
  654. * @param integer $opacity reflection opacity
  655. * @param boolean $fade_in TRUE to fade out, FALSE to fade in
  656. * @return void
  657. */
  658. abstract protected function _do_reflection($height, $opacity, $fade_in);
  659. /**
  660. * Execute a watermarking.
  661. *
  662. * @param Image $image watermarking Image
  663. * @param integer $offset_x offset from the left
  664. * @param integer $offset_y offset from the top
  665. * @param integer $opacity opacity of watermark
  666. * @return void
  667. */
  668. abstract protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity);
  669. /**
  670. * Execute a background.
  671. *
  672. * @param integer $r red
  673. * @param integer $g green
  674. * @param integer $b blue
  675. * @param integer $opacity opacity
  676. * @return void
  677. */
  678. abstract protected function _do_background($r, $g, $b, $opacity);
  679. /**
  680. * Execute a save.
  681. *
  682. * @param string $file new image filename
  683. * @param integer $quality quality
  684. * @return boolean
  685. */
  686. abstract protected function _do_save($file, $quality);
  687. /**
  688. * Execute a render.
  689. *
  690. * @param string $type image type: png, jpg, gif, etc
  691. * @param integer $quality quality
  692. * @return string
  693. */
  694. abstract protected function _do_render($type, $quality);
  695. }