Test Failed
Push — resize ( fbaf05...026e01 )
by Mikael
02:27
created

CImage::getTargetImageExtension()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 9.3211

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 0
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
ccs 1
cts 9
cp 0.1111
crap 9.3211
1
<?php
2
3
namespace Mos\CImage;
4
5
/**
6
 * Resize and crop images on the fly, store generated images in a cache.
7
 *
8
 * @author  Mikael Roos [email protected]
9
 * @example http://dbwebb.se/opensource/cimage
10
 * @link    https://github.com/mosbth/cimage
11
 */
12
class CImage
13
{
14
15
    /**
16
     * Constants type of PNG image
17
     */
18
    const PNG_GREYSCALE         = 0;
19
    const PNG_RGB               = 2;
20
    const PNG_RGB_PALETTE       = 3;
21
    const PNG_GREYSCALE_ALPHA   = 4;
22
    const PNG_RGB_ALPHA         = 6;
23
24
25
26
    /**
27
     * Constant for default image quality when not set
28
     */
29
    const JPEG_QUALITY_DEFAULT = 60;
30
31
32
33
    /**
34
     * Quality level for JPEG images.
35
     */
36
    private $quality;
37
38
39
40
    /**
41
     * Is the quality level set from external use (true) or is it default (false)?
42
     */
43
    private $useQuality = false;
44
45
46
47
    /**
48
     * Constant for default image quality when not set
49
     */
50
    const PNG_COMPRESSION_DEFAULT = -1;
51
52
53
54
    /**
55
     * Compression level for PNG images.
56
     */
57
    private $compress;
58
59
60
61
    /**
62
     * Is the compress level set from external use (true) or is it default (false)?
63
     */
64
    private $useCompress = false;
65
66
67
68
69
    /**
70
     * Add HTTP headers for outputing image.
71
     */
72
    private $HTTPHeader = array();
73
74
75
76
    /**
77
     * Default background color, red, green, blue, alpha.
78
     *
79
     * @todo remake when upgrading to PHP 5.5
80
     */
81
    /*
82
    const BACKGROUND_COLOR = array(
83
        'red'   => 0,
84
        'green' => 0,
85
        'blue'  => 0,
86
        'alpha' => null,
87
    );*/
88
89
90
91
    /**
92
     * Default background color to use.
93
     *
94
     * @todo remake when upgrading to PHP 5.5
95
     */
96
    //private $bgColorDefault = self::BACKGROUND_COLOR;
97
    private $bgColorDefault = array(
98
        'red'   => 0,
99
        'green' => 0,
100
        'blue'  => 0,
101
        'alpha' => null,
102
    );
103
104
105
    /**
106
     * Background color to use, specified as part of options.
107
     */
108
    private $bgColor;
109
110
111
112
    /**
113
     * Where to save the target file.
114
     */
115
    private $saveFolder;
116
117
118
119
    /**
120
     * The working image object.
121
     */
122
    private $image;
123
124
125
126
    /**
127
     * Image filename, may include subdirectory, relative from $imageFolder
128
     */
129
    private $imageSrc;
130
131
132
133
    /**
134
     * Actual path to the image, $imageFolder . '/' . $imageSrc
135
     */
136
    private $pathToImage;
137
138
139
140
    /**
141
     * File type for source image, as provided by getimagesize()
142
     */
143
    private $fileType;
144
145
146
147
    /**
148
     * File extension to use when saving image.
149
     */
150
    private $extension;
151
152
153
154
    /**
155
     * Output format, supports null (image) or json.
156
     */
157
    private $outputFormat = null;
158
159
160
161
    /**
162
     * Do lossy output using external postprocessing tools.
163
     */
164
    private $lossy = null;
165
166
167
168
    /**
169
     * Verbose mode to print out a trace and display the created image
170
     */
171
    private $verbose = false;
172
173
174
175
    /**
176
     * Keep a log/trace on what happens
177
     */
178
    private $log = array();
179
180
181
182
    /**
183
     * Handle image as palette image
184
     */
185
    private $palette;
186
187
188
189
    /**
190
     * Target filename, with path, to save resulting image in.
191
     */
192
    private $cacheFileName;
193
194
195
196
    /**
197
     * Set a format to save image as, or null to use original format.
198
     */
199
    private $saveAs;
200
201
202
    /**
203
     * Path to command for lossy optimize, for example pngquant.
204
     */
205
    private $pngLossy;
206
    private $pngLossyCmd;
207
208
209
210
    /**
211
     * Path to command for filter optimize, for example optipng.
212
     */
213
    private $pngFilter;
214
    private $pngFilterCmd;
215
216
217
218
    /**
219
     * Path to command for deflate optimize, for example pngout.
220
     */
221
    private $pngDeflate;
222
    private $pngDeflateCmd;
223
224
225
226
    /**
227
     * Path to command to optimize jpeg images, for example jpegtran or null.
228
     */
229
    private $jpegOptimize;
230
    private $jpegOptimizeCmd;
231
232
233
234
    /**
235
     * Image dimensions, calculated from loaded image.
236
     */
237
    private $width;  // Calculated from source image
238
    private $height; // Calculated from source image
239
240
241
    /**
242
     * New image dimensions, incoming as argument or calculated.
243
     */
244
    private $newWidth;
245
    private $newWidthOrig;  // Save original value
246
    private $newHeight;
247
    private $newHeightOrig; // Save original value
248
249
250
    /**
251
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
252
     */
253
    private $dpr = 1;
254
255
256
    /**
257
     * Always upscale images, even if they are smaller than target image.
258
     */
259
    const UPSCALE_DEFAULT = true;
260
    private $upscale = self::UPSCALE_DEFAULT;
261
262
263
264
    /**
265
     * Array with details on how to crop, incoming as argument and calculated.
266
     */
267
    public $crop;
268
    public $cropOrig; // Save original value
269
270
271
    /**
272
     * String with details on how to do image convolution. String
273
     * should map a key in the $convolvs array or be a string of
274
     * 11 float values separated by comma. The first nine builds
275
     * up the matrix, then divisor and last offset.
276
     */
277
    private $convolve;
278
279
280
    /**
281
     * Custom convolution expressions, matrix 3x3, divisor and offset.
282
     */
283
    private $convolves = array(
284
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
285
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
286
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
287
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
288
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
289
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
290
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
291
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
292
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
293
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
294
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
295
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
296
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
297
    );
298
299
300
    /**
301
     * Resize strategy to fill extra area with background color.
302
     * True or false.
303
     */
304
    private $fillToFit;
305
306
307
308
    /**
309
     * To store value for option scale.
310
     */
311
    private $scale;
312
313
314
315
    /**
316
     * To store value for option.
317
     */
318
    private $rotateBefore;
319
320
321
322
    /**
323
     * To store value for option.
324
     */
325
    private $rotateAfter;
326
327
328
329
    /**
330
     * To store value for option.
331
     */
332
    private $autoRotate;
333
334
335
336
    /**
337
     * To store value for option.
338
     */
339
    private $sharpen;
340
341
342
343
    /**
344
     * To store value for option.
345
     */
346
    private $emboss;
347
348
349
350
    /**
351
     * To store value for option.
352
     */
353
    private $blur;
354
355
356
357
    /**
358
     * Used with option area to set which parts of the image to use.
359
     */
360
    private $area;
361
    private $offset;
362
363
364
365
    /**
366
     * Calculate target dimension for image when using fill-to-fit resize strategy.
367
     */
368
    private $fillWidth;
369
    private $fillHeight;
370
371
372
373
    /**
374
     * Allow remote file download, default is to disallow remote file download.
375
     */
376
    private $allowRemote = false;
377
378
379
380
    /**
381
     * Path to cache for remote download.
382
     */
383
    private $remoteCache;
384
385
386
387
    /**
388
     * Pattern to recognize a remote file.
389
     */
390
    //private $remotePattern = '#^[http|https]://#';
391
    private $remotePattern = '#^https?://#';
392
393
394
395
    /**
396
     * Use the cache if true, set to false to ignore the cached file.
397
     */
398
    private $useCache = true;
399
400
401
    /**
402
    * Disable the fasttrackCacke to start with, inject an object to enable it.
403
    */
404
    private $fastTrackCache = null;
405
406
407
408
    /*
409
     * Set whitelist for valid hostnames from where remote source can be
410
     * downloaded.
411
     */
412
    private $remoteHostWhitelist = null;
413
414
415
416
    /*
417
     * Do verbose logging to file by setting this to a filename.
418
     */
419
    private $verboseFileName = null;
420
421
422
423
    /*
424
     * Output to ascii can take som options as an array.
425
     */
426
    private $asciiOptions = array();
427
428
429
430
    /*
431
     * Image copy strategy, defaults to RESAMPLE.
432
     */
433
     const RESIZE = 1;
434
     const RESAMPLE = 2;
435
    private $copyStrategy = null;
436
437
438
439
     /*
440
      * Class for image resizer.
441
      */
442
    private $imageResizer = null;
443
444
    /**
445
     * Properties, the class is mutable and the method setOptions()
446
     * decides (partly) what properties are created.
447 9
     *
448
     * @todo Clean up these and check if and how they are used
449 9
     */
450 9
451 9
    public $keepRatio;
452 9
    public $cropToFit;
453
    private $cropWidth;
454
    private $cropHeight;
455
    public $crop_x;
456
    public $crop_y;
457
    public $filters;
458
    private $attr; // Calculated from source image
459
460
461
462
463
    /**
464
     * Constructor, can take arguments to init the object.
465
     *
466
     * @param string $imageSrc    filename which may contain subdirectory.
467
     * @param string $imageFolder path to root folder for images.
468
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
469
     * @param string $saveName    name of target file when saveing.
470
     */
471
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
472
    {
473
        $this->setSource($imageSrc, $imageFolder);
474
        $this->setTarget($saveFolder, $saveName);
475
        $this->imageResizer = new CImageResizer(array($this, 'log'));
476
    }
477
478
479
480
481
    /**
482 2
     * Inject object and use it, must be available as member.
483
     *
484 2
     * @param string $property to set as object.
485 2
     * @param object $object   to set to property.
486
     *
487
     * @return $this
488
     */
489
    public function injectDependency($property, $object)
490
    {
491
        if (!property_exists($this, $property)) {
492
            $this->raiseError("Injecting unknown property.");
493
        }
494
        $this->$property = $object;
495
        return $this;
496
    }
497
498
499
500
    /**
501
     * Set verbose mode.
502
     *
503
     * @param boolean $mode true or false to enable and disable verbose mode,
504
     *                      default is true.
505
     *
506
     * @return $this
507
     */
508
    public function setVerbose($mode = true)
509
    {
510
        $this->verbose = $mode;
511
        return $this;
512
    }
513
514 2
515
516 2
    /**
517 2
     * Set save folder, base folder for saving cache files.
518
     *
519 2
     * @todo clean up how $this->saveFolder is used in other methods.
520
     *
521 2
     * @param string $path where to store cached files.
522
     *
523
     * @return $this
524
     */
525
    public function setSaveFolder($path)
526
    {
527
        $this->saveFolder = $path;
528
        return $this;
529
    }
530
531
532
533
    /**
534
     * Use cache or not.
535 2
     *
536
     * @param boolean $use true or false to use cache.
537 2
     *
538 2
     * @return $this
539 2
     */
540
    public function useCache($use = true)
541 2
    {
542
        $this->useCache = $use;
543 2
        return $this;
544 2
    }
545 2
546 2
547
548 2
    /**
549
     * Create and save a dummy image. Use dimensions as stated in
550
     * $this->newWidth, or $width or default to 100 (same for height.
551
     *
552
     * @param integer $width  use specified width for image dimension.
553
     * @param integer $height use specified width for image dimension.
554
     *
555
     * @return $this
556
     */
557
    public function createDummyImage($width = null, $height = null)
558
    {
559
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
560 2
        $this->newHeight = $this->newHeight ?: $height ?: 100;
561
562 2
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
563 2
564 2
        return $this;
565
    }
566
567
568
569
    /**
570
     * Allow or disallow remote image download.
571
     *
572
     * @param boolean $allow   true or false to enable and disable.
573
     * @param string  $cache   path to cache dir.
574
     * @param string  $pattern to use to detect if its a remote file.
575
     *
576
     * @return $this
577 2
     */
578
    public function setRemoteDownload($allow, $cache, $pattern = null)
579 2
    {
580 2
        $this->allowRemote = $allow;
581
        $this->remoteCache = $cache;
582 2
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
583 2
584 2
        $this->log(
585
            "Set remote download to: "
586
            . ($this->allowRemote ? "true" : "false")
587
            . " using pattern "
588
            . $this->remotePattern
589
        );
590
591
        return $this;
592
    }
593
594
595
596
    /**
597 3
     * Check if the image resource is a remote file or not.
598
     *
599 3
     * @param string $src check if src is remote.
600 1
     *
601 1
     * @return boolean true if $src is a remote file, else false.
602
     */
603
    public function isRemoteSource($src)
604 2
    {
605 2
        $remote = preg_match($this->remotePattern, $src);
606 2
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
607
        return !!$remote;
608 2
    }
609
610 2
611 2
612 2
    /**
613
     * Set whitelist for valid hostnames from where remote source can be
614
     * downloaded.
615
     *
616
     * @param array $whitelist with regexp hostnames to allow download from.
617
     *
618
     * @return $this
619
     */
620
    public function setRemoteHostWhitelist($whitelist = null)
621
    {
622
        $this->remoteHostWhitelist = $whitelist;
623
        $this->log(
624
            "Setting remote host whitelist to: "
625
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
626
        );
627
        return $this;
628
    }
629
630
631
632
    /**
633
     * Check if the hostname for the remote image, is on a whitelist,
634
     * if the whitelist is defined.
635
     *
636
     * @param string $src the remote source.
637
     *
638
     * @return boolean true if hostname on $src is in the whitelist, else false.
639
     */
640
    public function isRemoteSourceOnWhitelist($src)
641
    {
642
        if (is_null($this->remoteHostWhitelist)) {
643 2
            $this->log("Remote host on whitelist not configured - allowing.");
644
            return true;
645 2
        }
646
647 2
        $whitelist = new CWhitelist();
648
        $hostname = parse_url($src, PHP_URL_HOST);
649
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
0 ignored issues
show
Security Bug introduced by
It seems like $hostname defined by parse_url($src, PHP_URL_HOST) on line 648 can also be of type false; however, Mos\CImage\CWhitelist::check() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
650
651 2
        $this->log(
652
            "Remote host is on whitelist: "
653
            . ($allow ? "true" : "false")
654
        );
655
        return $allow;
656
    }
657
658
659
660
    /**
661
     * Check if file extension is valid as a file extension.
662
     *
663
     * @param string $extension of image file.
664
     *
665
     * @return $this
666
     */
667
    private function checkFileExtension($extension)
668
    {
669
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
670
671
        in_array(strtolower($extension), $valid)
672
            or $this->raiseError('Not a valid file extension.');
673
674
        return $this;
675
    }
676
677
678
679
    /**
680
     * Normalize the file extension.
681
     *
682
     * @param string $extension of image file or skip to use internal.
683
     *
684
     * @return string $extension as a normalized file extension.
685
     */
686
    private function normalizeFileExtension($extension = null)
687
    {
688
        $extension = strtolower($extension ? $extension : $this->extension);
689
690
        if ($extension == 'jpeg') {
691
                $extension = 'jpg';
692
        }
693
694
        return $extension;
695
    }
696 9
697
698 9
699 9
    /**
700 9
     * Download a remote image and return path to its local copy.
701 9
     *
702
     * @param string $src remote path to image.
703
     *
704 2
     * @return string as path to downloaded remote source.
705
     */
706
    public function downloadRemoteSource($src)
707
    {
708
        if (!$this->isRemoteSourceOnWhitelist($src)) {
709 2
            throw new Exception("Hostname is not on whitelist for remote sources.");
710
        }
711
712
        $remote = new CRemoteImage();
713
714 2
        if (!is_writable($this->remoteCache)) {
715 2
            $this->log("The remote cache is not writable.");
716 2
        }
717
718 2
        $remote->setCache($this->remoteCache);
719
        $remote->useCache($this->useCache);
720
        $src = $remote->download($src);
721
722
        $this->log("Remote HTTP status: " . $remote->getStatus());
723
        $this->log("Remote item is in local cache: $src");
724
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
725
726
        return $src;
727
    }
728
729
730
731
    /**
732 9
     * Set source file to use as image source.
733
     *
734 9
     * @param string $src of image.
735 9
     * @param string $dir as optional base directory where images are.
736 9
     *
737
     * @return $this
738
     */
739 2
    public function setSource($src, $dir = null)
740
    {
741
        if (!isset($src)) {
742
            $this->imageSrc = null;
743 2
            $this->pathToImage = null;
744
            return $this;
745
        }
746 2
747 2
        if ($this->allowRemote && $this->isRemoteSource($src)) {
748
            $src = $this->downloadRemoteSource($src);
749 2
            $dir = null;
750
        }
751
752
        if (!isset($dir)) {
753
            $dir = dirname($src);
754
            $src = basename($src);
755
        }
756
757
        $this->imageSrc     = ltrim($src, '/');
758
        $imageFolder        = rtrim($dir, '/');
759 2
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
760
761 2
        return $this;
762
    }
763
764
765
766
    /**
767
     * Set target file.
768
     *
769
     * @param string $src of target image.
770
     * @param string $dir as optional base directory where images are stored.
771
     *                    Uses $this->saveFolder if null.
772
     *
773
     * @return $this
774
     */
775
    public function setTarget($src = null, $dir = null)
776
    {
777
        if (!isset($src)) {
778
            $this->cacheFileName = null;
779
            return $this;
780
        }
781
782
        if (isset($dir)) {
783
            $this->saveFolder = rtrim($dir, '/');
784
        }
785
786
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
787
788
        // Sanitize filename
789
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
790
        $this->log("The cache file name is: " . $this->cacheFileName);
791
792
        return $this;
793
    }
794
795
796
797
    /**
798
     * Get filename of target file.
799
     *
800
     * @return Boolean|String as filename of target or false if not set.
801
     */
802
    public function getTarget()
803
    {
804
        return $this->cacheFileName;
805
    }
806
807
808
809
    /**
810
     * Set options to use when processing image.
811
     *
812
     * @param array $args used when processing image.
813
     *
814
     * @return $this
815
     */
816
    public function setOptions($args)
817
    {
818
        $this->log("Set new options for processing image.");
819
820
        $defaults = array(
821
            // Options for calculate dimensions
822
            'newWidth'    => null,
823
            'newHeight'   => null,
824
            'aspectRatio' => null,
825
            'keepRatio'   => true,
826
            'cropToFit'   => false,
827
            'fillToFit'   => null,
828
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
829
            'area'        => null, //'0,0,0,0',
830
            'upscale'     => self::UPSCALE_DEFAULT,
831
832
            // Options for caching or using original
833
            'useCache'    => true,
834
            'useOriginal' => true,
835
836
            // Pre-processing, before resizing is done
837
            'scale'        => null,
838
            'rotateBefore' => null,
839
            'autoRotate'  => false,
840
841
            // General options
842
            'bgColor'     => null,
843
844
            // Post-processing, after resizing is done
845
            'palette'     => null,
846
            'filters'     => null,
847
            'sharpen'     => null,
848
            'emboss'      => null,
849
            'blur'        => null,
850
            'convolve'       => null,
851
            'rotateAfter' => null,
852
853
            // Output format
854
            'outputFormat' => null,
855
            'dpr'          => 1,
856
857
            // Postprocessing using external tools
858
            'lossy' => null,
859
        );
860
861
        // Convert crop settings from string to array
862 View Code Duplication
        if (isset($args['crop']) && !is_array($args['crop'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
863
            $pices = explode(',', $args['crop']);
864
            $args['crop'] = array(
865
                'width'   => $pices[0],
866
                'height'  => $pices[1],
867
                'start_x' => $pices[2],
868
                'start_y' => $pices[3],
869
            );
870
        }
871
872
        // Convert area settings from string to array
873 View Code Duplication
        if (isset($args['area']) && !is_array($args['area'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
874
                $pices = explode(',', $args['area']);
875
                $args['area'] = array(
876
                    'top'    => $pices[0],
877
                    'right'  => $pices[1],
878
                    'bottom' => $pices[2],
879
                    'left'   => $pices[3],
880
                );
881
        }
882
883
        // Convert filter settings from array of string to array of array
884
        if (isset($args['filters']) && is_array($args['filters'])) {
885
            foreach ($args['filters'] as $key => $filterStr) {
886
                $parts = explode(',', $filterStr);
887
                $filter = $this->mapFilter($parts[0]);
888
                $filter['str'] = $filterStr;
889
                for ($i=1; $i<=$filter['argc']; $i++) {
890
                    if (isset($parts[$i])) {
891
                        $filter["arg{$i}"] = $parts[$i];
892
                    } else {
893
                        throw new Exception(
894
                            'Missing arg to filter, review how many arguments are needed at
895
                            http://php.net/manual/en/function.imagefilter.php'
896
                        );
897
                    }
898
                }
899
                $args['filters'][$key] = $filter;
900
            }
901
        }
902
903
        // Merge default arguments with incoming and set properties.
904
        //$args = array_merge_recursive($defaults, $args);
905
        $args = array_merge($defaults, $args);
906
        foreach ($defaults as $key => $val) {
907
            $this->{$key} = $args[$key];
908
        }
909
910
        if ($this->bgColor) {
911
            $this->setDefaultBackgroundColor($this->bgColor);
912
        }
913
914
        // Save original values to enable re-calculating
915
        $this->newWidthOrig  = $this->newWidth;
916
        $this->newHeightOrig = $this->newHeight;
917
        $this->cropOrig      = $this->crop;
918
919
        return $this;
920
    }
921
922
923
924
    /**
925
     * Map filter name to PHP filter and id.
926
     *
927
     * @param string $name the name of the filter.
928
     *
929
     * @return array with filter settings
930
     * @throws Exception
931
     */
932
    private function mapFilter($name)
933
    {
934
        $map = array(
935
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
936
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
937
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
938
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
939
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
940
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
941
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
942
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
943
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
944
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
945
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
946
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
947
        );
948
949
        if (isset($map[$name])) {
950
            return $map[$name];
951
        } else {
952
            throw new Exception('No such filter.');
953
        }
954
    }
955
956
957
958
    /**
959
     * Load image details from original image file.
960
     *
961
     * @param string $file the file to load or null to use $this->pathToImage.
962
     *
963
     * @return $this
964
     * @throws Exception
965
     */
966
    public function loadImageDetails($file = null)
967
    {
968
        $file = $file ? $file : $this->pathToImage;
969
970
        is_readable($file)
971
            or $this->raiseError('Image file does not exist.');
972
973
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
974
        if (empty($info)) {
975
            // To support webp
976
            $this->fileType = false;
977
            if (function_exists("exif_imagetype")) {
978
                $this->fileType = exif_imagetype($file);
979
                if ($this->fileType === false) {
980
                    if (function_exists("imagecreatefromwebp")) {
981
                        $webp = imagecreatefromwebp($file);
982
                        if ($webp !== false) {
983
                            $this->width  = imagesx($webp);
984
                            $this->height = imagesy($webp);
985
                            $this->fileType = IMG_WEBP;
986
                        }
987
                    }
988
                }
989
            }
990
        }
991
992
        if (!$this->fileType) {
993
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
994
        }
995
        
996
        $this->imageResizer->setSource($this->width, $this->height);
997
998
        if ($this->verbose) {
999
            $this->log("#Loading image details for: {$file}");
1000
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
1001
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
1002
            $this->log(" Image mimetype: " . $this->getMimeType());
1003
        }
1004
1005
        return $this;
1006
    }
1007
1008
1009
1010
    /**
1011
     * Get mime type for image type.
1012
     *
1013
     * @return $this
1014
     * @throws Exception
1015
     */
1016
    protected function getMimeType()
1017
    {
1018
        if ($this->fileType === IMG_WEBP) {
1019
            return "image/webp";
1020
        }
1021
1022
        return image_type_to_mime_type($this->fileType);
1023
    }
1024
1025
1026
1027
    /**
1028
     * Init new width and height and do some sanity checks on constraints,
1029
     * before anyprocessing can be done.
1030
     *
1031
     * @return $this
1032
     * @throws Exception
1033
     */
1034
    public function initDimensions()
1035
    {
1036
        $this->imageResizer->setBaseWidthHeight($this->newWidth, $this->newHeight)
0 ignored issues
show
Documentation introduced by
$this->newWidth is of type integer|double, but the function expects a object<Mos\CImage\numeric>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$this->newHeight is of type integer|double, but the function expects a object<Mos\CImage\numeric>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1037
                           ->setBaseAspecRatio($this->aspectRatio)
0 ignored issues
show
Bug introduced by
The property aspectRatio does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1038
                           ->setBaseDevicePixelRate($this->dpr)
1039
                           ->prepareTargetDimensions();
1040
1041
        return $this;
1042
    }
1043
1044
1045
1046
    /**
1047
     * Calculate new width and height of image, based on settings.
1048
     *
1049
     * @return $this
1050
     */
1051
    public function calculateNewWidthAndHeight()
1052
    {
1053
        $imres = $this->imageResizer;
1054
        $strategy = null;
0 ignored issues
show
Unused Code introduced by
$strategy is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1055
        
1056
        $strategy = $imres::KEEP_RATIO;
1057
1058
        if ($this->keepRatio == false) {
1059
            $strategy = $imres::STRETCH;
1060
        }
1061
1062
        if ($this->cropToFit == true) {
1063
            $strategy = $imres::CROP_TO_FIT;
1064
        }
1065
1066
        if ($this->fillToFit == true) {
1067
            $strategy = $imres::FILL_TO_FIT;
1068
        }
1069
        
1070
        $imres->setResizeStrategy($strategy)
1071
              ->allowUpscale($this->upscale)
1072
              ->calculateTargetWidthAndHeight();
1073
        
1074
        //$this->newWidth  = $imres->getTargetWidth();
1075
        //$this->newHeight = $imres->getTargetHeight();
1076
1077
        /*
1078
        // Crop, use cropped width and height as base for calulations
1079
        $this->log("Calculate new width and height.");
1080
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1081
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1082
1083
        // Check if there is an area to crop off
1084
        if (isset($this->area)) {
1085
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1086
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1087
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1088
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1089
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1090
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1091
            $this->width  = $this->offset['width'];
1092
            $this->height = $this->offset['height'];
1093
            $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%.");
1094
            $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px.");
1095
        }
1096
1097
        $width  = $this->width;
1098
        $height = $this->height;
1099
1100
        // Check if crop is set
1101
        if ($this->crop) {
1102
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1103
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1104
1105
            if ($this->crop['start_x'] == 'left') {
1106
                $this->crop['start_x'] = 0;
1107
            } elseif ($this->crop['start_x'] == 'right') {
1108
                $this->crop['start_x'] = $this->width - $width;
1109
            } elseif ($this->crop['start_x'] == 'center') {
1110
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1111
            }
1112
1113
            if ($this->crop['start_y'] == 'top') {
1114
                $this->crop['start_y'] = 0;
1115
            } elseif ($this->crop['start_y'] == 'bottom') {
1116
                $this->crop['start_y'] = $this->height - $height;
1117
            } elseif ($this->crop['start_y'] == 'center') {
1118
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1119
            }
1120
1121
            $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px.");
1122
        }
1123
1124
        // Calculate new width and height if keeping aspect-ratio.
1125
        if ($this->keepRatio) {
1126
1127
            $this->log("Keep aspect ratio.");
1128
1129
            // Crop-to-fit and both new width and height are set.
1130
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1131
1132
                // Use newWidth and newHeigh as width/height, image should fit in box.
1133
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1134
1135
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1136
1137
                // Both new width and height are set.
1138
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1139
                $ratioWidth  = $width  / $this->newWidth;
1140
                $ratioHeight = $height / $this->newHeight;
1141
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1142
                $this->newWidth  = round($width  / $ratio);
1143
                $this->newHeight = round($height / $ratio);
1144
                $this->log("New width and height was set.");
1145
1146
            } elseif (isset($this->newWidth)) {
1147
1148
                // Use new width as max-width
1149
                $factor = (float)$this->newWidth / (float)$width;
1150
                $this->newHeight = round($factor * $height);
1151
                $this->log("New width was set.");
1152
1153
            } elseif (isset($this->newHeight)) {
1154
1155
                // Use new height as max-hight
1156
                $factor = (float)$this->newHeight / (float)$height;
1157
                $this->newWidth = round($factor * $width);
1158
                $this->log("New height was set.");
1159
1160
            } else {
1161
1162
                // Use existing width and height as new width and height.
1163
                $this->newWidth = $width;
1164
                $this->newHeight = $height;
1165
            }
1166
            
1167
1168
            // Get image dimensions for pre-resize image.
1169
            if ($this->cropToFit || $this->fillToFit) {
1170
1171
                // Get relations of original & target image
1172
                $ratioWidth  = $width  / $this->newWidth;
1173
                $ratioHeight = $height / $this->newHeight;
1174
1175
                if ($this->cropToFit) {
1176
1177
                    // Use newWidth and newHeigh as defined width/height,
1178
                    // image should fit the area.
1179
                    $this->log("Crop to fit.");
1180
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1181
                    $this->cropWidth  = round($width  / $ratio);
1182
                    $this->cropHeight = round($height / $ratio);
1183
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1184
1185
                } elseif ($this->fillToFit) {
1186
1187
                    // Use newWidth and newHeigh as defined width/height,
1188
                    // image should fit the area.
1189
                    $this->log("Fill to fit.");
1190
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1191
                    $this->fillWidth  = round($width  / $ratio);
1192
                    $this->fillHeight = round($height / $ratio);
1193
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1194
                }
1195
            }
1196
        }
1197
1198
        // Crop, ensure to set new width and height
1199
        if ($this->crop) {
1200
            $this->log("Crop.");
1201
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1202
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1203
        }
1204
1205
        // No new height or width is set, use existing measures.
1206
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1207
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1208
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1209
1210
        */
1211
        return $this;
1212
    }
1213
1214
1215
1216
    /**
1217
     * Re-calculate image dimensions when original image dimension has changed.
1218
     *
1219
     * @return $this
1220
     */
1221
    public function reCalculateDimensions()
1222
    {
1223
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: "
1224
            . $this->newWidth
1225
            . " x "
1226
            . $this->newHeight);
1227
1228
        $this->newWidth  = $this->newWidthOrig;
1229
        $this->newHeight = $this->newHeightOrig;
1230
        $this->crop      = $this->cropOrig;
1231
1232
        $this->initDimensions()
1233
             ->calculateNewWidthAndHeight();
1234
1235
        return $this;
1236
    }
1237
1238
1239
1240
    /**
1241
     * Set extension for filename to save as.
1242
     *
1243
     * @param string $saveas extension to save image as
0 ignored issues
show
Documentation introduced by
There is no parameter named $saveas. Did you maybe mean $saveAs?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1244
     *
1245
     * @return $this
1246
     */
1247
    public function setSaveAsExtension($saveAs = null)
1248
    {
1249
        if (isset($saveAs)) {
1250
            $saveAs = strtolower($saveAs);
1251
            $this->checkFileExtension($saveAs);
1252
            $this->saveAs = $saveAs;
1253
            $this->extension = $saveAs;
1254
        }
1255
1256
        $this->log("Prepare to save image as: " . $this->extension);
1257
1258
        return $this;
1259
    }
1260
1261
1262
1263
    /**
1264
     * Set JPEG quality to use when saving image
1265
     *
1266
     * @param int $quality as the quality to set.
1267
     *
1268
     * @return $this
1269
     */
1270 View Code Duplication
    public function setJpegQuality($quality = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1271
    {
1272
        if ($quality) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $quality of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1273
            $this->useQuality = true;
1274
        }
1275
1276
        $this->quality = isset($quality)
1277
            ? $quality
1278 2
            : self::JPEG_QUALITY_DEFAULT;
1279
1280 2
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1281 2
            or $this->raiseError('Quality not in range.');
1282 2
1283 2
        $this->log("Setting JPEG quality to {$this->quality}.");
1284 2
1285 2
        return $this;
1286 2
    }
1287 2
1288 2
1289 2
1290 2
    /**
1291 2
     * Set PNG compressen algorithm to use when saving image
1292
     *
1293 2
     * @param int $compress as the algorithm to use.
1294 2
     *
1295
     * @return $this
1296 2
     */
1297 2 View Code Duplication
    public function setPngCompression($compress = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1298
    {
1299
        if ($compress) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $compress of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1300
            $this->useCompress = true;
1301 2
        }
1302 2
1303
        $this->compress = isset($compress)
1304 2
            ? $compress
1305 2
            : self::PNG_COMPRESSION_DEFAULT;
1306 2
1307
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1308 2
            or $this->raiseError('Quality not in range.');
1309 2
1310 2
        $this->log("Setting PNG compression level to {$this->compress}.");
1311
1312 2
        return $this;
1313 2
    }
1314
1315
1316
1317
    /**
1318
     * Use original image if possible, check options which affects image processing.
1319
     *
1320
     * @param boolean $useOrig default is to use original if possible, else set to false.
1321
     *
1322
     * @return $this
1323
     */
1324 2
    public function useOriginalIfPossible($useOrig = true)
1325 2
    {
1326 2
        if ($useOrig
1327 2
            && ($this->newWidth == $this->width)
1328
            && ($this->newHeight == $this->height)
1329 2
            && !$this->area
1330
            && !$this->crop
1331 2
            && !$this->cropToFit
1332 2
            && !$this->fillToFit
1333 2
            && !$this->filters
1334
            && !$this->sharpen
1335 2
            && !$this->emboss
1336 2
            && !$this->blur
1337
            && !$this->convolve
1338
            && !$this->palette
1339
            && !$this->useQuality
1340 2
            && !$this->useCompress
1341 2
            && !$this->saveAs
1342
            && !$this->rotateBefore
1343
            && !$this->rotateAfter
1344
            && !$this->autoRotate
1345 2
            && !$this->bgColor
1346 2
            && ($this->upscale === self::UPSCALE_DEFAULT)
1347
            && !$this->lossy
1348
        ) {
1349
            $this->log("Using original image.");
1350
            $this->output($this->pathToImage);
1351
        }
1352 2
1353 2
        return $this;
1354 2
    }
1355 2
1356 2
1357 2
1358 2
    /**
1359
     * Generate filename to save file in cache.
1360 2
     *
1361
     * @param string  $base      as optional basepath for storing file.
1362
     * @param boolean $useSubdir use or skip the subdir part when creating the
1363
     *                           filename.
1364
     * @param string  $prefix    to add as part of filename
1365
     *
1366
     * @return $this
1367
     */
1368
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1369
    {
1370
        $filename     = basename($this->pathToImage);
1371
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1372
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1373
        $stretch      = $this->keepRatio === false ? '_st'               : null;
1374
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1375
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1376
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1377
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1378
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1379
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1380
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1381
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1382
        $lossy        = $this->lossy        ? "_l"                       : null;
1383
1384
        $saveAs = $this->normalizeFileExtension();
1385
        $saveAs = $saveAs ? "_$saveAs" : null;
1386
1387
        $copyStrat = null;
1388
        if ($this->copyStrategy === self::RESIZE) {
1389
            $copyStrat = "_rs";
1390
        }
1391
1392
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1393
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1394
1395
        $offset = isset($this->offset)
1396
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1397
            : null;
1398
1399
        $crop = $this->crop
1400
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1401
            : null;
1402
1403
        $filters = null;
1404
        if (isset($this->filters)) {
1405
            foreach ($this->filters as $filter) {
1406
                if (is_array($filter)) {
1407
                    $filters .= "_f{$filter['id']}";
1408
                    for ($i=1; $i<=$filter['argc']; $i++) {
1409
                        $filters .= "-".$filter["arg{$i}"];
1410
                    }
1411
                }
1412
            }
1413
        }
1414
1415
        $sharpen = $this->sharpen ? 's' : null;
1416
        $emboss  = $this->emboss  ? 'e' : null;
1417
        $blur    = $this->blur    ? 'b' : null;
1418
        $palette = $this->palette ? 'p' : null;
1419
1420
        $autoRotate = $this->autoRotate ? 'ar' : null;
1421
1422
        $optimize  = $this->jpegOptimize ? 'o' : null;
1423
        $optimize .= $this->pngFilter    ? 'f' : null;
1424
        $optimize .= $this->pngDeflate   ? 'd' : null;
1425
1426
        $convolve = null;
1427
        if ($this->convolve) {
1428
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1429
        }
1430
1431
        $upscale = null;
1432
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1433
            $upscale = '_nu';
1434
        }
1435
1436
        $subdir = null;
1437
        if ($useSubdir === true) {
1438
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1439
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1440
            $subdir .= '_';
1441
        }
1442
        
1443
        $file = $prefix . $subdir . $filename . $width . $height
1444
            . $offset . $crop . $cropToFit . $fillToFit . $stretch
1445
            . $crop_x . $crop_y . $upscale
1446
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1447
            . $optimize . $compress
1448
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1449
            . $convolve . $copyStrat . $lossy . $saveAs;
1450
1451
        return $this->setTarget($file, $base);
1452
    }
1453
1454
1455
1456
    /**
1457
     * Use cached version of image, if possible.
1458
     *
1459
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1460
     *
1461
     * @return $this
1462
     */
1463
    public function useCacheIfPossible($useCache = true)
1464
    {
1465
        if ($useCache && is_readable($this->cacheFileName)) {
1466
            $fileTime   = filemtime($this->pathToImage);
1467
            $cacheTime  = filemtime($this->cacheFileName);
1468
1469
            if ($fileTime <= $cacheTime) {
1470
                if ($this->useCache) {
1471
                    if ($this->verbose) {
1472
                        $this->log("Use cached file.");
1473
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1474
                    }
1475
                    $this->output($this->cacheFileName, $this->outputFormat);
1476
                } else {
1477
                    $this->log("Cache is valid but ignoring it by intention.");
1478
                }
1479
            } else {
1480
                $this->log("Original file is modified, ignoring cache.");
1481
            }
1482
        } else {
1483
            $this->log("Cachefile does not exists or ignoring it.");
1484
        }
1485
1486
        return $this;
1487
    }
1488
1489
1490
1491
    /**
1492
     * Load image from disk.
1493
     *
1494
     * @param string $src of image.
1495
     * @param string $dir as base directory where images are.
1496
     *
1497
     * @return $this
1498
     *
1499
     */
1500
    public function load($src = null, $dir = null)
1501
    {
1502
        if (isset($src)) {
1503
            $this->setSource($src, $dir);
1504
        }
1505
1506
        $this->loadImageDetails();
1507
1508
        if ($this->fileType === IMG_WEBP) {
1509
            $this->image = imagecreatefromwebp($this->pathToImage);
1510
        } else {
1511
            $imageAsString = file_get_contents($this->pathToImage);
1512
            $this->image = imagecreatefromstring($imageAsString);
1513
        }
1514
        if ($this->image === false) {
1515
            throw new Exception("Could not load image.");
1516
        }
1517
1518
        /* Removed v0.7.7
1519
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1520
            $type = $this->getPngType();
1521
            $hasFewColors = imagecolorstotal($this->image);
1522
1523
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1524
                if ($this->verbose) {
1525
                    $this->log("Handle this image as a palette image.");
1526
                }
1527
                $this->palette = true;
1528
            }
1529
        }
1530
        */
1531
1532
        if ($this->verbose) {
1533
            $this->log("### Image successfully loaded from file.");
1534
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1535
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1536
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1537
            $index = imagecolortransparent($this->image);
1538
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1539
        }
1540
1541
        return $this;
1542
    }
1543
1544
1545
1546
    /**
1547
     * Get the type of PNG image.
1548
     *
1549
     * @param string $filename to use instead of default.
1550
     *
1551
     * @return int as the type of the png-image
1552
     *
1553
     */
1554
    public function getPngType($filename = null)
1555
    {
1556
        $filename = $filename ? $filename : $this->pathToImage;
1557
1558
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1559
1560
        if ($this->verbose) {
1561
            $this->log("Checking png type of: " . $filename);
1562
            $this->log($this->getPngTypeAsString($pngType));
1563
        }
1564
1565
        return $pngType;
1566
    }
1567
1568
1569
1570
    /**
1571
     * Get the type of PNG image as a verbose string.
1572
     *
1573
     * @param integer $type     to use, default is to check the type.
0 ignored issues
show
Bug introduced by
There is no parameter named $type. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1574
     * @param string  $filename to use instead of default.
1575
     *
1576
     * @return int as the type of the png-image
1577
     *
1578
     */
1579
    private function getPngTypeAsString($pngType = null, $filename = null)
1580
    {
1581
        if ($filename || !$pngType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1582
            $pngType = $this->getPngType($filename);
1583
        }
1584
1585
        $index = imagecolortransparent($this->image);
1586
        $transparent = null;
1587
        if ($index != -1) {
1588
            $transparent = " (transparent)";
1589
        }
1590
1591
        switch ($pngType) {
1592
            case self::PNG_GREYSCALE:
1593
                $text = "PNG is type 0, Greyscale$transparent";
1594
                break;
1595
1596
            case self::PNG_RGB:
1597
                $text = "PNG is type 2, RGB$transparent";
1598
                break;
1599
1600
            case self::PNG_RGB_PALETTE:
1601
                $text = "PNG is type 3, RGB with palette$transparent";
1602
                break;
1603
1604
            case self::PNG_GREYSCALE_ALPHA:
1605
                $text = "PNG is type 4, Greyscale with alpha channel";
1606
                break;
1607
1608
            case self::PNG_RGB_ALPHA:
1609
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1610
                break;
1611
1612
            default:
1613
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1614
        }
1615
1616
        return $text;
1617
    }
1618
1619
1620
1621
1622
    /**
1623
     * Calculate number of colors in an image.
1624
     *
1625
     * @param resource $im the image.
1626
     *
1627
     * @return int
1628
     */
1629
    private function colorsTotal($im)
1630
    {
1631
        if (imageistruecolor($im)) {
1632
            $this->log("Colors as true color.");
1633
            $h = imagesy($im);
1634
            $w = imagesx($im);
1635
            $c = array();
1636
            for ($x=0; $x < $w; $x++) {
1637
                for ($y=0; $y < $h; $y++) {
1638
                    @$c['c'.imagecolorat($im, $x, $y)]++;
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1639
                }
1640
            }
1641
            return count($c);
1642
        } else {
1643
            $this->log("Colors as palette.");
1644
            return imagecolorstotal($im);
1645
        }
1646
    }
1647
1648
1649
1650
    /**
1651
     * Preprocess image before rezising it.
1652
     *
1653
     * @return $this
1654
     */
1655
    public function preResize()
1656
    {
1657
        $this->log("### Pre-process before resizing");
1658
1659
        // Rotate image
1660
        if ($this->rotateBefore) {
1661
            $this->log("Rotating image.");
1662
            $this->rotate($this->rotateBefore, $this->bgColor)
1663
                 ->reCalculateDimensions();
1664
        }
1665
1666
        // Auto-rotate image
1667
        if ($this->autoRotate) {
1668
            $this->log("Auto rotating image.");
1669
            $this->rotateExif()
1670
                 ->reCalculateDimensions();
1671
        }
1672
1673
        // Scale the original image before starting
1674
        if (isset($this->scale)) {
1675
            $this->log("Scale by {$this->scale}%");
1676
            $newWidth  = $this->width * $this->scale / 100;
1677
            $newHeight = $this->height * $this->scale / 100;
1678
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1679
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1680
            $this->image = $img;
1681
            $this->width = $newWidth;
1682
            $this->height = $newHeight;
1683
        }
1684
1685
        return $this;
1686
    }
1687
1688
1689
1690
    /**
1691
     * Resize or resample the image while resizing.
1692
     *
1693
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1694
     *
1695
     * @return $this
1696
     */
1697
    public function setCopyResizeStrategy($strategy)
1698
    {
1699
        $this->copyStrategy = $strategy;
1700
        return $this;
1701
    }
1702
1703
1704
1705
    /**
1706
     * Resize and or crop the image.
1707
     *
1708
     * @return void
1709
     */
1710
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1711
    {
1712
        if ($this->copyStrategy == self::RESIZE) {
1713
            $this->log("Copy by resize");
1714
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1715
        } else {
1716
            $this->log("Copy by resample");
1717
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1718
        }
1719
    }
1720
1721
1722
1723
    /**
1724
     * Resize and or crop the image.
1725
     *
1726
     * @return $this
1727
     */
1728
    public function resize()
1729
    {
1730
        $res = $this->imageResizer;
1731
1732
        $this->log("### Starting to Resize()");
1733
        $this->log(" Upscale = '$this->upscale'");
1734
1735
        $sw = $res->getSourceWidth();
0 ignored issues
show
Unused Code introduced by
$sw is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1736
        $sh = $res->getSourceHeight();
0 ignored issues
show
Unused Code introduced by
$sh is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1737
        $tw = $res->getTargetWidth();
1738
        $th = $res->getTargetHeight();
1739
        $cx = $res->getCropX();
1740
        $cy = $res->getCropY();
1741
        $cw = $res->getCropWidth();
1742
        $ch = $res->getCropHeight();
1743
        $dx = $res->getDestinationX();
1744
        $dy = $res->getDestinationY();
1745
        $dw = $res->getDestinationWidth();
1746
        $dh = $res->getDestinationHeight();
1747
1748
        $img = $this->CreateImageKeepTransparency($tw, $th);
0 ignored issues
show
Bug introduced by
It seems like $tw defined by $res->getTargetWidth() on line 1737 can also be of type double or null; however, Mos\CImage\CImage::createImageKeepTransparency() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $th defined by $res->getTargetHeight() on line 1738 can also be of type double or null; however, Mos\CImage\CImage::createImageKeepTransparency() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1749
        $this->imageCopyResampled($img, $this->image, $dx, $dy, $cx, $cy, $dw, $dh, $cw, $ch);
1750
        $this->image = $img;
1751
        $this->width = $tw;
1752
        $this->height = $th;
1753
1754
        return $this;
1755
1756
1757
        // Only use a specified area of the image, $this->offset is defining the area to use
1758 View Code Duplication
        if (isset($this->offset)) {
0 ignored issues
show
Unused Code introduced by
// Only use a specified ...is->offset['height']; } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1759
            $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}");
1760
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1761
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1762
            $this->image = $img;
1763
            $this->width = $this->offset['width'];
1764
            $this->height = $this->offset['height'];
1765
        }
1766
1767 View Code Duplication
        if ($this->crop) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1768
            // Do as crop, take only part of image
1769
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1770
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1771
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1772
            $this->image = $img;
1773
            $this->width = $this->crop['width'];
1774
            $this->height = $this->crop['height'];
1775
        }
1776
1777
        if (!$this->upscale) {
1778
            // Consider rewriting the no-upscale code to fit within this if-statement,
1779
            // likely to be more readable code.
1780
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1781
        }
1782
1783
        if ($this->cropToFit) {
1784
            // Resize by crop to fit
1785
            $this->log("Resizing using strategy - Crop to fit");
1786
1787
            if (!$this->upscale
1788
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1789
                $this->log("Resizing - smaller image, do not upscale.");
1790
1791
                $posX = 0;
1792
                $posY = 0;
1793
                $cropX = 0;
1794
                $cropY = 0;
1795
1796 View Code Duplication
                if ($this->newWidth > $this->width) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1797
                    $posX = round(($this->newWidth - $this->width) / 2);
1798
                }
1799 View Code Duplication
                if ($this->newWidth < $this->width) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1800
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1801
                }
1802
1803 View Code Duplication
                if ($this->newHeight > $this->height) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1804
                    $posY = round(($this->newHeight - $this->height) / 2);
1805
                }
1806 View Code Duplication
                if ($this->newHeight < $this->height) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1807
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1808
                }
1809
                $this->log(" cwidth: $this->cropWidth");
1810
                $this->log(" cheight: $this->cropHeight");
1811
                $this->log(" nwidth: $this->newWidth");
1812
                $this->log(" nheight: $this->newHeight");
1813
                $this->log(" width: $this->width");
1814
                $this->log(" height: $this->height");
1815
                $this->log(" posX: $posX");
1816
                $this->log(" posY: $posY");
1817
                $this->log(" cropX: $cropX");
1818
                $this->log(" cropY: $cropY");
1819
1820
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1821
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1822
            } else {
1823
                $cropX      = $imgres->getCropX();
1824
                $cropY      = $imgres->getCropY();
1825
                $cropWidth  = $imgres->getCropWidth();
1826
                $cropHeight = $imgres->getCropHeight();
1827
                $this->log(" Crop from $cropX x $cropY by $cropWidth x $cropHeight.");
1828
1829
                
1830
                //$cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1831
                //$cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1832
                
1833
                //$imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1834
                $imgPreCrop   = $this->CreateImageKeepTransparency($cropWidth, $cropHeight);
1835
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1836
                // $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1837
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $cropWidth, $cropHeight, $this->width, $this->height);
1838
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1839
            }
1840
1841
            $this->image = $imageResized;
1842
            $this->width = $this->newWidth;
1843
            $this->height = $this->newHeight;
1844
        } elseif ($this->fillToFit) {
1845
            // Resize by fill to fit
1846
            $this->log("Resizing using strategy - Fill to fit");
1847
1848
            $posX = 0;
1849
            $posY = 0;
1850
1851
            $ratioOrig = $this->width / $this->height;
1852
            $ratioNew  = $this->newWidth / $this->newHeight;
1853
1854
            // Check ratio for landscape or portrait
1855
            if ($ratioOrig < $ratioNew) {
1856
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1857
            } else {
1858
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1859
            }
1860
1861
            if (!$this->upscale
1862
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1863
            ) {
1864
                $this->log("Resizing - smaller image, do not upscale.");
1865
                $posX = round(($this->newWidth - $this->width) / 2);
1866
                $posY = round(($this->newHeight - $this->height) / 2);
1867
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1868
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1869
            } else {
1870
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1871
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1872
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1873
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1874
            }
1875
1876
            $this->image = $imageResized;
1877
            $this->width = $this->newWidth;
1878
            $this->height = $this->newHeight;
1879
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1880
            // Resize it
1881
            $this->log("Resizing, new height and/or width");
1882
1883
            if (!$this->upscale
1884
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1885
            ) {
1886
                $this->log("Resizing - smaller image, do not upscale.");
1887
1888
                if (!$this->keepRatio) {
1889
                    $this->log("Resizing - stretch to fit selected.");
1890
1891
                    $posX = 0;
1892
                    $posY = 0;
1893
                    $cropX = 0;
1894
                    $cropY = 0;
1895
1896
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1897
                        $posX = round(($this->newWidth - $this->width) / 2);
1898
                        $posY = round(($this->newHeight - $this->height) / 2);
1899
                    } elseif ($this->newWidth > $this->width) {
1900
                        $posX = round(($this->newWidth - $this->width) / 2);
1901
                        $cropY = round(($this->height - $this->newHeight) / 2);
1902
                    } elseif ($this->newHeight > $this->height) {
1903
                        $posY = round(($this->newHeight - $this->height) / 2);
1904
                        $cropX = round(($this->width - $this->newWidth) / 2);
1905
                    }
1906
1907
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1908
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1909
                    $this->image = $imageResized;
1910
                    $this->width = $this->newWidth;
1911
                    $this->height = $this->newHeight;
1912
                }
1913
            } else {
1914
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1915
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1916
                $this->image = $imageResized;
1917
                $this->width = $this->newWidth;
1918
                $this->height = $this->newHeight;
1919
            }
1920
        }
1921
1922
        return $this;
1923
    }
1924
1925
1926
1927
    /**
1928
     * Postprocess image after rezising image.
1929
     *
1930
     * @return $this
1931
     */
1932
    public function postResize()
1933
    {
1934
        $this->log("### Post-process after resizing");
1935
1936
        // Rotate image
1937
        if ($this->rotateAfter) {
1938
            $this->log("Rotating image.");
1939
            $this->rotate($this->rotateAfter, $this->bgColor);
1940
        }
1941
1942
        // Apply filters
1943
        if (isset($this->filters) && is_array($this->filters)) {
1944
            foreach ($this->filters as $filter) {
1945
                $this->log("Applying filter {$filter['type']}.");
1946
1947
                switch ($filter['argc']) {
1948
                    case 0:
1949
                        imagefilter($this->image, $filter['type']);
1950
                        break;
1951
1952
                    case 1:
1953
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1954
                        break;
1955
1956
                    case 2:
1957
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1958
                        break;
1959
1960 View Code Duplication
                    case 3:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1961
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1962
                        break;
1963
1964 View Code Duplication
                    case 4:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1965
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1966
                        break;
1967
                }
1968
            }
1969
        }
1970
1971
        // Convert to palette image
1972
        if ($this->palette) {
1973
            $this->log("Converting to palette image.");
1974
            $this->trueColorToPalette();
1975
        }
1976
1977
        // Blur the image
1978
        if ($this->blur) {
1979
            $this->log("Blur.");
1980
            $this->blurImage();
1981
        }
1982
1983
        // Emboss the image
1984
        if ($this->emboss) {
1985
            $this->log("Emboss.");
1986
            $this->embossImage();
1987
        }
1988
1989
        // Sharpen the image
1990
        if ($this->sharpen) {
1991
            $this->log("Sharpen.");
1992
            $this->sharpenImage();
1993
        }
1994
1995
        // Custom convolution
1996
        if ($this->convolve) {
1997
            //$this->log("Convolve: " . $this->convolve);
1998
            $this->imageConvolution();
1999
        }
2000
2001
        return $this;
2002
    }
2003
2004
2005
2006
    /**
2007
     * Rotate image using angle.
2008
     *
2009
     * @param float $angle        to rotate image.
2010
     * @param int   $anglebgColor to fill image with if needed.
0 ignored issues
show
Documentation introduced by
There is no parameter named $anglebgColor. Did you maybe mean $bgColor?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
2011
     *
2012
     * @return $this
2013
     */
2014
    public function rotate($angle, $bgColor)
0 ignored issues
show
Unused Code introduced by
The parameter $bgColor is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2015
    {
2016
        $this->log("Rotate image " . $angle . " degrees with filler color.");
2017
2018
        $color = $this->getBackgroundColor();
2019
        $this->image = imagerotate($this->image, $angle, $color);
2020
2021
        $this->width  = imagesx($this->image);
2022
        $this->height = imagesy($this->image);
2023
2024
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2025
2026
        return $this;
2027
    }
2028
2029
2030
2031
    /**
2032
     * Rotate image using information in EXIF.
2033
     *
2034
     * @return $this
2035
     */
2036
    public function rotateExif()
2037
    {
2038
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2039
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2040
            return $this;
2041
        }
2042
2043
        $exif = exif_read_data($this->pathToImage);
2044
2045
        if (!empty($exif['Orientation'])) {
2046
            switch ($exif['Orientation']) {
2047
                case 3:
2048
                    $this->log("Autorotate 180.");
2049
                    $this->rotate(180, $this->bgColor);
2050
                    break;
2051
2052
                case 6:
2053
                    $this->log("Autorotate -90.");
2054
                    $this->rotate(-90, $this->bgColor);
2055
                    break;
2056
2057
                case 8:
2058
                    $this->log("Autorotate 90.");
2059
                    $this->rotate(90, $this->bgColor);
2060
                    break;
2061
2062
                default:
2063
                    $this->log("Autorotate ignored, unknown value as orientation.");
2064
            }
2065
        } else {
2066
            $this->log("Autorotate ignored, no orientation in EXIF.");
2067
        }
2068
2069
        return $this;
2070
    }
2071
2072
2073
2074
    /**
2075
     * Convert true color image to palette image, keeping alpha.
2076
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2077
     *
2078
     * @return void
2079
     */
2080
    public function trueColorToPalette()
2081
    {
2082
        $img = imagecreatetruecolor($this->width, $this->height);
2083
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2084
        imagecolortransparent($img, $bga);
2085
        imagefill($img, 0, 0, $bga);
2086
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2087
        imagetruecolortopalette($img, false, 255);
2088
        imagesavealpha($img, true);
2089
2090
        if (imageistruecolor($this->image)) {
2091
            $this->log("Matching colors with true color image.");
2092
            imagecolormatch($this->image, $img);
2093
        }
2094
2095
        $this->image = $img;
2096
    }
2097
2098
2099
2100
    /**
2101
     * Sharpen image using image convolution.
2102
     *
2103
     * @return $this
2104
     */
2105
    public function sharpenImage()
2106
    {
2107
        $this->imageConvolution('sharpen');
2108
        return $this;
2109
    }
2110
2111
2112
2113
    /**
2114
     * Emboss image using image convolution.
2115
     *
2116
     * @return $this
2117
     */
2118
    public function embossImage()
2119
    {
2120
        $this->imageConvolution('emboss');
2121
        return $this;
2122
    }
2123
2124
2125
2126
    /**
2127
     * Blur image using image convolution.
2128
     *
2129
     * @return $this
2130
     */
2131
    public function blurImage()
2132
    {
2133
        $this->imageConvolution('blur');
2134
        return $this;
2135
    }
2136
2137
2138
2139
    /**
2140
     * Create convolve expression and return arguments for image convolution.
2141
     *
2142
     * @param string $expression constant string which evaluates to a list of
2143
     *                           11 numbers separated by komma or such a list.
2144
     *
2145
     * @return array as $matrix (3x3), $divisor and $offset
2146
     */
2147
    public function createConvolveArguments($expression)
2148
    {
2149
        // Check of matching constant
2150
        if (isset($this->convolves[$expression])) {
2151
            $expression = $this->convolves[$expression];
2152
        }
2153
2154
        $part = explode(',', $expression);
2155
        $this->log("Creating convolution expressen: $expression");
2156
2157
        // Expect list of 11 numbers, split by , and build up arguments
2158
        if (count($part) != 11) {
2159
            throw new Exception(
2160
                "Missmatch in argument convolve. Expected comma-separated string with
2161
                11 float values. Got $expression."
2162
            );
2163
        }
2164
2165
        array_walk($part, function ($item, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2166
            if (!is_numeric($item)) {
2167
                throw new Exception("Argument to convolve expression should be float but is not.");
2168
            }
2169
        });
2170
2171
        return array(
2172
            array(
2173
                array($part[0], $part[1], $part[2]),
2174
                array($part[3], $part[4], $part[5]),
2175
                array($part[6], $part[7], $part[8]),
2176
            ),
2177
            $part[9],
2178
            $part[10],
2179
        );
2180
    }
2181
2182
2183
2184
    /**
2185
     * Add custom expressions (or overwrite existing) for image convolution.
2186
     *
2187
     * @param array $options Key value array with strings to be converted
2188 2
     *                       to convolution expressions.
2189
     *
2190 2
     * @return $this
2191
     */
2192 2
    public function addConvolveExpressions($options)
2193
    {
2194 2
        $this->convolves = array_merge($this->convolves, $options);
2195 2
        return $this;
2196 2
    }
2197 2
2198
2199 2
2200
    /**
2201
     * Image convolution.
2202 2
     *
2203
     * @param string $options A string with 11 float separated by comma.
2204
     *
2205 2
     * @return $this
2206
     */
2207
    public function imageConvolution($options = null)
2208
    {
2209
        // Use incoming options or use $this.
2210
        $options = $options ? $options : $this->convolve;
2211
2212
        // Treat incoming as string, split by +
2213
        $this->log("Convolution with '$options'");
2214
        $options = explode(":", $options);
2215
2216
        // Check each option if it matches constant value
2217
        foreach ($options as $option) {
2218
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2219
            imageconvolution($this->image, $matrix, $divisor, $offset);
2220
        }
2221
2222 2
        return $this;
2223
    }
2224 2
2225 2
2226 2
2227 2
    /**
2228
     * Set default background color between 000000-FFFFFF or if using
2229 2
     * alpha 00000000-FFFFFF7F.
2230 2
     *
2231 2
     * @param string $color as hex value.
2232
     *
2233 2
     * @return $this
2234
    */
2235
    public function setDefaultBackgroundColor($color)
2236
    {
2237
        $this->log("Setting default background color to '$color'.");
2238
2239
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2240
            throw new Exception(
2241
                "Background color needs a hex value of 6 or 8
2242 2
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2243
                Current value was: '$color'."
2244 2
            );
2245 2
        }
2246 2
2247 2
        $red    = hexdec(substr($color, 0, 2));
2248
        $green  = hexdec(substr($color, 2, 2));
2249 2
        $blue   = hexdec(substr($color, 4, 2));
2250
2251
        $alpha = (strlen($color) == 8)
2252
            ? hexdec(substr($color, 6, 2))
2253
            : null;
2254
2255
        if (($red < 0 || $red > 255)
2256
            || ($green < 0 || $green > 255)
2257
            || ($blue < 0 || $blue > 255)
2258
            || ($alpha < 0 || $alpha > 127)
2259
        ) {
2260
            throw new Exception(
2261
                "Background color out of range. Red, green blue
2262
                should be 00-FF and alpha should be 00-7F.
2263
                Current value was: '$color'."
2264
            );
2265
        }
2266
2267
        $this->bgColor = strtolower($color);
2268
        $this->bgColorDefault = array(
2269
            'red'   => $red,
2270
            'green' => $green,
2271
            'blue'  => $blue,
2272
            'alpha' => $alpha
2273
        );
2274
2275
        return $this;
2276
    }
2277
2278
2279
2280
    /**
2281
     * Get the background color.
2282
     *
2283
     * @param resource $img the image to work with or null if using $this->image.
2284
     *
2285
     * @return color value or null if no background color is set.
2286
    */
2287
    private function getBackgroundColor($img = null)
2288
    {
2289
        $img = isset($img) ? $img : $this->image;
2290
2291 2
        if ($this->bgColorDefault) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->bgColorDefault of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2292
            $red   = $this->bgColorDefault['red'];
2293
            $green = $this->bgColorDefault['green'];
2294 2
            $blue  = $this->bgColorDefault['blue'];
2295
            $alpha = $this->bgColorDefault['alpha'];
2296
2297 2
            if ($alpha) {
2298
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2299
            } else {
2300
                $color = imagecolorallocate($img, $red, $green, $blue);
2301
            }
2302
2303
            return $color;
2304
        } else {
2305
            return 0;
2306
        }
2307
    }
2308
2309
2310
2311
    /**
2312 2
     * Create a image and keep transparency for png and gifs.
2313
     *
2314 2
     * @param int $width of the new image.
2315
     * @param int $height of the new image.
2316
     *
2317
     * @return image resource.
2318 2
    */
2319
    private function createImageKeepTransparency($width, $height)
2320
    {
2321
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2322
        $img = imagecreatetruecolor($width, $height);
2323 2
        imagealphablending($img, false);
2324
        imagesavealpha($img, true);
2325
2326 2
        $index = $this->image
2327 2
            ? imagecolortransparent($this->image)
2328
            : -1;
2329
2330 2
        if ($index != -1) {
2331 2
            imagealphablending($img, true);
2332
            $transparent = imagecolorsforindex($this->image, $index);
2333
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2334
            imagefill($img, 0, 0, $color);
2335
            $index = imagecolortransparent($img, $color);
2336
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2337
        } elseif ($this->bgColorDefault) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->bgColorDefault of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2338
            $color = $this->getBackgroundColor($img);
2339
            imagefill($img, 0, 0, $color);
2340
            $this->Log("Filling image with background color.");
2341
        }
2342
2343
        return $img;
2344
    }
2345
2346
2347
2348
    /**
2349 2
     * Set optimizing  and post-processing options.
2350
     *
2351
     * @param array $options with config for postprocessing with external tools.
2352
     *
2353
     * @return $this
2354 2
     */
2355 2
    public function setPostProcessingOptions($options)
2356 2
    {
2357
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2358
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2359 2
        } else {
2360 2
            $this->jpegOptimizeCmd = null;
2361 2
        }
2362
2363
        if (array_key_exists("png_lossy", $options)
2364 2
            && $options['png_lossy'] !== false) {
2365
            $this->pngLossy = $options['png_lossy'];
2366
            $this->pngLossyCmd = $options['png_lossy_cmd'];
2367
        } else {
2368
            $this->pngLossyCmd = null;
2369
        }
2370
2371
        if (isset($options['png_filter']) && $options['png_filter']) {
2372
            $this->pngFilterCmd = $options['png_filter_cmd'];
2373
        } else {
2374
            $this->pngFilterCmd = null;
2375
        }
2376
2377 2
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2378
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2379
        } else {
2380
            $this->pngDeflateCmd = null;
2381
        }
2382
2383
        return $this;
2384
    }
2385
2386
2387
2388 2
    /**
2389 2
     * Find out the type (file extension) for the image to be saved.
2390
     *
2391 2
     * @return string as image extension.
2392
     */
2393
    protected function getTargetImageExtension()
2394
    {
2395
        // switch on mimetype
2396
        if (isset($this->extension)) {
2397
            return strtolower($this->extension);
2398
        } elseif ($this->fileType === IMG_WEBP) {
2399
            return "webp";
2400
        }
2401
2402 2
        return substr(image_type_to_extension($this->fileType), 1);
2403
    }
2404
2405
2406
2407
    /**
2408
     * Save image.
2409
     *
2410
     * @param string  $src       as target filename.
2411
     * @param string  $base      as base directory where to store images.
2412
     * @param boolean $overwrite or not, default to always overwrite file.
2413
     *
2414
     * @return $this or false if no folder is set.
2415
     */
2416
    public function save($src = null, $base = null, $overwrite = true)
2417
    {
2418
        if (isset($src)) {
2419
            $this->setTarget($src, $base);
2420 2
        }
2421
2422 2
        if ($overwrite === false && is_file($this->cacheFileName)) {
2423
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2424
            return;
2425
        }
2426 2
2427 2
        is_writable($this->saveFolder)
2428 2
            or $this->raiseError('Target directory is not writable.');
2429
2430
        $type = $this->getTargetImageExtension();
2431
        $this->Log("Saving image as " . $type);
2432
        switch ($type) {
2433
            case 'jpeg':
2434
            case 'jpg':
2435
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2436
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2437
2438
                // Use JPEG optimize if defined
2439 View Code Duplication
                if ($this->jpegOptimizeCmd) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2440
                    if ($this->verbose) {
2441
                        clearstatcache();
2442
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2443
                    }
2444
                    $res = array();
2445
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2446
                    exec($cmd, $res);
2447
                    $this->log($cmd);
2448
                    $this->log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2449
                }
2450
                break;
2451
2452
            case 'gif':
2453
                $this->Log("Saving image as GIF to cache.");
2454
                imagegif($this->image, $this->cacheFileName);
2455
                break;
2456
2457
            case 'webp':
2458
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2459
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2460
                break;
2461
2462
            case 'png':
2463
            default:
2464
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2465
2466
                // Turn off alpha blending and set alpha flag
2467
                imagealphablending($this->image, false);
2468
                imagesavealpha($this->image, true);
2469
                imagepng($this->image, $this->cacheFileName, $this->compress);
2470
2471
                // Use external program to process lossy PNG, if defined
2472
                $lossyEnabled = $this->pngLossy === true;
2473
                $lossySoftEnabled = $this->pngLossy === null;
2474
                $lossyActiveEnabled = $this->lossy === true;
2475
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
2476
                    if ($this->verbose) {
2477
                        clearstatcache();
2478
                        $this->log("Lossy enabled: $lossyEnabled");
2479
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
2480
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
2481
                    }
2482
                    $res = array();
2483
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
2484
                    exec($cmd, $res);
2485
                    $this->Log($cmd);
2486
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2487
                }
2488
2489
                // Use external program to filter PNG, if defined
2490 View Code Duplication
                if ($this->pngFilterCmd) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2491
                    if ($this->verbose) {
2492
                        clearstatcache();
2493
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2494
                    }
2495
                    $res = array();
2496
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2497
                    exec($cmd, $res);
2498
                    $this->Log($cmd);
2499
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2500
                }
2501
2502
                // Use external program to deflate PNG, if defined
2503 View Code Duplication
                if ($this->pngDeflateCmd) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2504
                    if ($this->verbose) {
2505
                        clearstatcache();
2506
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2507
                    }
2508
                    $res = array();
2509
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2510
                    exec($cmd, $res);
2511
                    $this->Log($cmd);
2512
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2513
                }
2514
                break;
2515
        }
2516
2517
        if ($this->verbose) {
2518
            clearstatcache();
2519
            $this->log("Saved image to cache.");
2520
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2521
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2522
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2523
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2524
            $index = imagecolortransparent($this->image);
2525
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2526
        }
2527
2528
        return $this;
2529
    }
2530
2531
2532
2533
    /**
2534
     * Convert image from one colorpsace/color profile to sRGB without
2535
     * color profile.
2536
     *
2537
     * @param string  $src      of image.
2538
     * @param string  $dir      as base directory where images are.
2539
     * @param string  $cache    as base directory where to store images.
2540
     * @param string  $iccFile  filename of colorprofile.
2541
     * @param boolean $useCache or not, default to always use cache.
2542
     *
2543
     * @return string | boolean false if no conversion else the converted
2544
     *                          filename.
2545
     */
2546
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2547
    {
2548
        if ($this->verbose) {
2549
            $this->log("# Converting image to sRGB colorspace.");
2550
        }
2551
2552
        if (!class_exists("\Imagick")) {
2553
            $this->log(" Ignoring since Imagemagick is not installed.");
2554
            return false;
2555
        }
2556
2557
        // Prepare
2558
        $this->setSaveFolder($cache)
2559
             ->setSource($src, $dir)
2560
             ->generateFilename(null, false, 'srgb_');
2561
2562
        // Check if the cached version is accurate.
2563
        if ($useCache && is_readable($this->cacheFileName)) {
2564
            $fileTime  = filemtime($this->pathToImage);
2565
            $cacheTime = filemtime($this->cacheFileName);
2566
2567
            if ($fileTime <= $cacheTime) {
2568
                $this->log(" Using cached version: " . $this->cacheFileName);
2569
                return $this->cacheFileName;
2570
            }
2571
        }
2572
2573
        // Only convert if cachedir is writable
2574
        if (is_writable($this->saveFolder)) {
2575
            // Load file and check if conversion is needed
2576
            $image      = new \Imagick($this->pathToImage);
2577
            $colorspace = $image->getImageColorspace();
2578
            $this->log(" Current colorspace: " . $colorspace);
2579
2580
            $profiles      = $image->getImageProfiles('*', false);
2581
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2582
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2583
2584
            if ($colorspace != \Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2585
                $this->log(" Converting to sRGB.");
2586
2587
                $sRGBicc = file_get_contents($iccFile);
2588
                $image->profileImage('icc', $sRGBicc);
2589
2590
                $image->transformImageColorspace(\Imagick::COLORSPACE_SRGB);
2591
                $image->writeImage($this->cacheFileName);
2592
                return $this->cacheFileName;
2593
            }
2594
        }
2595
2596
        return false;
2597
    }
2598
2599
2600
2601
    /**
2602
     * Create a hard link, as an alias, to the cached file.
2603
     *
2604
     * @param string $alias where to store the link,
2605
     *                      filename without extension.
2606
     *
2607
     * @return $this
2608
     */
2609
    public function linkToCacheFile($alias)
2610
    {
2611
        if ($alias === null) {
2612
            $this->log("Ignore creating alias.");
2613
            return $this;
2614
        }
2615
2616
        if (is_readable($alias)) {
2617
            unlink($alias);
0 ignored issues
show
Security File Manipulation introduced by
$alias can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET in src/functions.php on line 83
  1. Read from $_GET
    in src/functions.php on line 83
  2. get() returns tainted data, and $alias is assigned
    in webroot/img.php on line 873
  3. $aliasTarget is assigned
    in webroot/img.php on line 880
  4. $aliasTarget is passed to CImage::linkToCacheFile()
    in webroot/img.php on line 1182
  2. Path: Read from $_GET in src/functions.php on line 87
  1. Read from $_GET
    in src/functions.php on line 87
  2. get() returns tainted data, and $alias is assigned
    in webroot/img.php on line 873
  3. $aliasTarget is assigned
    in webroot/img.php on line 880
  4. $aliasTarget is passed to CImage::linkToCacheFile()
    in webroot/img.php on line 1182

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2618
        }
2619
2620
        $res = link($this->cacheFileName, $alias);
2621
2622
        if ($res) {
2623
            $this->log("Created an alias as: $alias");
2624
        } else {
2625
            $this->log("Failed to create the alias: $alias");
2626
        }
2627
2628
        return $this;
2629
    }
2630
2631
2632
2633
    /**
2634
     * Add HTTP header for output together with image.
2635
     *
2636
     * @param string $type  the header type such as "Cache-Control"
2637
     * @param string $value the value to use
2638
     *
2639
     * @return void
2640
     */
2641
    public function addHTTPHeader($type, $value)
2642
    {
2643
        $this->HTTPHeader[$type] = $value;
2644
    }
2645
2646
2647
2648
    /**
2649
     * Output image to browser using caching.
2650
     *
2651
     * @param string $file   to read and output, default is to
2652
     *                       use $this->cacheFileName
2653
     * @param string $format set to json to output file as json
2654
     *                       object with details
2655
     *
2656
     * @return void
2657
     */
2658
    public function output($file = null, $format = null)
2659
    {
2660
        if (is_null($file)) {
2661
            $file = $this->cacheFileName;
2662
        }
2663
2664
        if (is_null($format)) {
2665
            $format = $this->outputFormat;
2666
        }
2667
2668
        $this->log("### Output");
2669
        $this->log("Output format is: $format");
2670
2671
        if (!$this->verbose && $format == 'json') {
2672
            header('Content-type: application/json');
2673
            echo $this->json($file);
2674
            exit;
2675
        } elseif ($format == 'ascii') {
2676
            header('Content-type: text/plain');
2677
            echo $this->ascii($file);
0 ignored issues
show
Security Cross-Site Scripting introduced by
$this->ascii($file) can contain request data and is used in output context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET in src/functions.php on line 83
  1. Read from $_GET
    in src/functions.php on line 83
  2. get() returns tainted data, and $options is assigned
    in webroot/img.php on line 777
  3. $options is passed through explode(), and $options is assigned
    in webroot/img.php on line 778
  4. $defaultOptions is assigned
    in webroot/img.php on line 789
  5. $defaultOptions is passed to CImage::setAsciiOptions()
    in webroot/img.php on line 801
  6. CImage::$asciiOptions is assigned
    in src/CImage/CImage.php on line 2815
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in src/CImage/CImage.php on line 2832
  8. $options is passed through array_merge(), and $default is assigned
    in src/CImage/CAsciiArt.php on line 91
  9. $default['customCharacterSet'] is passed to CAsciiArt::addCharacterSet()
    in src/CImage/CAsciiArt.php on line 94
  10. CAsciiArt::$characterSet is assigned
    in src/CImage/CAsciiArt.php on line 70
  11. Tainted property CAsciiArt::$characterSet is read, and CAsciiArt::$characters is assigned
    in src/CImage/CAsciiArt.php on line 99
  12. Tainted property CAsciiArt::$characters is read, and $char is assigned
    in src/CImage/CAsciiArt.php on line 212
  13. CAsciiArt::luminance2character() returns tainted data, and $ascii is assigned
    in src/CImage/CAsciiArt.php on line 129
  14. CAsciiArt::createFromFile() returns tainted data
    in src/CImage/CImage.php on line 2833
  15. CImage::ascii() returns tainted data
    in src/CImage/CImage.php on line 2677
  2. Path: Read from $_GET in src/functions.php on line 87
  1. Read from $_GET
    in src/functions.php on line 87
  2. get() returns tainted data, and $options is assigned
    in webroot/img.php on line 777
  3. $options is passed through explode(), and $options is assigned
    in webroot/img.php on line 778
  4. $defaultOptions is assigned
    in webroot/img.php on line 789
  5. $defaultOptions is passed to CImage::setAsciiOptions()
    in webroot/img.php on line 801
  6. CImage::$asciiOptions is assigned
    in src/CImage/CImage.php on line 2815
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in src/CImage/CImage.php on line 2832
  8. $options is passed through array_merge(), and $default is assigned
    in src/CImage/CAsciiArt.php on line 91
  9. $default['customCharacterSet'] is passed to CAsciiArt::addCharacterSet()
    in src/CImage/CAsciiArt.php on line 94
  10. CAsciiArt::$characterSet is assigned
    in src/CImage/CAsciiArt.php on line 70
  11. Tainted property CAsciiArt::$characterSet is read, and CAsciiArt::$characters is assigned
    in src/CImage/CAsciiArt.php on line 99
  12. Tainted property CAsciiArt::$characters is read, and $char is assigned
    in src/CImage/CAsciiArt.php on line 212
  13. CAsciiArt::luminance2character() returns tainted data, and $ascii is assigned
    in src/CImage/CAsciiArt.php on line 129
  14. CAsciiArt::createFromFile() returns tainted data
    in src/CImage/CImage.php on line 2833
  15. CImage::ascii() returns tainted data
    in src/CImage/CImage.php on line 2677

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2678
            exit;
2679
        }
2680
2681
        $this->log("Outputting image: $file");
2682
2683
        // Get image modification time
2684
        clearstatcache();
2685
        $lastModified = filemtime($file);
2686
        $lastModifiedFormat = "D, d M Y H:i:s";
2687
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2688
2689
        if (!$this->verbose) {
2690
            $header = "Last-Modified: $gmdate GMT";
2691
            header($header);
2692
            $this->fastTrackCache->addHeader($header);
2693
            $this->fastTrackCache->setLastModified($lastModified);
2694
        }
2695
2696
        foreach ($this->HTTPHeader as $key => $val) {
2697
            $header = "$key: $val";
2698
            header($header);
2699
            $this->fastTrackCache->addHeader($header);
2700
        }
2701
2702
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
2703
            && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2704 9
            if ($this->verbose) {
2705
                $this->log("304 not modified");
2706 9
                $this->verboseOutput();
2707
                exit;
2708
            }
2709
2710 9
            header("HTTP/1.0 304 Not Modified");
2711
            if (CIMAGE_DEBUG) {
2712
                trace(__CLASS__ . " 304");
2713
            }
2714
        } else {
2715
            $this->loadImageDetails($file);
2716
            $mime = $this->getMimeType();
2717
            $size = filesize($file);
2718
2719
            if ($this->verbose) {
2720
                $this->log("Last-Modified: " . $gmdate . " GMT");
2721
                $this->log("Content-type: " . $mime);
2722
                $this->log("Content-length: " . $size);
2723
                $this->verboseOutput();
2724
2725
                if (is_null($this->verboseFileName)) {
2726
                    exit;
2727
                }
2728
            }
2729
2730
            $header = "Content-type: $mime";
2731
            header($header);
2732
            $this->fastTrackCache->addHeaderOnOutput($header);
2733
2734
            $header = "Content-length: $size";
2735
            header($header);
2736
            $this->fastTrackCache->addHeaderOnOutput($header);
2737
2738
            $this->fastTrackCache->setSource($file);
2739
            $this->fastTrackCache->writeToCache();
2740
            if (CIMAGE_DEBUG) {
2741
                trace(__CLASS__ . " 200");
2742
            }
2743
            readfile($file);
2744
        }
2745
2746
        exit;
2747
    }
2748
2749
2750
2751
    /**
2752
     * Create a JSON object from the image details.
2753
     *
2754
     * @param string $file the file to output.
2755
     *
2756
     * @return string json-encoded representation of the image.
2757
     */
2758
    public function json($file = null)
2759
    {
2760
        $file = $file ? $file : $this->cacheFileName;
2761
2762
        $details = array();
2763
2764
        clearstatcache();
2765
2766
        $details['src']       = $this->imageSrc;
2767
        $lastModified         = filemtime($this->pathToImage);
2768
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2769
2770
        $details['cache']       = basename($this->cacheFileName);
2771
        $lastModified           = filemtime($this->cacheFileName);
2772
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2773
2774
        $this->load($file);
2775
2776
        $details['filename']    = basename($file);
2777
        $details['mimeType']    = $this->getMimeType($this->fileType);
0 ignored issues
show
Unused Code introduced by
The call to CImage::getMimeType() has too many arguments starting with $this->fileType.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2778
        $details['width']       = $this->width;
2779
        $details['height']      = $this->height;
2780
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2781
        $details['size']        = filesize($file);
2782
        $details['colors'] = $this->colorsTotal($this->image);
2783
        $details['includedFiles'] = count(get_included_files());
2784
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2785
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2786
        $details['memoryLimit'] = ini_get('memory_limit');
2787
2788
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2789
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2790
        }
2791
2792
        if ($details['mimeType'] == 'image/png') {
2793
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2794
        }
2795
2796
        $options = null;
2797
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2798
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2799
        }
2800
2801
        return json_encode($details, $options);
2802
    }
2803
2804
2805
2806
    /**
2807
     * Set options for creating ascii version of image.
2808
     *
2809
     * @param array $options empty to use default or set options to change.
2810
     *
2811
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2812
     */
2813
    public function setAsciiOptions($options = array())
2814
    {
2815
        $this->asciiOptions = $options;
2816
    }
2817
2818
2819
2820
    /**
2821
     * Create an ASCII version from the image details.
2822
     *
2823
     * @param string $file the file to output.
2824
     *
2825
     * @return string ASCII representation of the image.
2826
     */
2827
    public function ascii($file = null)
2828
    {
2829
        $file = $file ? $file : $this->cacheFileName;
2830
2831
        $asciiArt = new CAsciiArt();
2832
        $asciiArt->setOptions($this->asciiOptions);
2833
        return $asciiArt->createFromFile($file);
2834
    }
2835
2836
2837
2838
    /**
2839
     * Log an event if verbose mode.
2840
     *
2841
     * @param string $message to log.
2842
     *
2843
     * @return this
2844
     */
2845
    public function log($message)
2846
    {
2847
        if ($this->verbose) {
2848
            $this->log[] = $message;
2849
        }
2850
2851
        return $this;
2852
    }
2853
2854
2855
2856
    /**
2857
     * Do verbose output to a file.
2858
     *
2859
     * @param string $fileName where to write the verbose output.
2860
     *
2861
     * @return void
2862
     */
2863
    public function setVerboseToFile($fileName)
2864
    {
2865
        $this->log("Setting verbose output to file.");
2866
        $this->verboseFileName = $fileName;
2867
    }
2868
2869
2870
2871
    /**
2872
     * Do verbose output and print out the log and the actual images.
2873
     *
2874
     * @return void
2875
     */
2876
    private function verboseOutput()
2877
    {
2878
        $log = null;
2879
        $this->log("### Summary of verbose log");
2880
        $this->log("As JSON: \n" . $this->json());
2881
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2882
        $this->log("Memory limit: " . ini_get('memory_limit'));
2883
2884
        $included = get_included_files();
2885
        $this->log("Included files: " . count($included));
2886
2887
        foreach ($this->log as $val) {
2888
            if (is_array($val)) {
2889
                foreach ($val as $val1) {
2890
                    $log .= htmlentities($val1) . '<br/>';
2891
                }
2892
            } else {
2893
                $log .= htmlentities($val) . '<br/>';
2894
            }
2895
        }
2896
2897
        if (!is_null($this->verboseFileName)) {
2898
            file_put_contents(
2899
                $this->verboseFileName,
2900
                str_replace("<br/>", "\n", $log)
0 ignored issues
show
Security File Manipulation introduced by
str_replace('<br/>', ' ', $log) can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET in src/functions.php on line 83
  1. Read from $_GET
    in src/functions.php on line 83
  2. get() returns tainted data, and $saveAs is assigned
    in webroot/img.php on line 648
  3. $saveAs is passed to CImage::setSaveAsExtension()
    in webroot/img.php on line 1170
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in src/CImage/CImage.php on line 1250
  5. CImage::$extension is assigned
    in src/CImage/CImage.php on line 1253
  6. Tainted property CImage::$extension is read, and 'Prepare to save image as: ' . $this->extension is passed to CImage::log()
    in src/CImage/CImage.php on line 1256
  7. CImage::$log is assigned
    in src/CImage/CImage.php on line 2848
  8. Tainted property CImage::$log is read, and $val is assigned
    in src/CImage/CImage.php on line 2887
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in src/CImage/CImage.php on line 2893
  10. $log is passed through str_replace()
    in src/CImage/CImage.php on line 2900
  2. Path: Read from $_GET in src/functions.php on line 87
  1. Read from $_GET
    in src/functions.php on line 87
  2. get() returns tainted data, and $saveAs is assigned
    in webroot/img.php on line 648
  3. $saveAs is passed to CImage::setSaveAsExtension()
    in webroot/img.php on line 1170
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in src/CImage/CImage.php on line 1250
  5. CImage::$extension is assigned
    in src/CImage/CImage.php on line 1253
  6. Tainted property CImage::$extension is read, and 'Prepare to save image as: ' . $this->extension is passed to CImage::log()
    in src/CImage/CImage.php on line 1256
  7. CImage::$log is assigned
    in src/CImage/CImage.php on line 2848
  8. Tainted property CImage::$log is read, and $val is assigned
    in src/CImage/CImage.php on line 2887
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in src/CImage/CImage.php on line 2893
  10. $log is passed through str_replace()
    in src/CImage/CImage.php on line 2900

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2901
            );
2902
        } else {
2903
            echo <<<EOD
2904
<h1>CImage Verbose Output</h1>
2905
<pre>{$log}</pre>
2906
EOD;
2907
        }
2908
    }
2909
2910
2911
2912
    /**
2913
     * Raise error, enables to implement a selection of error methods.
2914
     *
2915
     * @param string $message the error message to display.
2916
     *
2917
     * @return void
2918
     * @throws Exception
2919
     */
2920
    private function raiseError($message)
2921
    {
2922
        throw new Exception($message);
2923
    }
2924
}
2925