1 <?php
2
3 4 5 6 7 8 9 10 11 12
13
14 namespace Slimore\Image;
15
16 17 18 19 20 21
22
23 class Gd
24 {
25 26 27
28 const THUMB_EQUAL_RATIO = 0;
29 const THUMB_CENTER_CENTER = 1;
30 const THUMB_LEFT_TOP = 2;
31
32 33 34
35 const CROP_TOP_LEFT = 'tl';
36 const CROP_TOP_CENTER = 'tc';
37 const CROP_TOP_RIGHT = 'tr';
38 const CROP_CENTER_LEFT = 'cl';
39 const CROP_CENTER_CENTER = 'center';
40 const CROP_CENTER_RIGHT = 'cr';
41 const CROP_BOTTOM_LEFT = 'bl';
42 const CROP_BOTTOM_CENTER = 'bc';
43 const CROP_BOTTOM_RIGHT = 'br';
44
45 46 47
48 const POS_TOP_LEFT = 'tl';
49 const POS_TOP_CENTER = 'tc';
50 const POS_TOP_RIGHT = 'tr';
51 const POS_CENTER_LEFT = 'cl';
52 const POS_CENTER_CENTER = 'center';
53 const POS_CENTER_RIGHT = 'cr';
54 const POS_BOTTOM_LEFT = 'bl';
55 const POS_BOTTOM_CENTER = 'bc';
56 const POS_BOTTOM_RIGHT = 'br';
57 const POS_RAND = 0;
58 const POS_DEFAULT = 0;
59
60 61 62
63 public $source;
64
65 66 67
68 protected $fontFile;
69
70 71 72
73 protected $newImage;
74
75 76 77
78 protected $sourceImage;
79
80 81 82
83 protected $types = ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp'];
84
85 86 87 88 89
90
91 public function __construct($src = '')
92 {
93 if (! extension_loaded ( 'gd' )) {
94 throw new \RuntimeException('GD2 extension not loaded.');
95 }
96
97 if ( !empty($src) && file_exists($src))
98 {
99 $this->source($src);
100 }
101 }
102
103 104 105 106 107 108
109
110 public function getGDInfo($key = null)
111 {
112 $gdInfo = gd_info();
113
114 return ($key) ? $gdInfo[$key] : $gdInfo;
115 }
116
117 118 119 120 121 122
123
124 public function getImageInfo($image)
125 {
126 if ( empty($image) || !file_exists($image))
127 {
128 throw new \InvalidArgumentException('Image file not found.');
129 }
130
131 $pathInfo = pathinfo($image);
132 $info = getimagesize($image);
133
134 if ( !in_array($pathInfo['extension'], $this->types) )
135 {
136 throw new \InvalidArgumentException('Unsupported image file extension.');
137 }
138
139 $info['width'] = $info[0];
140 $info['height'] = $info[1];
141 $info['ext'] = $info['type'] = $pathInfo['extension'];
142 $info['size'] = filesize($image);
143 $info['dir'] = $pathInfo['dirname'];
144 $info['path'] = str_replace('/', DIRECTORY_SEPARATOR, $image);
145 $info['fullname'] = $pathInfo['basename'];
146 $info['filename'] = $pathInfo['filename'];
147 $info['type'] = ($info['type'] == 'jpg') ? 'jpeg' : $info['type'];
148
149 return $info;
150 }
151
152 153 154 155 156
157
158 public function ext()
159 {
160 return $this->source['ext'];
161 }
162
163 164 165 166 167
168
169 public function width()
170 {
171 return $this->source['width'];
172 }
173
174 175 176 177 178
179
180 public function height()
181 {
182 return $this->source['height'];
183 }
184
185 186 187 188 189
190
191 public function fileSize()
192 {
193 return $this->source['size'];
194 }
195
196 197 198 199 200 201
202
203 public function getType($type)
204 {
205 return (!$type) ? $this->source['type'] : (($type === 'jpg') ? 'jpeg' : $type);
206 }
207
208 209 210 211 212 213
214
215 public function source($src)
216 {
217 $this->source = $this->getImageInfo($src);
218
219 $type = $this->source['type'];
220 $createFrom = 'ImageCreateFrom' . $type;
221 $this->sourceImage = $createFrom($src);
222
223 return $this;
224 }
225
226 227 228 229 230 231 232
233
234 public function create($width, $height, $type = null)
235 {
236 if ( !is_numeric($width) ) {
237 throw new \InvalidArgumentException('Image create failed, width must be numeric');
238 }
239
240 if ( !is_numeric($height) ) {
241 throw new \InvalidArgumentException('Image create failed, height must be numeric');
242 }
243
244 $type = $this->getType($type);
245
246 if ($type !== 'gif' && function_exists('imagecreatetruecolor'))
247 {
248 $newImage = imagecreatetruecolor($width, $height);
249 }
250 else
251 {
252 $newImage = imagecreate($width, $height);
253 }
254
255 imagealphablending($newImage, true);
256
257 $transparent = imagecolorallocatealpha($newImage, 255, 255, 255, 0);
258
259 imagefilledrectangle($newImage, 0, 0, imagesx($newImage), imagesy($newImage), $transparent);
260 imagefill($newImage, 0, 0, $transparent);
261
262 imagesavealpha($newImage, true);
263
264 $this->newImage = $newImage;
265
266 return $this;
267 }
268
269 270 271 272 273 274
275
276 public function createFrom($image)
277 {
278 $type = pathinfo($image, PATHINFO_EXTENSION);
279 $type = ($type === 'jpg') ? 'jpeg' : $type;
280 $createFrom = 'ImageCreateFrom' . $type;
281
282 return $createFrom($image);
283 }
284
285 286 287 288 289 290 291 292
293
294 public function crop($width, $height, $mode = 'tl')
295 {
296 if ( !is_numeric($width) ) {
297 throw new \InvalidArgumentException('$width must be numeric');
298 }
299
300 if ( !is_numeric($height) ) {
301 throw new \InvalidArgumentException('$height must be numeric');
302 }
303
304 if ( $this->newImage )
305 {
306 $this->sourceImage = $this->newImage;
307 }
308
309 $oldWidth = ($this->newImage) ? imagesx($this->newImage) : $this->source['width'];
310 $oldHeight = ($this->newImage) ? imagesy($this->newImage) : $this->source['height'];
311
312 $this->create($width, $height);
313
314 $startX = $startY = 0;
315 $cropWidth = $sourceWidth = $width;
316 $cropHeight = $sourceHeight = $height;
317
318 if ( is_array($mode) )
319 {
320 $startX = $mode[0];
321 $startY = $mode[1];
322 }
323
324 if ($mode === self::CROP_TOP_CENTER)
325 {
326 $startX = ($oldWidth - $cropWidth) / 2;
327 }
328 else if ($mode === self::CROP_TOP_RIGHT)
329 {
330 $startX = $oldWidth - $cropWidth;
331 }
332 else if ($mode === self::CROP_CENTER_LEFT)
333 {
334 $startY = ($oldHeight - $cropHeight) / 2;
335 }
336 else if ($mode === self::CROP_CENTER_CENTER)
337 {
338 $startX = ($oldWidth - $cropWidth) / 2;
339 $startY = ($oldHeight - $cropHeight) / 2;
340 }
341 else if ($mode === self::CROP_CENTER_RIGHT)
342 {
343 $startX = $oldWidth - $cropWidth;
344 $startY = ($oldHeight - $cropHeight) / 2;
345 }
346 else if ($mode === self::CROP_BOTTOM_LEFT)
347 {
348 $startY = $oldHeight - $cropHeight;
349 }
350 else if ($mode === self::CROP_BOTTOM_CENTER)
351 {
352 $startX = ($oldWidth - $cropWidth) / 2;
353 $startY = $oldHeight - $cropHeight;
354 }
355 else if ($mode === self::CROP_BOTTOM_RIGHT)
356 {
357 $startX = $oldWidth - $cropWidth;
358 $startY = $oldHeight - $cropHeight;
359 }
360 else
361 {
362 }
363
364 imagecopyresampled(
365 $this->newImage,
366 $this->sourceImage,
367 0, 0, $startX, $startY,
368 $cropWidth, $cropHeight, $sourceWidth, $sourceHeight);
369
370 return $this;
371 }
372
373 374 375 376 377 378 379 380 381
382
383 public function thumb($width, $height, $mode = 0, $amplify = false)
384 {
385 if ( !is_numeric($width) ) {
386 throw new \InvalidArgumentException('$width must be numeric');
387 }
388
389 if ( !is_numeric($height) ) {
390 throw new \InvalidArgumentException('$height must be numeric');
391 }
392
393 if ( $this->newImage )
394 {
395 $this->sourceImage = $this->newImage;
396 }
397
398 $oldWidth = ($this->newImage) ? imagesx($this->newImage) : $this->source['width'];
399 $oldHeight = ($this->newImage) ? imagesy($this->newImage) : $this->source['height'];
400
401 $this->create($width, $height);
402
403 if ($oldWidth < $width && $oldHeight < $height && !$amplify)
404 {
405 return false;
406 }
407
408 $thumbWidth = $width;
409 $thumbHeight = $height;
410 $startX = $startY = 0;
411
412 if ($mode === self::THUMB_EQUAL_RATIO)
413 {
414 $scale = min($width / $oldWidth, $height / $oldHeight);
415 $thumbWidth = (int) ($oldWidth * $scale);
416 $thumbHeight = (int) ($oldHeight * $scale);
417 $sourceWidth = $oldWidth;
418 $sourceHeight = $oldHeight;
419 }
420 else if ($mode === self::THUMB_CENTER_CENTER)
421 {
422 $scale1 = round($width / $height, 2);
423 $scale2 = round($oldWidth / $oldHeight, 2);
424
425 if ($scale1 > $scale2)
426 {
427 $sourceWidth = $oldWidth;
428 $sourceHeight = round($oldWidth / $scale1, 2);
429 $startY = ($oldHeight - $sourceHeight) / 2;
430 }
431 else
432 {
433 $sourceWidth = round($oldHeight * $scale1, 2);
434 $sourceHeight = $oldHeight;
435 $startX = ($oldWidth - $sourceWidth) / 2;
436 }
437 }
438 else if ($mode === self::THUMB_LEFT_TOP)
439 {
440 $scale1 = round($width / $height, 2);
441 $scale2 = round($oldWidth / $oldHeight, 2);
442
443 if ($scale1 > $scale2)
444 {
445 $sourceHeight = round($oldWidth / $scale1, 2);
446 $sourceWidth = $oldWidth;
447 }
448 else
449 {
450 $sourceWidth = round($oldHeight * $scale1, 2);
451 $sourceHeight = $oldHeight;
452 }
453 }
454
455 imagecopyresampled(
456 $this->newImage,
457 $this->sourceImage,
458 0, 0, $startX, $startY,
459 $thumbWidth, $thumbHeight, $sourceWidth, $sourceHeight);
460
461 return $this;
462 }
463
464 465 466 467 468 469 470 471 472
473
474 public function resize($width, $height, $x = 0, $y = 0)
475 {
476 if ( !$this->newImage )
477 {
478 $this->create($width, $height);
479 }
480
481 if ( !is_numeric($width) ) {
482 throw new \InvalidArgumentException('$width must be numeric');
483 }
484
485 if ( !is_numeric($height) ) {
486 throw new \InvalidArgumentException('$height must be numeric');
487 }
488
489 $type = $this->source['type'];
490
491 imagecopyresampled(
492 $this->newImage,
493 $this->sourceImage,
494 0, 0,
495 $x, $y,
496 $width, $height,
497 $this->source['width'], $this->source['height']);
498
499 return $this;
500 }
501
502 503 504 505 506 507
508
509 public function resizePercent($percent = 50)
510 {
511 if ( $percent < 1)
512 {
513 throw new \InvalidArgumentException('percent must be >= 1');
514 }
515
516 $this->resize($this->source['width'] * ($percent / 100), $this->source['height'] * ($percent / 100));
517
518 return $this;
519 }
520
521 522 523 524 525 526 527 528
529
530 public function watermark($water, $pos = 0, $tile = false)
531 {
532 $waterInfo = $this->getImageInfo($water);
533
534 if ( empty($waterInfo['width']) || empty($waterInfo['height']) )
535 {
536 throw new \InvalidArgumentException('Get watermark file information is failed.');
537 }
538
539 $this->waterImage = $this->createFrom($water);
540
541 if (!$this->newImage || !is_resource($this->newImage))
542 {
543 $this->newImage = $this->sourceImage;
544 $sourceWidth = $this->source['width'];
545 $sourceHeight = $this->source['height'];
546 }
547 else
548 {
549 $sourceWidth = imagesx($this->newImage);
550 $sourceHeight = imagesy($this->newImage);
551 }
552
553 $waterWidth = ($waterInfo['width'] > $sourceWidth) ? $sourceWidth : $waterInfo['width'];
554 $waterHeight = ($waterInfo['height'] > $sourceHeight) ? $sourceHeight : $waterInfo['height'];
555
556 if ($tile)
557 {
558 imagealphablending($this->waterImage, true);
559 imagesettile($this->newImage, $this->waterImage);
560 imagefilledrectangle($this->newImage, 0, 0, $sourceWidth, $sourceHeight, IMG_COLOR_TILED);
561 }
562 else
563 {
564 $position = $this->position($pos, $sourceWidth, $sourceHeight, $waterWidth, $waterHeight);
565
566 imagecopy($this->newImage, $this->waterImage, $position['x'], $position['y'], 0, 0, $waterWidth, $waterHeight);
567 }
568
569 return $this;
570 }
571
572 573 574 575 576 577 578
579
580 public function watermarkTile($water, $pos = 0)
581 {
582 return $this->watermark($water, $pos, true);
583 }
584
585 586 587 588 589 590 591 592 593 594 595
596
597 public function watermarkText($text, $pos = 0, $fontSize = 14, array $color = null, $font = null, $shadow = true)
598 {
599 if (!$color)
600 {
601 $color = [255, 255, 255, 0, 0, 0];
602 }
603
604 $font = (!$font) ? $this->fontFile : $font;
605
606 if (!$this->newImage || !is_resource($this->newImage))
607 {
608 $this->newImage = $this->sourceImage;
609 $sourceWidth = $this->source['width'];
610 $sourceHeight = $this->source['height'];
611 }
612 else
613 {
614 $sourceWidth = imagesx($this->newImage);
615 $sourceHeight = imagesy($this->newImage);
616 }
617
618 $textImage = imagecreatetruecolor($sourceWidth, $sourceHeight);
619 $textColor = imagecolorallocate($textImage, $color[0], $color[1], $color[2]);
620 $shadowColor = imagecolorallocate($textImage, $color[3], $color[4], $color[5]);
621
622
623 $size = imagettfbbox($fontSize, 0, $font, $text);
624 $textWidth = $size[4];
625 $textHeight = abs($size[7]);
626
627 $position = $this->position($pos, $sourceWidth, $sourceHeight, $textWidth + 4, $textHeight, true, $fontSize);
628
629 $posX = $position['x'];
630 $posY = $position['y'];
631
632 imagealphablending($textImage, true);
633 imagesavealpha($textImage, true);
634
635 imagecopymerge($textImage, $this->newImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, 100);
636
637 if ($shadow)
638 {
639 imagettftext($textImage, $fontSize, 0, $posX + 1, $posY + 1, $shadowColor, $font, $text);
640 }
641
642 imagettftext($textImage, $fontSize, 0, $posX, $posY, $textColor, $font, $text);
643
644 $this->newImage = $textImage;
645
646 return $this;
647 }
648
649 650 651 652 653 654 655 656 657 658 659 660
661
662 private function position($pos, $oldWidth, $oldHeight, $waterWidth, $waterHeight, $isText = false, $fontSize = 14)
663 {
664 if ( is_array($pos) )
665 {
666 return [
667 'x' => $pos[0],
668 'y' => $pos[1]
669 ];
670 }
671
672 if ($pos === self::POS_TOP_LEFT)
673 {
674 $posX = 0;
675 $posY = ($isText) ? $waterHeight : 0;
676 }
677 elseif ($pos === self::POS_TOP_CENTER)
678 {
679 $posX = ($oldWidth - $waterWidth) / 2;
680 $posY = ($isText) ? $waterHeight : 0;
681 }
682 elseif ($pos === self::POS_TOP_RIGHT)
683 {
684 $posX = $oldWidth - $waterWidth;
685 $posY = ($isText) ? $waterHeight : 0;
686 }
687 elseif ($pos === self::POS_CENTER_LEFT)
688 {
689 $posX = 0;
690 $posY = ($isText) ? (($oldHeight - $waterHeight) / 2) + $fontSize : ($oldHeight - $waterHeight) / 2;
691 }
692 elseif ($pos === self::POS_CENTER_CENTER)
693 {
694 $posX = ($oldWidth - $waterWidth) / 2;
695 $posY = ($isText) ? (($oldHeight - $waterHeight) / 2) + $fontSize : ($oldHeight - $waterHeight) / 2;
696 }
697 elseif ($pos === self::POS_CENTER_RIGHT)
698 {
699 $posX = $oldWidth - $waterWidth;
700 $posY = ($isText) ? (($oldHeight - $waterHeight) / 2) + $fontSize : ($oldHeight - $waterHeight) / 2;
701 }
702 elseif ($pos === self::POS_BOTTOM_LEFT)
703 {
704 $posX = 0;
705 $posY = ($isText) ? ($oldHeight - $waterHeight) + $fontSize : $oldHeight - $waterHeight;
706 }
707 elseif ($pos === self::POS_BOTTOM_CENTER)
708 {
709 $posX = ($oldWidth - $waterWidth) / 2;
710 $posY = ($isText) ? ($oldHeight - $waterHeight) + $fontSize : $oldHeight - $waterHeight;
711 }
712 elseif ($pos === self::POS_BOTTOM_RIGHT)
713 {
714 $posX = $oldWidth - $waterWidth;
715 $posY = ($isText) ? ($oldHeight - $waterHeight) + $fontSize : $oldHeight - $waterHeight;
716 }
717 else
718 {
719 $posX = rand(0, ($oldWidth - $waterWidth));
720 $posY = rand(0, ($oldHeight - $waterHeight));
721 }
722
723 return [
724 "x" => $posX,
725 "y" => $posY
726 ];
727 }
728
729 730 731 732 733 734 735
736
737 public function setFontFile($fontFile)
738 {
739 if (!file_exists($fontFile))
740 {
741 throw new \InvalidArgumentException('font file ' .$fontFile . ' not found.');
742 }
743
744 $this->fontFile = $fontFile;
745
746 return $this;
747 }
748
749 750 751 752 753 754
755
756 public function display($type = 'jpeg')
757 {
758 $type = $this->getType($type);
759
760 header('Content-Type: image/' . $type);
761
762 if ($type === 'jpeg')
763 {
764 imageinterlace($this->newImage, true);
765 }
766
767 $imageFunc = 'image' . $type;
768 $imageFunc($this->newImage);
769
770 $this->destroyAll();
771
772 return $this;
773 }
774
775 776 777 778 779 780 781 782
783
784 public function save($saveName, $quality = 80)
785 {
786 $type = $this->getType(pathinfo($saveName, PATHINFO_EXTENSION));
787 $imageFunc = 'image' . $type;
788 $errorMessage = 'Image saved is failed! Check the directory is can write?';
789
790 if ($type === 'jpeg')
791 {
792 imageinterlace($this->newImage, true);
793
794 if ( !$imageFunc($this->newImage, $saveName, $quality) )
795 {
796 throw new \ErrorException($errorMessage);
797 }
798 }
799 else
800 {
801 if (!$imageFunc($this->newImage, $saveName))
802 {
803 throw new \ErrorException($errorMessage);
804 }
805 }
806
807 $this->destroyAll();
808
809 return $this;
810 }
811
812 813 814 815 816 817
818
819 public function dataUrl($type = 'jpeg')
820 {
821 $type = $this->getType($type);
822 $imageFunc = 'image' . $type;
823
824 ob_start();
825
826 $imageFunc($this->newImage);
827
828 $data = ob_get_contents();
829
830 ob_end_clean();
831
832 $this->destroyAll();
833
834 $dataUrl = 'data:image/'. $type . ';base64,' . base64_encode($data);
835
836 return $dataUrl;
837 }
838
839 840 841 842 843 844
845
846 public function destroy($resource)
847 {
848 if ( is_resource($resource) )
849 {
850 imagedestroy($resource);
851 }
852 }
853
854 855 856 857 858
859
860 public function destroyAll()
861 {
862 if ($this->newImage)
863 {
864 $this->destroy($this->newImage);
865 }
866
867 if ($this->sourceImage)
868 {
869 $this->destroy($this->sourceImage);
870 }
871 }
872
873 874 875 876 877
878
879 public function __destruct()
880 {
881 $this->destroyAll();
882 }
883 }