1: <?php
2:
3: namespace Gumlet;
4:
5: use Exception;
6:
7: /**
8: * PHP class to resize and scale images
9: */
10: class ImageResize
11: {
12: const CROPTOP = 1;
13: const CROPCENTRE = 2;
14: const CROPCENTER = 2;
15: const CROPBOTTOM = 3;
16: const CROPLEFT = 4;
17: const CROPRIGHT = 5;
18: const CROPTOPCENTER = 6;
19: const IMG_FLIP_HORIZONTAL = 0;
20: const IMG_FLIP_VERTICAL = 1;
21: const IMG_FLIP_BOTH = 2;
22:
23: public $quality_jpg = 85;
24: public $quality_webp = 85;
25: public $quality_avif = 60;
26: public $quality_png = 6;
27: public $quality_truecolor = true;
28: public $gamma_correct = false;
29:
30: public $interlace = 1;
31:
32: public $source_type;
33:
34: protected $source_image;
35:
36: protected $original_w;
37: protected $original_h;
38:
39: protected $dest_x = 0;
40: protected $dest_y = 0;
41:
42: protected $source_x;
43: protected $source_y;
44:
45: protected $dest_w;
46: protected $dest_h;
47:
48: protected $source_w;
49: protected $source_h;
50:
51: protected $source_info;
52:
53: protected $filters = [];
54:
55: /**
56: * Create instance from a strng
57: *
58: * @param string $image_data
59: * @return ImageResize
60: * @throws ImageResizeException
61: */
62: public static function createFromString($image_data)
63: {
64: if (empty($image_data) || $image_data === null) {
65: throw new ImageResizeException('image_data must not be empty');
66: }
67: $resize = new self('data://application/octet-stream;base64,' . base64_encode($image_data));
68: return $resize;
69: }
70:
71:
72: /**
73: * Add filter function for use right before save image to file.
74: *
75: * @param callable $filter
76: * @return $this
77: */
78: public function addFilter(callable $filter)
79: {
80: $this->filters[] = $filter;
81: return $this;
82: }
83:
84: /**
85: * Apply filters.
86: *
87: * @param $image resource an image resource identifier
88: * @param $filterType filter type and default value is IMG_FILTER_NEGATE
89: */
90: protected function applyFilter($image, $filterType = IMG_FILTER_NEGATE)
91: {
92: foreach ($this->filters as $function) {
93: $function($image, $filterType);
94: }
95: }
96:
97: /**
98: * Loads image source and its properties to the instanciated object
99: *
100: * @param string $filename
101: * @return ImageResize
102: * @throws ImageResizeException
103: */
104: public function __construct($filename)
105: {
106: if ($filename === null || empty($filename) || (substr($filename, 0, 5) !== 'data:' && !is_file($filename))) {
107: throw new ImageResizeException('File does not exist');
108: }
109:
110: $finfo = finfo_open(FILEINFO_MIME_TYPE);
111:
112: if (!$image_info = getimagesize($filename, $this->source_info)) {
113: $image_info = getimagesize($filename);
114: }
115:
116: if (!$image_info) {
117: if (strstr(finfo_file($finfo, $filename), 'image') !== false) {
118: throw new ImageResizeException('Unsupported image type');
119: }
120:
121: throw new ImageResizeException('Unsupported file type');
122: }
123:
124: $this->original_w = $image_info[0];
125: $this->original_h = $image_info[1];
126: $this->source_type = $image_info[2];
127:
128: switch ($this->source_type) {
129: case IMAGETYPE_GIF:
130: $this->source_image = imagecreatefromgif($filename);
131: break;
132:
133: case IMAGETYPE_JPEG:
134: $this->source_image = $this->imageCreateJpegfromExif($filename);
135:
136: // set new width and height for image, maybe it has changed
137: $this->original_w = imagesx($this->source_image);
138: $this->original_h = imagesy($this->source_image);
139:
140: break;
141:
142: case IMAGETYPE_PNG:
143: $this->source_image = imagecreatefrompng($filename);
144: break;
145:
146: case IMAGETYPE_WEBP:
147: $this->source_image = imagecreatefromwebp($filename);
148: break;
149:
150: case IMAGETYPE_AVIF:
151: $this->source_image = imagecreatefromavif($filename);
152: $this->original_w = imagesx($this->source_image);
153: $this->original_h = imagesy($this->source_image);
154: break;
155:
156: case IMAGETYPE_BMP:
157: $this->source_image = imagecreatefrombmp($filename);
158: break;
159:
160: default:
161: throw new ImageResizeException('Unsupported image type');
162: }
163:
164: if (!$this->source_image) {
165: throw new ImageResizeException('Could not load image');
166: }
167:
168: finfo_close($finfo);
169:
170: return $this->resize($this->getSourceWidth(), $this->getSourceHeight());
171: }
172:
173: // http://stackoverflow.com/a/28819866
174: public function imageCreateJpegfromExif($filename)
175: {
176: $img = imagecreatefromjpeg($filename);
177:
178: if (!function_exists('exif_read_data') || !isset($this->source_info['APP1']) || strpos($this->source_info['APP1'], 'Exif') !== 0) {
179: return $img;
180: }
181:
182: try {
183: $exif = @exif_read_data($filename);
184: } catch (Exception $e) {
185: $exif = null;
186: }
187:
188: if (!$exif || !isset($exif['Orientation'])) {
189: return $img;
190: }
191:
192: $orientation = $exif['Orientation'];
193:
194: if ($orientation === 6 || $orientation === 5) {
195: $img = imagerotate($img, 270, 0);
196: } elseif ($orientation === 3 || $orientation === 4) {
197: $img = imagerotate($img, 180, 0);
198: } elseif ($orientation === 8 || $orientation === 7) {
199: $img = imagerotate($img, 90, 0);
200: }
201:
202: if ($orientation === 5 || $orientation === 4 || $orientation === 7) {
203: imageflip($img, IMG_FLIP_HORIZONTAL);
204: }
205:
206: return $img;
207: }
208:
209: /**
210: * Saves new image
211: *
212: * @param string $filename
213: * @param integer $image_type
214: * @param integer $quality
215: * @param integer $permissions
216: * @param boolean $exact_size
217: * @return static
218: */
219: public function save($filename, $image_type = null, $quality = null, $permissions = null, $exact_size = false)
220: {
221: $image_type = $image_type ?: $this->source_type;
222: $quality = is_numeric($quality) ? (int) abs($quality) : null;
223:
224: switch ($image_type) {
225: case IMAGETYPE_GIF:
226: if( !empty($exact_size) && is_array($exact_size) ){
227: $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
228: } else{
229: $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
230: }
231:
232: $background = imagecolorallocatealpha($dest_image, 255, 255, 255, 1);
233: imagecolortransparent($dest_image, $background);
234: imagefill($dest_image, 0, 0, $background);
235: imagesavealpha($dest_image, true);
236: break;
237:
238: case IMAGETYPE_JPEG:
239: if( !empty($exact_size) && is_array($exact_size) ){
240: $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
241: $background = imagecolorallocate($dest_image, 255, 255, 255);
242: imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
243: } else{
244: $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
245: $background = imagecolorallocate($dest_image, 255, 255, 255);
246: imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
247: }
248: break;
249:
250: case IMAGETYPE_WEBP:
251: if( !empty($exact_size) && is_array($exact_size) ){
252: $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
253: $background = imagecolorallocate($dest_image, 255, 255, 255);
254: imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
255: } else{
256: $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
257: $background = imagecolorallocate($dest_image, 255, 255, 255);
258: imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
259: }
260:
261: imagealphablending($dest_image, false);
262: imagesavealpha($dest_image, true);
263:
264: break;
265:
266: case IMAGETYPE_AVIF:
267: if( !empty($exact_size) && is_array($exact_size) ){
268: $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
269: $background = imagecolorallocate($dest_image, 255, 255, 255);
270: imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
271: } else{
272: $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
273: $background = imagecolorallocate($dest_image, 255, 255, 255);
274: imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
275: }
276:
277: imagealphablending($dest_image, false);
278: imagesavealpha($dest_image, true);
279:
280: break;
281:
282: case IMAGETYPE_PNG:
283: if (!$this->quality_truecolor || !imageistruecolor($this->source_image)) {
284: if( !empty($exact_size) && is_array($exact_size) ){
285: $dest_image = imagecreate($exact_size[0], $exact_size[1]);
286: } else{
287: $dest_image = imagecreate($this->getDestWidth(), $this->getDestHeight());
288: }
289: } else {
290: if( !empty($exact_size) && is_array($exact_size) ){
291: $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
292: } else{
293: $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
294: }
295: }
296:
297: imagealphablending($dest_image, false);
298: imagesavealpha($dest_image, true);
299:
300: $background = imagecolorallocatealpha($dest_image, 255, 255, 255, 127);
301: imagecolortransparent($dest_image, $background);
302: imagefill($dest_image, 0, 0, $background);
303: break;
304:
305: case IMAGETYPE_BMP:
306: if(!empty($exact_size) && is_array($exact_size)) {
307: $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
308: $background = imagecolorallocate($dest_image, 255, 255, 255);
309: imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
310: } else {
311: $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
312: $background = imagecolorallocate($dest_image, 255, 255, 255);
313: imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
314: }
315: break;
316: }
317:
318: imageinterlace($dest_image, $this->interlace);
319:
320: if ($this->gamma_correct) {
321: imagegammacorrect($this->source_image, 2.2, 1.0);
322: }
323:
324: if( !empty($exact_size) && is_array($exact_size) ) {
325: if ($this->getSourceHeight() < $this->getSourceWidth()) {
326: $this->dest_x = 0;
327: $this->dest_y = ($exact_size[1] - $this->getDestHeight()) / 2;
328: }
329: if ($this->getSourceHeight() > $this->getSourceWidth()) {
330: $this->dest_x = ($exact_size[0] - $this->getDestWidth()) / 2;
331: $this->dest_y = 0;
332: }
333: }
334:
335: imagecopyresampled(
336: $dest_image,
337: $this->source_image,
338: $this->dest_x,
339: $this->dest_y,
340: $this->source_x,
341: $this->source_y,
342: $this->getDestWidth(),
343: $this->getDestHeight(),
344: $this->source_w,
345: $this->source_h
346: );
347:
348: if ($this->gamma_correct) {
349: imagegammacorrect($dest_image, 1.0, 2.2);
350: }
351:
352:
353: $this->applyFilter($dest_image);
354:
355: switch ($image_type) {
356: case IMAGETYPE_GIF:
357: imagegif($dest_image, $filename);
358: break;
359:
360: case IMAGETYPE_JPEG:
361: if ($quality === null || $quality > 100) {
362: $quality = $this->quality_jpg;
363: }
364:
365: imagejpeg($dest_image, $filename, $quality);
366: break;
367:
368: case IMAGETYPE_WEBP:
369: if ($quality === null) {
370: $quality = $this->quality_webp;
371: }
372:
373: imagewebp($dest_image, $filename, $quality);
374: break;
375:
376: case IMAGETYPE_AVIF:
377: if ($quality === null) {
378: $quality = $this->quality_avif;
379: }
380:
381: imageavif($dest_image, $filename, $quality);
382: break;
383:
384: case IMAGETYPE_PNG:
385: if ($quality === null || $quality > 9) {
386: $quality = $this->quality_png;
387: }
388:
389: imagepng($dest_image, $filename, $quality);
390: break;
391:
392: case IMAGETYPE_BMP:
393: imagebmp($dest_image, $filename, $quality);
394: break;
395: }
396:
397: if ($permissions) {
398: chmod($filename, $permissions);
399: }
400:
401: imagedestroy($dest_image);
402:
403: return $this;
404: }
405:
406: /**
407: * Convert the image to string
408: *
409: * @param int $image_type
410: * @param int $quality
411: * @return string
412: */
413: public function getImageAsString($image_type = null, $quality = null)
414: {
415: $string_temp = tempnam(sys_get_temp_dir(), '');
416:
417: $this->save($string_temp, $image_type, $quality);
418:
419: $string = file_get_contents($string_temp);
420:
421: unlink($string_temp);
422:
423: return $string;
424: }
425:
426: /**
427: * Convert the image to string with the current settings
428: *
429: * @return string
430: */
431: public function __toString()
432: {
433: return $this->getImageAsString();
434: }
435:
436: /**
437: * Outputs image to browser
438: * @param string $image_type
439: * @param integer $quality
440: */
441: public function output($image_type = null, $quality = null)
442: {
443: $image_type = $image_type ?: $this->source_type;
444:
445: header('Content-Type: ' . image_type_to_mime_type($image_type));
446:
447: $this->save(null, $image_type, $quality);
448: }
449:
450: /**
451: * Resizes image according to the given short side (short side proportional)
452: *
453: * @param integer $max_short
454: * @param boolean $allow_enlarge
455: * @return static
456: */
457: public function resizeToShortSide($max_short, $allow_enlarge = false)
458: {
459: if ($this->getSourceHeight() < $this->getSourceWidth()) {
460: $ratio = $max_short / $this->getSourceHeight();
461: $long = (int) round($this->getSourceWidth() * $ratio);
462:
463: $this->resize($long, $max_short, $allow_enlarge);
464: } else {
465: $ratio = $max_short / $this->getSourceWidth();
466: $long = (int) round($this->getSourceHeight() * $ratio);
467:
468: $this->resize($max_short, $long, $allow_enlarge);
469: }
470:
471: return $this;
472: }
473:
474: /**
475: * Resizes image according to the given long side (short side proportional)
476: *
477: * @param integer $max_long
478: * @param boolean $allow_enlarge
479: * @return static
480: */
481: public function resizeToLongSide($max_long, $allow_enlarge = false)
482: {
483: if ($this->getSourceHeight() > $this->getSourceWidth()) {
484: $ratio = $max_long / $this->getSourceHeight();
485: $short = (int) round($this->getSourceWidth() * $ratio);
486:
487: $this->resize($short, $max_long, $allow_enlarge);
488: } else {
489: $ratio = $max_long / $this->getSourceWidth();
490: $short = (int) round($this->getSourceHeight() * $ratio);
491:
492: $this->resize($max_long, $short, $allow_enlarge);
493: }
494:
495: return $this;
496: }
497:
498: /**
499: * Resizes image according to the given height (width proportional)
500: *
501: * @param integer $height
502: * @param boolean $allow_enlarge
503: * @return static
504: */
505: public function resizeToHeight($height, $allow_enlarge = false)
506: {
507: $ratio = $height / $this->getSourceHeight();
508: $width = (int) round($this->getSourceWidth() * $ratio);
509:
510: $this->resize($width, $height, $allow_enlarge);
511:
512: return $this;
513: }
514:
515: /**
516: * Resizes image according to the given width (height proportional)
517: *
518: * @param integer $width
519: * @param boolean $allow_enlarge
520: * @return static
521: */
522: public function resizeToWidth($width, $allow_enlarge = false)
523: {
524: $ratio = $width / $this->getSourceWidth();
525: $height = (int) round($this->getSourceHeight() * $ratio);
526:
527: $this->resize($width, $height, $allow_enlarge);
528:
529: return $this;
530: }
531:
532: /**
533: * Resizes image to best fit inside the given dimensions
534: *
535: * @param integer $max_width
536: * @param integer $max_height
537: * @param boolean $allow_enlarge
538: * @return static
539: */
540: public function resizeToBestFit($max_width, $max_height, $allow_enlarge = false)
541: {
542: if ($this->getSourceWidth() <= $max_width && $this->getSourceHeight() <= $max_height && $allow_enlarge === false) {
543: return $this;
544: }
545:
546: $ratio = $this->getSourceHeight() / $this->getSourceWidth();
547: $width = $max_width;
548: $height = (int) round($width * $ratio);
549:
550: if ($height > $max_height) {
551: $height = $max_height;
552: $width = (int) round($height / $ratio);
553: }
554:
555: return $this->resize($width, $height, $allow_enlarge);
556: }
557:
558: /**
559: * Resizes image according to given scale (proportionally)
560: *
561: * @param integer|float $scale
562: * @return static
563: */
564: public function scale($scale)
565: {
566: $width = (int) round($this->getSourceWidth() * $scale / 100);
567: $height = (int) round($this->getSourceHeight() * $scale / 100);
568:
569: $this->resize($width, $height, true);
570:
571: return $this;
572: }
573:
574: /**
575: * Resizes image according to the given width and height
576: *
577: * @param integer $width
578: * @param integer $height
579: * @param boolean $allow_enlarge
580: * @return static
581: */
582: public function resize($width, $height, $allow_enlarge = false)
583: {
584: if (!$allow_enlarge) {
585: // if the user hasn't explicitly allowed enlarging,
586: // but either of the dimensions are larger then the original,
587: // then just use original dimensions - this logic may need rethinking
588:
589: if ($width > $this->getSourceWidth() || $height > $this->getSourceHeight()) {
590: $width = $this->getSourceWidth();
591: $height = $this->getSourceHeight();
592: }
593: }
594:
595: $this->source_x = 0;
596: $this->source_y = 0;
597:
598: $this->dest_w = $width;
599: $this->dest_h = $height;
600:
601: $this->source_w = $this->getSourceWidth();
602: $this->source_h = $this->getSourceHeight();
603:
604: return $this;
605: }
606:
607: /**
608: * Crops image according to the given width, height and crop position
609: *
610: * @param integer $width
611: * @param integer $height
612: * @param boolean $allow_enlarge
613: * @param integer $position
614: * @return static
615: */
616: public function crop($width, $height, $allow_enlarge = false, $position = self::CROPCENTER)
617: {
618: if (!$allow_enlarge) {
619: // this logic is slightly different to resize(),
620: // it will only reset dimensions to the original
621: // if that particular dimenstion is larger
622:
623: if ($width > $this->getSourceWidth()) {
624: $width = $this->getSourceWidth();
625: }
626:
627: if ($height > $this->getSourceHeight()) {
628: $height = $this->getSourceHeight();
629: }
630: }
631:
632: $ratio_source = $this->getSourceWidth() / $this->getSourceHeight();
633: $ratio_dest = $width / $height;
634:
635: if ($ratio_dest < $ratio_source) {
636: $this->resizeToHeight($height, $allow_enlarge);
637:
638: $excess_width = (int) round(($this->getDestWidth() - $width) * $this->getSourceWidth() / $this->getDestWidth());
639:
640: $this->source_w = $this->getSourceWidth() - $excess_width;
641: $this->source_x = $this->getCropPosition($excess_width, $position);
642:
643: $this->dest_w = $width;
644: } else {
645: $this->resizeToWidth($width, $allow_enlarge);
646:
647: $excess_height = (int) round(($this->getDestHeight() - $height) * $this->getSourceHeight() / $this->getDestHeight());
648:
649: $this->source_h = $this->getSourceHeight() - $excess_height;
650: $this->source_y = $this->getCropPosition($excess_height, $position);
651:
652: $this->dest_h = $height;
653: }
654:
655: return $this;
656: }
657:
658: /**
659: * Crops image according to the given width, height, x and y
660: *
661: * @param integer $width
662: * @param integer $height
663: * @param integer $x
664: * @param integer $y
665: * @return static
666: */
667: public function freecrop($width, $height, $x = false, $y = false)
668: {
669: if ($x === false || $y === false) {
670: return $this->crop($width, $height);
671: }
672: $this->source_x = $x;
673: $this->source_y = $y;
674: if ($width > $this->getSourceWidth() - $x) {
675: $this->source_w = $this->getSourceWidth() - $x;
676: } else {
677: $this->source_w = $width;
678: }
679:
680: if ($height > $this->getSourceHeight() - $y) {
681: $this->source_h = $this->getSourceHeight() - $y;
682: } else {
683: $this->source_h = $height;
684: }
685:
686: $this->dest_w = $width;
687: $this->dest_h = $height;
688:
689: return $this;
690: }
691:
692: /**
693: * Gets source width
694: *
695: * @return integer
696: */
697: public function getSourceWidth()
698: {
699: return $this->original_w;
700: }
701:
702: /**
703: * Gets source height
704: *
705: * @return integer
706: */
707: public function getSourceHeight()
708: {
709: return $this->original_h;
710: }
711:
712: /**
713: * Gets width of the destination image
714: *
715: * @return integer
716: */
717: public function getDestWidth()
718: {
719: return $this->dest_w;
720: }
721:
722: /**
723: * Gets height of the destination image
724: * @return integer
725: */
726: public function getDestHeight()
727: {
728: return $this->dest_h;
729: }
730:
731: /**
732: * Gets crop position (X or Y) according to the given position
733: *
734: * @param integer $expectedSize
735: * @param integer $position
736: * @return integer
737: */
738: protected function getCropPosition($expectedSize, $position = self::CROPCENTER)
739: {
740: $size = 0;
741: switch ($position) {
742: case self::CROPBOTTOM:
743: case self::CROPRIGHT:
744: $size = $expectedSize;
745: break;
746: case self::CROPCENTER:
747: case self::CROPCENTRE:
748: $size = $expectedSize / 2;
749: break;
750: case self::CROPTOPCENTER:
751: $size = $expectedSize / 4;
752: break;
753: }
754: return (int) round($size);
755: }
756:
757: /**
758: * Enable or not the gamma color correction on the image, enabled by default
759: *
760: * @param bool $enable
761: * @return static
762: */
763: public function gamma($enable = false)
764: {
765: $this->gamma_correct = $enable;
766:
767: return $this;
768: }
769: }
770: