ImageHelper::getImageSize_svg()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 70
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 41
c 1
b 0
f 0
dl 0
loc 70
rs 9.264
cc 4
nc 4
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
138
            $this->type = $type;
139
            $this->info = self::getImageSize($resource);
140
        } 
141
        else 
142
        {
143
            $this->file = $sourceFile;
144
            if (!file_exists($this->file)) {
145
                throw new ImageHelper_Exception(
146
                    'Image file does not exist',
147
                    sprintf(
148
                        'Could not find the image file on disk at location [%s]',
149
                        $this->file
150
                    ),
151
                    self::ERROR_IMAGE_FILE_DOES_NOT_EXIST
152
                );
153
            }
154
    
155
            $type = self::getFileImageType($this->file);
156
            if ($type === null) {
157
                throw new ImageHelper_Exception(
158
                    'Error opening image',
159
                    'Not a valid supported image type for image ' . $this->file,
160
                    self::ERROR_UNSUPPORTED_IMAGE_TYPE
161
                );
162
            }
163
164
            $this->type = $type;
165
            $this->info = self::getImageSize($this->file);
166
167
            if(!$this->isVector()) 
168
            {
169
                $method = 'imagecreatefrom' . $this->type;
170
                $this->sourceImage = $method($this->file);
171
                if (!$this->sourceImage) {
172
                    throw new ImageHelper_Exception(
173
                        'Error creating new image',
174
                        $method . ' failed',
175
                        self::ERROR_FAILED_TO_CREATE_NEW_IMAGE
176
                    );
177
                }
178
                
179
                imagesavealpha($this->sourceImage, true);
180
            }
181
        }
182
183
        $this->width = $this->info->getWidth();
184
        $this->height = $this->info->getHeight();
185
186
        if(!$this->isVector()) {
187
            $this->setNewImage($this->duplicateImage($this->sourceImage));
188
        }
189
    }
190
191
   /**
192
    * Factory method: creates a new helper with a blank image.
193
    * 
194
    * @param integer $width
195
    * @param integer $height
196
    * @param string $type The target file type when saving
197
    * @return ImageHelper
198
    * @throws ImageHelper_Exception
199
    *
200
    * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_OBJECT
201
    */
202
    public static function createNew(int $width, int $height, string $type='png') : self
203
    {
204
        $img = imagecreatetruecolor($width, $height);
205
        if($img !== false) {
206
            return self::createFromResource($img, $type);
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

206
            return self::createFromResource(/** @scrutinizer ignore-type */ $img, $type);
Loading history...
207
        }
208
        
209
        throw new ImageHelper_Exception(
210
            'Could not create new true color image.',
211
            null,
212
            self::ERROR_CANNOT_CREATE_IMAGE_OBJECT
213
        );
214
    }
215
216
    /**
217
     * Factory method: creates an image helper from an
218
     * existing image resource.
219
     *
220
     * Note: while the resource is type independent, the
221
     * type parameter is required for some methods, as well
222
     * as to be able to save the image.
223
     *
224
     * @param resource $resource
225
     * @param string|null $type The target image type, e.g. "jpeg", "png", etc.
226
     * @return ImageHelper
227
     * @throws ImageHelper_Exception
228
     */
229
    public static function createFromResource($resource, ?string $type=null) : ImageHelper
230
    {
231
        self::requireResource($resource);
232
        
233
        return new ImageHelper(null, $resource, $type);
234
    }
235
236
    /**
237
     * Factory method: creates an image helper from an
238
     * image file on disk.
239
     *
240
     * @param string $file
241
     * @return ImageHelper
242
     *
243
     * @throws ClassNotExistsException
244
     * @throws ClassNotImplementsException
245
     * @throws ImageHelper_Exception
246
     */
247
    public static function createFromFile(string $file) : ImageHelper
248
    {
249
        return new ImageHelper($file, null, self::getFileImageType($file));
250
    }
251
    
252
   /**
253
    * Sets a global image helper configuration value. Available
254
    * configuration settings are:
255
    * 
256
    * <ul>
257
    * <li><code>auto-memory-adjustment</code> <i>boolean</i> Whether to try and adjust the memory limit automatically so there will be enough to load/process the target image.</li>
258
    * </ul>
259
    * 
260
    * @param string $name
261
    * @param mixed|NULL $value
262
    */
263
    public static function setConfig(string $name, $value) : void
264
    {
265
        if(isset(self::$config[$name])) {
266
            self::$config[$name] = $value;
267
        }
268
    }
269
270
   /**
271
    * Shorthand for setting the automatic memory adjustment
272
    * global configuration setting.
273
    *
274
    * @param bool $enabled
275
    * @return void
276
    */
277
    public static function setAutoMemoryAdjustment(bool $enabled=true) : void
278
    {
279
        self::setConfig('auto-memory-adjustment', $enabled);
280
    }
281
282
    /**
283
     * Duplicates an image resource.
284
     * @param resource $img
285
     * @return resource
286
     * @throws ImageHelper_Exception
287
     */
288
    protected function duplicateImage($img)
289
    {
290
        self::requireResource($img);
291
        
292
        $width = imagesx($img);
293
        $height = imagesy($img);
294
        $duplicate = $this->createNewImage($width, $height);
295
        imagecopy($duplicate, $img, 0, 0, 0, 0, $width, $height);
296
        return $duplicate;
297
    }
298
299
    /**
300
     * Duplicates the current state of the image into a new
301
     * image helper instance.
302
     *
303
     * @return ImageHelper
304
     * @throws ImageHelper_Exception
305
     */
306
    public function duplicate() : ImageHelper
307
    {
308
        return self::createFromResource($this->duplicateImage($this->newImage));
309
    }
310
311
    /**
312
     * @return $this
313
     * @throws ImageHelper_Exception
314
     */
315
    public function enableAlpha() : self
316
    {
317
        if(!$this->alpha) 
318
        {
319
            self::addAlphaSupport($this->newImage, false);
320
            $this->alpha = true;
321
        }
322
        
323
        return $this;
324
    }
325
326
    /**
327
     * @param int $width
328
     * @param int $height
329
     * @return $this
330
     * @throws ImageHelper_Exception
331
     */
332
    public function resize(int $width, int $height) : self
333
    {
334
        $new = $this->createNewImage($width, $height);
335
        
336
        imagecopy($new, $this->newImage, 0, 0, 0, 0, $width, $height);
337
        
338
        $this->setNewImage($new);
339
        
340
        return $this;
341
    }
342
343
    /**
344
     * @return array{0:int,1:int}
345
     */
346
    public function getNewSize() : array
347
    {
348
        return array($this->newWidth, $this->newHeight);
349
    }
350
    
351
    /**
352
     * Sharpens the image by the specified percentage.
353
     *
354
     * @param int|float $percent
355
     * @return $this
356
     */
357
    public function sharpen($percent=0) : self
358
    {
359
        if($percent <= 0) {
360
            return $this;
361
        }
362
        
363
        // the factor goes from 0 to 64 for sharpening.
364
        $factor = $percent * 64 / 100;
365
        return $this->convolute($factor);
366
    }
367
368
    /**
369
     * @param int|float $percent
370
     * @return $this
371
     */
372
    public function blur($percent=0) : self
373
    {
374
        if($percent <= 0) {
375
            return $this;
376
        }
377
        
378
        // the factor goes from -64 to 0 for blurring.
379
        $factor = ($percent * 64 / 100) * -1;
380
        return $this->convolute($factor);
381
    }
382
383
    /**
384
     * @param int|float $factor
385
     * @return $this
386
     */
387
    protected function convolute($factor) : self
388
    {
389
        // get a value that's equal to 64 - abs( factor )
390
        // ( using min/max to limit the factor to 0 - 64 to not get out of range values )
391
        $val1Adjustment = 64 - min( 64, max( 0, abs( $factor ) ) );
392
        
393
        // the base factor for the "current" pixel depends on if we are blurring or sharpening.
394
        // If we are blurring use 1, if sharpening use 9.
395
        $val1Base = 9;
396
        if( abs( $factor ) !== $factor ) {
397
            $val1Base = 1;
398
        }
399
        
400
        // value for the center/current pixel is:
401
        //  1 + 0 - max blurring
402
        //  1 + 64- minimal blurring
403
        //  9 + 64- minimal sharpening
404
        //  9 + 0 - maximum sharpening
405
        $val1 = $val1Base + $val1Adjustment;
406
        
407
        // the value for the surrounding pixels is either positive or negative depending on if we are blurring or sharpening.
408
        $val2 = -1;
409
        if( abs( $factor ) !== $factor ) {
410
            $val2 = 1;
411
        }
412
        
413
        // setup matrix ..
414
        $matrix = array(
415
            array( $val2, $val2, $val2 ),
416
            array( $val2, $val1, $val2 ),
417
            array( $val2, $val2, $val2 )
418
        );
419
        
420
        // calculate the correct divisor
421
        // actual divisor is equal to "$divisor = $val1 + $val2 * 8;"
422
        // but the following line is more generic
423
        $divisor = array_sum( array_map( 'array_sum', $matrix ) );
424
        
425
        // apply the matrix
426
        imageconvolution( $this->newImage, $matrix, $divisor, 0 );
427
        
428
        return $this;
429
    }
430
    
431
    /**
432
     * Whether the image is an SVG image.
433
     * @return boolean
434
     */
435
    public function isTypeSVG() : bool
436
    {
437
        return $this->type === 'svg';
438
    }
439
    
440
    /**
441
     * Whether the image is a PNG image.
442
     * @return boolean
443
     */
444
    public function isTypePNG() : bool
445
    {
446
        return $this->type === 'png';
447
    }
448
    
449
    /**
450
     * Whether the image is a JPEG image.
451
     * @return boolean
452
     */
453
    public function isTypeJPEG() : bool
454
    {
455
        return $this->type === 'jpeg';
456
    }
457
    
458
    /**
459
     * Whether the image is a vector image.
460
     * @return boolean
461
     */
462
    public function isVector() : bool
463
    {
464
        return $this->isTypeSVG();
465
    }
466
    
467
    /**
468
     * Retrieves the type of the image.
469
     * @return string e.g. "jpeg", "png"
470
     */
471
    public function getType() : string
472
    {
473
        return $this->type;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->type could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
474
    }
475
    
476
    /**
477
     * Calculates the size of the image by the specified width,
478
     * and returns an indexed array with the width and height size.
479
     *
480
     * @param integer $width
481
     * @return ImageHelper_Size
482
     */
483
    public function getSizeByWidth(int $width) : ImageHelper_Size
484
    {
485
        $height = (int)floor(($width * $this->height) / $this->width);
486
        
487
        return new ImageHelper_Size(array(
488
            $width,
489
            $height,
490
            $this->info['bits'],
491
            $this->info['channels']
492
        ));
493
    }
494
    
495
    /**
496
     * Calculates the size of the image by the specified height,
497
     * and returns an indexed array with the width and height size.
498
     *
499
     * @param integer $height
500
     * @return ImageHelper_Size
501
     */
502
    public function getSizeByHeight(int $height) : ImageHelper_Size
503
    {
504
        $width = (int)floor(($height * $this->width) / $this->height);
505
        
506
        return new ImageHelper_Size(array(
507
            $width,
508
            $height,
509
            $this->info['bits'],
510
            $this->info['channels']
511
        ));
512
    }
513
514
    /**
515
     * Resamples the image to a new width, maintaining
516
     * aspect ratio.
517
     *
518
     * @param int $width
519
     * @return ImageHelper
520
     * @throws ImageHelper_Exception
521
     */
522
    public function resampleByWidth(int $width) : ImageHelper
523
    {
524
        $size = $this->getSizeByWidth($width);
525
526
        $this->resampleImage($size->getWidth(), $size->getHeight());
527
        
528
        return $this;
529
    }
530
531
    /**
532
     * Resamples the image by height, and creates a new image file on disk.
533
     *
534
     * @param int $height
535
     * @return ImageHelper
536
     * @throws ImageHelper_Exception
537
     */
538
    public function resampleByHeight(int $height) : ImageHelper
539
    {
540
        $size = $this->getSizeByHeight($height);
541
542
        return $this->resampleImage($size->getWidth(), $size->getHeight());
543
    }
544
545
    /**
546
     * Resamples the image without keeping the aspect ratio.
547
     *
548
     * @param int|null $width
549
     * @param int|null $height
550
     * @return ImageHelper
551
     * @throws ImageHelper_Exception
552
     */
553
    public function resample(?int $width = null, ?int $height = null) : ImageHelper
554
    {
555
        if($this->isVector()) {
556
            return $this;
557
        }
558
        
559
        if ($width === null && $height === null) {
560
            return $this->resampleByWidth($this->width);
561
        }
562
563
        if ($width === null) {
564
            return $this->resampleByHeight($height);
0 ignored issues
show
Bug introduced by
It seems like $height can also be of type null; however, parameter $height of AppUtils\ImageHelper::resampleByHeight() does only seem to accept integer, 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

564
            return $this->resampleByHeight(/** @scrutinizer ignore-type */ $height);
Loading history...
565
        }
566
567
        if ($height === null) {
568
            return $this->resampleByWidth($width);
569
        }
570
571
        return $this->resampleAndCrop($width, $height);
572
    }
573
574
    public function resampleAndCrop(int $width, int $height) : ImageHelper
575
    {
576
        if($this->isVector()) {
577
            return $this;
578
        }
579
580
        if ($this->width <= $this->height) 
581
        {
582
            $this->resampleByWidth($width);
583
        } 
584
        else 
585
        {
586
            $this->resampleByHeight($height);
587
        }
588
        
589
        $newCanvas = $this->createNewImage($width, $height);
590
        
591
        // and now we can add the crop
592
        if (!imagecopy(
593
            $newCanvas,
594
            $this->newImage,
595
            0, // destination X
596
            0, // destination Y
597
            0, // source X
598
            0, // source Y
599
            $width,
600
            $height
601
        )
602
        ) {
603
            throw new ImageHelper_Exception(
604
                'Error creating new image',
605
                'Cannot create crop of the image',
606
                self::ERROR_CANNOT_CREATE_IMAGE_CROP
607
            );
608
        }
609
610
        $this->setNewImage($newCanvas);
611
612
        return $this;
613
    }
614
    
615
    /**
616
     * Configures the specified image resource to make it alpha compatible.
617
     *
618
     * @param resource $canvas
619
     * @param bool $fill Whether to fill the whole canvas with the transparency
620
     * @throws ImageHelper_Exception
621
     */
622
    public static function addAlphaSupport($canvas, bool $fill=true) : void
623
    {
624
        self::requireResource($canvas);
625
        
626
        imagealphablending($canvas,true);
627
        imagesavealpha($canvas, true);
628
629
        if($fill) {
630
            self::fillImageTransparent($canvas);
631
        }
632
    }
633
    
634
    public function isAlpha() : bool
635
    {
636
        return $this->alpha;
637
    }
638
639
    /**
640
     * @param string $targetFile
641
     * @param bool $dispose
642
     * @return $this
643
     * @throws ImageHelper_Exception
644
     */
645
    public function save(string $targetFile, bool $dispose=true) : self
646
    {
647
        if($this->isVector()) {
648
            return $this;
649
        }
650
        
651
        if(!is_resource($this->newImage)) {
652
            throw new ImageHelper_Exception(
653
                'Error creating new image',
654
                'Cannot save an image, no valid image resource was created. You have to call one of the resample methods to create a new image.',
655
                self::ERROR_SAVE_NO_IMAGE_CREATED
656
            );
657
        }
658
659
        if (file_exists($targetFile)) {
660
            unlink($targetFile);
661
        }
662
        
663
        $method = 'image' . $this->type;
664
        if (!$method($this->newImage, $targetFile, $this->resolveQuality())) {
665
            throw new ImageHelper_Exception(
666
                'Error creating new image',
667
                sprintf(
668
                    'The %s method could not write the new image to %s',
669
                    $method,
670
                    $targetFile
671
                ),
672
                self::ERROR_CANNOT_WRITE_NEW_IMAGE_FILE
673
            );
674
        }
675
676
        if (filesize($targetFile) < 1) {
677
            throw new ImageHelper_Exception(
678
                'Error creating new image',
679
                'Resampling completed successfully, but the generated file is 0 bytes big.',
680
                self::ERROR_CREATED_AN_EMPTY_FILE
681
            );
682
        }
683
684
        if($dispose) {
685
            $this->dispose();
686
        }
687
        
688
        return $this;
689
    }
690
691
    /**
692
     * @return $this
693
     */
694
    public function dispose() : self
695
    {
696
        if(is_resource($this->sourceImage)) {
697
            imagedestroy($this->sourceImage);
698
        }
699
        
700
        if(is_resource($this->newImage)) {
701
            imagedestroy($this->newImage);
702
        }
703
704
        return $this;
705
    }
706
707
    protected function resolveQuality() : int
708
    {
709
        switch ($this->type)
710
        {
711
            case 'jpeg':
712
                return $this->quality;
713
714
            case 'png':
715
            default:
716
                return 0;
717
        }
718
    }
719
720
    /**
721
     * Sets the quality for image types like jpg that use compression.
722
     *
723
     * @param int $quality
724
     * @return ImageHelper
725
     * @throws ImageHelper_Exception
726
     */
727
    public function setQuality(int $quality) : self
728
    {
729
        if ($quality < 0) {
730
            throw new ImageHelper_Exception(
731
                'Invalid configuration',
732
                'Cannot set a quality less than 0.',
733
                self::ERROR_QUALITY_VALUE_BELOW_ZERO
734
            );
735
        }
736
737
        if ($quality > 100) {
738
            throw new ImageHelper_Exception(
739
                'Invalid configuration',
740
                'Cannot set a quality higher than 100.',
741
                self::ERROR_QUALITY_ABOVE_ONE_HUNDRED
742
            );
743
        }
744
745
        $this->quality = $quality;
746
747
        return $this;
748
    }
749
750
   /**
751
    * Attempts to adjust the memory to the required size
752
    * to work with the current image.
753
    * 
754
    * @return boolean
755
    */
756
    protected function adjustMemory() : bool
757
    {
758
        if(!self::$config['auto-memory-adjustment']) {
759
            return true;
760
        }
761
        
762
        $MB = 1048576; // number of bytes in 1M
763
        $K64 = 65536; // number of bytes in 64K
764
        $tweakFactor = 25; // magic adjustment value as safety threshold
765
        $memoryNeeded = ceil(
766
            (
767
                $this->info->getWidth() 
768
                * 
769
                $this->info->getHeight() 
770
                * 
771
                $this->info->getBits() 
772
                * 
773
                ($this->info->getChannels() / 8) 
774
                + 
775
                $K64
776
            )
777
            * $tweakFactor
778
        );
779
780
        // ini_get('memory_limit') only works if compiled with "--enable-memory-limit".
781
        // Also, default memory limit is 8MB, so we will stick with that.
782
        $memoryLimit = 8 * $MB;
783
            
784
        if (function_exists('memory_get_usage') && memory_get_usage() + $memoryNeeded > $memoryLimit) {
785
            $newLimit = ($memoryLimit + (memory_get_usage() + $memoryNeeded)) / $MB;
786
            $newLimit = ceil($newLimit);
787
            ini_set('memory_limit', $newLimit . 'M');
788
789
            return true;
790
        }
791
792
        return false;
793
    }
794
795
    /**
796
     * Stretches the image to the specified dimensions.
797
     *
798
     * @param int $width
799
     * @param int $height
800
     * @return ImageHelper
801
     * @throws ImageHelper_Exception
802
     */
803
    public function stretch(int $width, int $height) : ImageHelper
804
    {
805
        return $this->resampleImage($width, $height);
806
    }
807
808
   /**
809
    * Creates a new image from the current image,
810
    * resampling it to the new size.
811
    * 
812
    * @param int $newWidth
813
    * @param int $newHeight   
814
    * @throws ImageHelper_Exception
815
    * @return ImageHelper
816
    */
817
    protected function resampleImage(int $newWidth, int $newHeight) : ImageHelper
818
    {
819
        if($this->isVector()) {
820
            return $this;
821
        }
822
823
        if($this->newWidth===$newWidth && $this->newHeight===$newHeight) {
824
            return $this;
825
        }
826
        
827
        if($newWidth < 1) { $newWidth = 1; }
828
        if($newHeight < 1) { $newHeight = 1; }
829
        
830
        $this->adjustMemory();
831
832
        $new = $this->createNewImage($newWidth, $newHeight);
833
       
834
        if (!imagecopyresampled($new, $this->newImage, 0, 0, 0, 0, $newWidth, $newHeight, $this->newWidth, $this->newHeight)) 
835
        {
836
            throw new ImageHelper_Exception(
837
                'Error creating new image',
838
                'Cannot copy resampled image data',
839
                self::ERROR_CANNOT_COPY_RESAMPLED_IMAGE_DATA
840
            );
841
        }
842
843
        $this->setNewImage($new);
844
845
        return $this;
846
    }
847
848
    /**
849
     * Gets the image type for the specified file name.
850
     * Like {@link getImageType()}, except that it automatically
851
     * extracts the file extension from the file name.
852
     *
853
     * @param string |resource|GdImage $pathOrResource
854
     * @return string|NULL
855
     * @see getImageType()
856
     */
857
    public static function getFileImageType($pathOrResource) : ?string
858
    {
859
        if(!is_string($pathOrResource)) {
860
            return 'png';
861
        }
862
863
        return self::getImageType(strtolower(pathinfo($pathOrResource, PATHINFO_EXTENSION)));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($pathOrResource...ils\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

863
        return self::getImageType(strtolower(/** @scrutinizer ignore-type */ pathinfo($pathOrResource, PATHINFO_EXTENSION)));
Loading history...
864
    }
865
866
    /**
867
     * Gets the image type for the specified file extension,
868
     * or NULL if the extension is not among the supported
869
     * file types.
870
     *
871
     * @param string $extension
872
     * @return string|NULL
873
     */
874
    public static function getImageType(string $extension) : ?string
875
    {
876
        return self::$imageTypes[$extension] ?? null;
877
    }
878
879
    /**
880
     * @return string[]
881
     */
882
    public static function getImageTypes() : array
883
    {
884
        $types = array_values(self::$imageTypes);
885
        return array_unique($types);
886
    }
887
    
888
   /**
889
    * Displays an existing image resource.
890
    *
891
    * @param resource $resource
892
    * @param string $imageType The image format to send, i.e. "jpeg", "png"
893
    * @param int $quality The quality to use for the image. This is 0-9 (0=no compression, 9=max) for PNG, and 0-100 (0=lowest, 100=highest quality) for JPG
894
    *
895
    * @throws ImageHelper_Exception
896
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
897
    * @see ImageHelper::ERROR_INVALID_STREAM_IMAGE_TYPE
898
    */
899
    public static function displayImageStream($resource, string $imageType, int $quality=-1) : void
900
    {
901
        self::requireResource($resource);
902
903
        $imageType = self::requireValidStreamType($imageType);
904
        
905
        header('Content-type:image/' . $imageType);
906
907
        $function = 'image' . $imageType;
908
        
909
        $function($resource, null, $quality);
910
    }
911
912
    /**
913
     * @param string $imageType
914
     * @return string
915
     *
916
     * @throws ImageHelper_Exception
917
     * @see ImageHelper::ERROR_INVALID_STREAM_IMAGE_TYPE
918
     * @see ImageHelper::$streamTypes
919
     */
920
    public static function requireValidStreamType(string $imageType) : string
921
    {
922
        $imageType = strtolower($imageType);
923
924
        if(in_array($imageType, self::$streamTypes))
925
        {
926
            return $imageType;
927
        }
928
929
        throw new ImageHelper_Exception(
930
            'Invalid image stream type',
931
            sprintf(
932
                'The image type [%s] cannot be used for a stream.',
933
                $imageType
934
            ),
935
            self::ERROR_INVALID_STREAM_IMAGE_TYPE
936
        );
937
    }
938
939
    /**
940
     * Displays an image from an existing image file.
941
     * @param string $imageFile
942
     * @throws ImageHelper_Exception
943
     */
944
    public static function displayImage(string $imageFile) : void
945
    {
946
        $file = null;
947
        $line = null;
948
        if (headers_sent($file, $line)) {
949
            throw new ImageHelper_Exception(
950
                'Error displaying image',
951
                'Headers have already been sent: in file ' . $file . ':' . $line,
952
                self::ERROR_HEADERS_ALREADY_SENT
953
            );
954
        }
955
956
        if (!file_exists($imageFile)) {
957
            throw new ImageHelper_Exception(
958
                'Image file does not exist',
959
                sprintf(
960
                    'Cannot display image, the file does not exist on disk: [%s].',
961
                    $imageFile
962
                ),
963
                self::ERROR_IMAGE_FILE_DOES_NOT_EXIST
964
            );
965
        }
966
967
        $format = self::getFileImageType($imageFile);
968
        if($format === 'svg') {
969
            $format = 'svg+xml';
970
        }
971
972
        $contentType = 'image/' . $format;
973
        
974
        header('Content-Type: '.$contentType);
975
        header("Last-Modified: " . gmdate("D, d M Y H:i:s", filemtime($imageFile)) . " GMT");
976
        header('Cache-Control: public');
977
        header('Content-Length: ' . filesize($imageFile));
978
979
        readfile($imageFile);
980
    }
981
    
982
   /**
983
    * Displays the current image.
984
    *
985
    * NOTE: You must call `exit()` manually after this.
986
    */
987
    public function display() : void
988
    {
989
        self::displayImageStream(
990
            $this->newImage,
991
            $this->getType(),
992
            $this->resolveQuality()
993
        );
994
    }
995
996
    /**
997
     * Trims the current loaded image.
998
     *
999
     * @param RGBAColor|null $color A color definition, as an associative array with red, green, and blue keys. If not specified, the color at pixel position 0,0 will be used.
1000
     *
1001
     * @return ImageHelper
1002
     * @throws ImageHelper_Exception
1003
     * @throws ColorException
1004
     *
1005
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1006
     * @see ImageHelper::ERROR_CANNOT_CREATE_IMAGE_CANVAS
1007
     */
1008
    public function trim(?RGBAColor $color=null) : ImageHelper
1009
    {
1010
        return $this->trimImage($this->newImage, $color);
1011
    }
1012
1013
    /**
1014
     * Retrieves a color definition by its index.
1015
     *
1016
     * @param resource $img A valid image resource.
1017
     * @param int $colorIndex The color index, as returned by `imagecolorat` for example.
1018
     * @return RGBAColor An array with red, green, blue and alpha keys.
1019
     *
1020
     * @throws ImageHelper_Exception
1021
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1022
     */
1023
    public function getIndexedColors($img, int $colorIndex) : RGBAColor
1024
    {
1025
        self::requireResource($img);
1026
1027
        return ColorFactory::createFromIndex($img, $colorIndex);
1028
    }
1029
1030
    /**
1031
     * @param resource $img
1032
     * @param int $x
1033
     * @param int $y
1034
     * @return RGBAColor
1035
     * @throws ImageHelper_Exception
1036
     */
1037
    public function getIndexedColorsAt($img, int $x, int $y) : RGBAColor
1038
    {
1039
        self::requireResource($img);
1040
1041
        return $this->getIndexedColors(
1042
            $img,
1043
            imagecolorat($this->sourceImage, $x, $y)
1044
        );
1045
    }
1046
1047
    /**
1048
     * Trims the specified image resource by removing the specified color.
1049
     * Also works with transparency.
1050
     *
1051
     * @param resource|GdImage $img
1052
     * @param RGBAColor|null $trimColor
1053
     * @return ImageHelper
1054
     *
1055
     * @throws ImageHelper_Exception
1056
     */
1057
    protected function trimImage($img, ?RGBAColor $trimColor=null) : ImageHelper
1058
    {
1059
        if($this->isVector()) {
1060
            return $this;
1061
        }
1062
1063
        self::requireResource($img);
1064
1065
        $trimmer = new ImageTrimmer($this, $img, $trimColor);
1066
        $new = $trimmer->trim();
1067
1068
        if($new === null) {
1069
            return $this;
1070
        }
1071
        
1072
        // To finish up, we replace the old image which is referenced.
1073
        imagedestroy($img);
1074
        
1075
        $this->setNewImage($new);
0 ignored issues
show
Bug introduced by
It seems like $new can also be of type GdImage; however, parameter $image of AppUtils\ImageHelper::setNewImage() 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

1075
        $this->setNewImage(/** @scrutinizer ignore-type */ $new);
Loading history...
1076
1077
        return $this;
1078
    }
1079
1080
    /**
1081
     * Sets the new image after a transformation operation:
1082
     * automatically adjusts the new size information.
1083
     *
1084
     * @param resource $image
1085
     *
1086
     * @throws ImageHelper_Exception
1087
     * @see ImageHelper::ERROR_NOT_A_RESOURCE
1088
     */
1089
    protected function setNewImage($image) : ImageHelper
1090
    {
1091
        self::requireResource($image);
1092
        
1093
        $this->newImage = $image;
1094
        $this->newWidth = imagesx($image);
1095
        $this->newHeight= imagesy($image);
1096
1097
        return $this;
1098
    }
1099
    
1100
   /**
1101
    * Requires the subject to be a resource.
1102
    * 
1103
    * @param resource|GdImage|mixed $subject
1104
    *
1105
    * @throws ImageHelper_Exception
1106
    * @see ImageHelper::ERROR_NOT_A_RESOURCE
1107
    */
1108
    public static function requireResource($subject) : void
1109
    {
1110
        if(is_resource($subject) && imagesx($subject)) {
1111
            return;
1112
        }
1113
1114
        if($subject instanceof GdImage) {
1115
            return;
1116
        }
1117
        
1118
        throw new ImageHelper_Exception(
1119
            'Not an image resource',
1120
            sprintf(
1121
                'Specified image should be a resource, [%s] given.',
1122
                gettype($subject)
1123
            ),
1124
            self::ERROR_NOT_A_RESOURCE
1125
        );
1126
    }
1127
    
1128
   /**
1129
    * Creates a new image resource, with transparent background.
1130
    * 
1131
    * @param int $width
1132
    * @param int $height
1133
    * @throws ImageHelper_Exception
1134
    * @return resource
1135
    */
1136
    public function createNewImage(int $width, int $height)
1137
    {
1138
        $img = imagecreatetruecolor($width, $height);
1139
        
1140
        if($img === false) 
1141
        {
1142
            throw new ImageHelper_Exception(
1143
                'Error creating new image',
1144
                'Cannot create new image canvas',
1145
                self::ERROR_CANNOT_CREATE_IMAGE_CANVAS
1146
            );
1147
        }
1148
1149
        self::addAlphaSupport($img);
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

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