Issues (84)

src/ImageHelper.php (2 issues)

1
<?php
2
/**
3
 * File containing the {@link ImageHelper} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage ImageHelper
7
 * @see ImageHelper
8
 */
9
10
declare(strict_types=1);
11
12
namespace AppUtils;
13
14
use AppUtils\ClassHelper\ClassNotExistsException;
15
use AppUtils\ClassHelper\ClassNotImplementsException;
16
use AppUtils\ImageHelper\ComputedTextSize;
17
use AppUtils\ImageHelper\ImageTrimmer;
18
use AppUtils\RGBAColor\ColorException;
19
use AppUtils\RGBAColor\ColorFactory;
20
use GdImage;
21
use JsonException;
22
23
/**
24
 * Image helper class that can be used to transform images,
25
 * and retrieve information about them.
26
 * 
27
 * @package Application Utils
28
 * @subpackage ImageHelper
29
 * @author Sebastian Mordziol <[email protected]>
30
 */
31
class ImageHelper
32
{
33
    public const ERROR_CANNOT_CREATE_IMAGE_CANVAS = 513001;
34
    public const ERROR_IMAGE_FILE_DOES_NOT_EXIST = 513002;
35
    public const ERROR_CANNOT_GET_IMAGE_SIZE = 513003;
36
    public const ERROR_UNSUPPORTED_IMAGE_TYPE = 513004;
37
    public const ERROR_FAILED_TO_CREATE_NEW_IMAGE = 513005;
38
    public const ERROR_SAVE_NO_IMAGE_CREATED = 513006;
39
    public const ERROR_CANNOT_WRITE_NEW_IMAGE_FILE = 513007;
40
    public const ERROR_CREATED_AN_EMPTY_FILE = 513008;
41
    public const ERROR_QUALITY_VALUE_BELOW_ZERO = 513009;
42
    public const ERROR_QUALITY_ABOVE_ONE_HUNDRED = 513010;
43
    public const ERROR_CANNOT_CREATE_IMAGE_OBJECT = 513011;
44
    public const ERROR_CANNOT_COPY_RESAMPLED_IMAGE_DATA = 513012;
45
    public const ERROR_HEADERS_ALREADY_SENT = 513013;
46
    public const ERROR_CANNOT_READ_SVG_IMAGE = 513014;
47
    public const ERROR_SVG_SOURCE_VIEWBOX_MISSING = 513015;
48
    public const ERROR_SVG_VIEWBOX_INVALID = 513016;
49
    public const ERROR_NOT_A_RESOURCE = 513017;
50
    public const ERROR_INVALID_STREAM_IMAGE_TYPE = 513018;
51
    public const ERROR_NO_TRUE_TYPE_FONT_SET = 513019;
52
    public const ERROR_POSITION_OUT_OF_BOUNDS = 513020;
53
    public const ERROR_IMAGE_CREATION_FAILED = 513021;
54
    public const ERROR_CANNOT_CREATE_IMAGE_CROP = 513023;
55
    public const ERROR_GD_LIBRARY_NOT_INSTALLED = 513024;
56
    public const ERROR_UNEXPECTED_COLOR_VALUE = 513025;
57
    public const ERROR_HASH_NO_IMAGE_LOADED = 513026;
58
59
    protected string $file = '';
60
    protected ImageHelper_Size $info;
61
    protected ?string $type = null;
62
    protected int $width;
63
    protected int $height;
64
    protected int $newWidth = 0;
65
    protected int $newHeight = 0;
66
    protected int $quality = 85;
67
    protected bool $alpha = false;
68
    protected string $TTFFile = '';
69
70
    /**
71
     * @var array<string,int>
72
     */
73
    protected array $colors = array();
74
75
    /**
76
    * @var resource|NULL
77
    */
78
    protected $newImage;
79
80
   /**
81
    * @var resource
82
    */
83
    protected $sourceImage;
84
85
    /**
86
     * @var array<string,string>
87
     */
88
    protected static array $imageTypes = array(
89
        'png' => 'png',
90
        'jpg' => 'jpeg',
91
        'jpeg' => 'jpeg',
92
        'gif' => 'gif',
93
        'svg' => 'svg'
94
    );
95
96
    /**
97
     * @var array<string,mixed>
98
     */
99
    protected static array $config = array(
100
        'auto-memory-adjustment' => true
101
    );
102
103
    /**
104
     * @var string[]
105
     */
106
    protected static array $streamTypes = array(
107
        'jpeg',
108
        'png',
109
        'gif'
110
    );
111
112
    /**
113
     * @param string|null $sourceFile
114
     * @param resource|GdImage|null $resource
115
     * @param string|null $type The image type, e.g. "png", "jpeg".
116
     *
117
     * @throws ClassNotExistsException
118
     * @throws ClassNotImplementsException
119
     * @throws ImageHelper_Exception
120
     *
121
     * @see ImageHelper::ERROR_GD_LIBRARY_NOT_INSTALLED
122
     */
123
    public function __construct(?string $sourceFile=null, $resource=null, ?string $type=null)
124
    {
125
        // ensure that the GD library is installed
126
        if(!function_exists('imagecreate')) 
127
        {
128
            throw new ImageHelper_Exception(
129
                'The PHP GD extension is not installed or not enabled.',
130
                null,
131
                self::ERROR_GD_LIBRARY_NOT_INSTALLED
132
            );
133
        }
134
        
135
        if(is_resource($resource) || $resource instanceof GdImage)
136
        {
137
            $this->sourceImage = $resource;
138
            $this->type = $type;
139
            $this->info = self::getImageSize($resource);
140
        } 
141
        else 
142
        {
143
            $this->file = $sourceFile;
144
            if (!file_exists($this->file)) {
145
                throw new ImageHelper_Exception(
146
                    'Image file does not exist',
147
                    sprintf(
148
                        'Could not find the image file on disk at location [%s]',
149
                        $this->file
150
                    ),
151
                    self::ERROR_IMAGE_FILE_DOES_NOT_EXIST
152
                );
153
            }
154
    
155
            $type = self::getFileImageType($this->file);
156
            if ($type === null) {
157
                throw new ImageHelper_Exception(
158
                    'Error opening image',
159
                    'Not a valid supported image type for image ' . $this->file,
160
                    self::ERROR_UNSUPPORTED_IMAGE_TYPE
161
                );
162
            }
163
164
            $this->type = $type;
165
            $this->info = self::getImageSize($this->file);
166
167
            if(!$this->isVector()) 
168
            {
169
                $method = 'imagecreatefrom' . $this->type;
170
                $this->sourceImage = $method($this->file);
171
                if (!$this->sourceImage) {
172
                    throw new ImageHelper_Exception(
173
                        'Error creating new image',
174
                        $method . ' failed',
175
                        self::ERROR_FAILED_TO_CREATE_NEW_IMAGE
176
                    );
177
                }
178
                
179
                imagesavealpha($this->sourceImage, true);
180
            }
181
        }
182
183
        $this->width = $this->info->getWidth();
184
        $this->height = $this->info->getHeight();
185
186
        if(!$this->isVector()) {
187
            $this->setNewImage($this->duplicateImage($this->sourceImage));
188
        }
189
    }
190
191
   /**
192
    * Factory method: creates a new helper with a blank image.
193
    * 
194
    * @param integer $width
195
    * @param integer $height
196
    * @param string $type The target file type when saving
197
    * @return ImageHelper
198
    * @throws ImageHelper_Exception
199
    *
200
    * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_OBJECT
201
    */
202
    public static function createNew(int $width, int $height, string $type='png') : self
203
    {
204
        $img = imagecreatetruecolor($width, $height);
205
        if($img !== false) {
206
            return self::createFromResource($img, $type);
207
        }
208
        
209
        throw new ImageHelper_Exception(
210
            'Could not create new true color image.',
211
            null,
212
            self::ERROR_CANNOT_CREATE_IMAGE_OBJECT
213
        );
214
    }
215
216
    /**
217
     * Factory method: creates an image helper from an
218
     * existing image resource.
219
     *
220
     * Note: while the resource is type independent, the
221
     * type parameter is required for some methods, as well
222
     * as to be able to save the image.
223
     *
224
     * @param resource $resource
225
     * @param string|null $type The target image type, e.g. "jpeg", "png", etc.
226
     * @return ImageHelper
227
     * @throws ImageHelper_Exception
228
     */
229
    public static function createFromResource($resource, ?string $type=null) : ImageHelper
230
    {
231
        self::requireResource($resource);
232
        
233
        return new ImageHelper(null, $resource, $type);
234
    }
235
236
    /**
237
     * Factory method: creates an image helper from an
238
     * image file on disk.
239
     *
240
     * @param string $file
241
     * @return ImageHelper
242
     *
243
     * @throws ClassNotExistsException
244
     * @throws ClassNotImplementsException
245
     * @throws ImageHelper_Exception
246
     */
247
    public static function createFromFile(string $file) : ImageHelper
248
    {
249
        return new ImageHelper($file, null, self::getFileImageType($file));
250
    }
251
    
252
   /**
253
    * Sets a global image helper configuration value. Available
254
    * configuration settings are:
255
    * 
256
    * <ul>
257
    * <li><code>auto-memory-adjustment</code> <i>boolean</i> Whether to try and adjust the memory limit automatically so there will be enough to load/process the target image.</li>
258
    * </ul>
259
    * 
260
    * @param string $name
261
    * @param mixed|NULL $value
262
    */
263
    public static function setConfig(string $name, $value) : void
264
    {
265
        if(isset(self::$config[$name])) {
266
            self::$config[$name] = $value;
267
        }
268
    }
269
270
   /**
271
    * Shorthand for setting the automatic memory adjustment
272
    * global configuration setting.
273
    *
274
    * @param bool $enabled
275
    * @return void
276
    */
277
    public static function setAutoMemoryAdjustment(bool $enabled=true) : void
278
    {
279
        self::setConfig('auto-memory-adjustment', $enabled);
280
    }
281
282
    /**
283
     * Duplicates an image resource.
284
     * @param resource $img
285
     * @return resource
286
     * @throws ImageHelper_Exception
287
     */
288
    protected function duplicateImage($img)
289
    {
290
        self::requireResource($img);
291
        
292
        $width = imagesx($img);
293
        $height = imagesy($img);
294
        $duplicate = $this->createNewImage($width, $height);
295
        imagecopy($duplicate, $img, 0, 0, 0, 0, $width, $height);
296
        return $duplicate;
297
    }
298
299
    /**
300
     * Duplicates the current state of the image into a new
301
     * image helper instance.
302
     *
303
     * @return ImageHelper
304
     * @throws ImageHelper_Exception
305
     */
306
    public function duplicate() : ImageHelper
307
    {
308
        return self::createFromResource($this->duplicateImage($this->newImage));
309
    }
310
311
    /**
312
     * @return $this
313
     * @throws ImageHelper_Exception
314
     */
315
    public function enableAlpha() : self
316
    {
317
        if(!$this->alpha) 
318
        {
319
            self::addAlphaSupport($this->newImage, false);
320
            $this->alpha = true;
321
        }
322
        
323
        return $this;
324
    }
325
326
    /**
327
     * @param int $width
328
     * @param int $height
329
     * @return $this
330
     * @throws ImageHelper_Exception
331
     */
332
    public function resize(int $width, int $height) : self
333
    {
334
        $new = $this->createNewImage($width, $height);
335
        
336
        imagecopy($new, $this->newImage, 0, 0, 0, 0, $width, $height);
337
        
338
        $this->setNewImage($new);
339
        
340
        return $this;
341
    }
342
343
    /**
344
     * @return array{0:int,1:int}
345
     */
346
    public function getNewSize() : array
347
    {
348
        return array($this->newWidth, $this->newHeight);
349
    }
350
    
351
    /**
352
     * Sharpens the image by the specified percentage.
353
     *
354
     * @param int|float $percent
355
     * @return $this
356
     */
357
    public function sharpen($percent=0) : self
358
    {
359
        if($percent <= 0) {
360
            return $this;
361
        }
362
        
363
        // the factor goes from 0 to 64 for sharpening.
364
        $factor = $percent * 64 / 100;
365
        return $this->convolute($factor);
366
    }
367
368
    /**
369
     * @param int|float $percent
370
     * @return $this
371
     */
372
    public function blur($percent=0) : self
373
    {
374
        if($percent <= 0) {
375
            return $this;
376
        }
377
        
378
        // the factor goes from -64 to 0 for blurring.
379
        $factor = ($percent * 64 / 100) * -1;
380
        return $this->convolute($factor);
381
    }
382
383
    /**
384
     * @param int|float $factor
385
     * @return $this
386
     */
387
    protected function convolute($factor) : self
388
    {
389
        // get a value that's equal to 64 - abs( factor )
390
        // ( using min/max to limit the factor to 0 - 64 to not get out of range values )
391
        $val1Adjustment = 64 - min( 64, max( 0, abs( $factor ) ) );
392
        
393
        // the base factor for the "current" pixel depends on if we are blurring or sharpening.
394
        // If we are blurring use 1, if sharpening use 9.
395
        $val1Base = 9;
396
        if( abs( $factor ) !== $factor ) {
397
            $val1Base = 1;
398
        }
399
        
400
        // value for the center/current pixel is:
401
        //  1 + 0 - max blurring
402
        //  1 + 64- minimal blurring
403
        //  9 + 64- minimal sharpening
404
        //  9 + 0 - maximum sharpening
405
        $val1 = $val1Base + $val1Adjustment;
406
        
407
        // the value for the surrounding pixels is either positive or negative depending on if we are blurring or sharpening.
408
        $val2 = -1;
409
        if( abs( $factor ) !== $factor ) {
410
            $val2 = 1;
411
        }
412
        
413
        // setup matrix ..
414
        $matrix = array(
415
            array( $val2, $val2, $val2 ),
416
            array( $val2, $val1, $val2 ),
417
            array( $val2, $val2, $val2 )
418
        );
419
        
420
        // calculate the correct divisor
421
        // actual divisor is equal to "$divisor = $val1 + $val2 * 8;"
422
        // but the following line is more generic
423
        $divisor = array_sum( array_map( 'array_sum', $matrix ) );
424
        
425
        // apply the matrix
426
        imageconvolution( $this->newImage, $matrix, $divisor, 0 );
427
        
428
        return $this;
429
    }
430
    
431
    /**
432
     * Whether the image is an SVG image.
433
     * @return boolean
434
     */
435
    public function isTypeSVG() : bool
436
    {
437
        return $this->type === 'svg';
438
    }
439
    
440
    /**
441
     * Whether the image is a PNG image.
442
     * @return boolean
443
     */
444
    public function isTypePNG() : bool
445
    {
446
        return $this->type === 'png';
447
    }
448
    
449
    /**
450
     * Whether the image is a JPEG image.
451
     * @return boolean
452
     */
453
    public function isTypeJPEG() : bool
454
    {
455
        return $this->type === 'jpeg';
456
    }
457
    
458
    /**
459
     * Whether the image is a vector image.
460
     * @return boolean
461
     */
462
    public function isVector() : bool
463
    {
464
        return $this->isTypeSVG();
465
    }
466
    
467
    /**
468
     * Retrieves the type of the image.
469
     * @return string e.g. "jpeg", "png"
470
     */
471
    public function getType() : string
472
    {
473
        return $this->type;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->type could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
474
    }
475
    
476
    /**
477
     * Calculates the size of the image by the specified width,
478
     * and returns an indexed array with the width and height size.
479
     *
480
     * @param integer $width
481
     * @return ImageHelper_Size
482
     */
483
    public function getSizeByWidth(int $width) : ImageHelper_Size
484
    {
485
        $height = (int)floor(($width * $this->height) / $this->width);
486
        
487
        return new ImageHelper_Size(array(
488
            $width,
489
            $height,
490
            $this->info['bits'],
491
            $this->info['channels']
492
        ));
493
    }
494
    
495
    /**
496
     * Calculates the size of the image by the specified height,
497
     * and returns an indexed array with the width and height size.
498
     *
499
     * @param integer $height
500
     * @return ImageHelper_Size
501
     */
502
    public function getSizeByHeight(int $height) : ImageHelper_Size
503
    {
504
        $width = (int)floor(($height * $this->width) / $this->height);
505
        
506
        return new ImageHelper_Size(array(
507
            $width,
508
            $height,
509
            $this->info['bits'],
510
            $this->info['channels']
511
        ));
512
    }
513
514
    /**
515
     * Resamples the image to a new width, maintaining
516
     * aspect ratio.
517
     *
518
     * @param int $width
519
     * @return ImageHelper
520
     * @throws ImageHelper_Exception
521
     */
522
    public function resampleByWidth(int $width) : ImageHelper
523
    {
524
        $size = $this->getSizeByWidth($width);
525
526
        $this->resampleImage($size->getWidth(), $size->getHeight());
527
        
528
        return $this;
529
    }
530
531
    /**
532
     * Resamples the image by height, and creates a new image file on disk.
533
     *
534
     * @param int $height
535
     * @return ImageHelper
536
     * @throws ImageHelper_Exception
537
     */
538
    public function resampleByHeight(int $height) : ImageHelper
539
    {
540
        $size = $this->getSizeByHeight($height);
541
542
        return $this->resampleImage($size->getWidth(), $size->getHeight());
543
    }
544
545
    /**
546
     * Resamples the image without keeping the aspect ratio.
547
     *
548
     * @param int|null $width
549
     * @param int|null $height
550
     * @return ImageHelper
551
     * @throws ImageHelper_Exception
552
     */
553
    public function resample(?int $width = null, ?int $height = null) : ImageHelper
554
    {
555
        if($this->isVector()) {
556
            return $this;
557
        }
558
        
559
        if ($width === null && $height === null) {
560
            return $this->resampleByWidth($this->width);
561
        }
562
563
        if ($width === null) {
564
            return $this->resampleByHeight($height);
565
        }
566
567
        if ($height === null) {
568
            return $this->resampleByWidth($width);
569
        }
570
571
        return $this->resampleAndCrop($width, $height);
572
    }
573
574
    public function resampleAndCrop(int $width, int $height) : ImageHelper
575
    {
576
        if($this->isVector()) {
577
            return $this;
578
        }
579
580
        if ($this->width <= $this->height) 
581
        {
582
            $this->resampleByWidth($width);
583
        } 
584
        else 
585
        {
586
            $this->resampleByHeight($height);
587
        }
588
        
589
        $newCanvas = $this->createNewImage($width, $height);
590
        
591
        // and now we can add the crop
592
        if (!imagecopy(
593
            $newCanvas,
594
            $this->newImage,
595
            0, // destination X
596
            0, // destination Y
597
            0, // source X
598
            0, // source Y
599
            $width,
600
            $height
601
        )
602
        ) {
603
            throw new ImageHelper_Exception(
604
                'Error creating new image',
605
                'Cannot create crop of the image',
606
                self::ERROR_CANNOT_CREATE_IMAGE_CROP
607
            );
608
        }
609
610
        $this->setNewImage($newCanvas);
611
612
        return $this;
613
    }
614
    
615
    /**
616
     * Configures the specified image resource to make it alpha compatible.
617
     *
618
     * @param resource $canvas
619
     * @param bool $fill Whether to fill the whole canvas with the transparency
620
     * @throws ImageHelper_Exception
621
     */
622
    public static function addAlphaSupport($canvas, bool $fill=true) : void
623
    {
624
        self::requireResource($canvas);
625
        
626
        imagealphablending($canvas,true);
627
        imagesavealpha($canvas, true);
628
629
        if($fill) {
630
            self::fillImageTransparent($canvas);
631
        }
632
    }
633
    
634
    public function isAlpha() : bool
635
    {
636
        return $this->alpha;
637
    }
638
639
    /**
640
     * @param string $targetFile
641
     * @param bool $dispose
642
     * @return $this
643
     * @throws ImageHelper_Exception
644
     */
645
    public function save(string $targetFile, bool $dispose=true) : self
646
    {
647
        if($this->isVector()) {
648
            return $this;
649
        }
650
        
651
        if(!is_resource($this->newImage)) {
652
            throw new ImageHelper_Exception(
653
                'Error creating new image',
654
                'Cannot save an image, no valid image resource was created. You have to call one of the resample methods to create a new image.',
655
                self::ERROR_SAVE_NO_IMAGE_CREATED
656
            );
657
        }
658
659
        if (file_exists($targetFile)) {
660
            unlink($targetFile);
661
        }
662
        
663
        $method = 'image' . $this->type;
664
        if (!$method($this->newImage, $targetFile, $this->resolveQuality())) {
665
            throw new ImageHelper_Exception(
666
                'Error creating new image',
667
                sprintf(
668
                    'The %s method could not write the new image to %s',
669
                    $method,
670
                    $targetFile
671
                ),
672
                self::ERROR_CANNOT_WRITE_NEW_IMAGE_FILE
673
            );
674
        }
675
676
        if (filesize($targetFile) < 1) {
677
            throw new ImageHelper_Exception(
678
                'Error creating new image',
679
                'Resampling completed successfully, but the generated file is 0 bytes big.',
680
                self::ERROR_CREATED_AN_EMPTY_FILE
681
            );
682
        }
683
684
        if($dispose) {
685
            $this->dispose();
686
        }
687
        
688
        return $this;
689
    }
690
691
    /**
692
     * @return $this
693
     */
694
    public function dispose() : self
695
    {
696
        if(is_resource($this->sourceImage)) {
697
            imagedestroy($this->sourceImage);
698
        }
699
        
700
        if(is_resource($this->newImage)) {
701
            imagedestroy($this->newImage);
702
        }
703
704
        return $this;
705
    }
706
707
    protected function resolveQuality() : int
708
    {
709
        switch ($this->type)
710
        {
711
            case 'jpeg':
712
                return $this->quality;
713
714
            case 'png':
715
            default:
716
                return 0;
717
        }
718
    }
719
720
    /**
721
     * Sets the quality for image types like jpg that use compression.
722
     *
723
     * @param int $quality
724
     * @return ImageHelper
725
     * @throws ImageHelper_Exception
726
     */
727
    public function setQuality(int $quality) : self
728
    {
729
        if ($quality < 0) {
730
            throw new ImageHelper_Exception(
731
                'Invalid configuration',
732
                'Cannot set a quality less than 0.',
733
                self::ERROR_QUALITY_VALUE_BELOW_ZERO
734
            );
735
        }
736
737
        if ($quality > 100) {
738
            throw new ImageHelper_Exception(
739
                'Invalid configuration',
740
                'Cannot set a quality higher than 100.',
741
                self::ERROR_QUALITY_ABOVE_ONE_HUNDRED
742
            );
743
        }
744
745
        $this->quality = $quality;
746
747
        return $this;
748
    }
749
750
   /**
751
    * Attempts to adjust the memory to the required size
752
    * to work with the current image.
753
    * 
754
    * @return boolean
755
    */
756
    protected function adjustMemory() : bool
757
    {
758
        if(!self::$config['auto-memory-adjustment']) {
759
            return true;
760
        }
761
        
762
        $MB = 1048576; // number of bytes in 1M
763
        $K64 = 65536; // number of bytes in 64K
764
        $tweakFactor = 25; // magic adjustment value as safety threshold
765
        $memoryNeeded = ceil(
766
            (
767
                $this->info->getWidth() 
768
                * 
769
                $this->info->getHeight() 
770
                * 
771
                $this->info->getBits() 
772
                * 
773
                ($this->info->getChannels() / 8) 
774
                + 
775
                $K64
776
            )
777
            * $tweakFactor
778
        );
779
780
        // ini_get('memory_limit') only works if compiled with "--enable-memory-limit".
781
        // Also, default memory limit is 8MB, so we will stick with that.
782
        $memoryLimit = 8 * $MB;
783
            
784
        if (function_exists('memory_get_usage') && memory_get_usage() + $memoryNeeded > $memoryLimit) {
785
            $newLimit = ($memoryLimit + (memory_get_usage() + $memoryNeeded)) / $MB;
786
            $newLimit = ceil($newLimit);
787
            ini_set('memory_limit', $newLimit . 'M');
788
789
            return true;
790
        }
791
792
        return false;
793
    }
794
795
    /**
796
     * Stretches the image to the specified dimensions.
797
     *
798
     * @param int $width
799
     * @param int $height
800
     * @return ImageHelper
801
     * @throws ImageHelper_Exception
802
     */
803
    public function stretch(int $width, int $height) : ImageHelper
804
    {
805
        return $this->resampleImage($width, $height);
806
    }
807
808
   /**
809
    * Creates a new image from the current image,
810
    * resampling it to the new size.
811
    * 
812
    * @param int $newWidth
813
    * @param int $newHeight   
814
    * @throws ImageHelper_Exception
815
    * @return ImageHelper
816
    */
817
    protected function resampleImage(int $newWidth, int $newHeight) : ImageHelper
818
    {
819
        if($this->isVector()) {
820
            return $this;
821
        }
822
823
        if($this->newWidth===$newWidth && $this->newHeight===$newHeight) {
824
            return $this;
825
        }
826
        
827
        if($newWidth < 1) { $newWidth = 1; }
828
        if($newHeight < 1) { $newHeight = 1; }
829
        
830
        $this->adjustMemory();
831
832
        $new = $this->createNewImage($newWidth, $newHeight);
833
       
834
        if (!imagecopyresampled($new, $this->newImage, 0, 0, 0, 0, $newWidth, $newHeight, $this->newWidth, $this->newHeight)) 
835
        {
836
            throw new ImageHelper_Exception(
837
                'Error creating new image',
838
                'Cannot copy resampled image data',
839
                self::ERROR_CANNOT_COPY_RESAMPLED_IMAGE_DATA
840
            );
841
        }
842
843
        $this->setNewImage($new);
844
845
        return $this;
846
    }
847
848
    /**
849
     * Gets the image type for the specified file name.
850
     * Like {@link getImageType()}, except that it automatically
851
     * extracts the file extension from the file name.
852
     *
853
     * @param string |resource|GdImage $pathOrResource
854
     * @return string|NULL
855
     * @see getImageType()
856
     */
857
    public static function getFileImageType($pathOrResource) : ?string
858
    {
859
        if(!is_string($pathOrResource)) {
860
            return 'png';
861
        }
862
863
        return self::getImageType(strtolower(pathinfo($pathOrResource, PATHINFO_EXTENSION)));
864
    }
865
866
    /**
867
     * Gets the image type for the specified file extension,
868
     * or NULL if the extension is not among the supported
869
     * file types.
870
     *
871
     * @param string $extension
872
     * @return string|NULL
873
     */
874
    public static function getImageType(string $extension) : ?string
875
    {
876
        return self::$imageTypes[$extension] ?? null;
877
    }
878
879
    /**
880
     * @return string[]
881
     */
882
    public static function getImageTypes() : array
883
    {
884
        $types = array_values(self::$imageTypes);
885
        return array_unique($types);
886
    }
887
    
888
   /**
889
    * Displays an existing image resource.
890
    *
891
    * @param resource $resource
892
    * @param string $imageType The image format to send, i.e. "jpeg", "png"
893
    * @param int $quality The quality to use for the image. This is 0-9 (0=no compression, 9=max) for PNG, and 0-100 (0=lowest, 100=highest quality) for JPG
894
    *
895
    * @throws ImageHelper_Exception
896
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
897
    * @see ImageHelper::ERROR_INVALID_STREAM_IMAGE_TYPE
898
    */
899
    public static function displayImageStream($resource, string $imageType, int $quality=-1) : void
900
    {
901
        self::requireResource($resource);
902
903
        $imageType = self::requireValidStreamType($imageType);
904
        
905
        header('Content-type:image/' . $imageType);
906
907
        $function = 'image' . $imageType;
908
        
909
        $function($resource, null, $quality);
910
    }
911
912
    /**
913
     * @param string $imageType
914
     * @return string
915
     *
916
     * @throws ImageHelper_Exception
917
     * @see ImageHelper::ERROR_INVALID_STREAM_IMAGE_TYPE
918
     * @see ImageHelper::$streamTypes
919
     */
920
    public static function requireValidStreamType(string $imageType) : string
921
    {
922
        $imageType = strtolower($imageType);
923
924
        if(in_array($imageType, self::$streamTypes))
925
        {
926
            return $imageType;
927
        }
928
929
        throw new ImageHelper_Exception(
930
            'Invalid image stream type',
931
            sprintf(
932
                'The image type [%s] cannot be used for a stream.',
933
                $imageType
934
            ),
935
            self::ERROR_INVALID_STREAM_IMAGE_TYPE
936
        );
937
    }
938
939
    /**
940
     * Displays an image from an existing image file.
941
     * @param string $imageFile
942
     * @throws ImageHelper_Exception
943
     */
944
    public static function displayImage(string $imageFile) : void
945
    {
946
        $file = null;
947
        $line = null;
948
        if (headers_sent($file, $line)) {
949
            throw new ImageHelper_Exception(
950
                'Error displaying image',
951
                'Headers have already been sent: in file ' . $file . ':' . $line,
952
                self::ERROR_HEADERS_ALREADY_SENT
953
            );
954
        }
955
956
        if (!file_exists($imageFile)) {
957
            throw new ImageHelper_Exception(
958
                'Image file does not exist',
959
                sprintf(
960
                    'Cannot display image, the file does not exist on disk: [%s].',
961
                    $imageFile
962
                ),
963
                self::ERROR_IMAGE_FILE_DOES_NOT_EXIST
964
            );
965
        }
966
967
        $format = self::getFileImageType($imageFile);
968
        if($format === 'svg') {
969
            $format = 'svg+xml';
970
        }
971
972
        $contentType = 'image/' . $format;
973
        
974
        header('Content-Type: '.$contentType);
975
        header("Last-Modified: " . gmdate("D, d M Y H:i:s", filemtime($imageFile)) . " GMT");
976
        header('Cache-Control: public');
977
        header('Content-Length: ' . filesize($imageFile));
978
979
        readfile($imageFile);
980
    }
981
    
982
   /**
983
    * Displays the current image.
984
    *
985
    * NOTE: You must call `exit()` manually after this.
986
    */
987
    public function display() : void
988
    {
989
        self::displayImageStream(
990
            $this->newImage,
991
            $this->getType(),
992
            $this->resolveQuality()
993
        );
994
    }
995
996
    /**
997
     * Trims the current loaded image.
998
     *
999
     * @param RGBAColor|null $color A color definition, as an associative array with red, green, and blue keys. If not specified, the color at pixel position 0,0 will be used.
1000
     *
1001
     * @return ImageHelper
1002
     * @throws ImageHelper_Exception
1003
     * @throws ColorException
1004
     *
1005
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1006
     * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_CANVAS
1007
     */
1008
    public function trim(?RGBAColor $color=null) : ImageHelper
1009
    {
1010
        return $this->trimImage($this->newImage, $color);
1011
    }
1012
1013
    /**
1014
     * Retrieves a color definition by its index.
1015
     *
1016
     * @param resource $img A valid image resource.
1017
     * @param int $colorIndex The color index, as returned by `imagecolorat` for example.
1018
     * @return RGBAColor An array with red, green, blue and alpha keys.
1019
     *
1020
     * @throws ImageHelper_Exception
1021
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1022
     */
1023
    public function getIndexedColors($img, int $colorIndex) : RGBAColor
1024
    {
1025
        self::requireResource($img);
1026
1027
        return ColorFactory::createFromIndex($img, $colorIndex);
1028
    }
1029
1030
    /**
1031
     * @param resource $img
1032
     * @param int $x
1033
     * @param int $y
1034
     * @return RGBAColor
1035
     * @throws ImageHelper_Exception
1036
     */
1037
    public function getIndexedColorsAt($img, int $x, int $y) : RGBAColor
1038
    {
1039
        self::requireResource($img);
1040
1041
        return $this->getIndexedColors(
1042
            $img,
1043
            imagecolorat($this->sourceImage, $x, $y)
1044
        );
1045
    }
1046
1047
    /**
1048
     * Trims the specified image resource by removing the specified color.
1049
     * Also works with transparency.
1050
     *
1051
     * @param resource|GdImage $img
1052
     * @param RGBAColor|null $trimColor
1053
     * @return ImageHelper
1054
     *
1055
     * @throws ImageHelper_Exception
1056
     */
1057
    protected function trimImage($img, ?RGBAColor $trimColor=null) : ImageHelper
1058
    {
1059
        if($this->isVector()) {
1060
            return $this;
1061
        }
1062
1063
        self::requireResource($img);
1064
1065
        $trimmer = new ImageTrimmer($this, $img, $trimColor);
1066
        $new = $trimmer->trim();
1067
1068
        if($new === null) {
1069
            return $this;
1070
        }
1071
        
1072
        // To finish up, we replace the old image which is referenced.
1073
        imagedestroy($img);
1074
        
1075
        $this->setNewImage($new);
1076
1077
        return $this;
1078
    }
1079
1080
    /**
1081
     * Sets the new image after a transformation operation:
1082
     * automatically adjusts the new size information.
1083
     *
1084
     * @param resource $image
1085
     *
1086
     * @throws ImageHelper_Exception
1087
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1088
     */
1089
    protected function setNewImage($image) : ImageHelper
1090
    {
1091
        self::requireResource($image);
1092
        
1093
        $this->newImage = $image;
1094
        $this->newWidth = imagesx($image);
1095
        $this->newHeight= imagesy($image);
1096
1097
        return $this;
1098
    }
1099
    
1100
   /**
1101
    * Requires the subject to be a resource.
1102
    * 
1103
    * @param resource|GdImage|mixed $subject
1104
    *
1105
    * @throws ImageHelper_Exception
1106
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
1107
    */
1108
    public static function requireResource($subject) : void
1109
    {
1110
        if(is_resource($subject) && imagesx($subject)) {
1111
            return;
1112
        }
1113
1114
        if($subject instanceof GdImage) {
1115
            return;
1116
        }
1117
        
1118
        throw new ImageHelper_Exception(
1119
            'Not an image resource',
1120
            sprintf(
1121
                'Specified image should be a resource, [%s] given.',
1122
                gettype($subject)
1123
            ),
1124
            self::ERROR_NOT_A_RESOURCE
1125
        );
1126
    }
1127
    
1128
   /**
1129
    * Creates a new image resource, with transparent background.
1130
    * 
1131
    * @param int $width
1132
    * @param int $height
1133
    * @throws ImageHelper_Exception
1134
    * @return resource
1135
    */
1136
    public function createNewImage(int $width, int $height)
1137
    {
1138
        $img = imagecreatetruecolor($width, $height);
1139
        
1140
        if($img === false) 
1141
        {
1142
            throw new ImageHelper_Exception(
1143
                'Error creating new image',
1144
                'Cannot create new image canvas',
1145
                self::ERROR_CANNOT_CREATE_IMAGE_CANVAS
1146
            );
1147
        }
1148
1149
        self::addAlphaSupport($img);
1150
        
1151
        return $img;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $img also could return the type GdImage which is incompatible with the documented return type resource.
Loading history...
1152
    }
1153
1154
    /**
1155
     * @param int $x
1156
     * @param int $y
1157
     * @return $this
1158
     */
1159
	public function fillWhite(int $x=0, int $y=0) : self
1160
	{
1161
	    $this->addRGBColor('white', 255, 255, 255);
1162
        return $this->fill('white', $x, $y);
1163
	}
1164
1165
    /**
1166
     * @return $this
1167
     * @throws ImageHelper_Exception
1168
     */
1169
	public function fillTransparent() : self
1170
	{
1171
        $this->enableAlpha();
1172
	    
1173
	    self::fillImageTransparent($this->newImage);
1174
	    
1175
	    return $this;
1176
	}
1177
1178
    /**
1179
     * @param resource $resource
1180
     * @return void
1181
     * @throws ImageHelper_Exception
1182
     */
1183
	public static function fillImageTransparent($resource) : void
1184
	{
1185
	    self::requireResource($resource);
1186
	    
1187
	    $transparent = imagecolorallocatealpha($resource, 89, 14, 207, 127);
1188
	    imagecolortransparent ($resource, $transparent);
1189
	    imagefill($resource, 0, 0, $transparent);
1190
	}
1191
1192
    /**
1193
     * @param string $colorName
1194
     * @param int $x
1195
     * @param int $y
1196
     * @return $this
1197
     */
1198
	public function fill(string $colorName, int $x=0, int $y=0) : self
1199
	{
1200
	    imagefill($this->newImage, $x, $y, $this->colors[$colorName]);
1201
	    return $this;
1202
	}
1203
1204
    /**
1205
     * @param string $name
1206
     * @param int $red
1207
     * @param int $green
1208
     * @param int $blue
1209
     * @return $this
1210
     */
1211
    public function addRGBColor(string $name, int $red, int $green, int $blue) : self
1212
    {
1213
        $this->colors[$name] = (int)imagecolorallocate($this->newImage, $red, $green, $blue);
1214
        return $this;
1215
    }
1216
1217
    /**
1218
     * @param string $text
1219
     * @param int|float $size
1220
     * @param string $colorName
1221
     * @param int $x
1222
     * @param int $y
1223
     * @param int|float $angle
1224
     * @return $this
1225
     */
1226
    public function textTTF(string $text, $size, string $colorName, int $x=0, int $y=0, $angle=0) : self
1227
    {
1228
        imagealphablending($this->newImage, true);
1229
        
1230
        imagettftext(
1231
            $this->newImage,
1232
            (float)$size,
1233
            (float)$angle,
1234
            $x,
1235
            $y,
1236
            $this->colors[$colorName],
1237
            $this->TTFFile,
1238
            $text
1239
        );
1240
        
1241
        imagealphablending($this->newImage, false);
1242
        
1243
        return $this;
1244
    }
1245
    
1246
   /**
1247
    * @return resource
1248
    */
1249
    public function getImage()
1250
    {
1251
        return $this->newImage;
1252
    }
1253
1254
    /**
1255
     * @param ImageHelper $target
1256
     * @param int $xpos
1257
     * @param int $ypos
1258
     * @param int $sourceX
1259
     * @param int $sourceY
1260
     * @return $this
1261
     * @throws ImageHelper_Exception
1262
     */
1263
    public function paste(ImageHelper $target, int $xpos=0, int $ypos=0, int $sourceX=0, int $sourceY=0) : self
1264
    {
1265
        $img = $target->getImage();
1266
        
1267
        if($target->isAlpha()) {
1268
            $this->enableAlpha();
1269
        }
1270
        
1271
        imagecopy(
1272
            $this->newImage,
1273
            $img,
1274
            $xpos,
1275
            $ypos,
1276
            $sourceX,
1277
            $sourceY,
1278
            imagesx($img),
1279
            imagesy($img)
1280
        );
1281
1282
        return $this;
1283
    }
1284
1285
    /**
1286
     * Retrieves the size of the image.
1287
     *
1288
     * @return ImageHelper_Size
1289
     *
1290
     * @throws ClassNotExistsException
1291
     * @throws ClassNotImplementsException
1292
     * @throws ImageHelper_Exception
1293
     *
1294
     * @see ImageHelper::ERROR_CANNOT_GET_IMAGE_SIZE
1295
     */
1296
	public function getSize() : ImageHelper_Size
1297
    {
1298
	    return self::getImageSize($this->newImage);
1299
    }
1300
    
1301
   /**
1302
    * Sets the TTF font file to use for text operations.
1303
    * 
1304
    * @param string $filePath
1305
    * @return $this
1306
    */
1307
    public function setFontTTF(string $filePath) : self
1308
    {
1309
        $this->TTFFile = $filePath;
1310
        return $this;
1311
    }
1312
1313
    /**
1314
     * Goes through a series of text sizes to find the closest match to
1315
     * fit the text into the target width.
1316
     *
1317
     * @param string $text
1318
     * @param integer $matchWidth
1319
     * @return ComputedTextSize
1320
     * @throws ImageHelper_Exception
1321
     */
1322
    public function fitText(string $text, int $matchWidth) : ComputedTextSize
1323
    {
1324
        /**
1325
         * @var ComputedTextSize[]
1326
         */
1327
        $sizes = array();
1328
1329
        for($i=1; $i<=1000; $i += 0.1) {
1330
            $size = $this->calcTextSize($text, $i);
1331
            $sizes[] = $size;
1332
            if($size->getWidth() >= $matchWidth) {
1333
                break;
1334
            }
1335
        }
1336
    
1337
        $last = array_pop($sizes);
1338
        $prev = array_pop($sizes);
1339
    
1340
        // determine which is the closest match, and use that
1341
        $diffLast = $last->getWidth() - $matchWidth;
1342
        $diffPrev = $matchWidth - $prev->getWidth();
1343
    
1344
        if($diffLast <= $diffPrev) {
1345
            return $last;
1346
        }
1347
    
1348
        return $prev;
1349
    }
1350
1351
    /**
1352
     * @param string $text
1353
     * @param int|float $fontSize
1354
     * @return ComputedTextSize
1355
     * @throws ImageHelper_Exception
1356
     */
1357
    public function calcTextSize(string $text, $fontSize) : ComputedTextSize
1358
    {
1359
        $this->requireTTFFont();
1360
1361
        $box = imagettfbbox((float)$fontSize, 0, $this->TTFFile, $text);
1362
    
1363
        return new ComputedTextSize(array(
1364
            'size' => (float)$fontSize,
1365
            'top_left_x' => $box[6],
1366
            'top_left_y' => $box[7],
1367
            'top_right_x' => $box[4],
1368
            'top_right_y' => $box[5],
1369
            'bottom_left_x' => $box[0],
1370
            'bottom_left_y' => $box[1],
1371
            'bottom_right_x' => $box[2],
1372
            'bottom_right_y' => $box[3],
1373
            'width' => $box[4]-$box[0],
1374
            'height' => $box[1]-$box[7]
1375
        ));
1376
    }
1377
    
1378
    protected function requireTTFFont() : void
1379
    {
1380
        if(!empty($this->TTFFile)) {
1381
            return;
1382
        }
1383
        
1384
	    throw new ImageHelper_Exception(
1385
            'No true type font specified',
1386
            'This functionality requires a TTF font file to be specified with the [setFontTTF] method.',
1387
            self::ERROR_NO_TRUE_TYPE_FONT_SET    
1388
        );
1389
    }
1390
1391
    /**
1392
     * Retrieves the size of an image file on disk, or
1393
     * an existing image resource.
1394
     *
1395
     * @param resource|GdImage|string $pathOrResource
1396
     * @return ImageHelper_Size Size object, can also be accessed like the traditional array from getimagesize
1397
     * @throws ClassNotExistsException
1398
     * @throws ClassNotImplementsException
1399
     * @throws ImageHelper_Exception
1400
     *
1401
     * @see ImageHelper_Size
1402
     * @see ImageHelper::ERROR_CANNOT_GET_IMAGE_SIZE
1403
     * @see ImageHelper::ERROR_CANNOT_READ_SVG_IMAGE
1404
     * @see ImageHelper::ERROR_SVG_SOURCE_VIEWBOX_MISSING
1405
     * @see ImageHelper::ERROR_SVG_VIEWBOX_INVALID
1406
     */
1407
	public static function getImageSize($pathOrResource) : ImageHelper_Size
1408
	{
1409
	    if(is_resource($pathOrResource) || $pathOrResource instanceof GdImage)
1410
	    {
1411
	        return new ImageHelper_Size(array(
1412
	            'width' => imagesx($pathOrResource),
1413
	            'height' => imagesy($pathOrResource),
1414
	            'channels' => 1,
1415
	            'bits' => 8
1416
	        ));
1417
	    }
1418
1419
	    $type = self::getFileImageType($pathOrResource);
1420
1421
        $sizeMethods = array(
1422
            'svg' => array(self::class, 'getImageSize_svg')
1423
        );
1424
1425
	    if(isset($sizeMethods[$type]))
1426
	    {
1427
	        return ClassHelper::requireObjectInstanceOf(
1428
                ImageHelper_Size::class,
1429
                $sizeMethods[$type]($pathOrResource)
1430
            );
1431
	    }
1432
1433
	    $info = getimagesize($pathOrResource);
1434
1435
	    if($info !== false) {
1436
	        return new ImageHelper_Size($info);
1437
	    }
1438
	    
1439
        throw new ImageHelper_Exception(
1440
            'Error opening image file',
1441
            sprintf(
1442
                'Could not get image size for image [%s]',
1443
                $pathOrResource
1444
            ),
1445
            self::ERROR_CANNOT_GET_IMAGE_SIZE
1446
        );
1447
	}
1448
1449
    /**
1450
     * @param string $imagePath
1451
     * @return ImageHelper_Size
1452
     *
1453
     * @throws ImageHelper_Exception
1454
     * @throws XMLHelper_Exception
1455
     * @throws JsonException
1456
     */
1457
	protected static function getImageSize_svg(string $imagePath) : ImageHelper_Size
1458
	{
1459
	    $xml = XMLHelper::createSimplexml();
1460
	    $xml->loadFile($imagePath);
1461
	    
1462
	    if($xml->hasErrors()) {
1463
	        throw new ImageHelper_Exception(
1464
	            'Error opening SVG image',
1465
	            sprintf(
1466
	                'The XML content of the image [%s] could not be parsed.',
1467
	                $imagePath
1468
                ),
1469
	            self::ERROR_CANNOT_READ_SVG_IMAGE
1470
            );
1471
	    }
1472
	    
1473
	    $data = $xml->toArray();
1474
	    $xml->dispose();
1475
	    unset($xml);
1476
	    
1477
	    if(!isset($data['@attributes']['viewBox'])) {
1478
	        throw new ImageHelper_Exception(
1479
	            'SVG Image is corrupted',
1480
	            sprintf(
1481
	                'The [viewBox] attribute is missing in the XML of the image at path [%s].',
1482
	                $imagePath
1483
                ),
1484
	            self::ERROR_SVG_SOURCE_VIEWBOX_MISSING
1485
            );
1486
	    }
1487
	    
1488
	    $svgWidth = parseNumber($data['@attributes']['width'])->getNumber();
1489
	    $svgHeight = parseNumber($data['@attributes']['height'])->getNumber();
1490
	    
1491
	    $viewBox = str_replace(' ', ',', $data['@attributes']['viewBox']);
1492
	    $size = explode(',', $viewBox);
1493
	    
1494
	    if(count($size) !== 4)
1495
	    {
1496
	        throw new ImageHelper_Exception(
1497
	            'SVG image has an invalid viewBox attribute',
1498
	            sprintf(
1499
	               'The [viewBox] attribute does not have an expected value: [%s] in path [%s].',
1500
	                $viewBox,
1501
	                $imagePath
1502
                ),
1503
	            self::ERROR_SVG_VIEWBOX_INVALID
1504
            );
1505
	    }
1506
	    
1507
	    $boxWidth = (float)$size[2];
1508
	    $boxHeight = (float)$size[3];
1509
	    
1510
	    // calculate the x and y units of the document: 
1511
	    // @see http://tutorials.jenkov.com/svg/svg-viewport-view-box.html#viewbox
1512
	    //
1513
	    // The viewbox combined with the width and height of the svg
1514
	    // allow calculating how many pixels are in one unit of the 
1515
	    // width and height of the document.
1516
        //
1517
	    $xUnits = $svgWidth / $boxWidth;
1518
	    $yUnits = $svgHeight / $boxHeight;
1519
	    
1520
	    $pxWidth = $xUnits * $svgWidth;
1521
	    $pxHeight = $yUnits * $svgHeight;
1522
	    
1523
	    return new ImageHelper_Size(array(
1524
            (int)$pxWidth,
1525
            (int)$pxHeight,
1526
	        'bits' => 8
1527
	    ));
1528
	}
1529
1530
    /**
1531
     * Crops the image to the specified width and height, optionally
1532
     * specifying the origin position to crop from.
1533
     *
1534
     * @param integer $width
1535
     * @param integer $height
1536
     * @param integer $x
1537
     * @param integer $y
1538
     * @return $this
1539
     * @throws ImageHelper_Exception
1540
     */
1541
    public function crop(int $width, int $height, int $x=0, int $y=0) : ImageHelper
1542
    {
1543
        $new = $this->createNewImage($width, $height);
1544
        
1545
        imagecopy($new, $this->newImage, 0, 0, $x, $y, $width, $height);
1546
        
1547
        $this->setNewImage($new);
1548
        
1549
        return $this;
1550
    }
1551
    
1552
    public function getWidth() : int
1553
    {
1554
        return $this->newWidth;
1555
    }
1556
    
1557
    public function getHeight() : int
1558
    {
1559
        return $this->newHeight;
1560
    }
1561
1562
    /**
1563
     * Calculates the average color value used in
1564
     * the image. Returns an associative array
1565
     * with the red, green, blue and alpha components,
1566
     * or a HEX color string depending on the selected
1567
     * format.
1568
     *
1569
     * NOTE: Use the calcAverageColorXXX methods for
1570
     * strict return types.
1571
     *
1572
     * @return RGBAColor
1573
     *
1574
     * @throws ImageHelper_Exception
1575
     *
1576
     * @see ImageHelper::calcAverageColorHEX()
1577
     * @see ImageHelper::calcAverageColorRGB()
1578
     */
1579
    public function calcAverageColor() : RGBAColor
1580
    {
1581
        $image = $this->duplicate();
1582
        $image->resample(1, 1);
1583
        
1584
        return $image->getColorAt(0, 0);
1585
    }
1586
    
1587
   /**
1588
    * Calculates the image's average color value, and
1589
    * returns an associative array with red, green,
1590
    * blue and alpha keys.
1591
    * 
1592
    * @throws ImageHelper_Exception
1593
    * @return RGBAColor
1594
    */
1595
    public function calcAverageColorRGB() : RGBAColor
1596
    {
1597
       return $this->calcAverageColor();
1598
    }
1599
    
1600
   /**
1601
    * Calculates the image's average color value, and
1602
    * returns a hex color string (without the #).
1603
    * 
1604
    * @throws ImageHelper_Exception
1605
    * @return string
1606
    */
1607
    public function calcAverageColorHex() : string
1608
    {
1609
        return $this->calcAverageColor()->toHEX();
1610
    }
1611
    
1612
    /**
1613
     * Retrieves the color value at the specified pixel
1614
     * coordinates in the image.
1615
     *
1616
     * @param int $x
1617
     * @param int $y
1618
     * @return RGBAColor
1619
     *
1620
     * @throws ImageHelper_Exception
1621
     * @see ImageHelper::ERROR_POSITION_OUT_OF_BOUNDS
1622
     */
1623
    public function getColorAt(int $x, int $y) : RGBAColor
1624
    {
1625
        if($x > $this->getWidth() || $y > $this->getHeight()) 
1626
        {
1627
            throw new ImageHelper_Exception(
1628
                'Position out of bounds',
1629
                sprintf(
1630
                    'The position [%sx%s] does not exist in the image, it is [%sx%s] pixels in size.',
1631
                    $x,
1632
                    $y,
1633
                    $this->getWidth(),
1634
                    $this->getHeight()
1635
                ),
1636
                self::ERROR_POSITION_OUT_OF_BOUNDS
1637
            );
1638
        }
1639
        
1640
        $idx = imagecolorat($this->newImage, $x, $y);
1641
1642
        return $this->getIndexedColors($this->newImage, $idx);
1643
    }
1644
    
1645
    /**
1646
     * Retrieves the brightness of the image, in percent.
1647
     *
1648
     * @return float
1649
     * @throws ImageHelper_Exception
1650
     */
1651
    public function getBrightness() : float
1652
    {
1653
        return $this
1654
            ->calcAverageColorRGB()
1655
            ->getBrightness()
1656
            ->getValue();
1657
    }
1658
    
1659
   /**
1660
    * Retrieves a md5 hash of the source image file.
1661
    * 
1662
    * NOTE: Only works when the helper has been created
1663
    * from a file. Otherwise, an exception is thrown.
1664
    * 
1665
    * @return string
1666
    * @throws ImageHelper_Exception|OutputBuffering_Exception
1667
    */
1668
    public function getHash() : string
1669
    {
1670
        if($this->newImage === null)
1671
        {
1672
            throw new ImageHelper_Exception(
1673
                'No image loaded to create a hash for.',
1674
                'The newImage property is null.',
1675
                self::ERROR_HASH_NO_IMAGE_LOADED
1676
            );
1677
        }
1678
1679
        OutputBuffering::start();
1680
        imagepng($this->newImage);
1681
        return md5(OutputBuffering::get());
1682
    }
1683
}
1684