Passed
Push — master ( 289f9d...bae7fb )
by Sebastian
04:42
created

ImageHelper::requireValidStreamType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 16
rs 9.9666
cc 2
nc 2
nop 1
1
<?php
2
/**
3
 * File containing the {@link ImageHelper} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage ImageHelper
7
 * @see ImageHelper
8
 */
9
10
namespace AppUtils;
11
12
/**
13
 * Image helper class that can be used to transform images,
14
 * and retrieve information about them.
15
 * 
16
 * @package Application Utils
17
 * @subpackage ImageHelper
18
 * @author Sebastian Mordziol <[email protected]>
19
 * @version 2.0
20
 */
21
class ImageHelper
22
{
23
    const ERROR_CANNOT_CREATE_IMAGE_CANVAS = 513001;
24
    const ERROR_IMAGE_FILE_DOES_NOT_EXIST = 513002;
25
    const ERROR_CANNOT_GET_IMAGE_SIZE = 513003;
26
    const ERROR_UNSUPPORTED_IMAGE_TYPE = 513004;
27
    const ERROR_FAILED_TO_CREATE_NEW_IMAGE = 513005;
28
    const ERROR_SAVE_NO_IMAGE_CREATED = 513006;
29
    const ERROR_CANNOT_WRITE_NEW_IMAGE_FILE = 513007;
30
    const ERROR_CREATED_AN_EMPTY_FILE = 513008;
31
    const ERROR_QUALITY_VALUE_BELOW_ZERO = 513009;
32
    const ERROR_QUALITY_ABOVE_ONE_HUNDRED = 513010;
33
    const ERROR_CANNOT_CREATE_IMAGE_OBJECT = 513011;
34
    const ERROR_CANNOT_COPY_RESAMPLED_IMAGE_DATA = 513012;
35
    const ERROR_HEADERS_ALREADY_SENT = 513013;
36
    const ERROR_CANNOT_READ_SVG_IMAGE = 513014;
37
    const ERROR_SVG_SOURCE_VIEWBOX_MISSING = 513015;
38
    const ERROR_SVG_VIEWBOX_INVALID = 513016;
39
    const ERROR_NOT_A_RESOURCE = 513017;
40
    const ERROR_INVALID_STREAM_IMAGE_TYPE = 513018;
41
    const ERROR_NO_TRUE_TYPE_FONT_SET = 513019;
42
    const ERROR_POSITION_OUT_OF_BOUNDS = 513020;
43
    const ERROR_IMAGE_CREATION_FAILED = 513021;
44
    const ERROR_CANNOT_CREATE_IMAGE_CROP = 513023;
45
    const ERROR_GD_LIBRARY_NOT_INSTALLED = 513024;
46
    const ERROR_UNEXPECTED_COLOR_VALUE = 513025;
47
    const ERROR_HASH_NO_IMAGE_LOADED = 513026;
48
49
    const COLORFORMAT_RGB = 1;
50
    const COLORFORMAT_HEX = 2;
51
52
    /**
53
    * @var string
54
    */
55
    protected $file;
56
57
   /**
58
    * @var ImageHelper_Size
59
    */
60
    protected $info;
61
62
   /**
63
    * @var string
64
    */
65
    protected $type;
66
67
   /**
68
    * @var resource|NULL
69
    */
70
    protected $newImage;
71
72
   /**
73
    * @var resource
74
    */
75
    protected $sourceImage;
76
77
   /**
78
    * @var int
79
    */
80
    protected $width;
81
82
   /**
83
    * @var int
84
    */
85
    protected $height;
86
87
   /**
88
    * @var int
89
    */
90
    protected $newWidth = 0;
91
92
   /**
93
    * @var int
94
    */
95
    protected $newHeight = 0;
96
97
   /**
98
    * @var int
99
    */
100
    protected $quality = 85;
101
102
    /**
103
     * @var array<string,string>
104
     */
105
    protected static $imageTypes = array(
106
        'png' => 'png',
107
        'jpg' => 'jpeg',
108
        'jpeg' => 'jpeg',
109
        'gif' => 'gif',
110
        'svg' => 'svg'
111
    );
112
113
    /**
114
     * @var array<string,mixed>
115
     */
116
    protected static $config = array(
117
        'auto-memory-adjustment' => true
118
    );
119
120
    /**
121
     * @var string[]
122
     */
123
    protected static $streamTypes = array(
124
        'jpeg',
125
        'png',
126
        'gif'
127
    );
128
129
    /**
130
     * @param string|null $sourceFile
131
     * @param resource|null $resource
132
     * @param string|null $type The image type, e.g. "png", "jpeg".
133
     *
134
     * @throws ImageHelper_Exception
135
     * @see ImageHelper::ERROR_GD_LIBRARY_NOT_INSTALLED
136
     */
137
    public function __construct(?string $sourceFile=null, $resource=null, ?string $type=null)
138
    {
139
        // ensure that the GD library is installed
140
        if(!function_exists('imagecreate')) 
141
        {
142
            throw new ImageHelper_Exception(
143
                'The PHP GD extension is not installed or not enabled.',
144
                null,
145
                self::ERROR_GD_LIBRARY_NOT_INSTALLED
146
            );
147
        }
148
        
149
        if(is_resource($resource)) 
150
        {
151
            $this->sourceImage = $resource;
152
            $this->type = $type;
153
            $this->info = self::getImageSize($resource);
154
        } 
155
        else 
156
        {
157
            $this->file = $sourceFile;
158
            if (!file_exists($this->file)) {
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type null; however, parameter $filename of file_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
            if (!file_exists(/** @scrutinizer ignore-type */ $this->file)) {
Loading history...
159
                throw new ImageHelper_Exception(
160
                    'Image file does not exist',
161
                    sprintf(
162
                        'Could not find the image file on disk at location [%s]',
163
                        $this->file
164
                    ),
165
                    self::ERROR_IMAGE_FILE_DOES_NOT_EXIST
166
                );
167
            }
168
    
169
            $this->type = self::getFileImageType($this->file);
170
            if (is_null($this->type)) {
171
                throw new ImageHelper_Exception(
172
                    'Error opening image',
173
                    'Not a valid supported image type for image ' . $this->file,
174
                    self::ERROR_UNSUPPORTED_IMAGE_TYPE
175
                );
176
            }
177
178
            $this->info = self::getImageSize($this->file);
179
180
            if(!$this->isVector()) 
181
            {
182
                $method = 'imagecreatefrom' . $this->type;
183
                $this->sourceImage = $method($this->file);
184
                if (!$this->sourceImage) {
185
                    throw new ImageHelper_Exception(
186
                        'Error creating new image',
187
                        $method . ' failed',
188
                        self::ERROR_FAILED_TO_CREATE_NEW_IMAGE
189
                    );
190
                }
191
                
192
                imagesavealpha($this->sourceImage, true);
193
            }
194
        }
195
196
        $this->width = $this->info->getWidth();
197
        $this->height = $this->info->getHeight();
198
199
        if(!$this->isVector()) {
200
            $this->setNewImage($this->duplicateImage($this->sourceImage));
201
        }
202
    }
203
204
   /**
205
    * Factory method: creates a new helper with a blank image.
206
    * 
207
    * @param integer $width
208
    * @param integer $height
209
    * @param string $type The target file type when saving
210
    * @return ImageHelper
211
    * @throws ImageHelper_Exception
212
    *
213
    * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_OBJECT
214
    */
215
    public static function createNew($width, $height, $type='png')
216
    {
217
        $img = imagecreatetruecolor($width, $height);
218
        if($img !== false) {
219
            return self::createFromResource($img, 'png');
0 ignored issues
show
Bug introduced by
It seems like $img can also be of type GdImage; however, parameter $resource of AppUtils\ImageHelper::createFromResource() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

219
            return self::createFromResource(/** @scrutinizer ignore-type */ $img, 'png');
Loading history...
220
        }
221
        
222
        throw new ImageHelper_Exception(
223
            'Could not create new true color image.',
224
            null,
225
            self::ERROR_CANNOT_CREATE_IMAGE_OBJECT
226
        );
227
    }
228
    
229
   /**
230
    * Factory method: creates an image helper from an
231
    * existing image resource.
232
    *
233
    * Note: while the resource is type independent, the
234
    * type parameter is required for some methods, as well
235
    * as to be able to save the image.
236
    *
237
    * @param resource $resource
238
    * @param string $type The target image type, e.g. "jpeg", "png", etc.
239
    * @return ImageHelper
240
    */
241
    public static function createFromResource($resource, ?string $type=null)
242
    {
243
        self::requireResource($resource);
244
        
245
        return new ImageHelper(null, $resource, $type);
246
    }
247
    
248
   /**
249
    * Factory method: creates an image helper from an
250
    * image file on disk.
251
    *
252
    * @param string $file
253
    * @return ImageHelper
254
    */
255
    public static function createFromFile(string $file) : ImageHelper
256
    {
257
        return new ImageHelper($file);
258
    }
259
    
260
   /**
261
    * Sets a global image helper configuration value. Available
262
    * configuration settings are:
263
    * 
264
    * <ul>
265
    * <li><code>auto-memory-adjustment</code> <i>boolean</i> Whether totry and adjust the memory limit automatically so there will be enough to load/process the target image.</li>
266
    * </ul>
267
    * 
268
    * @param string $name
269
    * @param mixed $value
270
    */
271
    public static function setConfig($name, $value)
272
    {
273
        if(isset(self::$config[$name])) {
274
            self::$config[$name] = $value;
275
        }
276
    }
277
    
278
   /**
279
    * Shorthand for setting the automatic memory adjustment
280
    * global configuration setting.
281
    * 
282
    * @param bool $enabled
283
    */
284
    public static function setAutoMemoryAdjustment($enabled=true)
285
    {
286
        self::setConfig('auto-memory-adjustment', $enabled);
287
    }
288
    
289
   /**
290
    * Duplicates an image resource.
291
    * @param resource $img
292
    * @return resource
293
    */
294
    protected function duplicateImage($img)
295
    {
296
        self::requireResource($img);
297
        
298
        $width = imagesx($img);
299
        $height = imagesy($img);
300
        $duplicate = $this->createNewImage($width, $height);
301
        imagecopy($duplicate, $img, 0, 0, 0, 0, $width, $height);
302
        return $duplicate;
303
    }
304
    
305
   /**
306
    * Duplicates the current state of the image into a new
307
    * image helper instance.
308
    * 
309
    * @return ImageHelper
310
    */
311
    public function duplicate()
312
    {
313
        return ImageHelper::createFromResource($this->duplicateImage($this->newImage));
314
    }
315
316
    public function enableAlpha()
317
    {
318
        if(!$this->alpha) 
319
        {
320
            self::addAlphaSupport($this->newImage, false);
321
            $this->alpha = true;
322
        }
323
        
324
        return $this;
325
    }
326
    
327
    public function resize($width, $height)
328
    {
329
        $new = $this->createNewImage($width, $height);
330
        
331
        imagecopy($new, $this->newImage, 0, 0, 0, 0, $width, $height);
332
        
333
        $this->setNewImage($new);
334
        
335
        return $this;
336
    }
337
    
338
    public function getNewSize()
339
    {
340
        return array($this->newWidth, $this->newHeight);
341
    }
342
    
343
    /**
344
     * Sharpens the image by the specified percentage.
345
     *
346
     * @param number $percent
347
     * @return ImageHelper
348
     */
349
    public function sharpen($percent=0)
350
    {
351
        if($percent <= 0) {
352
            return $this;
353
        }
354
        
355
        // the factor goes from 0 to 64 for sharpening.
356
        $factor = $percent * 64 / 100;
357
        return $this->convolute($factor);
358
    }
359
    
360
    public function blur($percent=0)
361
    {
362
        if($percent <= 0) {
363
            return $this;
364
        }
365
        
366
        // the factor goes from -64 to 0 for blurring.
367
        $factor = ($percent * 64 / 100) * -1;
368
        return $this->convolute($factor);
369
    }
370
    
371
    protected function convolute($factor)
372
    {
373
        // get a value thats equal to 64 - abs( factor )
374
        // ( using min/max to limited the factor to 0 - 64 to not get out of range values )
375
        $val1Adjustment = 64 - min( 64, max( 0, abs( $factor ) ) );
376
        
377
        // the base factor for the "current" pixel depends on if we are blurring or sharpening.
378
        // If we are blurring use 1, if sharpening use 9.
379
        $val1Base = 9;
380
        if( abs( $factor ) != $factor ) {
381
            $val1Base = 1;
382
        }
383
        
384
        // value for the center/currrent pixel is:
385
        //  1 + 0 - max blurring
386
        //  1 + 64- minimal blurring
387
        //  9 + 64- minimal sharpening
388
        //  9 + 0 - maximum sharpening
389
        $val1 = $val1Base + $val1Adjustment;
390
        
391
        // the value for the surrounding pixels is either positive or negative depending on if we are blurring or sharpening.
392
        $val2 = -1;
393
        if( abs( $factor ) != $factor ) {
394
            $val2 = 1;
395
        }
396
        
397
        // setup matrix ..
398
        $matrix = array(
399
            array( $val2, $val2, $val2 ),
400
            array( $val2, $val1, $val2 ),
401
            array( $val2, $val2, $val2 )
402
        );
403
        
404
        // calculate the correct divisor
405
        // actual divisor is equal to "$divisor = $val1 + $val2 * 8;"
406
        // but the following line is more generic
407
        $divisor = array_sum( array_map( 'array_sum', $matrix ) );
408
        
409
        // apply the matrix
410
        imageconvolution( $this->newImage, $matrix, $divisor, 0 );
411
        
412
        return $this;
413
    }
414
    
415
    /**
416
     * Whether the image is an SVG image.
417
     * @return boolean
418
     */
419
    public function isTypeSVG()
420
    {
421
        return $this->type === 'svg';
422
    }
423
    
424
    /**
425
     * Whether the image is a PNG image.
426
     * @return boolean
427
     */
428
    public function isTypePNG()
429
    {
430
        return $this->type === 'png';
431
    }
432
    
433
    /**
434
     * Whether the image is a JPEG image.
435
     * @return boolean
436
     */
437
    public function isTypeJPEG()
438
    {
439
        return $this->type === 'jpeg';
440
    }
441
    
442
    /**
443
     * Whether the image is a vector image.
444
     * @return boolean
445
     */
446
    public function isVector()
447
    {
448
        return $this->isTypeSVG();
449
    }
450
    
451
    /**
452
     * Retrieves the type of the image.
453
     * @return string e.g. "jpeg", "png"
454
     */
455
    public function getType() : string
456
    {
457
        return $this->type;
458
    }
459
    
460
    /**
461
     * Calculates the size of the image by the specified width,
462
     * and returns an indexed array with the width and height size.
463
     *
464
     * @param integer $width
465
     * @return ImageHelper_Size
466
     */
467
    public function getSizeByWidth(int $width) : ImageHelper_Size
468
    {
469
        $height = floor(($width * $this->height) / $this->width);
470
        
471
        return new ImageHelper_Size(array(
472
            $width,
473
            $height,
474
            $this->info['bits'],
475
            $this->info['channels']
476
        ));
477
    }
478
    
479
    /**
480
     * Calculates the size of the image by the specified height,
481
     * and returns an indexed array with the width and height size.
482
     *
483
     * @param integer $height
484
     * @return ImageHelper_Size
485
     */
486
    public function getSizeByHeight($height) : ImageHelper_Size
487
    {
488
        $width = floor(($height * $this->width) / $this->height);
489
        
490
        return new ImageHelper_Size(array(
491
            $width,
492
            $height,
493
            $this->info['bits'],
494
            $this->info['channels']
495
        ));
496
    }
497
    
498
   /**
499
    * Resamples the image to a new width, maintaining
500
    * aspect ratio.
501
    * 
502
    * @param int $width
503
    * @return ImageHelper
504
    */
505
    public function resampleByWidth(int $width) : ImageHelper
506
    {
507
        $size = $this->getSizeByWidth($width);
508
509
        $this->resampleImage($size->getWidth(), $size->getHeight());
510
        
511
        return $this;
512
    }
513
514
   /**
515
    * Resamples the image by height, and creates a new image file on disk.
516
    * 
517
    * @param int $height
518
    * @return ImageHelper
519
    */
520
    public function resampleByHeight($height) : ImageHelper
521
    {
522
        $size = $this->getSizeByHeight($height);
523
524
        return $this->resampleImage($size->getWidth(), $size->getHeight());
525
    }
526
527
   /**
528
    * Resamples the image without keeping the aspect ratio.
529
    * 
530
    * @param int $width
531
    * @param int $height
532
    * @return ImageHelper
533
    */
534
    public function resample(?int $width = null, ?int $height = null) : ImageHelper
535
    {
536
        if($this->isVector()) {
537
            return $this;
538
        }
539
        
540
        if ($width === null && $height === null) {
541
            return $this->resampleByWidth($this->width);
542
        }
543
544
        if (empty($width)) {
545
            return $this->resampleByHeight($height);
546
        }
547
548
        if (empty($height)) {
549
            return $this->resampleByWidth($width);
550
        }
551
552
        return $this->resampleAndCrop($width, $height);
553
    }
554
555
    public function resampleAndCrop($width, $height) : ImageHelper
556
    {
557
        if($this->isVector()) {
558
            return $this;
559
        }
560
561
        if ($this->width <= $this->height) 
562
        {
563
            $this->resampleByWidth($width);
564
        } 
565
        else 
566
        {
567
            $this->resampleByHeight($height);
568
        }
569
        
570
        $newCanvas = $this->createNewImage($width, $height);
571
        
572
        // and now we can add the crop
573
        if (!imagecopy(
574
            $newCanvas,
575
            $this->newImage,
576
            0, // destination X
577
            0, // destination Y
578
            0, // source X
579
            0, // source Y
580
            $width,
581
            $height
582
        )
583
        ) {
584
            throw new ImageHelper_Exception(
585
                'Error creating new image',
586
                'Cannot create crop of the image',
587
                self::ERROR_CANNOT_CREATE_IMAGE_CROP
588
            );
589
        }
590
591
        $this->setNewImage($newCanvas);
592
593
        return $this;
594
    }
595
    
596
    protected $alpha = false;
597
598
   /**
599
    * Configures the specified image resource to make it alpha compatible.
600
    * 
601
    * @param resource $canvas
602
    * @param bool $fill Whether to fill the whole canvas with the transparency
603
    */
604
    public static function addAlphaSupport($canvas, $fill=true)
605
    {
606
        self::requireResource($canvas);
607
        
608
        imagealphablending($canvas,true);
609
        imagesavealpha($canvas, true);
610
611
        if($fill) {
612
            self::fillImageTransparent($canvas);
613
        }
614
    }
615
    
616
    public function isAlpha()
617
    {
618
        return $this->alpha;
619
    }
620
621
    public function save(string $targetFile, $dispose=true)
622
    {
623
        if($this->isVector()) {
624
            return true;
625
        }
626
        
627
        if(!is_resource($this->newImage)) {
628
            throw new ImageHelper_Exception(
629
                'Error creating new image',
630
                'Cannot save an image, no valid image resource was created. You have to call one of the resample methods to create a new image.',
631
                self::ERROR_SAVE_NO_IMAGE_CREATED
632
            );
633
        }
634
635
        if (file_exists($targetFile)) {
636
            unlink($targetFile);
637
        }
638
        
639
        $method = 'image' . $this->type;
640
        if (!$method($this->newImage, $targetFile, $this->resolveQuality())) {
641
            throw new ImageHelper_Exception(
642
                'Error creating new image',
643
                sprintf(
644
                    'The %s method could not write the new image to %s',
645
                    $method,
646
                    $targetFile
647
                ),
648
                self::ERROR_CANNOT_WRITE_NEW_IMAGE_FILE
649
            );
650
        }
651
652
        if (filesize($targetFile) < 1) {
653
            throw new ImageHelper_Exception(
654
                'Error creating new image',
655
                'Resampling completed sucessfully, but the generated file is 0 bytes big.',
656
                self::ERROR_CREATED_AN_EMPTY_FILE
657
            );
658
        }
659
660
        if($dispose) {
661
            $this->dispose();
662
        }
663
        
664
        return true;
665
    }
666
    
667
    public function dispose()
668
    {
669
        if(is_resource($this->sourceImage)) {
670
            imagedestroy($this->sourceImage);
671
        }
672
        
673
        if(is_resource($this->newImage)) {
674
            imagedestroy($this->newImage);
675
        }
676
    }
677
678
    protected function resolveQuality()
679
    {
680
        switch ($this->type) {
681
            case 'png':
682
                return 0;
683
684
            case 'jpeg':
685
                return $this->quality;
686
687
            default:
688
                return 0;
689
        }
690
    }
691
692
    /**
693
     * Sets the quality for image types like jpg that use compression.
694
     * @param int $quality
695
     */
696
    public function setQuality($quality)
697
    {
698
        $quality = $quality * 1;
699
        if ($quality < 0) {
700
            throw new ImageHelper_Exception(
701
                'Invalid configuration',
702
                'Cannot set a quality less than 0.',
703
                self::ERROR_QUALITY_VALUE_BELOW_ZERO
704
            );
705
        }
706
707
        if ($quality > 100) {
708
            throw new ImageHelper_Exception(
709
                'Invalid configuration',
710
                'Cannot set a quality higher than 100.',
711
                self::ERROR_QUALITY_ABOVE_ONE_HUNDRED
712
            );
713
        }
714
715
        $this->quality = $quality * 1;
716
    }
717
718
   /**
719
    * Attempts to adjust the memory to the required size
720
    * to work with the current image.
721
    * 
722
    * @return boolean
723
    */
724
    protected function adjustMemory() : bool
725
    {
726
        if(!self::$config['auto-memory-adjustment']) {
727
            return true;
728
        }
729
        
730
        $MB = 1048576; // number of bytes in 1M
731
        $K64 = 65536; // number of bytes in 64K
732
        $tweakFactor = 25; // magic adjustment value as safety threshold
733
        $memoryNeeded = ceil(
734
            (
735
                $this->info->getWidth() 
736
                * 
737
                $this->info->getHeight() 
738
                * 
739
                $this->info->getBits() 
740
                * 
741
                ($this->info->getChannels() / 8) 
742
                + 
743
                $K64
744
            )
745
            * $tweakFactor
746
        );
747
748
        //ini_get('memory_limit') only works if compiled with "--enable-memory-limit" also
749
        //default memory limit is 8MB so we will stick with that.
750
        $memoryLimit = 8 * $MB;
751
            
752
        if (function_exists('memory_get_usage') && memory_get_usage() + $memoryNeeded > $memoryLimit) {
753
            $newLimit = ($memoryLimit + (memory_get_usage() + $memoryNeeded)) / $MB;
754
            $newLimit = ceil($newLimit);
755
            ini_set('memory_limit', $newLimit . 'M');
756
757
            return true;
758
        }
759
760
        return false;
761
    }
762
763
   /**
764
    * Stretches the image to the specified dimensions.
765
    * 
766
    * @param int $width
767
    * @param int $height
768
    * @return ImageHelper
769
    */
770
    public function stretch(int $width, int $height) : ImageHelper
771
    {
772
        return $this->resampleImage($width, $height);
773
    }
774
775
   /**
776
    * Creates a new image from the current image,
777
    * resampling it to the new size.
778
    * 
779
    * @param int $newWidth
780
    * @param int $newHeight   
781
    * @throws ImageHelper_Exception
782
    * @return ImageHelper
783
    */
784
    protected function resampleImage(int $newWidth, int $newHeight) : ImageHelper
785
    {
786
        if($this->isVector()) {
787
            return $this;
788
        }
789
790
        if($this->newWidth==$newWidth && $this->newHeight==$newHeight) {
791
            return $this;
792
        }
793
        
794
        if($newWidth < 1) { $newWidth = 1; }
795
        if($newHeight < 1) { $newHeight = 1; }
796
        
797
        $this->adjustMemory();
798
799
        $new = $this->createNewImage($newWidth, $newHeight);
800
       
801
        if (!imagecopyresampled($new, $this->newImage, 0, 0, 0, 0, $newWidth, $newHeight, $this->newWidth, $this->newHeight)) 
802
        {
803
            throw new ImageHelper_Exception(
804
                'Error creating new image',
805
                'Cannot copy resampled image data',
806
                self::ERROR_CANNOT_COPY_RESAMPLED_IMAGE_DATA
807
            );
808
        }
809
810
        $this->setNewImage($new);
811
812
        return $this;
813
    }
814
815
    /**
816
     * Gets the image type for the specified file name.
817
     * Like {@link getImageType()}, except that it automatically
818
     * extracts the file extension from the file name.
819
     *
820
     * @param string $fileName
821
     * @return string|NULL
822
     * @see getImageType()
823
     */
824
    public static function getFileImageType($fileName)
825
    {
826
        return self::getImageType(strtolower(pathinfo($fileName, PATHINFO_EXTENSION)));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($fileName, AppUtils\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

826
        return self::getImageType(strtolower(/** @scrutinizer ignore-type */ pathinfo($fileName, PATHINFO_EXTENSION)));
Loading history...
827
    }
828
829
    /**
830
     * Gets the image type for the specified file extension,
831
     * or NULL if the extension is not among the supported
832
     * file types.
833
     *
834
     * @param string $extension
835
     * @return string|NULL
836
     */
837
    public static function getImageType($extension)
838
    {
839
        if (isset(self::$imageTypes[$extension])) {
840
            return self::$imageTypes[$extension];
841
        }
842
843
        return null;
844
    }
845
846
    public static function getImageTypes()
847
    {
848
        $types = array_values(self::$imageTypes);
849
        return array_unique($types);
850
    }
851
    
852
   /**
853
    * Displays an existing image resource.
854
    *
855
    * @param resource $resource
856
    * @param string $imageType The image format to send, i.e. "jpeg", "png"
857
    * @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
858
    *
859
    * @throws ImageHelper_Exception
860
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
861
    * @see ImageHelper::ERROR_INVALID_STREAM_IMAGE_TYPE
862
    */
863
    public static function displayImageStream($resource, string $imageType, int $quality=-1) : void
864
    {
865
        self::requireResource($resource);
866
867
        $imageType = self::requireValidStreamType($imageType);
868
        
869
        header('Content-type:image/' . $imageType);
870
871
        $function = 'image' . $imageType;
872
        
873
        $function($resource, null, $quality);
874
    }
875
876
    /**
877
     * @param string $imageType
878
     * @return string
879
     *
880
     * @throws ImageHelper_Exception
881
     * @see ImageHelper::ERROR_INVALID_STREAM_IMAGE_TYPE
882
     * @see ImageHelper::$streamTypes
883
     */
884
    public static function requireValidStreamType(string $imageType) : string
885
    {
886
        $imageType = strtolower($imageType);
887
888
        if(in_array($imageType, self::$streamTypes))
889
        {
890
            return $imageType;
891
        }
892
893
        throw new ImageHelper_Exception(
894
            'Invalid image stream type',
895
            sprintf(
896
                'The image type [%s] cannot be used for a stream.',
897
                $imageType
898
            ),
899
            self::ERROR_INVALID_STREAM_IMAGE_TYPE
900
        );
901
    }
902
903
    /**
904
     * Displays an image from an existing image file.
905
     * @param string $imageFile
906
     */
907
    public static function displayImage(string $imageFile) : void
908
    {
909
        $file = null;
910
        $line = null;
911
        if (headers_sent($file, $line)) {
912
            throw new ImageHelper_Exception(
913
                'Error displaying image',
914
                'Headers have already been sent: in file ' . $file . ':' . $line,
915
                self::ERROR_HEADERS_ALREADY_SENT
916
            );
917
        }
918
919
        if (!file_exists($imageFile)) {
920
            throw new ImageHelper_Exception(
921
                'Image file does not exist',
922
                sprintf(
923
                    'Cannot display image, the file does not exist on disk: [%s].',
924
                    $imageFile
925
                ),
926
                self::ERROR_IMAGE_FILE_DOES_NOT_EXIST
927
            );
928
        }
929
930
        $format = self::getFileImageType($imageFile);
931
        if($format == 'svg') {
932
            $format = 'svg+xml';
933
        }
934
935
        $contentType = 'image/' . $format;
936
        
937
        header('Content-Type: '.$contentType);
938
        header("Last-Modified: " . gmdate("D, d M Y H:i:s", filemtime($imageFile)) . " GMT");
939
        header('Cache-Control: public');
940
        header('Content-Length: ' . filesize($imageFile));
941
942
        readfile($imageFile);
943
    }
944
    
945
   /**
946
    * Displays the current image.
947
    *
948
    * NOTE: You must call `exit()` manually after this.
949
    */
950
    public function display() : void
951
    {
952
        $this->displayImageStream(
953
            $this->newImage,
954
            $this->getType(),
955
            $this->resolveQuality()
956
        );
957
    }
958
959
    /**
960
     * Trims the current loaded image.
961
     *
962
     * @param array|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.
963
     *
964
     * @throws ImageHelper_Exception
965
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
966
     * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_CANVAS
967
     */
968
    public function trim(?array $color=null) : ImageHelper
969
    {
970
        return $this->trimImage($this->newImage, $color);
971
    }
972
    
973
   /**
974
    * Retrieves a color definition by its index.
975
    * 
976
    * @param resource $img A valid image resource.
977
    * @param int $colorIndex The color index, as returned by `imagecolorat` for example.
978
    * @return array<string,int> An array with red, green, blue and alpha keys.
979
    *
980
    * @throws ImageHelper_Exception
981
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
982
    */
983
    public function getIndexedColors($img, int $colorIndex) : array
984
    {
985
        self::requireResource($img);
986
987
        $color = imagecolorsforindex($img, $colorIndex);
988
        
989
        // it seems imagecolorsforindex may return false (undocumented, unproven)
990
        if(is_array($color)) {
0 ignored issues
show
introduced by
The condition is_array($color) is always true.
Loading history...
991
            return $color;
992
        }
993
        
994
        return array(
995
            'red' => 0,
996
            'green' => 0,
997
            'blue' => 0,
998
            'alpha' => 1
999
        );
1000
    }
1001
        
1002
   /**
1003
    * Trims the specified image resource by removing the specified color.
1004
    * Also works with transparency.
1005
    * 
1006
    * @param resource $img
1007
    * @param array|NULL $color A color definition, as an associative array with red, green, blue and alpha keys. If not specified, the color at pixel position 0,0 will be used.
1008
    * @return ImageHelper
1009
    *
1010
    * @throws ImageHelper_Exception
1011
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
1012
    * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_CANVAS
1013
    */
1014
    protected function trimImage($img, ?array $color=null) : ImageHelper
1015
    {
1016
        if($this->isVector()) {
1017
            return $this;
1018
        }
1019
1020
        self::requireResource($img);
1021
        
1022
        if(empty($color)) 
1023
        {
1024
            $color = imagecolorat($img, 0, 0);
1025
            $color = $this->getIndexedColors($img, $color);
1026
        }
1027
        
1028
        // Get the image width and height.
1029
        $imw = imagesx($img);
1030
        $imh = imagesy($img);
1031
1032
        // Set the X variables.
1033
        $xmin = $imw;
1034
        $xmax = 0;
1035
        $ymin = null;
1036
        $ymax = null;
1037
         
1038
        // Start scanning for the edges.
1039
        for ($iy=0; $iy<$imh; $iy++)
1040
        {
1041
            $first = true;
1042
            
1043
            for ($ix=0; $ix<$imw; $ix++)
1044
            {
1045
                $ndx = imagecolorat($img, $ix, $iy);
1046
                $colors = $this->getIndexedColors($img, $ndx);
1047
                
1048
                if(!$this->colorsMatch($colors, $color)) 
1049
                {
1050
                    if ($xmin > $ix) { $xmin = $ix; }
1051
                    if ($xmax < $ix) { $xmax = $ix; }
1052
                    if (!isset($ymin)) { $ymin = $iy; }
1053
                    
1054
                    $ymax = $iy;
1055
                    
1056
                    if($first)
1057
                    { 
1058
                        $ix = $xmax; 
1059
                        $first = false; 
1060
                    }
1061
                }
1062
            }
1063
        }
1064
        
1065
        // no trimming border found
1066
        if($ymax === null) {
1067
            return $this;
1068
        }
1069
        
1070
        // The new width and height of the image. 
1071
        $imw = 1+$xmax-$xmin; // Image width in pixels
1072
        $imh = 1+$ymax-$ymin; // Image height in pixels
1073
1074
        // Make another image to place the trimmed version in.
1075
        $im2 = $this->createNewImage($imw, $imh);
1076
        
1077
        if($color['alpha'] > 0) 
1078
        {
1079
            $bg2 = imagecolorallocatealpha($im2, $color['red'], $color['green'], $color['blue'], $color['alpha']);
1080
            imagecolortransparent($im2, $bg2);
1081
        }
1082
        else
1083
        {
1084
            $bg2 = imagecolorallocate($im2, $color['red'], $color['green'], $color['blue']);
1085
        }
1086
        
1087
        // Make the background of the new image the same as the background of the old one.
1088
        imagefill($im2, 0, 0, $bg2);
1089
1090
        // Copy it over to the new image.
1091
        imagecopy($im2, $img, 0, 0, $xmin, $ymin, $imw, $imh);
1092
        
1093
        // To finish up, we replace the old image which is referenced.
1094
        imagedestroy($img);
1095
        
1096
        $this->setNewImage($im2);
1097
1098
        return $this;
1099
    }
1100
1101
    /**
1102
     * Sets the new image after a transformation operation:
1103
     * automatically adjusts the new size information.
1104
     *
1105
     * @param resource $image
1106
     *
1107
     * @throws ImageHelper_Exception
1108
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1109
     */
1110
    protected function setNewImage($image) : ImageHelper
1111
    {
1112
        self::requireResource($image);
1113
        
1114
        $this->newImage = $image;
1115
        $this->newWidth = imagesx($image);
1116
        $this->newHeight= imagesy($image);
1117
1118
        return $this;
1119
    }
1120
    
1121
   /**
1122
    * Requires the subject to be a resource.
1123
    * 
1124
    * @param resource|mixed $subject
1125
    *
1126
    * @throws ImageHelper_Exception
1127
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
1128
    */
1129
    protected static function requireResource($subject) : void
1130
    {
1131
        if(is_resource($subject)) {
1132
            return;
1133
        }
1134
        
1135
        throw new ImageHelper_Exception(
1136
            'Not an image resource',
1137
            sprintf(
1138
                'Specified image should be a resource, [%s] given.',
1139
                gettype($subject)
1140
            ),
1141
            self::ERROR_NOT_A_RESOURCE
1142
        );
1143
    }
1144
    
1145
   /**
1146
    * Creates a new image resource, with transparent background.
1147
    * 
1148
    * @param int $width
1149
    * @param int $height
1150
    * @throws ImageHelper_Exception
1151
    * @return resource
1152
    */
1153
    protected function createNewImage(int $width, int $height)
1154
    {
1155
        $img = imagecreatetruecolor($width, $height);
1156
        
1157
        if($img === false) 
1158
        {
1159
            throw new ImageHelper_Exception(
1160
                'Error creating new image',
1161
                'Cannot create new image canvas',
1162
                self::ERROR_CANNOT_CREATE_IMAGE_CANVAS
1163
            );
1164
        }
1165
1166
        self::addAlphaSupport($img, true);
0 ignored issues
show
Bug introduced by
It seems like $img can also be of type GdImage; however, parameter $canvas of AppUtils\ImageHelper::addAlphaSupport() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1166
        self::addAlphaSupport(/** @scrutinizer ignore-type */ $img, true);
Loading history...
1167
        
1168
        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...
1169
    }
1170
    
1171
   /**
1172
    * Whether the two specified colors are the same.
1173
    * 
1174
    * @param array<string,int> $a
1175
    * @param array<string,int> $b
1176
    * @return boolean
1177
    */
1178
	protected function colorsMatch(array $a, array $b) : bool
1179
	{
1180
		$parts = array('red', 'green', 'blue');
1181
		foreach($parts as $part) {
1182
			if($a[$part] != $b[$part]) {
1183
				return false;
1184
			}
1185
		} 
1186
		
1187
		return true;
1188
	}
1189
	
1190
	public function fillWhite($x=0, $y=0)
1191
	{
1192
	    $this->addRGBColor('white', 255, 255, 255);
1193
        return $this->fill('white', $x, $y);
1194
	}
1195
	
1196
	public function fillTransparent() : ImageHelper
1197
	{
1198
        $this->enableAlpha();
1199
	    
1200
	    self::fillImageTransparent($this->newImage);
1201
	    
1202
	    return $this;
1203
	}
1204
	
1205
	public static function fillImageTransparent($resource)
1206
	{
1207
	    self::requireResource($resource);
1208
	    
1209
	    $transparent = imagecolorallocatealpha($resource, 89, 14, 207, 127);
1210
	    imagecolortransparent ($resource, $transparent);
1211
	    imagefill($resource, 0, 0, $transparent);
1212
	}
1213
	
1214
	public function fill($colorName, $x=0, $y=0)
1215
	{
1216
	    imagefill($this->newImage, $x, $y, $this->colors[$colorName]);
1217
	    return $this;
1218
	}
1219
	
1220
    protected $colors = array();
1221
1222
    public function addRGBColor($name, $red, $green, $blue)
1223
    {
1224
        $this->colors[$name] = imagecolorallocate($this->newImage, $red, $green, $blue);
1225
        return $this;
1226
    }
1227
    
1228
    public function textTTF($text, $size, $colorName, $x=0, $y=0, $angle=0)
1229
    {
1230
        imagealphablending($this->newImage, true);
1231
        
1232
        imagettftext($this->newImage, $size, $angle, $x, $y, $this->colors[$colorName], $this->TTFFile, $text);
1233
        
1234
        imagealphablending($this->newImage, false);
1235
        
1236
        return $this;
1237
    }
1238
    
1239
   /**
1240
    * @return resource
1241
    */
1242
    public function getImage()
1243
    {
1244
        return $this->newImage;
1245
    }
1246
    
1247
    public function paste(ImageHelper $target, $xpos=0, $ypos=0, $sourceX=0, $sourceY=0)
1248
    {
1249
        $img = $target->getImage();
1250
        
1251
        if($target->isAlpha()) {
1252
            $this->enableAlpha();
1253
        }
1254
        
1255
        imagecopy($this->newImage, $img, $xpos, $ypos, $sourceX, $sourceY, imagesx($img), imagesy($img));
1256
        return $this;
1257
    }
1258
    
1259
   /**
1260
    * Retrieves the size of the image.
1261
    * 
1262
    * @return ImageHelper_Size
1263
    * @throws ImageHelper_Exception
1264
    * @see ImageHelper::ERROR_CANNOT_GET_IMAGE_SIZE
1265
    */
1266
	public function getSize() : ImageHelper_Size
1267
    {
1268
	    return self::getImageSize($this->newImage);
1269
    }
1270
    
1271
    protected $TTFFile;
1272
    
1273
   /**
1274
    * Sets the TTF font file to use for text operations.
1275
    * 
1276
    * @param string $filePath
1277
    * @return ImageHelper
1278
    */
1279
    public function setFontTTF($filePath)
1280
    {
1281
        $this->TTFFile = $filePath;
1282
        return $this;
1283
    }
1284
    
1285
    /**
1286
     * Goes through a series of text sizes to find the closest match to
1287
     * fit the text into the target width.
1288
     *
1289
     * @param string $text
1290
     * @param integer $matchWidth
1291
     * @return array
1292
     */
1293
    public function fitText($text, $matchWidth)
1294
    {
1295
        $sizes = array();
1296
        for($i=1; $i<=1000; $i=$i+0.1) {
1297
            $size = $this->calcTextSize($text, $i);
1298
            $sizes[] = $size;
1299
            if($size['width'] >= $matchWidth) {
1300
                break;
1301
            }
1302
        }
1303
    
1304
        $last = array_pop($sizes);
1305
        $prev = array_pop($sizes);
1306
    
1307
        // determine which is the closest match, and use that
1308
        $diffLast = $last['width'] - $matchWidth;
1309
        $diffPrev = $matchWidth - $prev['width'];
1310
    
1311
        if($diffLast <= $diffPrev) {
1312
            return $last;
1313
        }
1314
    
1315
        return $prev;
1316
    }
1317
    
1318
    public function calcTextSize($text, $size)
1319
    {
1320
        $this->requireTTFFont();
1321
        
1322
        $box = imagettfbbox($size, 0, $this->TTFFile, $text);
1323
    
1324
        $left = $box[0];
1325
        $right = $box[4];
1326
        $bottom = $box[1];
1327
        $top = $box[7];
1328
    
1329
        return array(
1330
            'size' => $size,
1331
            'top_left_x' => $box[6],
1332
            'top_left_y' => $box[7],
1333
            'top_right_x' => $box[4],
1334
            'top_right_y' => $box[5],
1335
            'bottom_left_x' => $box[0],
1336
            'bottom_left_y' => $box[1],
1337
            'bottom_right_x' => $box[2],
1338
            'bottom_right_y' => $box[3],
1339
            'width' => $right-$left,
1340
            'height' => $bottom-$top
1341
        );
1342
    }
1343
    
1344
    protected function requireTTFFont()
1345
    {
1346
        if(isset($this->TTFFile)) {
1347
            return;
1348
        }
1349
        
1350
	    throw new ImageHelper_Exception(
1351
            'No true type font specified',
1352
            'This functionality requires a TTF font file to be specified with the [setFontTTF] method.',
1353
            self::ERROR_NO_TRUE_TYPE_FONT_SET    
1354
        );
1355
    }
1356
    
1357
   /**
1358
	 * Retrieves the size of an image file on disk, or
1359
	 * an existing image resource.
1360
	 *
1361
	 * <pre>
1362
	 * array(
1363
	 *     0: (width),
1364
	 *     1: (height),
1365
	 *     "channels": the amount of channels
1366
	 *     "bits": bits per channel
1367
     * )     
1368
	 * </pre>
1369
	 *
1370
	 * @param string|resource $pathOrResource
1371
	 * @return ImageHelper_Size Size object, can also be accessed like the traditional array from getimagesize
1372
	 * @see ImageHelper_Size
1373
	 * @throws ImageHelper_Exception
1374
	 * @see ImageHelper::ERROR_CANNOT_GET_IMAGE_SIZE
1375
	 * @see ImageHelper::ERROR_CANNOT_READ_SVG_IMAGE
1376
	 * @see ImageHelper::ERROR_SVG_SOURCE_VIEWBOX_MISSING
1377
	 * @see ImageHelper::ERROR_SVG_VIEWBOX_INVALID
1378
	 */
1379
	public static function getImageSize($pathOrResource) : ImageHelper_Size
1380
	{
1381
	    if(is_resource($pathOrResource)) 
1382
	    {
1383
	        return new ImageHelper_Size(array(
1384
	            'width' => imagesx($pathOrResource),
1385
	            'height' => imagesy($pathOrResource),
1386
	            'channels' => 1,
1387
	            'bits' => 8
1388
	        ));
1389
	    }
1390
	    
1391
	    $type = self::getFileImageType($pathOrResource);
1392
	    
1393
	    $info = false;
1394
	    $method = 'getImageSize_'.$type;
1395
	    if(method_exists(__CLASS__, $method)) 
1396
	    {
1397
	        $info = call_user_func(array(__CLASS__, $method), $pathOrResource);
1398
	    } 
1399
	    else 
1400
	    {
1401
	        $info = getimagesize($pathOrResource);
1402
	    }
1403
	    
1404
	    if($info !== false) {
1405
	        return new ImageHelper_Size($info);
1406
	    }
1407
	    
1408
        throw new ImageHelper_Exception(
1409
            'Error opening image file',
1410
            sprintf(
1411
                'Could not get image size for image [%s]',
1412
                $pathOrResource
1413
            ),
1414
            self::ERROR_CANNOT_GET_IMAGE_SIZE
1415
        );
1416
	}
1417
	
1418
   /**
1419
    * @param string $imagePath
1420
    * @throws ImageHelper_Exception
1421
    * @return array
1422
    * 
1423
    * @todo This should return a ImageHelper_Size instance.
1424
    */
1425
	protected static function getImageSize_svg(string $imagePath) : array
1426
	{
1427
	    $xml = XMLHelper::createSimplexml();
1428
	    $xml->loadFile($imagePath);
1429
	    
1430
	    if($xml->hasErrors()) {
1431
	        throw new ImageHelper_Exception(
1432
	            'Error opening SVG image',
1433
	            sprintf(
1434
	                'The XML content of the image [%s] could not be parsed.',
1435
	                $imagePath
1436
                ),
1437
	            self::ERROR_CANNOT_READ_SVG_IMAGE
1438
            );
1439
	    }
1440
	    
1441
	    $data = $xml->toArray();
1442
	    $xml->dispose();
1443
	    unset($xml);
1444
	    
1445
	    if(!isset($data['@attributes']) || !isset($data['@attributes']['viewBox'])) {
1446
	        throw new ImageHelper_Exception(
1447
	            'SVG Image is corrupted',
1448
	            sprintf(
1449
	                'The [viewBox] attribute is missing in the XML of the image at path [%s].',
1450
	                $imagePath
1451
                ),
1452
	            self::ERROR_SVG_SOURCE_VIEWBOX_MISSING
1453
            );
1454
	    }
1455
	    
1456
	    $svgWidth = parseNumber($data['@attributes']['width'])->getNumber();
1457
	    $svgHeight = parseNumber($data['@attributes']['height'])->getNumber();
1458
	    
1459
	    $viewBox = str_replace(' ', ',', $data['@attributes']['viewBox']);
1460
	    $size = explode(',', $viewBox);
1461
	    
1462
	    if(count($size) != 4) 
1463
	    {
1464
	        throw new ImageHelper_Exception(
1465
	            'SVG image has an invalid viewBox attribute',
1466
	            sprintf(
1467
	               'The [viewBox] attribute does not have an expected value: [%s] in path [%s].',
1468
	                $viewBox,
1469
	                $imagePath
1470
                ),
1471
	            self::ERROR_SVG_VIEWBOX_INVALID
1472
            );
1473
	    }
1474
	    
1475
	    $boxWidth = $size[2];
1476
	    $boxHeight = $size[3];
1477
	    
1478
	    // calculate the x and y units of the document: 
1479
	    // @see http://tutorials.jenkov.com/svg/svg-viewport-view-box.html#viewbox
1480
	    //
1481
	    // The viewbox combined with the width and heigt of the svg
1482
	    // allow calculating how many pixels are in one unit of the 
1483
	    // width and height of the document.
1484
        //
1485
	    $xUnits = $svgWidth / $boxWidth;
1486
	    $yUnits = $svgHeight / $boxHeight;
1487
	    
1488
	    $pxWidth = $xUnits * $svgWidth;
1489
	    $pxHeight = $yUnits * $svgHeight;
1490
	    
1491
	    return array(
1492
	        $pxWidth,
1493
	        $pxHeight,
1494
	        'bits' => 8
1495
	    );
1496
	}
1497
	
1498
	/**
1499
    * Crops the image to the specified width and height, optionally
1500
    * specifying the origin position to crop from.
1501
    * 
1502
    * @param integer $width
1503
    * @param integer $height
1504
    * @param integer $x
1505
    * @param integer $y
1506
    * @return ImageHelper
1507
    */
1508
    public function crop(int $width, int $height, int $x=0, int $y=0) : ImageHelper
1509
    {
1510
        $new = $this->createNewImage($width, $height);
1511
        
1512
        imagecopy($new, $this->newImage, 0, 0, $x, $y, $width, $height);
1513
        
1514
        $this->setNewImage($new);
1515
        
1516
        return $this;
1517
    }
1518
    
1519
    public function getWidth() : int
1520
    {
1521
        return $this->newWidth;
1522
    }
1523
    
1524
    public function getHeight() : int
1525
    {
1526
        return $this->newHeight;
1527
    }
1528
1529
   /**
1530
    * Calculates the average color value used in 
1531
    * the image. Returns an associative array
1532
    * with the red, green, blue and alpha components,
1533
    * or a HEX color string depending on the selected
1534
    * format.
1535
    * 
1536
    * NOTE: Use the calcAverageColorXXX methods for
1537
    * strict return types. 
1538
    * 
1539
    * @param int $format The format in which to return the color value.
1540
    * @return array|string
1541
    * 
1542
    * @see ImageHelper::calcAverageColorRGB()
1543
    * @see ImageHelper::calcAverageColorHEX()
1544
    */
1545
    public function calcAverageColor(int $format=self::COLORFORMAT_RGB)
1546
    {
1547
        $image = $this->duplicate();
1548
        $image->resample(1, 1);
1549
        
1550
        return $image->getColorAt(0, 0, $format);
1551
    }
1552
    
1553
   /**
1554
    * Calculates the image's average color value, and
1555
    * returns an associative array with red, green,
1556
    * blue and alpha keys.
1557
    * 
1558
    * @throws ImageHelper_Exception
1559
    * @return array
1560
    */
1561
    public function calcAverageColorRGB() : array
1562
    {
1563
       $result = $this->calcAverageColor(self::COLORFORMAT_RGB);
1564
       if(is_array($result)) {
1565
           return $result;
1566
       }
1567
       
1568
       throw new ImageHelper_Exception(
1569
           'Unexpected color value',
1570
           sprintf('Expected an array, got [%s].', gettype($result)),
1571
           self::ERROR_UNEXPECTED_COLOR_VALUE
1572
       );
1573
    }
1574
    
1575
   /**
1576
    * Calculates the image's average color value, and
1577
    * returns a hex color string (without the #).
1578
    * 
1579
    * @throws ImageHelper_Exception
1580
    * @return string
1581
    */
1582
    public function calcAverageColorHex() : string
1583
    {
1584
        $result = $this->calcAverageColor(self::COLORFORMAT_HEX);
1585
        if(is_string($result)) {
1586
            return $result;
1587
        }
1588
        
1589
        throw new ImageHelper_Exception(
1590
            'Unexpected color value',
1591
            sprintf('Expected a hex string, got [%s].', gettype($result)),
1592
            self::ERROR_UNEXPECTED_COLOR_VALUE
1593
        );
1594
    }
1595
    
1596
    public static function rgb2hex(array $rgb) : string
1597
    {
1598
        return sprintf(
1599
            "%02x%02x%02x",
1600
            $rgb['red'],
1601
            $rgb['green'],
1602
            $rgb['blue']
1603
        );
1604
    }
1605
    
1606
    /**
1607
     * Retrieves the color value at the specified pixel
1608
     * coordinates in the image.
1609
     *
1610
     * @param int $x
1611
     * @param int $y
1612
     * @param int $format The format in which to return the color value.
1613
     * @return array|string
1614
     *
1615
     * @see ImageHelper::COLORFORMAT_HEX
1616
     * @see ImageHelper::COLORFORMAT_RGB
1617
     *
1618
     * @throws ImageHelper_Exception
1619
     * @see ImageHelper::ERROR_POSITION_OUT_OF_BOUNDS
1620
     */
1621
    public function getColorAt(int $x, int $y, int $format=self::COLORFORMAT_RGB)
1622
    {
1623
        if($x > $this->getWidth() || $y > $this->getHeight()) 
1624
        {
1625
            throw new ImageHelper_Exception(
1626
                'Position out of bounds',
1627
                sprintf(
1628
                    'The position [%sx%s] does not exist in the image, it is [%sx%s] pixels in size.',
1629
                    $x,
1630
                    $y,
1631
                    $this->getWidth(),
1632
                    $this->getHeight()
1633
                ),
1634
                self::ERROR_POSITION_OUT_OF_BOUNDS
1635
            );
1636
        }
1637
        
1638
        $idx = imagecolorat($this->newImage, $x, $y);
1639
        $rgb = $this->getIndexedColors($this->newImage, $idx);
1640
        
1641
        if($format == self::COLORFORMAT_HEX) {
1642
            return self::rgb2hex($rgb);
1643
        }
1644
1645
        return $rgb;
1646
    }
1647
    
1648
   /**
1649
    * Converts an RGB value to its luminance equivalent.
1650
    * 
1651
    * @param array<string,int> $rgb
1652
    * @return integer Integer, from 0 to 255 (0=black, 255=white)
1653
    */
1654
    public static function rgb2luma(array $rgb) : int
1655
    {
1656
        return (int)floor((($rgb['red']*2)+$rgb['blue']+($rgb['green']*3))/6);
1657
    }
1658
1659
    /**
1660
     * Retrieves the brightness of the image, in percent.
1661
     *
1662
     * @return float
1663
     *
1664
     * @throws ImageHelper_Exception
1665
     * @see ImageHelper::ERROR_UNEXPECTED_COLOR_VALUE
1666
     */
1667
    public function getBrightness() : float
1668
    {
1669
        $luma = self::rgb2luma($this->calcAverageColorRGB());
1670
        return $luma * 100 / 255;
1671
    }
1672
    
1673
   /**
1674
    * Retrieves an md5 hash of the source image file.
1675
    * 
1676
    * NOTE: Only works when the helper has been created
1677
    * from a file. Otherwise, an exception is thrown.
1678
    * 
1679
    * @return string
1680
    * @throws ImageHelper_Exception|OutputBuffering_Exception
1681
    */
1682
    public function getHash() : string
1683
    {
1684
        if($this->newImage === null)
1685
        {
1686
            throw new ImageHelper_Exception(
1687
                'No image loaded to create a hash for.',
1688
                'The newImage property is null.',
1689
                self::ERROR_HASH_NO_IMAGE_LOADED
1690
            );
1691
        }
1692
1693
        OutputBuffering::start();
1694
        imagepng($this->newImage);
1695
        return md5(OutputBuffering::get());
1696
    }
1697
}
1698