1 <?php
2 namespace Elboletaire\Watimage;
3
4 use Exception;
5 use Elboletaire\Watimage\Exception\ExtensionNotLoadedException;
6 use Elboletaire\Watimage\Exception\FileNotExistException;
7 use Elboletaire\Watimage\Exception\InvalidArgumentException;
8 use Elboletaire\Watimage\Exception\InvalidExtensionException;
9 use Elboletaire\Watimage\Exception\InvalidMimeException;
10
11 /**
12 * The Image class. The main code of Watimage.
13 *
14 * @author Òscar Casajuana <elboletaire at underave dot net>
15 * @copyright 2015 Òscar Casajuana <elboletaire at underave dot net>
16 * @license https://opensource.org/licenses/MIT MIT
17 * @link https://github.com/elboletaire/Watimage
18 */
19 class Image
20 {
21 /**
22 * Constant for the (deprecated) transparent color
23 */
24 const COLOR_TRANSPARENT = -1;
25
26 /**
27 * Current image location.
28 *
29 * @var string
30 */
31 protected $filename;
32
33 /**
34 * Image GD resource.
35 *
36 * @var resource
37 */
38 protected $image;
39
40 /**
41 * Image metadata.
42 *
43 * @var array
44 */
45 protected $metadata = [];
46
47 /**
48 * Current image width
49 *
50 * @var float
51 */
52 protected $width;
53
54 /**
55 * Current image height
56 *
57 * @var float
58 */
59 protected $height;
60
61 /**
62 * Image export quality for gif and jpg files.
63 *
64 * You can set it with setQuality or setImage methods.
65 *
66 * @var integer
67 */
68 protected $quality = 80;
69
70 /**
71 * Image compression value for png files.
72 *
73 * You can set it with setCompression method.
74 *
75 * @var integer
76 */
77 protected $compression = 9;
78
79 /**
80 * Constructor method. You can pass a filename to be loaded by default
81 * or load it later with load('filename.ext')
82 *
83 * @param string $file Filepath of the image to be loaded.
84 */
85 public function __construct($file = null, $autoOrientate = true)
86 {
87 if (!extension_loaded('gd')) {
88 throw new ExtensionNotLoadedException("GD");
89 }
90
91 if (!empty($file)) {
92 $this->load($file);
93
94 if ($autoOrientate) {
95 $this->autoOrientate();
96 }
97 }
98 }
99
100 /**
101 * Ensure everything gets emptied on object destruction.
102 *
103 * @codeCoverageIgnore
104 */
105 public function __destruct()
106 {
107 $this->destroy();
108 }
109
110 /**
111 * Creates a resource image.
112 *
113 * This method was using imagecreatefromstring but I decided to switch after
114 * reading this: https://thenewphalls.wordpress.com/2012/12/27/imagecreatefromstring-vs-imagecreatefromformat
115 *
116 * @param string $filename Image file path/name.
117 * @param string $mime Image mime or `string` if creating from string
118 * (no base64 encoded).
119 * @return resource
120 * @throws InvalidMimeException
121 */
122 public function createResourceImage($filename, $mime)
123 {
124 switch ($mime) {
125 case 'image/gif':
126 $image = imagecreatefromgif($filename);
127 break;
128
129 case 'image/png':
130 $image = imagecreatefrompng($filename);
131 break;
132
133 case 'image/jpeg':
134 $image = imagecreatefromjpeg($filename);
135 break;
136
137 case 'string':
138 $image = imagecreatefromstring($filename);
139 break;
140
141 default:
142 throw new InvalidMimeException($mime);
143 }
144
145 // Handle transparencies
146 imagesavealpha($image, true);
147 imagealphablending($image, true);
148
149 return $image;
150 }
151
152 /**
153 * Cleans up everything to start again.
154 *
155 * @return Image
156 */
157 public function destroy()
158 {
159 if (!empty($this->image)
160 && is_resource($this->image)
161 && get_resource_type($this->image) == 'gd'
162 ) {
163 imagedestroy($this->image);
164 }
165 $this->metadata = [];
166 $this->filename = $this->width = $this->height = null;
167
168 return $this;
169 }
170
171 /**
172 * Outputs or saves the image.
173 *
174 * @param string $filename Filename to be saved. Empty to directly print on screen.
175 * @param string $output Use it to overwrite the output format when no $filename is passed.
176 * @param bool $header Wheather or not generate the output header.
177 * @return Image
178 * @throws InvalidArgumentException If output format is not recognised.
179 */
180 public function generate($filename = null, $output = null, $header = true)
181 {
182 $output = $output ?: $this->metadata['mime'];
183 if (!empty($filename)) {
184 $output = $this->getMimeFromExtension($filename);
185 } elseif ($header) {
186 header("Content-type: {$output}");
187 }
188
189 switch ($output) {
190 case 'image/gif':
191 imagegif($this->image, $filename, $this->quality);
192 break;
193 case 'image/png':
194 imagesavealpha($this->image, true);
195 imagepng($this->image, $filename, $this->compression);
196 break;
197 case 'image/jpeg':
198 imageinterlace($this->image, true);
199 imagejpeg($this->image, $filename, $this->quality);
200 break;
201 default:
202 throw new InvalidArgumentException("Invalid output format \"%s\"", $output);
203 }
204
205 return $this;
206 }
207
208 /**
209 * Similar to generate, except that passing an empty $filename here will
210 * overwrite the original file.
211 *
212 * @param string $filename Filename to be saved. Empty to overwrite original file.
213 * @return bool
214 */
215 public function save($filename = null)
216 {
217 $filename = $filename ?: $this->filename;
218
219 return $this->generate($filename);
220 }
221
222 /**
223 * Returns the base64 version for the current Image.
224 *
225 * @param bool $prefix Whether or not prefix the string
226 * with `data:{mime};base64,`.
227 * @return string
228 */
229 public function toString($prefix = false)
230 {
231 ob_start();
232 $this->generate(null, null, false);
233 $image = ob_get_contents();
234 ob_end_clean();
235
236 $string = base64_encode($image);
237
238 if ($prefix) {
239 $prefix = "data:{$this->metadata['mime']};base64,";
240 $string = $prefix . $string;
241 }
242
243 return $string;
244 }
245
246 /**
247 * Loads image and (optionally) its options.
248 *
249 * @param mixed $filename Filename string or array containing both filename and quality
250 * @return Watimage
251 * @throws FileNotExistException
252 * @throws InvalidArgumentException
253 */
254 public function load($filename)
255 {
256 if (empty($filename)) {
257 throw new InvalidArgumentException("Image file has not been set.");
258 }
259
260 if (is_array($filename)) {
261 if (isset($filename['quality'])) {
262 $this->setQuality($filename['quality']);
263 }
264 $filename = $filename['file'];
265 }
266
267 if (!file_exists($filename)) {
268 throw new FileNotExistException($filename);
269 }
270
271 $this->destroy();
272
273 $this->filename = $filename;
274 $this->getMetadataForImage();
275 $this->image = $this->createResourceImage($filename, $this->metadata['mime']);
276
277 return $this;
278 }
279
280 /**
281 * Loads an image from string. Can be either base64 encoded or not.
282 *
283 * @param string $string The image string to be loaded.
284 * @return Image
285 */
286 public function fromString($string)
287 {
288 if (strpos($string, 'data:image') === 0) {
289 preg_match('/^data:(image\/[a-z]+);base64,(.+)/', $string, $matches);
290 array_shift($matches);
291 list($this->metadata['mime'], $string) = $matches;
292 }
293
294 if (!$string = base64_decode($string)) {
295 throw new InvalidArgumentException(
296 'The given value does not seem a valid base64 string'
297 );
298 }
299
300 $this->image = $this->createResourceImage($string, 'string');
301 $this->updateSize();
302
303 if (function_exists('finfo_buffer') && !isset($this->metadata['mime'])) {
304 $finfo = finfo_open();
305 $this->metadata['mime'] = finfo_buffer($finfo, $string, FILEINFO_MIME_TYPE);
306 finfo_close($finfo);
307 }
308
309 return $this;
310 }
311
312 /**
313 * Auto-orients an image based on its exif Orientation information.
314 *
315 * @return Image
316 */
317 public function autoOrientate()
318 {
319 if (empty($this->metadata['exif']['Orientation'])) {
320 return $this;
321 }
322
323 switch ((int)$this->metadata['exif']['Orientation']) {
324 case 2:
325 return $this->flip('horizontal');
326 case 3:
327 return $this->flip('both');
328 case 4:
329 return $this->flip('vertical');
330 case 5:
331 $this->flip('horizontal');
332 return $this->rotate(-90);
333 case 6:
334 return $this->rotate(-90);
335 case 7:
336 $this->flip('horizontal');
337 return $this->rotate(90);
338 case 8:
339 return $this->rotate(90);
340 default:
341 return $this;
342 }
343 }
344
345 /**
346 * Rotates an image.
347 *
348 * Will rotate clockwise when using positive degrees.
349 *
350 * @param int $degrees Rotation angle in degrees.
351 * @param mixed $bgcolor Background to be used for the background, transparent by default.
352 * @return Image
353 */
354 public function rotate($degrees, $bgcolor = self::COLOR_TRANSPARENT)
355 {
356 $bgcolor = $this->color($bgcolor);
357
358 $this->image = imagerotate($this->image, $degrees, $bgcolor);
359
360 $this->updateSize();
361
362 return $this;
363 }
364
365 /**
366 * All in one method for all resize methods.
367 *
368 * @param string $type Type of resize: resize, resizemin, reduce, crop & resizecrop.
369 * @param mixed $width Can be just max width or an array containing both params.
370 * @param int $height Max height.
371 * @return Image
372 */
373 public function resize($type, $width, $height = null)
374 {
375 $types = [
376 'classic' => 'classicResize',
377 'resize' => 'classicResize',
378 'reduce' => 'reduce',
379 'resizemin' => 'reduce',
380 'min' => 'reduce',
381 'crop' => 'classicCrop',
382 'resizecrop' => 'resizeCrop'
383 ];
384
385 $lowertype = strtolower($type);
386
387 if (!array_key_exists($lowertype, $types)) {
388 throw new InvalidArgumentException("Invalid resize type %s.", $type);
389 }
390
391 return $this->{$types[$lowertype]}($width, $height);
392 }
393
394 /**
395 * Resizes maintaining aspect ratio.
396 *
397 * Maintains the aspect ratio of the image and makes sure that it fits
398 * within the max width and max height (thus some side will be smaller).
399 *
400 * @param mixed $width Can be just max width or an array containing both params.
401 * @param int $height Max height.
402 * @return Image
403 */
404 public function classicResize($width, $height = null)
405 {
406 list($width, $height) = Normalize::size($width, $height);
407
408 if ($this->width == $width && $this->height == $height) {
409 return $this;
410 }
411
412 if ($this->width > $this->height) {
413 $height = ($this->height * $width) / $this->width;
414 } elseif ($this->width < $this->height) {
415 $width = ($this->width * $height) / $this->height;
416 } elseif ($this->width == $this->height) {
417 $width = $height;
418 }
419
420 $this->image = $this->imagecopy($width, $height);
421
422 $this->updateSize();
423
424 return $this;
425 }
426
427 /**
428 * Backwards compatibility alias for reduce (which has the same logic).
429 *
430 * @param mixed $width Can be just max width or an array containing both params.
431 * @param int $height Max height.
432 * @return Image
433 * @deprecated
434 * @codeCoverageIgnore
435 */
436 public function resizeMin($width, $height = null)
437 {
438 return $this->reduce($width, $height);
439 }
440
441 /**
442 * A straight centered crop.
443 *
444 * @param mixed $width Can be just max width or an array containing both params.
445 * @param int $height Max height.
446 * @return Image
447 */
448 public function classicCrop($width, $height = null)
449 {
450 list($width, $height) = Normalize::size($width, $height);
451
452 $startY = ($this->height - $height) / 2;
453 $startX = ($this->width - $width) / 2;
454
455 $this->image = $this->imagecopy($width, $height, $startX, $startY, $width, $height);
456
457 $this->updateSize();
458
459 return $this;
460 }
461
462 /**
463 * Resizes to max, then crops to center.
464 *
465 * @param mixed $width Can be just max width or an array containing both params.
466 * @param int $height Max height.
467 * @return Image
468 */
469 public function resizeCrop($width, $height = null)
470 {
471 list($width, $height) = Normalize::size($width, $height);
472
473 $ratioX = $width / $this->width;
474 $ratioY = $height / $this->height;
475 $srcW = $this->width;
476 $srcH = $this->height;
477
478 if ($ratioX < $ratioY) {
479 $startX = round(($this->width - ($width / $ratioY)) / 2);
480 $startY = 0;
481 $srcW = round($width / $ratioY);
482 } else {
483 $startX = 0;
484 $startY = round(($this->height - ($height / $ratioX)) / 2);
485 $srcH = round($height / $ratioX);
486 }
487
488 $this->image = $this->imagecopy($width, $height, $startX, $startY, $srcW, $srcH);
489
490 $this->updateSize();
491
492 return $this;
493 }
494
495 /**
496 * Resizes maintaining aspect ratio but not exceeding width / height.
497 *
498 * @param mixed $width Can be just max width or an array containing both params.
499 * @param int $height Max height.
500 * @return Image
501 */
502 public function reduce($width, $height = null)
503 {
504 list($width, $height) = Normalize::size($width, $height);
505
506 if ($this->width < $width && $this->height < $height) {
507 return $this;
508 }
509
510 $ratioX = $this->width / $width;
511 $ratioY = $this->height / $height;
512
513 $ratio = $ratioX > $ratioY ? $ratioX : $ratioY;
514
515 if ($ratio === 1) {
516 return $this;
517 }
518
519 // Getting the new image size
520 $width = (int)($this->width / $ratio);
521 $height = (int)($this->height / $ratio);
522
523 $this->image = $this->imagecopy($width, $height);
524
525 $this->updateSize();
526
527 return $this;
528 }
529
530 /**
531 * Flips an image. If PHP version is 5.5.0 or greater will use
532 * proper php gd imageflip method. Otherwise will fallback to
533 * convenienceflip.
534 *
535 * @param string $type Type of flip, can be any of: horizontal, vertical, both
536 * @return Image
537 */
538 public function flip($type = 'horizontal')
539 {
540 if (version_compare(PHP_VERSION, '5.5.0', '<')) {
541 return $this->convenienceFlip($type);
542 }
543
544 imageflip($this->image, Normalize::flip($type));
545
546 return $this;
547 }
548
549 /**
550 * Flip method for PHP versions < 5.5.0
551 *
552 * @param string $type Type of flip, can be any of: horizontal, vertical, both
553 * @return Image
554 */
555 public function convenienceFlip($type = 'horizontal')
556 {
557 $type = Normalize::flip($type);
558
559 $resampled = $this->imagecreate($this->width, $this->height);
560
561 // @codingStandardsIgnoreStart
562 switch ($type) {
563 case IMG_FLIP_VERTICAL:
564 imagecopyresampled(
565 $resampled, $this->image,
566 0, 0, 0, ($this->height - 1),
567 $this->width, $this->height, $this->width, 0 - $this->height
568 );
569 break;
570 case IMG_FLIP_HORIZONTAL:
571 imagecopyresampled(
572 $resampled, $this->image,
573 0, 0, ($this->width - 1), 0,
574 $this->width, $this->height, 0 - $this->width, $this->height
575 );
576 break;
577 // same as $this->rotate(180)
578 case IMG_FLIP_BOTH:
579 imagecopyresampled(
580 $resampled, $this->image,
581 0, 0, ($this->width - 1), ($this->height - 1),
582 $this->width, $this->height, 0 - $this->width, 0 - $this->height
583 );
584 break;
585 }
586 // @codingStandardsIgnoreEnd
587
588 $this->image = $resampled;
589
590 return $this;
591 }
592
593 /**
594 * Creates an empty canvas.
595 *
596 * If no arguments are passed and we have previously created an
597 * image it will create a new canvas with the previous canvas size.
598 * Due to this, you can use this method to "empty" the current canvas.
599 *
600 * @param int $width Canvas width.
601 * @param int $height Canvas height.
602 * @return Image
603 */
604 public function create($width = null, $height = null)
605 {
606 if (!isset($width)) {
607 if (!isset($this->width, $this->height)) {
608 throw new InvalidArgumentException("You must set the canvas size.");
609 }
610 $width = $this->width;
611 $height = $this->height;
612 }
613
614 if (!isset($height)) {
615 $height = $width;
616 }
617
618 $this->image = $this->imagecreate($width, $height);
619 $exif = null;
620 $this->metadata = compact('width', 'height', 'exif');
621
622 $this->updateSize();
623
624 return $this;
625 }
626
627 /**
628 * Creates an empty canvas.
629 *
630 * @param int $width Canvas width.
631 * @param int $height Canvas height.
632 * @param bool $transparency Whether or not to set transparency values.
633 * @return resource Image resource with the canvas.
634 */
635 protected function imagecreate($width, $height, $transparency = true)
636 {
637 $image = imagecreatetruecolor($width, $height);
638
639 if ($transparency) {
640 // Required for transparencies
641 $bgcolor = imagecolortransparent(
642 $image,
643 imagecolorallocatealpha($image, 255, 255, 255, 127)
644 );
645 imagefill($image, 0, 0, $bgcolor);
646 imagesavealpha($image, true);
647 imagealphablending($image, true);
648 }
649
650 return $image;
651 }
652
653 /**
654 * Helper method for all resize methods and others that require
655 * imagecopyresampled method.
656 *
657 * @param int $dstW New width.
658 * @param int $dstH New height.
659 * @param int $srcX Starting source point X.
660 * @param int $srcY Starting source point Y.
661 * @return resource GD image resource containing the resized image.
662 */
663 protected function imagecopy($dstW, $dstH, $srcX = 0, $srcY = 0, $srcW = false, $srcH = false)
664 {
665 $destImage = $this->imagecreate($dstW, $dstH);
666
667 if ($srcW === false) {
668 $srcW = $this->width;
669 }
670
671 if ($srcH === false) {
672 $srcH = $this->height;
673 }
674
675 // @codingStandardsIgnoreStart
676 imagecopyresampled(
677 $destImage, $this->image,
678 0, 0, $srcX, $srcY,
679 $dstW, $dstH, $srcW, $srcH
680 );
681 // @codingStandardsIgnoreEnd
682
683 return $destImage;
684 }
685
686 /**
687 * Fills current canvas with specified color.
688 *
689 * It works with newly created canvas. If you want to overwrite the current
690 * canvas you must first call `create` method to empty current canvas.
691 *
692 * @param mixed $color The color. Check out getColorArray for allowed formats.
693 * @return Image
694 */
695 public function fill($color = '#fff')
696 {
697 imagefill($this->image, 0, 0, $this->color($color));
698
699 return $this;
700 }
701
702 /**
703 * Allocates a color for the current image resource and returns it.
704 *
705 * Useful for directly treating images.
706 *
707 * @param mixed $color The color. Check out getColorArray for allowed formats.
708 * @return int
709 * @codeCoverageIgnore
710 */
711 public function color($color)
712 {
713 $color = Normalize::color($color);
714
715 if ($color['a'] !== 0) {
716 return imagecolorallocatealpha($this->image, $color['r'], $color['g'], $color['b'], $color['a']);
717 }
718
719 return imagecolorallocate($this->image, $color['r'], $color['g'], $color['b']);
720 }
721
722 /**
723 * Crops an image based on specified coords and size.
724 *
725 * You can pass arguments one by one or an array passing arguments
726 * however you like.
727 *
728 * @param int $x X position where start to crop.
729 * @param int $y Y position where start to crop.
730 * @param int $width New width of the image.
731 * @param int $height New height of the image.
732 * @return Image
733 */
734 public function crop($x, $y = null, $width = null, $height = null)
735 {
736 list($x, $y, $width, $height) = Normalize::crop($x, $y, $width, $height);
737
738 $crop = $this->imagecreate($width, $height);
739
740 // @codingStandardsIgnoreStart
741 imagecopyresampled(
742 $crop, $this->image,
743 0, 0, $x, $y,
744 $width, $height, $width, $height
745 );
746 // @codingStandardsIgnoreEnd
747
748 $this->image = $crop;
749
750 $this->updateSize();
751
752 return $this;
753 }
754
755 /**
756 * Blurs the image.
757 *
758 * @param mixed $type Type of blur to be used between: gaussian, selective.
759 * @param integer $passes Number of times to apply the filter.
760 * @return Image
761 * @throws InvalidArgumentException
762 */
763 public function blur($type = null, $passes = 1)
764 {
765 switch (strtolower($type)) {
766 case IMG_FILTER_GAUSSIAN_BLUR:
767 case 'selective':
768 $type = IMG_FILTER_GAUSSIAN_BLUR;
769 break;
770
771 // gaussian by default (just because I like it more)
772 case null:
773 case 'gaussian':
774 case IMG_FILTER_SELECTIVE_BLUR:
775 $type = IMG_FILTER_SELECTIVE_BLUR;
776 break;
777
778 default:
779 throw new InvalidArgumentException("Incorrect blur type \"%s\"", $type);
780 }
781
782 for ($i = 0; $i < Normalize::fitInRange($passes, 1); $i++) {
783 imagefilter($this->image, $type);
784 }
785
786 return $this;
787 }
788
789 /**
790 * Changes the brightness of the image.
791 *
792 * @param integer $level Brightness value; range between -255 & 255.
793 * @return Image
794 */
795 public function brightness($level)
796 {
797 imagefilter(
798 $this->image,
799 IMG_FILTER_BRIGHTNESS,
800 Normalize::fitInRange($level, -255, 255)
801 );
802
803 return $this;
804 }
805
806 /**
807 * Like grayscale, except you can specify the color.
808 *
809 * @param mixed $color Color in any format accepted by Normalize::color
810 * @return Image
811 */
812 public function colorize($color)
813 {
814 $color = Normalize::color($color);
815
816 imagefilter(
817 $this->image,
818 IMG_FILTER_COLORIZE,
819 $color['r'],
820 $color['g'],
821 $color['b'],
822 $color['a']
823 );
824
825 return $this;
826 }
827
828 /**
829 * Changes the contrast of the image.
830 *
831 * @param integer $level Use for adjunting level of contrast (-100 to 100)
832 * @return Image
833 */
834 public function contrast($level)
835 {
836 imagefilter(
837 $this->image,
838 IMG_FILTER_CONTRAST,
839 Normalize::fitInRange($level, -100, 100)
840 );
841
842 return $this;
843 }
844
845 /**
846 * Uses edge detection to highlight the edges in the image.
847 *
848 * @return Image
849 */
850 public function edgeDetection()
851 {
852 imagefilter($this->image, IMG_FILTER_EDGEDETECT);
853
854 return $this;
855 }
856
857 /**
858 * Embosses the image.
859 *
860 * @return Image
861 */
862 public function emboss()
863 {
864 imagefilter($this->image, IMG_FILTER_EMBOSS);
865
866 return $this;
867 }
868
869 /**
870 * Applies grayscale filter.
871 *
872 * @return Image
873 */
874 public function grayscale()
875 {
876 imagefilter($this->image, IMG_FILTER_GRAYSCALE);
877
878 return $this;
879 }
880
881 /**
882 * Uses mean removal to achieve a "sketchy" effect.
883 *
884 * @return Image
885 */
886 public function meanRemove()
887 {
888 imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
889
890 return $this;
891 }
892
893 /**
894 * Reverses all colors of the image.
895 *
896 * @return Image
897 */
898 public function negate()
899 {
900 imagefilter($this->image, IMG_FILTER_NEGATE);
901
902 return $this;
903 }
904
905 /**
906 * Pixelates the image.
907 *
908 * @param int $blockSize Block size in pixels.
909 * @param bool $advanced Set to true to enable advanced pixelation.
910 * @return Image
911 */
912 public function pixelate($blockSize = 3, $advanced = false)
913 {
914 imagefilter(
915 $this->image,
916 IMG_FILTER_PIXELATE,
917 Normalize::fitInRange($blockSize, 1),
918 $advanced
919 );
920
921 return $this;
922 }
923
924 /**
925 * A combination of various effects to achieve a sepia like effect.
926 *
927 * TODO: Create an additional class with instagram-like effects and move it there.
928 *
929 * @param int $alpha Defines the transparency of the effect: from 0 to 100
930 * @return Image
931 */
932 public function sepia($alpha = 0)
933 {
934 return $this
935 ->grayscale()
936 ->contrast(-3)
937 ->brightness(-15)
938 ->colorize([
939 'r' => 100,
940 'g' => 70,
941 'b' => 50,
942 'a' => Normalize::fitInRange($alpha, 0, 100)
943 ])
944 ;
945 }
946
947 /**
948 * Makes the image smoother.
949 *
950 * @param int $level Level of smoothness, between -15 and 15.
951 * @return Image
952 */
953 public function smooth($level)
954 {
955 imagefilter(
956 $this->image,
957 IMG_FILTER_SMOOTH,
958 Normalize::fitInRange($level, -15, 15)
959 );
960
961 return $this;
962 }
963
964 /**
965 * Adds a vignette to image.
966 *
967 * @param float $size Size of the vignette, between 0 and 10. Low is sharper.
968 * @param float $level Vignete transparency, between 0 and 1
969 * @return Image
970 * @link http://php.net/manual/en/function.imagefilter.php#109809
971 */
972 public function vignette($size = 0.7, $level = 0.8)
973 {
974 for ($x = 0; $x < $this->width; ++$x) {
975 for ($y = 0; $y < $this->height; ++$y) {
976 $index = imagecolorat($this->image, $x, $y);
977 $rgb = imagecolorsforindex($this->image, $index);
978
979 $this->vignetteEffect($size, $level, $x, $y, $rgb);
980 $color = imagecolorallocate($this->image, $rgb['red'], $rgb['green'], $rgb['blue']);
981
982 imagesetpixel($this->image, $x, $y, $color);
983 }
984 }
985
986 return $this;
987 }
988
989 /**
990 * Sets quality for gif and jpg files.
991 *
992 * @param int $quality A value from 0 (zero quality) to 100 (max quality).
993 * @return Image
994 * @codeCoverageIgnore
995 */
996 public function setQuality($quality)
997 {
998 $this->quality = $quality;
999
1000 return $this;
1001 }
1002
1003 /**
1004 * Sets compression for png files.
1005 *
1006 * @param int $compression A value from 0 (no compression, not recommended) to 9.
1007 * @return Image
1008 * @codeCoverageIgnore
1009 */
1010 public function setCompression($compression)
1011 {
1012 $this->compression = $compression;
1013
1014 return $this;
1015 }
1016
1017 /**
1018 * Allows you to set the current image resource.
1019 *
1020 * This is intented for use it in conjuntion with getImage.
1021 *
1022 * @param resource $image Image resource to be set.
1023 * @throws Exception If given image is not a GD resource.
1024 * @return Image
1025 */
1026 public function setImage($image)
1027 {
1028 if (!is_resource($image) || !get_resource_type($image) == 'gd') {
1029 throw new Exception("Given image is not a GD image resource");
1030 }
1031
1032 $this->image = $image;
1033 $this->updateSize();
1034
1035 return $this;
1036 }
1037
1038 /**
1039 * Useful method to calculate real crop measures. Used when you crop an image
1040 * which is smaller than the original one. In those cases you can call
1041 * calculateCropMeasures to retrieve the real $ox, $oy, $dx & $dy of the
1042 * image to be cropped.
1043 *
1044 * Note that you need to set the destiny image and pass the smaller (cropped)
1045 * image to this function.
1046 *
1047 * @param string|Image $croppedFile The cropped image.
1048 * @param mixed $ox Origin X.
1049 * @param int $oy Origin Y.
1050 * @param int $dx Destiny X.
1051 * @param int $dy Destiny Y.
1052 * @return array
1053 */
1054 public function calculateCropMeasures($croppedFile, $ox, $oy = null, $dx = null, $dy = null)
1055 {
1056 list($ox, $oy, $dx, $dy) = Normalize::cropMeasures($ox, $oy, $dx, $dy);
1057
1058 if (!($croppedFile instanceof self)) {
1059 $croppedFile = new self($croppedFile);
1060 }
1061
1062 $meta = $croppedFile->getMetadata();
1063
1064 $rateWidth = $this->width / $meta['width'];
1065 $rateHeight = $this->height / $meta['height'];
1066
1067 $ox = round($ox * $rateWidth);
1068 $oy = round($oy * $rateHeight);
1069 $dx = round($dx * $rateHeight);
1070 $dy = round($dy * $rateHeight);
1071
1072 $width = $dx - $ox;
1073 $height = $dy - $oy;
1074
1075 return [$ox, $oy, $dx, $dy, $width, $height];
1076 }
1077
1078 /**
1079 * Returns image resource, so you can use it however you wan.
1080 *
1081 * @return resource
1082 * @codeCoverageIgnore
1083 */
1084 public function getImage()
1085 {
1086 return $this->image;
1087 }
1088
1089 /**
1090 * Returns metadata for current image.
1091 *
1092 * @return array
1093 * @codeCoverageIgnore
1094 */
1095 public function getMetadata()
1096 {
1097 return $this->metadata;
1098 }
1099
1100 /**
1101 * Gets metadata information from given $filename.
1102 *
1103 * @param string $filename File path
1104 * @return array
1105 */
1106 public static function getMetadataFromFile($filename)
1107 {
1108 $info = getimagesize($filename);
1109
1110 $metadata = [
1111 'width' => $info[0],
1112 'height' => $info[1],
1113 'mime' => $info['mime'],
1114 'exif' => null // set later, if necessary
1115 ];
1116
1117 if (function_exists('exif_read_data') && $metadata['mime'] == 'image/jpeg') {
1118 $metadata['exif'] = @exif_read_data($filename);
1119 }
1120
1121 return $metadata;
1122 }
1123
1124 /**
1125 * Loads metadata to internal variables.
1126 *
1127 * @return void
1128 * @codeCoverageIgnore
1129 */
1130 protected function getMetadataForImage()
1131 {
1132 $this->metadata = $this->getMetadataFromFile($this->filename);
1133
1134 $this->width = $this->metadata['width'];
1135 $this->height = $this->metadata['height'];
1136 }
1137
1138 /**
1139 * Gets mime for an image from its extension.
1140 *
1141 * @param string $filename Filename to be checked.
1142 * @return string Mime for the filename given.
1143 * @throws InvalidExtensionException
1144 */
1145 protected function getMimeFromExtension($filename)
1146 {
1147 $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
1148
1149 switch ($extension) {
1150 case 'jpg':
1151 case 'jpeg':
1152 return 'image/jpeg';
1153 case 'png':
1154 return 'image/png';
1155 case 'gif':
1156 return 'image/gif';
1157 default:
1158 throw new InvalidExtensionException($extension);
1159 }
1160 }
1161
1162 /**
1163 * Updates current image metadata.
1164 *
1165 * @return void
1166 * @codeCoverageIgnore
1167 */
1168 protected function updateMetadata()
1169 {
1170 $this->metadata['width'] = $this->width;
1171 $this->metadata['height'] = $this->height;
1172 }
1173
1174 /**
1175 * Resets width and height of the current image.
1176 *
1177 * @return void
1178 * @codeCoverageIgnore
1179 */
1180 protected function updateSize()
1181 {
1182 $this->width = imagesx($this->image);
1183 $this->height = imagesy($this->image);
1184
1185 $this->updateMetadata();
1186 }
1187
1188 /**
1189 * Required by vignette to generate the propper colors.
1190 *
1191 * @param float $size Size of the vignette, between 0 and 10. Low is sharper.
1192 * @param float $level Vignete transparency, between 0 and 1
1193 * @param int $x X position of the pixel.
1194 * @param int $y Y position of the pixel.
1195 * @param array &$rgb Current pixel olor information.
1196 * @return void
1197 * @codeCoverageIgnore
1198 */
1199 protected function vignetteEffect($size, $level, $x, $y, &$rgb)
1200 {
1201 $l = sin(M_PI / $this->width * $x) * sin(M_PI / $this->height * $y);
1202 $l = pow($l, Normalize::fitInRange($size, 0, 10));
1203
1204 $l = 1 - Normalize::fitInRange($level, 0, 1) * (1 - $l);
1205
1206 $rgb['red'] *= $l;
1207 $rgb['green'] *= $l;
1208 $rgb['blue'] *= $l;
1209 }
1210 }
1211