PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Joomla/Image/Image.php

https://github.com/piotr-cz/joomla-framework
PHP | 1094 lines | 589 code | 144 blank | 361 comment | 54 complexity | ae27d983693940800e81f147c2abe6c6 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Part of the Joomla Framework Image Package
  4. *
  5. * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
  6. * @license GNU General Public License version 2 or later; see LICENSE
  7. */
  8. namespace Joomla\Image;
  9. use Psr\Log\NullLogger;
  10. use Psr\Log\LoggerInterface;
  11. use Psr\Log\LoggerAwareInterface;
  12. /**
  13. * Class to manipulate an image.
  14. *
  15. * @since 1.0
  16. */
  17. class Image implements LoggerAwareInterface
  18. {
  19. /**
  20. * @const integer
  21. * @since 1.0
  22. */
  23. const SCALE_FILL = 1;
  24. /**
  25. * @const integer
  26. * @since 1.0
  27. */
  28. const SCALE_INSIDE = 2;
  29. /**
  30. * @const integer
  31. * @since 1.0
  32. */
  33. const SCALE_OUTSIDE = 3;
  34. /**
  35. * @const integer
  36. * @since 1.0
  37. */
  38. const CROP = 4;
  39. /**
  40. * @const integer
  41. * @since 1.0
  42. */
  43. const CROP_RESIZE = 5;
  44. /**
  45. * @const integer
  46. * @since 1.0
  47. */
  48. const SCALE_FIT = 6;
  49. /**
  50. * @var resource The image resource handle.
  51. * @since 1.0
  52. */
  53. protected $handle;
  54. /**
  55. * @var string The source image path.
  56. * @since 1.0
  57. */
  58. protected $path = null;
  59. /**
  60. * @var array Whether or not different image formats are supported.
  61. * @since 1.0
  62. */
  63. protected static $formats = array();
  64. /**
  65. * @var LoggerInterface Logger object
  66. * @since 1.0
  67. */
  68. protected $logger = null;
  69. /**
  70. * Class constructor.
  71. *
  72. * @param mixed $source Either a file path for a source image or a GD resource handler for an image.
  73. *
  74. * @since 1.0
  75. * @throws \RuntimeException
  76. */
  77. public function __construct($source = null)
  78. {
  79. // Verify that GD support for PHP is available.
  80. if (!extension_loaded('gd'))
  81. {
  82. // @codeCoverageIgnoreStart
  83. throw new \RuntimeException('The GD extension for PHP is not available.');
  84. // @codeCoverageIgnoreEnd
  85. }
  86. // Determine which image types are supported by GD, but only once.
  87. if (!isset(self::$formats[IMAGETYPE_JPEG]))
  88. {
  89. $info = gd_info();
  90. self::$formats[IMAGETYPE_JPEG] = ($info['JPEG Support']) ? true : false;
  91. self::$formats[IMAGETYPE_PNG] = ($info['PNG Support']) ? true : false;
  92. self::$formats[IMAGETYPE_GIF] = ($info['GIF Read Support']) ? true : false;
  93. }
  94. // If the source input is a resource, set it as the image handle.
  95. if (is_resource($source) && (get_resource_type($source) == 'gd'))
  96. {
  97. $this->handle = &$source;
  98. }
  99. elseif (!empty($source) && is_string($source))
  100. {
  101. // If the source input is not empty, assume it is a path and populate the image handle.
  102. $this->loadFile($source);
  103. }
  104. }
  105. /**
  106. * Get the logger.
  107. *
  108. * @return LoggerInterface
  109. *
  110. * @since 1.0
  111. */
  112. public function getLogger()
  113. {
  114. // If a logger hasn't been set, use NullLogger
  115. if (! ($this->logger instanceof LoggerInterface))
  116. {
  117. $this->logger = new NullLogger;
  118. }
  119. return $this->logger;
  120. }
  121. /**
  122. * Sets a logger instance on the object
  123. *
  124. * @param LoggerInterface $logger A PSR-3 compliant logger.
  125. *
  126. * @return Image This object for message chaining.
  127. *
  128. * @since 1.0
  129. */
  130. public function setLogger(LoggerInterface $logger)
  131. {
  132. $this->logger = $logger;
  133. return $this;
  134. }
  135. /**
  136. * Method to return a properties object for an image given a filesystem path. The
  137. * result object has values for image width, height, type, attributes, mime type, bits,
  138. * and channels.
  139. *
  140. * @param string $path The filesystem path to the image for which to get properties.
  141. *
  142. * @return \stdClass
  143. *
  144. * @since 1.0
  145. * @throws \InvalidArgumentException
  146. * @throws \RuntimeException
  147. */
  148. public static function getImageFileProperties($path)
  149. {
  150. // Make sure the file exists.
  151. if (!file_exists($path))
  152. {
  153. throw new \InvalidArgumentException('The image file does not exist.');
  154. }
  155. // Get the image file information.
  156. $info = getimagesize($path);
  157. if (!$info)
  158. {
  159. // @codeCoverageIgnoreStart
  160. throw new \RuntimeException('Unable to get properties for the image.');
  161. // @codeCoverageIgnoreEnd
  162. }
  163. // Build the response object.
  164. $properties = (object) array(
  165. 'width' => $info[0],
  166. 'height' => $info[1],
  167. 'type' => $info[2],
  168. 'attributes' => $info[3],
  169. 'bits' => isset($info['bits']) ? $info['bits'] : null,
  170. 'channels' => isset($info['channels']) ? $info['channels'] : null,
  171. 'mime' => $info['mime']
  172. );
  173. return $properties;
  174. }
  175. /**
  176. * Method to generate thumbnails from the current image. It allows
  177. * creation by resizing or cropping the original image.
  178. *
  179. * @param mixed $thumbSizes String or array of strings. Example: $thumbSizes = array('150x75','250x150');
  180. * @param integer $creationMethod 1-3 resize $scaleMethod | 4 create croppping | 5 resize then crop
  181. *
  182. * @return array
  183. *
  184. * @since 1.0
  185. * @throws \LogicException
  186. * @throws \InvalidArgumentException
  187. */
  188. public function generateThumbs($thumbSizes, $creationMethod = self::SCALE_INSIDE)
  189. {
  190. // Make sure the resource handle is valid.
  191. if (!$this->isLoaded())
  192. {
  193. throw new \LogicException('No valid image was loaded.');
  194. }
  195. // Accept a single thumbsize string as parameter
  196. if (!is_array($thumbSizes))
  197. {
  198. $thumbSizes = array($thumbSizes);
  199. }
  200. // Process thumbs
  201. $generated = array();
  202. if (!empty($thumbSizes))
  203. {
  204. foreach ($thumbSizes as $thumbSize)
  205. {
  206. // Desired thumbnail size
  207. $size = explode('x', strtolower($thumbSize));
  208. if (count($size) != 2)
  209. {
  210. throw new \InvalidArgumentException('Invalid thumb size received: ' . $thumbSize);
  211. }
  212. $thumbWidth = $size[0];
  213. $thumbHeight = $size[1];
  214. switch ($creationMethod)
  215. {
  216. // Case for self::CROP
  217. case 4:
  218. $thumb = $this->crop($thumbWidth, $thumbHeight, null, null, true);
  219. break;
  220. // Case for self::CROP_RESIZE
  221. case 5:
  222. $thumb = $this->cropResize($thumbWidth, $thumbHeight, true);
  223. break;
  224. default:
  225. $thumb = $this->resize($thumbWidth, $thumbHeight, true, $creationMethod);
  226. break;
  227. }
  228. // Store the thumb in the results array
  229. $generated[] = $thumb;
  230. }
  231. }
  232. return $generated;
  233. }
  234. /**
  235. * Method to create thumbnails from the current image and save them to disk. It allows creation by resizing
  236. * or croppping the original image.
  237. *
  238. * @param mixed $thumbSizes string or array of strings. Example: $thumbSizes = array('150x75','250x150');
  239. * @param integer $creationMethod 1-3 resize $scaleMethod | 4 create croppping
  240. * @param string $thumbsFolder destination thumbs folder. null generates a thumbs folder in the image folder
  241. *
  242. * @return array
  243. *
  244. * @since 1.0
  245. * @throws \LogicException
  246. * @throws \InvalidArgumentException
  247. */
  248. public function createThumbs($thumbSizes, $creationMethod = self::SCALE_INSIDE, $thumbsFolder = null)
  249. {
  250. // Make sure the resource handle is valid.
  251. if (!$this->isLoaded())
  252. {
  253. throw new \LogicException('No valid image was loaded.');
  254. }
  255. // No thumbFolder set -> we will create a thumbs folder in the current image folder
  256. if (is_null($thumbsFolder))
  257. {
  258. $thumbsFolder = dirname($this->getPath()) . '/thumbs';
  259. }
  260. // Check destination
  261. if (!is_dir($thumbsFolder) && (!is_dir(dirname($thumbsFolder)) || !@mkdir($thumbsFolder)))
  262. {
  263. throw new \InvalidArgumentException('Folder does not exist and cannot be created: ' . $thumbsFolder);
  264. }
  265. // Process thumbs
  266. $thumbsCreated = array();
  267. if ($thumbs = $this->generateThumbs($thumbSizes, $creationMethod))
  268. {
  269. // Parent image properties
  270. $imgProperties = self::getImageFileProperties($this->getPath());
  271. foreach ($thumbs as $thumb)
  272. {
  273. // Get thumb properties
  274. $thumbWidth = $thumb->getWidth();
  275. $thumbHeight = $thumb->getHeight();
  276. // Generate thumb name
  277. $filename = pathinfo($this->getPath(), PATHINFO_FILENAME);
  278. $fileExtension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
  279. $thumbFileName = $filename . '_' . $thumbWidth . 'x' . $thumbHeight . '.' . $fileExtension;
  280. // Save thumb file to disk
  281. $thumbFileName = $thumbsFolder . '/' . $thumbFileName;
  282. if ($thumb->toFile($thumbFileName, $imgProperties->type))
  283. {
  284. // Return Image object with thumb path to ease further manipulation
  285. $thumb->path = $thumbFileName;
  286. $thumbsCreated[] = $thumb;
  287. }
  288. }
  289. }
  290. return $thumbsCreated;
  291. }
  292. /**
  293. * Method to crop the current image.
  294. *
  295. * @param mixed $width The width of the image section to crop in pixels or a percentage.
  296. * @param mixed $height The height of the image section to crop in pixels or a percentage.
  297. * @param integer $left The number of pixels from the left to start cropping.
  298. * @param integer $top The number of pixels from the top to start cropping.
  299. * @param boolean $createNew If true the current image will be cloned, cropped and returned; else
  300. * the current image will be cropped and returned.
  301. *
  302. * @return Image
  303. *
  304. * @since 1.0
  305. * @throws \LogicException
  306. */
  307. public function crop($width, $height, $left = null, $top = null, $createNew = true)
  308. {
  309. // Make sure the resource handle is valid.
  310. if (!$this->isLoaded())
  311. {
  312. throw new \LogicException('No valid image was loaded.');
  313. }
  314. // Sanitize width.
  315. $width = $this->sanitizeWidth($width, $height);
  316. // Sanitize height.
  317. $height = $this->sanitizeHeight($height, $width);
  318. // Autocrop offsets
  319. if (is_null($left))
  320. {
  321. $left = round(($this->getWidth() - $width) / 2);
  322. }
  323. if (is_null($top))
  324. {
  325. $top = round(($this->getHeight() - $height) / 2);
  326. }
  327. // Sanitize left.
  328. $left = $this->sanitizeOffset($left);
  329. // Sanitize top.
  330. $top = $this->sanitizeOffset($top);
  331. // Create the new truecolor image handle.
  332. $handle = imagecreatetruecolor($width, $height);
  333. // Allow transparency for the new image handle.
  334. imagealphablending($handle, false);
  335. imagesavealpha($handle, true);
  336. if ($this->isTransparent())
  337. {
  338. // Get the transparent color values for the current image.
  339. $rgba = imageColorsForIndex($this->handle, imagecolortransparent($this->handle));
  340. $color = imageColorAllocate($this->handle, $rgba['red'], $rgba['green'], $rgba['blue']);
  341. // Set the transparent color values for the new image.
  342. imagecolortransparent($handle, $color);
  343. imagefill($handle, 0, 0, $color);
  344. imagecopyresized($handle, $this->handle, 0, 0, $left, $top, $width, $height, $width, $height);
  345. }
  346. else
  347. {
  348. imagecopyresampled($handle, $this->handle, 0, 0, $left, $top, $width, $height, $width, $height);
  349. }
  350. // If we are cropping to a new image, create a new Image object.
  351. if ($createNew)
  352. {
  353. // @codeCoverageIgnoreStart
  354. $new = new Image($handle);
  355. return $new;
  356. // @codeCoverageIgnoreEnd
  357. }
  358. else
  359. // Swap out the current handle for the new image handle.
  360. {
  361. // Free the memory from the current handle
  362. $this->destroy();
  363. $this->handle = $handle;
  364. return $this;
  365. }
  366. }
  367. /**
  368. * Method to apply a filter to the image by type. Two examples are: grayscale and sketchy.
  369. *
  370. * @param string $type The name of the image filter to apply.
  371. * @param array $options An array of options for the filter.
  372. *
  373. * @return Image
  374. *
  375. * @since 1.0
  376. * @see Joomla\Image\Filter
  377. * @throws \LogicException
  378. */
  379. public function filter($type, array $options = array())
  380. {
  381. // Make sure the resource handle is valid.
  382. if (!$this->isLoaded())
  383. {
  384. throw new \LogicException('No valid image was loaded.');
  385. }
  386. // Get the image filter instance.
  387. $filter = $this->getFilterInstance($type);
  388. // Execute the image filter.
  389. $filter->execute($options);
  390. return $this;
  391. }
  392. /**
  393. * Method to get the height of the image in pixels.
  394. *
  395. * @return integer
  396. *
  397. * @since 1.0
  398. * @throws \LogicException
  399. */
  400. public function getHeight()
  401. {
  402. // Make sure the resource handle is valid.
  403. if (!$this->isLoaded())
  404. {
  405. throw new \LogicException('No valid image was loaded.');
  406. }
  407. return imagesy($this->handle);
  408. }
  409. /**
  410. * Method to get the width of the image in pixels.
  411. *
  412. * @return integer
  413. *
  414. * @since 1.0
  415. * @throws \LogicException
  416. */
  417. public function getWidth()
  418. {
  419. // Make sure the resource handle is valid.
  420. if (!$this->isLoaded())
  421. {
  422. throw new \LogicException('No valid image was loaded.');
  423. }
  424. return imagesx($this->handle);
  425. }
  426. /**
  427. * Method to return the path
  428. *
  429. * @return string
  430. *
  431. * @since 1.0
  432. */
  433. public function getPath()
  434. {
  435. return $this->path;
  436. }
  437. /**
  438. * Method to determine whether or not an image has been loaded into the object.
  439. *
  440. * @return boolean
  441. *
  442. * @since 1.0
  443. */
  444. public function isLoaded()
  445. {
  446. // Make sure the resource handle is valid.
  447. if (!is_resource($this->handle) || (get_resource_type($this->handle) != 'gd'))
  448. {
  449. return false;
  450. }
  451. return true;
  452. }
  453. /**
  454. * Method to determine whether or not the image has transparency.
  455. *
  456. * @return bool
  457. *
  458. * @since 1.0
  459. * @throws \LogicException
  460. */
  461. public function isTransparent()
  462. {
  463. // Make sure the resource handle is valid.
  464. if (!$this->isLoaded())
  465. {
  466. throw new \LogicException('No valid image was loaded.');
  467. }
  468. return (imagecolortransparent($this->handle) >= 0);
  469. }
  470. /**
  471. * Method to load a file into the Image object as the resource.
  472. *
  473. * @param string $path The filesystem path to load as an image.
  474. *
  475. * @return void
  476. *
  477. * @since 1.0
  478. * @throws \InvalidArgumentException
  479. * @throws \RuntimeException
  480. */
  481. public function loadFile($path)
  482. {
  483. // Destroy the current image handle if it exists
  484. $this->destroy();
  485. // Make sure the file exists.
  486. if (!file_exists($path))
  487. {
  488. throw new \InvalidArgumentException('The image file does not exist.');
  489. }
  490. // Get the image properties.
  491. $properties = self::getImageFileProperties($path);
  492. // Attempt to load the image based on the MIME-Type
  493. switch ($properties->mime)
  494. {
  495. case 'image/gif':
  496. // Make sure the image type is supported.
  497. if (empty(self::$formats[IMAGETYPE_GIF]))
  498. {
  499. // @codeCoverageIgnoreStart
  500. $this->getLogger()->error('Attempting to load an image of unsupported type GIF.');
  501. throw new \RuntimeException('Attempting to load an image of unsupported type GIF.');
  502. // @codeCoverageIgnoreEnd
  503. }
  504. // Attempt to create the image handle.
  505. $handle = imagecreatefromgif($path);
  506. if (!is_resource($handle))
  507. {
  508. // @codeCoverageIgnoreStart
  509. throw new \RuntimeException('Unable to process GIF image.');
  510. // @codeCoverageIgnoreEnd
  511. }
  512. $this->handle = $handle;
  513. break;
  514. case 'image/jpeg':
  515. // Make sure the image type is supported.
  516. if (empty(self::$formats[IMAGETYPE_JPEG]))
  517. {
  518. // @codeCoverageIgnoreStart
  519. $this->getLogger()->error('Attempting to load an image of unsupported type JPG.');
  520. throw new \RuntimeException('Attempting to load an image of unsupported type JPG.');
  521. // @codeCoverageIgnoreEnd
  522. }
  523. // Attempt to create the image handle.
  524. $handle = imagecreatefromjpeg($path);
  525. if (!is_resource($handle))
  526. {
  527. // @codeCoverageIgnoreStart
  528. throw new \RuntimeException('Unable to process JPG image.');
  529. // @codeCoverageIgnoreEnd
  530. }
  531. $this->handle = $handle;
  532. break;
  533. case 'image/png':
  534. // Make sure the image type is supported.
  535. if (empty(self::$formats[IMAGETYPE_PNG]))
  536. {
  537. // @codeCoverageIgnoreStart
  538. $this->getLogger()->error('Attempting to load an image of unsupported type PNG.');
  539. throw new \RuntimeException('Attempting to load an image of unsupported type PNG.');
  540. // @codeCoverageIgnoreEnd
  541. }
  542. // Attempt to create the image handle.
  543. $handle = imagecreatefrompng($path);
  544. if (!is_resource($handle))
  545. {
  546. // @codeCoverageIgnoreStart
  547. throw new \RuntimeException('Unable to process PNG image.');
  548. // @codeCoverageIgnoreEnd
  549. }
  550. $this->handle = $handle;
  551. // Set transparency for non-transparent PNGs.
  552. if (!$this->isTransparent())
  553. {
  554. // Assign to black which is default for transparent PNGs
  555. $transparency = imagecolorallocatealpha($handle, 0, 0, 0, 127);
  556. imagecolortransparent($handle, $transparency);
  557. }
  558. break;
  559. default:
  560. $this->getLogger()->error('Attempting to load an image of unsupported type ' . $properties->mime);
  561. throw new \InvalidArgumentException('Attempting to load an image of unsupported type ' . $properties->mime);
  562. }
  563. // Set the filesystem path to the source image.
  564. $this->path = $path;
  565. }
  566. /**
  567. * Method to resize the current image.
  568. *
  569. * @param mixed $width The width of the resized image in pixels or a percentage.
  570. * @param mixed $height The height of the resized image in pixels or a percentage.
  571. * @param boolean $createNew If true the current image will be cloned, resized and returned; else
  572. * the current image will be resized and returned.
  573. * @param integer $scaleMethod Which method to use for scaling
  574. *
  575. * @return Image
  576. *
  577. * @since 1.0
  578. * @throws \LogicException
  579. */
  580. public function resize($width, $height, $createNew = true, $scaleMethod = self::SCALE_INSIDE)
  581. {
  582. // Make sure the resource handle is valid.
  583. if (!$this->isLoaded())
  584. {
  585. throw new \LogicException('No valid image was loaded.');
  586. }
  587. // Sanitize width.
  588. $width = $this->sanitizeWidth($width, $height);
  589. // Sanitize height.
  590. $height = $this->sanitizeHeight($height, $width);
  591. // Prepare the dimensions for the resize operation.
  592. $dimensions = $this->prepareDimensions($width, $height, $scaleMethod);
  593. // Instantiate offset.
  594. $offset = new \stdClass;
  595. $offset->x = $offset->y = 0;
  596. // Center image if needed and create the new truecolor image handle.
  597. if ($scaleMethod == self::SCALE_FIT)
  598. {
  599. // Get the offsets
  600. $offset->x = round(($width - $dimensions->width) / 2);
  601. $offset->y = round(($height - $dimensions->height) / 2);
  602. $handle = imagecreatetruecolor($width, $height);
  603. // Make image transparent, otherwise canvas outside initial image would default to black
  604. if (!$this->isTransparent())
  605. {
  606. $transparency = imagecolorAllocateAlpha($this->handle, 0, 0, 0, 127);
  607. imagecolorTransparent($this->handle, $transparency);
  608. }
  609. }
  610. else
  611. {
  612. $handle = imagecreatetruecolor($dimensions->width, $dimensions->height);
  613. }
  614. // Allow transparency for the new image handle.
  615. imagealphablending($handle, false);
  616. imagesavealpha($handle, true);
  617. if ($this->isTransparent())
  618. {
  619. // Get the transparent color values for the current image.
  620. $rgba = imageColorsForIndex($this->handle, imagecolortransparent($this->handle));
  621. $color = imageColorAllocateAlpha($this->handle, $rgba['red'], $rgba['green'], $rgba['blue'], $rgba['alpha']);
  622. // Set the transparent color values for the new image.
  623. imagecolortransparent($handle, $color);
  624. imagefill($handle, 0, 0, $color);
  625. }
  626. // Use resampling for better quality
  627. imagecopyresampled(
  628. $handle, $this->handle,
  629. $offset->x, $offset->y, 0, 0, $dimensions->width, $dimensions->height, $this->getWidth(), $this->getHeight()
  630. );
  631. // If we are resizing to a new image, create a new JImage object.
  632. if ($createNew)
  633. {
  634. // @codeCoverageIgnoreStart
  635. $new = new Image($handle);
  636. return $new;
  637. // @codeCoverageIgnoreEnd
  638. }
  639. else
  640. // Swap out the current handle for the new image handle.
  641. {
  642. // Free the memory from the current handle
  643. $this->destroy();
  644. $this->handle = $handle;
  645. return $this;
  646. }
  647. }
  648. /**
  649. * Method to crop an image after resizing it to maintain
  650. * proportions without having to do all the set up work.
  651. *
  652. * @param integer $width The desired width of the image in pixels or a percentage.
  653. * @param integer $height The desired height of the image in pixels or a percentage.
  654. * @param integer $createNew If true the current image will be cloned, resized, cropped and returned.
  655. *
  656. * @return object Image Object for chaining.
  657. *
  658. * @since 1.0
  659. */
  660. public function cropResize($width, $height, $createNew = true)
  661. {
  662. $width = $this->sanitizeWidth($width, $height);
  663. $height = $this->sanitizeHeight($height, $width);
  664. if (($this->getWidth() / $width) < ($this->getHeight() / $height))
  665. {
  666. $this->resize($width, 0, false);
  667. }
  668. else
  669. {
  670. $this->resize(0, $height, false);
  671. }
  672. return $this->crop($width, $height, null, null, $createNew);
  673. }
  674. /**
  675. * Method to rotate the current image.
  676. *
  677. * @param mixed $angle The angle of rotation for the image
  678. * @param integer $background The background color to use when areas are added due to rotation
  679. * @param boolean $createNew If true the current image will be cloned, rotated and returned; else
  680. * the current image will be rotated and returned.
  681. *
  682. * @return Image
  683. *
  684. * @since 1.0
  685. * @throws \LogicException
  686. */
  687. public function rotate($angle, $background = -1, $createNew = true)
  688. {
  689. // Make sure the resource handle is valid.
  690. if (!$this->isLoaded())
  691. {
  692. throw new \LogicException('No valid image was loaded.');
  693. }
  694. // Sanitize input
  695. $angle = (float) $angle;
  696. // Create the new truecolor image handle.
  697. $handle = imagecreatetruecolor($this->getWidth(), $this->getHeight());
  698. // Allow transparency for the new image handle.
  699. imagealphablending($handle, false);
  700. imagesavealpha($handle, true);
  701. // Copy the image
  702. imagecopy($handle, $this->handle, 0, 0, 0, 0, $this->getWidth(), $this->getHeight());
  703. // Rotate the image
  704. $handle = imagerotate($handle, $angle, $background);
  705. // If we are resizing to a new image, create a new Image object.
  706. if ($createNew)
  707. {
  708. // @codeCoverageIgnoreStart
  709. $new = new Image($handle);
  710. return $new;
  711. // @codeCoverageIgnoreEnd
  712. }
  713. else
  714. // Swap out the current handle for the new image handle.
  715. {
  716. // Free the memory from the current handle
  717. $this->destroy();
  718. $this->handle = $handle;
  719. return $this;
  720. }
  721. }
  722. /**
  723. * Method to write the current image out to a file or output directly.
  724. *
  725. * @param mixed $path The filesystem path to save the image.
  726. * When null, the raw image stream will be outputted directly.
  727. * @param integer $type The image type to save the file as.
  728. * @param array $options The image type options to use in saving the file.
  729. * For PNG and JPEG formats use `quality` key to set compression level (0..9 and 0..100)
  730. *
  731. * @return boolean
  732. *
  733. * @see http://www.php.net/manual/image.constants.php
  734. * @since 1.0
  735. * @throws \LogicException
  736. */
  737. public function toFile($path, $type = IMAGETYPE_JPEG, array $options = array())
  738. {
  739. // Make sure the resource handle is valid.
  740. if (!$this->isLoaded())
  741. {
  742. throw new \LogicException('No valid image was loaded.');
  743. }
  744. switch ($type)
  745. {
  746. case IMAGETYPE_GIF:
  747. return imagegif($this->handle, $path);
  748. break;
  749. case IMAGETYPE_PNG:
  750. return imagepng($this->handle, $path, (array_key_exists('quality', $options)) ? $options['quality'] : 0);
  751. break;
  752. case IMAGETYPE_JPEG:
  753. default:
  754. return imagejpeg($this->handle, $path, (array_key_exists('quality', $options)) ? $options['quality'] : 100);
  755. }
  756. }
  757. /**
  758. * Method to get an image filter instance of a specified type.
  759. *
  760. * @param string $type The image filter type to get.
  761. *
  762. * @return ImageFilter
  763. *
  764. * @since 1.0
  765. * @throws \RuntimeException
  766. */
  767. protected function getFilterInstance($type)
  768. {
  769. // Sanitize the filter type.
  770. $type = strtolower(preg_replace('#[^A-Z0-9_]#i', '', $type));
  771. // Verify that the filter type exists.
  772. $className = 'Joomla\\Image\\Filter\\' . ucfirst($type);
  773. if (!class_exists($className))
  774. {
  775. $this->getLogger()->error('The ' . ucfirst($type) . ' image filter is not available.');
  776. throw new \RuntimeException('The ' . ucfirst($type) . ' image filter is not available.');
  777. }
  778. // Instantiate the filter object.
  779. $instance = new $className($this->handle);
  780. // Verify that the filter type is valid.
  781. if (!($instance instanceof ImageFilter))
  782. {
  783. // @codeCoverageIgnoreStart
  784. $this->getLogger()->error('The ' . ucfirst($type) . ' image filter is not valid.');
  785. throw new \RuntimeException('The ' . ucfirst($type) . ' image filter is not valid.');
  786. // @codeCoverageIgnoreEnd
  787. }
  788. return $instance;
  789. }
  790. /**
  791. * Method to get the new dimensions for a resized image.
  792. *
  793. * @param integer $width The width of the resized image in pixels.
  794. * @param integer $height The height of the resized image in pixels.
  795. * @param integer $scaleMethod The method to use for scaling
  796. *
  797. * @return \stdClass
  798. *
  799. * @since 1.0
  800. * @throws \InvalidArgumentException If width, height or both given as zero
  801. */
  802. protected function prepareDimensions($width, $height, $scaleMethod)
  803. {
  804. // Instantiate variables.
  805. $dimensions = new \stdClass;
  806. switch ($scaleMethod)
  807. {
  808. case self::SCALE_FILL:
  809. $dimensions->width = (int) round($width);
  810. $dimensions->height = (int) round($height);
  811. break;
  812. case self::SCALE_INSIDE:
  813. case self::SCALE_OUTSIDE:
  814. case self::SCALE_FIT:
  815. $rx = ($width > 0) ? ($this->getWidth() / $width) : 0;
  816. $ry = ($height > 0) ? ($this->getHeight() / $height) : 0;
  817. if ($scaleMethod != self::SCALE_OUTSIDE)
  818. {
  819. $ratio = max($rx, $ry);
  820. }
  821. else
  822. {
  823. $ratio = min($rx, $ry);
  824. }
  825. $dimensions->width = (int) round($this->getWidth() / $ratio);
  826. $dimensions->height = (int) round($this->getHeight() / $ratio);
  827. break;
  828. default:
  829. throw new \InvalidArgumentException('Invalid scale method.');
  830. break;
  831. }
  832. return $dimensions;
  833. }
  834. /**
  835. * Method to sanitize a height value.
  836. *
  837. * @param mixed $height The input height value to sanitize.
  838. * @param mixed $width The input width value for reference.
  839. *
  840. * @return integer
  841. *
  842. * @since 1.0
  843. */
  844. protected function sanitizeHeight($height, $width)
  845. {
  846. // If no height was given we will assume it is a square and use the width.
  847. $height = ($height === null) ? $width : $height;
  848. // If we were given a percentage, calculate the integer value.
  849. if (preg_match('/^[0-9]+(\.[0-9]+)?\%$/', $height))
  850. {
  851. $height = (int) round($this->getHeight() * (float) str_replace('%', '', $height) / 100);
  852. }
  853. else
  854. // Else do some rounding so we come out with a sane integer value.
  855. {
  856. $height = (int) round((float) $height);
  857. }
  858. return $height;
  859. }
  860. /**
  861. * Method to sanitize an offset value like left or top.
  862. *
  863. * @param mixed $offset An offset value.
  864. *
  865. * @return integer
  866. *
  867. * @since 1.0
  868. */
  869. protected function sanitizeOffset($offset)
  870. {
  871. return (int) round((float) $offset);
  872. }
  873. /**
  874. * Method to sanitize a width value.
  875. *
  876. * @param mixed $width The input width value to sanitize.
  877. * @param mixed $height The input height value for reference.
  878. *
  879. * @return integer
  880. *
  881. * @since 1.0
  882. */
  883. protected function sanitizeWidth($width, $height)
  884. {
  885. // If no width was given we will assume it is a square and use the height.
  886. $width = ($width === null) ? $height : $width;
  887. // If we were given a percentage, calculate the integer value.
  888. if (preg_match('/^[0-9]+(\.[0-9]+)?\%$/', $width))
  889. {
  890. $width = (int) round($this->getWidth() * (float) str_replace('%', '', $width) / 100);
  891. }
  892. else
  893. // Else do some rounding so we come out with a sane integer value.
  894. {
  895. $width = (int) round((float) $width);
  896. }
  897. return $width;
  898. }
  899. /**
  900. * Method to destroy an image handle and
  901. * free the memory associated with the handle
  902. *
  903. * @return boolean True on success, false on failure or if no image is loaded
  904. *
  905. * @since 1.0
  906. */
  907. public function destroy()
  908. {
  909. if ($this->isLoaded())
  910. {
  911. return imagedestroy($this->handle);
  912. }
  913. return false;
  914. }
  915. /**
  916. * Method to call the destroy() method one last time
  917. * to free any memory when the object is unset
  918. *
  919. * @see Image::destroy()
  920. * @since 1.0
  921. */
  922. public function __destruct()
  923. {
  924. $this->destroy();
  925. }
  926. }