1: | <?php |
2: | |
3: | namespace Gumlet; |
4: | |
5: | use Exception; |
6: | |
7: | |
8: | |
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: | |
57: | |
58: | |
59: | |
60: | |
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: | |
74: | |
75: | |
76: | |
77: | |
78: | public function addFilter(callable $filter) |
79: | { |
80: | $this->filters[] = $filter; |
81: | return $this; |
82: | } |
83: | |
84: | |
85: | |
86: | |
87: | |
88: | |
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: | |
99: | |
100: | |
101: | |
102: | |
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: | |
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: | |
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: | |
211: | |
212: | |
213: | |
214: | |
215: | |
216: | |
217: | |
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: | |
408: | |
409: | |
410: | |
411: | |
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: | |
428: | |
429: | |
430: | |
431: | public function __toString() |
432: | { |
433: | return $this->getImageAsString(); |
434: | } |
435: | |
436: | |
437: | |
438: | |
439: | |
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: | |
452: | |
453: | |
454: | |
455: | |
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: | |
476: | |
477: | |
478: | |
479: | |
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: | |
500: | |
501: | |
502: | |
503: | |
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: | |
517: | |
518: | |
519: | |
520: | |
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: | |
534: | |
535: | |
536: | |
537: | |
538: | |
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: | |
560: | |
561: | |
562: | |
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: | |
576: | |
577: | |
578: | |
579: | |
580: | |
581: | |
582: | public function resize($width, $height, $allow_enlarge = false) |
583: | { |
584: | if (!$allow_enlarge) { |
585: | |
586: | |
587: | |
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: | |
609: | |
610: | |
611: | |
612: | |
613: | |
614: | |
615: | |
616: | public function crop($width, $height, $allow_enlarge = false, $position = self::CROPCENTER) |
617: | { |
618: | if (!$allow_enlarge) { |
619: | |
620: | |
621: | |
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: | |
660: | |
661: | |
662: | |
663: | |
664: | |
665: | |
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: | |
694: | |
695: | |
696: | |
697: | public function getSourceWidth() |
698: | { |
699: | return $this->original_w; |
700: | } |
701: | |
702: | |
703: | |
704: | |
705: | |
706: | |
707: | public function getSourceHeight() |
708: | { |
709: | return $this->original_h; |
710: | } |
711: | |
712: | |
713: | |
714: | |
715: | |
716: | |
717: | public function getDestWidth() |
718: | { |
719: | return $this->dest_w; |
720: | } |
721: | |
722: | |
723: | |
724: | |
725: | |
726: | public function getDestHeight() |
727: | { |
728: | return $this->dest_h; |
729: | } |
730: | |
731: | |
732: | |
733: | |
734: | |
735: | |
736: | |
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: | |
759: | |
760: | |
761: | |
762: | |
763: | public function gamma($enable = false) |
764: | { |
765: | $this->gamma_correct = $enable; |
766: | |
767: | return $this; |
768: | } |
769: | } |
770: | |