Passed
Push — master ( 4f13bc...123cfe )
by Sebastian
02:27
created

ImageHelper::getSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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