1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Image
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * Class to manipulate an image.
14 *
15 * @since 11.3
16 */
17 class JImage
18 {
19 /**
20 * @const integer
21 * @since 11.3
22 */
23 const SCALE_FILL = 1;
24
25 /**
26 * @const integer
27 * @since 11.3
28 */
29 const SCALE_INSIDE = 2;
30
31 /**
32 * @const integer
33 * @since 11.3
34 */
35 const SCALE_OUTSIDE = 3;
36
37 /**
38 * @const integer
39 * @since 12.2
40 */
41 const CROP = 4;
42
43 /**
44 * @const integer
45 * @since 12.3
46 */
47 const CROP_RESIZE = 5;
48
49 /**
50 * @const integer
51 * @since 3.2
52 */
53 const SCALE_FIT = 6;
54
55 /**
56 * @const string
57 * @since 3.4.2
58 */
59 const ORIENTATION_LANDSCAPE = 'landscape';
60
61 /**
62 * @const string
63 * @since 3.4.2
64 */
65 const ORIENTATION_PORTRAIT = 'portrait';
66
67 /**
68 * @const string
69 * @since 3.4.2
70 */
71 const ORIENTATION_SQUARE = 'square';
72
73 /**
74 * @var resource The image resource handle.
75 * @since 11.3
76 */
77 protected $handle;
78
79 /**
80 * @var string The source image path.
81 * @since 11.3
82 */
83 protected $path = null;
84
85 /**
86 * @var array Whether or not different image formats are supported.
87 * @since 11.3
88 */
89 protected static $formats = array();
90
91 /**
92 * @var boolean True for best quality. False for speed
93 *
94 * @since 3.7.0
95 */
96 protected $generateBestQuality = true;
97
98 /**
99 * Class constructor.
100 *
101 * @param mixed $source Either a file path for a source image or a GD resource handler for an image.
102 *
103 * @since 11.3
104 * @throws RuntimeException
105 */
106 public function __construct($source = null)
107 {
108 // Verify that GD support for PHP is available.
109 if (!extension_loaded('gd'))
110 {
111 // @codeCoverageIgnoreStart
112 JLog::add('The GD extension for PHP is not available.', JLog::ERROR);
113 throw new RuntimeException('The GD extension for PHP is not available.');
114
115 // @codeCoverageIgnoreEnd
116 }
117
118 // Determine which image types are supported by GD, but only once.
119 if (!isset(self::$formats[IMAGETYPE_JPEG]))
120 {
121 $info = gd_info();
122 self::$formats[IMAGETYPE_JPEG] = ($info['JPEG Support']) ? true : false;
123 self::$formats[IMAGETYPE_PNG] = ($info['PNG Support']) ? true : false;
124 self::$formats[IMAGETYPE_GIF] = ($info['GIF Read Support']) ? true : false;
125 }
126
127 // If the source input is a resource, set it as the image handle.
128 if (is_resource($source) && (get_resource_type($source) == 'gd'))
129 {
130 $this->handle = &$source;
131 }
132 elseif (!empty($source) && is_string($source))
133 {
134 // If the source input is not empty, assume it is a path and populate the image handle.
135 $this->loadFile($source);
136 }
137 }
138
139 /**
140 * Method to return a properties object for an image given a filesystem path.
141 * The result object has values for image width, height, type, attributes, bits, channels, mime type, file size and orientation.
142 *
143 * @param string $path The filesystem path to the image for which to get properties.
144 *
145 * @return stdClass
146 *
147 * @since 11.3
148 *
149 * @throws InvalidArgumentException
150 * @throws RuntimeException
151 */
152 public static function getImageFileProperties($path)
153 {
154 // Make sure the file exists.
155 if (!file_exists($path))
156 {
157 throw new InvalidArgumentException('The image file does not exist.');
158 }
159
160 // Get the image file information.
161 $info = getimagesize($path);
162
163 if (!$info)
164 {
165 // @codeCoverageIgnoreStart
166 throw new RuntimeException('Unable to get properties for the image.');
167
168 // @codeCoverageIgnoreEnd
169 }
170
171 // Build the response object.
172 $properties = (object) array(
173 'width' => $info[0],
174 'height' => $info[1],
175 'type' => $info[2],
176 'attributes' => $info[3],
177 'bits' => isset($info['bits']) ? $info['bits'] : null,
178 'channels' => isset($info['channels']) ? $info['channels'] : null,
179 'mime' => $info['mime'],
180 'filesize' => filesize($path),
181 'orientation' => self::getOrientationString((int) $info[0], (int) $info[1]),
182 );
183
184 return $properties;
185 }
186
187 /**
188 * Method to detect whether an image's orientation is landscape, portrait or square.
189 * The orientation will be returned as a string.
190 *
191 * @return mixed Orientation string or null.
192 *
193 * @since 3.4.2
194 */
195 public function getOrientation()
196 {
197 if ($this->isLoaded())
198 {
199 return self::getOrientationString($this->getWidth(), $this->getHeight());
200 }
201
202 return;
203 }
204
205 /**
206 * Compare width and height integers to determine image orientation.
207 *
208 * @param integer $width The width value to use for calculation
209 * @param integer $height The height value to use for calculation
210 *
211 * @return string Orientation string
212 *
213 * @since 3.4.2
214 */
215 private static function getOrientationString($width, $height)
216 {
217 if ($width > $height)
218 {
219 return self::ORIENTATION_LANDSCAPE;
220 }
221
222 if ($width < $height)
223 {
224 return self::ORIENTATION_PORTRAIT;
225 }
226
227 return self::ORIENTATION_SQUARE;
228 }
229
230 /**
231 * Method to generate thumbnails from the current image. It allows
232 * creation by resizing or cropping the original image.
233 *
234 * @param mixed $thumbSizes String or array of strings. Example: $thumbSizes = array('150x75','250x150');
235 * @param integer $creationMethod 1-3 resize $scaleMethod | 4 create cropping | 5 resize then crop
236 *
237 * @return array returns the generated thumb in the results array
238 *
239 * @since 12.2
240 * @throws LogicException
241 * @throws InvalidArgumentException
242 */
243 public function generateThumbs($thumbSizes, $creationMethod = self::SCALE_INSIDE)
244 {
245 // Make sure the resource handle is valid.
246 if (!$this->isLoaded())
247 {
248 throw new LogicException('No valid image was loaded.');
249 }
250
251 // Accept a single thumbsize string as parameter
252 if (!is_array($thumbSizes))
253 {
254 $thumbSizes = array($thumbSizes);
255 }
256
257 // Process thumbs
258 $generated = array();
259
260 if (!empty($thumbSizes))
261 {
262 foreach ($thumbSizes as $thumbSize)
263 {
264 // Desired thumbnail size
265 $size = explode('x', strtolower($thumbSize));
266
267 if (count($size) != 2)
268 {
269 throw new InvalidArgumentException('Invalid thumb size received: ' . $thumbSize);
270 }
271
272 $thumbWidth = $size[0];
273 $thumbHeight = $size[1];
274
275 switch ($creationMethod)
276 {
277 // Case for self::CROP
278 case 4:
279 $thumb = $this->crop($thumbWidth, $thumbHeight, null, null, true);
280 break;
281
282 // Case for self::CROP_RESIZE
283 case 5:
284 $thumb = $this->cropResize($thumbWidth, $thumbHeight, true);
285 break;
286
287 default:
288 $thumb = $this->resize($thumbWidth, $thumbHeight, true, $creationMethod);
289 break;
290 }
291
292 // Store the thumb in the results array
293 $generated[] = $thumb;
294 }
295 }
296
297 return $generated;
298 }
299
300 /**
301 * Method to create thumbnails from the current image and save them to disk. It allows creation by resizing
302 * or cropping the original image.
303 *
304 * @param mixed $thumbSizes string or array of strings. Example: $thumbSizes = array('150x75','250x150');
305 * @param integer $creationMethod 1-3 resize $scaleMethod | 4 create cropping
306 * @param string $thumbsFolder destination thumbs folder. null generates a thumbs folder in the image folder
307 *
308 * @return array An array of JImage objects with thumb paths.
309 *
310 * @since 12.2
311 * @throws LogicException
312 * @throws InvalidArgumentException
313 */
314 public function createThumbs($thumbSizes, $creationMethod = self::SCALE_INSIDE, $thumbsFolder = null)
315 {
316 // Make sure the resource handle is valid.
317 if (!$this->isLoaded())
318 {
319 throw new LogicException('No valid image was loaded.');
320 }
321
322 // No thumbFolder set -> we will create a thumbs folder in the current image folder
323 if (is_null($thumbsFolder))
324 {
325 $thumbsFolder = dirname($this->getPath()) . '/thumbs';
326 }
327
328 // Check destination
329 if (!is_dir($thumbsFolder) && (!is_dir(dirname($thumbsFolder)) || !@mkdir($thumbsFolder)))
330 {
331 throw new InvalidArgumentException('Folder does not exist and cannot be created: ' . $thumbsFolder);
332 }
333
334 // Process thumbs
335 $thumbsCreated = array();
336
337 if ($thumbs = $this->generateThumbs($thumbSizes, $creationMethod))
338 {
339 // Parent image properties
340 $imgProperties = self::getImageFileProperties($this->getPath());
341
342 foreach ($thumbs as $thumb)
343 {
344 // Get thumb properties
345 $thumbWidth = $thumb->getWidth();
346 $thumbHeight = $thumb->getHeight();
347
348 // Generate thumb name
349 $filename = pathinfo($this->getPath(), PATHINFO_FILENAME);
350 $fileExtension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
351 $thumbFileName = $filename . '_' . $thumbWidth . 'x' . $thumbHeight . '.' . $fileExtension;
352
353 // Save thumb file to disk
354 $thumbFileName = $thumbsFolder . '/' . $thumbFileName;
355
356 if ($thumb->toFile($thumbFileName, $imgProperties->type))
357 {
358 // Return JImage object with thumb path to ease further manipulation
359 $thumb->path = $thumbFileName;
360 $thumbsCreated[] = $thumb;
361 }
362 }
363 }
364
365 return $thumbsCreated;
366 }
367
368 /**
369 * Method to crop the current image.
370 *
371 * @param mixed $width The width of the image section to crop in pixels or a percentage.
372 * @param mixed $height The height of the image section to crop in pixels or a percentage.
373 * @param integer $left The number of pixels from the left to start cropping.
374 * @param integer $top The number of pixels from the top to start cropping.
375 * @param boolean $createNew If true the current image will be cloned, cropped and returned; else
376 * the current image will be cropped and returned.
377 *
378 * @return JImage
379 *
380 * @since 11.3
381 * @throws LogicException
382 */
383 public function crop($width, $height, $left = null, $top = null, $createNew = true)
384 {
385 // Make sure the resource handle is valid.
386 if (!$this->isLoaded())
387 {
388 throw new LogicException('No valid image was loaded.');
389 }
390
391 // Sanitize width.
392 $width = $this->sanitizeWidth($width, $height);
393
394 // Sanitize height.
395 $height = $this->sanitizeHeight($height, $width);
396
397 // Autocrop offsets
398 if (is_null($left))
399 {
400 $left = round(($this->getWidth() - $width) / 2);
401 }
402
403 if (is_null($top))
404 {
405 $top = round(($this->getHeight() - $height) / 2);
406 }
407
408 // Sanitize left.
409 $left = $this->sanitizeOffset($left);
410
411 // Sanitize top.
412 $top = $this->sanitizeOffset($top);
413
414 // Create the new truecolor image handle.
415 $handle = imagecreatetruecolor($width, $height);
416
417 // Allow transparency for the new image handle.
418 imagealphablending($handle, false);
419 imagesavealpha($handle, true);
420
421 if ($this->isTransparent())
422 {
423 // Get the transparent color values for the current image.
424 $rgba = imageColorsForIndex($this->handle, imagecolortransparent($this->handle));
425 $color = imageColorAllocateAlpha($handle, $rgba['red'], $rgba['green'], $rgba['blue'], $rgba['alpha']);
426
427 // Set the transparent color values for the new image.
428 imagecolortransparent($handle, $color);
429 imagefill($handle, 0, 0, $color);
430 }
431
432 if (!$this->generateBestQuality)
433 {
434 imagecopyresized($handle, $this->handle, 0, 0, $left, $top, $width, $height, $width, $height);
435 }
436 else
437 {
438 imagecopyresampled($handle, $this->handle, 0, 0, $left, $top, $width, $height, $width, $height);
439 }
440
441 // If we are cropping to a new image, create a new JImage object.
442 if ($createNew)
443 {
444 // @codeCoverageIgnoreStart
445 $new = new JImage($handle);
446
447 return $new;
448
449 // @codeCoverageIgnoreEnd
450 }
451 // Swap out the current handle for the new image handle.
452 else
453 {
454 // Free the memory from the current handle
455 $this->destroy();
456
457 $this->handle = $handle;
458
459 return $this;
460 }
461 }
462
463 /**
464 * Method to apply a filter to the image by type. Two examples are: grayscale and sketchy.
465 *
466 * @param string $type The name of the image filter to apply.
467 * @param array $options An array of options for the filter.
468 *
469 * @return JImage
470 *
471 * @since 11.3
472 * @see JImageFilter
473 * @throws LogicException
474 */
475 public function filter($type, array $options = array())
476 {
477 // Make sure the resource handle is valid.
478 if (!$this->isLoaded())
479 {
480 throw new LogicException('No valid image was loaded.');
481 }
482
483 // Get the image filter instance.
484 $filter = $this->getFilterInstance($type);
485
486 // Execute the image filter.
487 $filter->execute($options);
488
489 return $this;
490 }
491
492 /**
493 * Method to get the height of the image in pixels.
494 *
495 * @return integer
496 *
497 * @since 11.3
498 * @throws LogicException
499 */
500 public function getHeight()
501 {
502 // Make sure the resource handle is valid.
503 if (!$this->isLoaded())
504 {
505 throw new LogicException('No valid image was loaded.');
506 }
507
508 return imagesy($this->handle);
509 }
510
511 /**
512 * Method to get the width of the image in pixels.
513 *
514 * @return integer
515 *
516 * @since 11.3
517 * @throws LogicException
518 */
519 public function getWidth()
520 {
521 // Make sure the resource handle is valid.
522 if (!$this->isLoaded())
523 {
524 throw new LogicException('No valid image was loaded.');
525 }
526
527 return imagesx($this->handle);
528 }
529
530 /**
531 * Method to return the path
532 *
533 * @return string
534 *
535 * @since 11.3
536 */
537 public function getPath()
538 {
539 return $this->path;
540 }
541
542 /**
543 * Method to determine whether or not an image has been loaded into the object.
544 *
545 * @return boolean
546 *
547 * @since 11.3
548 */
549 public function isLoaded()
550 {
551 // Make sure the resource handle is valid.
552 if (!is_resource($this->handle) || (get_resource_type($this->handle) != 'gd'))
553 {
554 return false;
555 }
556
557 return true;
558 }
559
560 /**
561 * Method to determine whether or not the image has transparency.
562 *
563 * @return boolean
564 *
565 * @since 11.3
566 * @throws LogicException
567 */
568 public function isTransparent()
569 {
570 // Make sure the resource handle is valid.
571 if (!$this->isLoaded())
572 {
573 throw new LogicException('No valid image was loaded.');
574 }
575
576 return imagecolortransparent($this->handle) >= 0;
577 }
578
579 /**
580 * Method to load a file into the JImage object as the resource.
581 *
582 * @param string $path The filesystem path to load as an image.
583 *
584 * @return void
585 *
586 * @since 11.3
587 * @throws InvalidArgumentException
588 * @throws RuntimeException
589 */
590 public function loadFile($path)
591 {
592 // Destroy the current image handle if it exists
593 $this->destroy();
594
595 // Make sure the file exists.
596 if (!file_exists($path))
597 {
598 throw new InvalidArgumentException('The image file does not exist.');
599 }
600
601 // Get the image properties.
602 $properties = self::getImageFileProperties($path);
603
604 // Attempt to load the image based on the MIME-Type
605 switch ($properties->mime)
606 {
607 case 'image/gif':
608 // Make sure the image type is supported.
609 if (empty(self::$formats[IMAGETYPE_GIF]))
610 {
611 // @codeCoverageIgnoreStart
612 JLog::add('Attempting to load an image of unsupported type GIF.', JLog::ERROR);
613 throw new RuntimeException('Attempting to load an image of unsupported type GIF.');
614
615 // @codeCoverageIgnoreEnd
616 }
617
618 // Attempt to create the image handle.
619 $handle = imagecreatefromgif($path);
620
621 if (!is_resource($handle))
622 {
623 // @codeCoverageIgnoreStart
624 throw new RuntimeException('Unable to process GIF image.');
625
626 // @codeCoverageIgnoreEnd
627 }
628
629 $this->handle = $handle;
630 break;
631
632 case 'image/jpeg':
633 // Make sure the image type is supported.
634 if (empty(self::$formats[IMAGETYPE_JPEG]))
635 {
636 // @codeCoverageIgnoreStart
637 JLog::add('Attempting to load an image of unsupported type JPG.', JLog::ERROR);
638 throw new RuntimeException('Attempting to load an image of unsupported type JPG.');
639
640 // @codeCoverageIgnoreEnd
641 }
642
643 // Attempt to create the image handle.
644 $handle = imagecreatefromjpeg($path);
645
646 if (!is_resource($handle))
647 {
648 // @codeCoverageIgnoreStart
649 throw new RuntimeException('Unable to process JPG image.');
650
651 // @codeCoverageIgnoreEnd
652 }
653
654 $this->handle = $handle;
655 break;
656
657 case 'image/png':
658 // Make sure the image type is supported.
659 if (empty(self::$formats[IMAGETYPE_PNG]))
660 {
661 // @codeCoverageIgnoreStart
662 JLog::add('Attempting to load an image of unsupported type PNG.', JLog::ERROR);
663 throw new RuntimeException('Attempting to load an image of unsupported type PNG.');
664
665 // @codeCoverageIgnoreEnd
666 }
667
668 // Attempt to create the image handle.
669 $handle = imagecreatefrompng($path);
670
671 if (!is_resource($handle))
672 {
673 // @codeCoverageIgnoreStart
674 throw new RuntimeException('Unable to process PNG image.');
675
676 // @codeCoverageIgnoreEnd
677 }
678
679 $this->handle = $handle;
680
681 break;
682
683 default:
684 JLog::add('Attempting to load an image of unsupported type: ' . $properties->mime, JLog::ERROR);
685 throw new InvalidArgumentException('Attempting to load an image of unsupported type: ' . $properties->mime);
686 break;
687 }
688
689 // Set the filesystem path to the source image.
690 $this->path = $path;
691 }
692
693 /**
694 * Method to resize the current image.
695 *
696 * @param mixed $width The width of the resized image in pixels or a percentage.
697 * @param mixed $height The height of the resized image in pixels or a percentage.
698 * @param boolean $createNew If true the current image will be cloned, resized and returned; else
699 * the current image will be resized and returned.
700 * @param integer $scaleMethod Which method to use for scaling
701 *
702 * @return JImage
703 *
704 * @since 11.3
705 * @throws LogicException
706 */
707 public function resize($width, $height, $createNew = true, $scaleMethod = self::SCALE_INSIDE)
708 {
709 // Make sure the resource handle is valid.
710 if (!$this->isLoaded())
711 {
712 throw new LogicException('No valid image was loaded.');
713 }
714
715 // Sanitize width.
716 $width = $this->sanitizeWidth($width, $height);
717
718 // Sanitize height.
719 $height = $this->sanitizeHeight($height, $width);
720
721 // Prepare the dimensions for the resize operation.
722 $dimensions = $this->prepareDimensions($width, $height, $scaleMethod);
723
724 // Instantiate offset.
725 $offset = new stdClass;
726 $offset->x = $offset->y = 0;
727
728 // Center image if needed and create the new truecolor image handle.
729 if ($scaleMethod == self::SCALE_FIT)
730 {
731 // Get the offsets
732 $offset->x = round(($width - $dimensions->width) / 2);
733 $offset->y = round(($height - $dimensions->height) / 2);
734
735 $handle = imagecreatetruecolor($width, $height);
736
737 // Make image transparent, otherwise cavas outside initial image would default to black
738 if (!$this->isTransparent())
739 {
740 $transparency = imagecolorAllocateAlpha($this->handle, 0, 0, 0, 127);
741 imagecolorTransparent($this->handle, $transparency);
742 }
743 }
744 else
745 {
746 $handle = imagecreatetruecolor($dimensions->width, $dimensions->height);
747 }
748
749 // Allow transparency for the new image handle.
750 imagealphablending($handle, false);
751 imagesavealpha($handle, true);
752
753 if ($this->isTransparent())
754 {
755 // Get the transparent color values for the current image.
756 $rgba = imageColorsForIndex($this->handle, imagecolortransparent($this->handle));
757 $color = imageColorAllocateAlpha($handle, $rgba['red'], $rgba['green'], $rgba['blue'], $rgba['alpha']);
758
759 // Set the transparent color values for the new image.
760 imagecolortransparent($handle, $color);
761 imagefill($handle, 0, 0, $color);
762 }
763
764 if (!$this->generateBestQuality)
765 {
766 imagecopyresized(
767 $handle,
768 $this->handle,
769 $offset->x,
770 $offset->y,
771 0,
772 0,
773 $dimensions->width,
774 $dimensions->height,
775 $this->getWidth(),
776 $this->getHeight()
777 );
778 }
779 else
780 {
781 imagecopyresampled(
782 $handle,
783 $this->handle,
784 $offset->x,
785 $offset->y,
786 0,
787 0,
788 $dimensions->width,
789 $dimensions->height,
790 $this->getWidth(),
791 $this->getHeight()
792 );
793 }
794
795 // If we are resizing to a new image, create a new JImage object.
796 if ($createNew)
797 {
798 // @codeCoverageIgnoreStart
799 $new = new JImage($handle);
800
801 return $new;
802
803 // @codeCoverageIgnoreEnd
804 }
805 // Swap out the current handle for the new image handle.
806 else
807 {
808 // Free the memory from the current handle
809 $this->destroy();
810
811 $this->handle = $handle;
812
813 return $this;
814 }
815 }
816
817 /**
818 * Method to crop an image after resizing it to maintain
819 * proportions without having to do all the set up work.
820 *
821 * @param integer $width The desired width of the image in pixels or a percentage.
822 * @param integer $height The desired height of the image in pixels or a percentage.
823 * @param boolean $createNew If true the current image will be cloned, resized, cropped and returned.
824 *
825 * @return object JImage Object for chaining.
826 *
827 * @since 12.3
828 */
829 public function cropResize($width, $height, $createNew = true)
830 {
831 $width = $this->sanitizeWidth($width, $height);
832 $height = $this->sanitizeHeight($height, $width);
833
834 $resizewidth = $width;
835 $resizeheight = $height;
836
837 if (($this->getWidth() / $width) < ($this->getHeight() / $height))
838 {
839 $resizeheight = 0;
840 }
841 else
842 {
843 $resizewidth = 0;
844 }
845
846 return $this->resize($resizewidth, $resizeheight, $createNew)->crop($width, $height, null, null, false);
847 }
848
849 /**
850 * Method to rotate the current image.
851 *
852 * @param mixed $angle The angle of rotation for the image
853 * @param integer $background The background color to use when areas are added due to rotation
854 * @param boolean $createNew If true the current image will be cloned, rotated and returned; else
855 * the current image will be rotated and returned.
856 *
857 * @return JImage
858 *
859 * @since 11.3
860 * @throws LogicException
861 */
862 public function rotate($angle, $background = -1, $createNew = true)
863 {
864 // Make sure the resource handle is valid.
865 if (!$this->isLoaded())
866 {
867 throw new LogicException('No valid image was loaded.');
868 }
869
870 // Sanitize input
871 $angle = (float) $angle;
872
873 // Create the new truecolor image handle.
874 $handle = imagecreatetruecolor($this->getWidth(), $this->getHeight());
875
876 // Make background transparent if no external background color is provided.
877 if ($background == -1)
878 {
879 // Allow transparency for the new image handle.
880 imagealphablending($handle, false);
881 imagesavealpha($handle, true);
882
883 $background = imagecolorallocatealpha($handle, 0, 0, 0, 127);
884 }
885
886 // Copy the image
887 imagecopy($handle, $this->handle, 0, 0, 0, 0, $this->getWidth(), $this->getHeight());
888
889 // Rotate the image
890 $handle = imagerotate($handle, $angle, $background);
891
892 // If we are resizing to a new image, create a new JImage object.
893 if ($createNew)
894 {
895 // @codeCoverageIgnoreStart
896 $new = new JImage($handle);
897
898 return $new;
899
900 // @codeCoverageIgnoreEnd
901 }
902 // Swap out the current handle for the new image handle.
903 else
904 {
905 // Free the memory from the current handle
906 $this->destroy();
907
908 $this->handle = $handle;
909
910 return $this;
911 }
912 }
913
914 /**
915 * Method to flip the current image.
916 *
917 * @param integer $mode The flip mode for flipping the image {@link https://secure.php.net/imageflip#refsect1-function.imageflip-parameters}
918 * @param boolean $createNew If true the current image will be cloned, flipped and returned; else the current image will be flipped and returned.
919 *
920 * @return JImage
921 *
922 * @since 11.3
923 * @throws LogicException
924 */
925 public function flip($mode, $createNew = true)
926 {
927 // Make sure the resource handle is valid.
928 if (!$this->isLoaded())
929 {
930 throw new LogicException('No valid image was loaded.');
931 }
932
933 // Create the new truecolor image handle.
934 $handle = imagecreatetruecolor($this->getWidth(), $this->getHeight());
935
936 // Copy the image
937 imagecopy($handle, $this->handle, 0, 0, 0, 0, $this->getWidth(), $this->getHeight());
938
939 // Flip the image
940 if (!imageflip($handle, $mode))
941 {
942 throw new LogicException('Unable to flip the image.');
943 }
944
945 // If we are resizing to a new image, create a new JImage object.
946 if ($createNew)
947 {
948 // @codeCoverageIgnoreStart
949 $new = new JImage($handle);
950
951 return $new;
952
953 // @codeCoverageIgnoreEnd
954 }
955
956 // Free the memory from the current handle
957 $this->destroy();
958
959 // Swap out the current handle for the new image handle.
960 $this->handle = $handle;
961
962 return $this;
963 }
964
965 /**
966 * Method to write the current image out to a file.
967 *
968 * @param string $path The filesystem path to save the image.
969 * @param integer $type The image type to save the file as.
970 * @param array $options The image type options to use in saving the file.
971 *
972 * @return boolean
973 *
974 * @link https://secure.php.net/manual/image.constants.php
975 * @since 11.3
976 * @throws LogicException
977 */
978 public function toFile($path, $type = IMAGETYPE_JPEG, array $options = array())
979 {
980 // Make sure the resource handle is valid.
981 if (!$this->isLoaded())
982 {
983 throw new LogicException('No valid image was loaded.');
984 }
985
986 switch ($type)
987 {
988 case IMAGETYPE_GIF:
989 return imagegif($this->handle, $path);
990 break;
991
992 case IMAGETYPE_PNG:
993 return imagepng($this->handle, $path, (array_key_exists('quality', $options)) ? $options['quality'] : 0);
994 break;
995
996 case IMAGETYPE_JPEG:
997 default:
998 return imagejpeg($this->handle, $path, (array_key_exists('quality', $options)) ? $options['quality'] : 100);
999 }
1000 }
1001
1002 /**
1003 * Method to get an image filter instance of a specified type.
1004 *
1005 * @param string $type The image filter type to get.
1006 *
1007 * @return JImageFilter
1008 *
1009 * @since 11.3
1010 * @throws RuntimeException
1011 */
1012 protected function getFilterInstance($type)
1013 {
1014 // Sanitize the filter type.
1015 $type = strtolower(preg_replace('#[^A-Z0-9_]#i', '', $type));
1016
1017 // Verify that the filter type exists.
1018 $className = 'JImageFilter' . ucfirst($type);
1019
1020 if (!class_exists($className))
1021 {
1022 JLog::add('The ' . ucfirst($type) . ' image filter is not available.', JLog::ERROR);
1023 throw new RuntimeException('The ' . ucfirst($type) . ' image filter is not available.');
1024 }
1025
1026 // Instantiate the filter object.
1027 $instance = new $className($this->handle);
1028
1029 // Verify that the filter type is valid.
1030 if (!($instance instanceof JImageFilter))
1031 {
1032 // @codeCoverageIgnoreStart
1033 JLog::add('The ' . ucfirst($type) . ' image filter is not valid.', JLog::ERROR);
1034 throw new RuntimeException('The ' . ucfirst($type) . ' image filter is not valid.');
1035
1036 // @codeCoverageIgnoreEnd
1037 }
1038
1039 return $instance;
1040 }
1041
1042 /**
1043 * Method to get the new dimensions for a resized image.
1044 *
1045 * @param integer $width The width of the resized image in pixels.
1046 * @param integer $height The height of the resized image in pixels.
1047 * @param integer $scaleMethod The method to use for scaling
1048 *
1049 * @return stdClass
1050 *
1051 * @since 11.3
1052 * @throws InvalidArgumentException If width, height or both given as zero
1053 */
1054 protected function prepareDimensions($width, $height, $scaleMethod)
1055 {
1056 // Instantiate variables.
1057 $dimensions = new stdClass;
1058
1059 switch ($scaleMethod)
1060 {
1061 case self::SCALE_FILL:
1062 $dimensions->width = (int) round($width);
1063 $dimensions->height = (int) round($height);
1064 break;
1065
1066 case self::SCALE_INSIDE:
1067 case self::SCALE_OUTSIDE:
1068 case self::SCALE_FIT:
1069 $rx = ($width > 0) ? ($this->getWidth() / $width) : 0;
1070 $ry = ($height > 0) ? ($this->getHeight() / $height) : 0;
1071
1072 if ($scaleMethod != self::SCALE_OUTSIDE)
1073 {
1074 $ratio = max($rx, $ry);
1075 }
1076 else
1077 {
1078 $ratio = min($rx, $ry);
1079 }
1080
1081 $dimensions->width = (int) round($this->getWidth() / $ratio);
1082 $dimensions->height = (int) round($this->getHeight() / $ratio);
1083 break;
1084
1085 default:
1086 throw new InvalidArgumentException('Invalid scale method.');
1087 break;
1088 }
1089
1090 return $dimensions;
1091 }
1092
1093 /**
1094 * Method to sanitize a height value.
1095 *
1096 * @param mixed $height The input height value to sanitize.
1097 * @param mixed $width The input width value for reference.
1098 *
1099 * @return integer
1100 *
1101 * @since 11.3
1102 */
1103 protected function sanitizeHeight($height, $width)
1104 {
1105 // If no height was given we will assume it is a square and use the width.
1106 $height = ($height === null) ? $width : $height;
1107
1108 // If we were given a percentage, calculate the integer value.
1109 if (preg_match('/^[0-9]+(\.[0-9]+)?\%$/', $height))
1110 {
1111 $height = (int) round($this->getHeight() * (float) str_replace('%', '', $height) / 100);
1112 }
1113 // Else do some rounding so we come out with a sane integer value.
1114 else
1115 {
1116 $height = (int) round((float) $height);
1117 }
1118
1119 return $height;
1120 }
1121
1122 /**
1123 * Method to sanitize an offset value like left or top.
1124 *
1125 * @param mixed $offset An offset value.
1126 *
1127 * @return integer
1128 *
1129 * @since 11.3
1130 */
1131 protected function sanitizeOffset($offset)
1132 {
1133 return (int) round((float) $offset);
1134 }
1135
1136 /**
1137 * Method to sanitize a width value.
1138 *
1139 * @param mixed $width The input width value to sanitize.
1140 * @param mixed $height The input height value for reference.
1141 *
1142 * @return integer
1143 *
1144 * @since 11.3
1145 */
1146 protected function sanitizeWidth($width, $height)
1147 {
1148 // If no width was given we will assume it is a square and use the height.
1149 $width = ($width === null) ? $height : $width;
1150
1151 // If we were given a percentage, calculate the integer value.
1152 if (preg_match('/^[0-9]+(\.[0-9]+)?\%$/', $width))
1153 {
1154 $width = (int) round($this->getWidth() * (float) str_replace('%', '', $width) / 100);
1155 }
1156 // Else do some rounding so we come out with a sane integer value.
1157 else
1158 {
1159 $width = (int) round((float) $width);
1160 }
1161
1162 return $width;
1163 }
1164
1165 /**
1166 * Method to destroy an image handle and
1167 * free the memory associated with the handle
1168 *
1169 * @return boolean True on success, false on failure or if no image is loaded
1170 *
1171 * @since 12.3
1172 */
1173 public function destroy()
1174 {
1175 if ($this->isLoaded())
1176 {
1177 return imagedestroy($this->handle);
1178 }
1179
1180 return false;
1181 }
1182
1183 /**
1184 * Method to call the destroy() method one last time
1185 * to free any memory when the object is unset
1186 *
1187 * @see JImage::destroy()
1188 * @since 12.3
1189 */
1190 public function __destruct()
1191 {
1192 $this->destroy();
1193 }
1194
1195 /**
1196 * Method for set option of generate thumbnail method
1197 *
1198 * @param boolean $quality True for best quality. False for best speed.
1199 *
1200 * @return void
1201 *
1202 * @since 3.7.0
1203 */
1204 public function setThumbnailGenerate($quality = true)
1205 {
1206 $this->generateBestQuality = (boolean) $quality;
1207 }
1208 }
1209