CImage::imageCopyResampled()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 10
dl 0
loc 8
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

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

614
            . (/** @scrutinizer ignore-type */ is_null($whitelist) ? "null" : print_r($whitelist, 1))
Loading history...
615
        );
616
        return $this;
617
    }
618
619
620
621
    /**
622
     * Check if the hostname for the remote image, is on a whitelist,
623
     * if the whitelist is defined.
624
     *
625
     * @param string $src the remote source.
626
     *
627
     * @return boolean true if hostname on $src is in the whitelist, else false.
628
     */
629
    public function isRemoteSourceOnWhitelist($src)
630
    {
631
        if (is_null($this->remoteHostWhitelist)) {
632
            $this->log("Remote host on whitelist not configured - allowing.");
633
            return true;
634
        }
635
636
        $whitelist = new CWhitelist();
637
        $hostname = parse_url($src, PHP_URL_HOST);
638
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
639
640
        $this->log(
641
            "Remote host is on whitelist: "
642
            . ($allow ? "true" : "false")
643
        );
644
        return $allow;
645
    }
646
647
648
649
    /**
650
     * Check if file extension is valid as a file extension.
651
     *
652
     * @param string $extension of image file.
653
     *
654
     * @return $this
655
     */
656
    private function checkFileExtension($extension)
657
    {
658
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
659
660
        in_array(strtolower($extension), $valid)
661
            or $this->raiseError('Not a valid file extension.');
662
663
        return $this;
664
    }
665
666
667
668
    /**
669
     * Normalize the file extension.
670
     *
671
     * @param string $extension of image file or skip to use internal.
672
     *
673
     * @return string $extension as a normalized file extension.
674
     */
675
    private function normalizeFileExtension($extension = null)
676
    {
677
        $extension = strtolower($extension ? $extension : $this->extension);
678
679
        if ($extension == 'jpeg') {
680
                $extension = 'jpg';
681
        }
682
683
        return $extension;
684
    }
685
686
687
688
    /**
689
     * Download a remote image and return path to its local copy.
690
     *
691
     * @param string $src remote path to image.
692
     *
693
     * @return string as path to downloaded remote source.
694
     */
695
    public function downloadRemoteSource($src)
696
    {
697
        if (!$this->isRemoteSourceOnWhitelist($src)) {
698
            throw new Exception("Hostname is not on whitelist for remote sources.");
699
        }
700
701
        $remote = new CRemoteImage();
702
703
        if (!is_writable($this->remoteCache)) {
704
            $this->log("The remote cache is not writable.");
705
        }
706
707
        $remote->setCache($this->remoteCache);
708
        $remote->useCache($this->useCache);
709
        $src = $remote->download($src);
710
711
        $this->log("Remote HTTP status: " . $remote->getStatus());
712
        $this->log("Remote item is in local cache: $src");
713
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($remote->getDetails(), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

713
        $this->log("Remote details on cache:" . /** @scrutinizer ignore-type */ print_r($remote->getDetails(), true));
Loading history...
714
715
        return $src;
716
    }
717
718
719
720
    /**
721
     * Set source file to use as image source.
722
     *
723
     * @param string $src of image.
724
     * @param string $dir as optional base directory where images are.
725
     *
726
     * @return $this
727
     */
728
    public function setSource($src, $dir = null)
729
    {
730
        if (!isset($src)) {
731
            $this->imageSrc = null;
732
            $this->pathToImage = null;
733
            return $this;
734
        }
735
736
        if ($this->allowRemote && $this->isRemoteSource($src)) {
737
            $src = $this->downloadRemoteSource($src);
738
            $dir = null;
739
        }
740
741
        if (!isset($dir)) {
742
            $dir = dirname($src);
743
            $src = basename($src);
744
        }
745
746
        $this->imageSrc     = ltrim($src, '/');
747
        $imageFolder        = rtrim($dir, '/');
748
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
749
750
        return $this;
751
    }
752
753
754
755
    /**
756
     * Set target file.
757
     *
758
     * @param string $src of target image.
759
     * @param string $dir as optional base directory where images are stored.
760
     *                    Uses $this->saveFolder if null.
761
     *
762
     * @return $this
763
     */
764
    public function setTarget($src = null, $dir = null)
765
    {
766
        if (!isset($src)) {
767
            $this->cacheFileName = null;
768
            return $this;
769
        }
770
771
        if (isset($dir)) {
772
            $this->saveFolder = rtrim($dir, '/');
773
        }
774
775
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
776
777
        // Sanitize filename
778
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
779
        $this->log("The cache file name is: " . $this->cacheFileName);
780
781
        return $this;
782
    }
783
784
785
786
    /**
787
     * Get filename of target file.
788
     *
789
     * @return Boolean|String as filename of target or false if not set.
790
     */
791
    public function getTarget()
792
    {
793
        return $this->cacheFileName;
794
    }
795
796
797
798
    /**
799
     * Set options to use when processing image.
800
     *
801
     * @param array $args used when processing image.
802
     *
803
     * @return $this
804
     */
805
    public function setOptions($args)
806
    {
807
        $this->log("Set new options for processing image.");
808
809
        $defaults = array(
810
            // Options for calculate dimensions
811
            'newWidth'    => null,
812
            'newHeight'   => null,
813
            'aspectRatio' => null,
814
            'keepRatio'   => true,
815
            'cropToFit'   => false,
816
            'fillToFit'   => null,
817
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
818
            'area'        => null, //'0,0,0,0',
819
            'upscale'     => self::UPSCALE_DEFAULT,
820
821
            // Options for caching or using original
822
            'useCache'    => true,
823
            'useOriginal' => true,
824
825
            // Pre-processing, before resizing is done
826
            'scale'        => null,
827
            'rotateBefore' => null,
828
            'autoRotate'  => false,
829
830
            // General options
831
            'bgColor'     => null,
832
833
            // Post-processing, after resizing is done
834
            'palette'     => null,
835
            'filters'     => null,
836
            'sharpen'     => null,
837
            'emboss'      => null,
838
            'blur'        => null,
839
            'convolve'       => null,
840
            'rotateAfter' => null,
841
842
            // Output format
843
            'outputFormat' => null,
844
            'dpr'          => 1,
845
846
            // Postprocessing using external tools
847
            'lossy' => null,
848
        );
849
850
        // Convert crop settings from string to array
851
        if (isset($args['crop']) && !is_array($args['crop'])) {
852
            $pices = explode(',', $args['crop']);
853
            $args['crop'] = array(
854
                'width'   => $pices[0],
855
                'height'  => $pices[1],
856
                'start_x' => $pices[2],
857
                'start_y' => $pices[3],
858
            );
859
        }
860
861
        // Convert area settings from string to array
862
        if (isset($args['area']) && !is_array($args['area'])) {
863
                $pices = explode(',', $args['area']);
864
                $args['area'] = array(
865
                    'top'    => $pices[0],
866
                    'right'  => $pices[1],
867
                    'bottom' => $pices[2],
868
                    'left'   => $pices[3],
869
                );
870
        }
871
872
        // Convert filter settings from array of string to array of array
873
        if (isset($args['filters']) && is_array($args['filters'])) {
874
            foreach ($args['filters'] as $key => $filterStr) {
875
                $parts = explode(',', $filterStr);
876
                $filter = $this->mapFilter($parts[0]);
877
                $filter['str'] = $filterStr;
878
                for ($i=1; $i<=$filter['argc']; $i++) {
879
                    if (isset($parts[$i])) {
880
                        $filter["arg{$i}"] = $parts[$i];
881
                    } else {
882
                        throw new Exception(
883
                            'Missing arg to filter, review how many arguments are needed at
884
                            http://php.net/manual/en/function.imagefilter.php'
885
                        );
886
                    }
887
                }
888
                $args['filters'][$key] = $filter;
889
            }
890
        }
891
892
        // Merge default arguments with incoming and set properties.
893
        //$args = array_merge_recursive($defaults, $args);
894
        $args = array_merge($defaults, $args);
895
        foreach ($defaults as $key => $val) {
896
            $this->{$key} = $args[$key];
897
        }
898
899
        if ($this->bgColor) {
900
            $this->setDefaultBackgroundColor($this->bgColor);
901
        }
902
903
        // Save original values to enable re-calculating
904
        $this->newWidthOrig  = $this->newWidth;
905
        $this->newHeightOrig = $this->newHeight;
906
        $this->cropOrig      = $this->crop;
907
908
        return $this;
909
    }
910
911
912
913
    /**
914
     * Map filter name to PHP filter and id.
915
     *
916
     * @param string $name the name of the filter.
917
     *
918
     * @return array with filter settings
919
     * @throws Exception
920
     */
921
    private function mapFilter($name)
922
    {
923
        $map = array(
924
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
925
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
926
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
927
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
928
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
929
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
930
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
931
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
932
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
933
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
934
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
935
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
936
        );
937
938
        if (isset($map[$name])) {
939
            return $map[$name];
940
        } else {
941
            throw new Exception('No such filter.');
942
        }
943
    }
944
945
946
947
    /**
948
     * Load image details from original image file.
949
     *
950
     * @param string $file the file to load or null to use $this->pathToImage.
951
     *
952
     * @return $this
953
     * @throws Exception
954
     */
955
    public function loadImageDetails($file = null)
956
    {
957
        $file = $file ? $file : $this->pathToImage;
958
959
        is_readable($file)
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of is_readable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

959
        is_readable(/** @scrutinizer ignore-type */ $file)
Loading history...
960
            or $this->raiseError('Image file does not exist.');
961
962
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of getimagesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

962
        $info = list($this->width, $this->height, $this->fileType) = getimagesize(/** @scrutinizer ignore-type */ $file);
Loading history...
963
        if (empty($info)) {
964
            // To support webp
965
            $this->fileType = false;
966
            if (function_exists("exif_imagetype")) {
967
                $this->fileType = exif_imagetype($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of exif_imagetype() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

967
                $this->fileType = exif_imagetype(/** @scrutinizer ignore-type */ $file);
Loading history...
968
                if ($this->fileType === false) {
969
                    if (function_exists("imagecreatefromwebp")) {
970
                        $webp = imagecreatefromwebp($file);
971
                        if ($webp !== false) {
972
                            $this->width  = imagesx($webp);
973
                            $this->height = imagesy($webp);
974
                            $this->fileType = IMG_WEBP;
975
                        }
976
                    }
977
                }
978
            }
979
        }
980
981
        if (!$this->fileType) {
982
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
983
        }
984
985
        if ($this->verbose) {
986
            $this->log("Loading image details for: {$file}");
987
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
988
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

988
            $this->log(" Image filesize: " . filesize(/** @scrutinizer ignore-type */ $file) . " bytes.");
Loading history...
989
            $this->log(" Image mimetype: " . $this->getMimeType());
0 ignored issues
show
Bug introduced by
Are you sure $this->getMimeType() of type CImage can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

989
            $this->log(" Image mimetype: " . /** @scrutinizer ignore-type */ $this->getMimeType());
Loading history...
990
        }
991
992
        return $this;
993
    }
994
995
996
997
    /**
998
     * Get mime type for image type.
999
     *
1000
     * @return $this
1001
     * @throws Exception
1002
     */
1003
    protected function getMimeType()
1004
    {
1005
        if ($this->fileType === IMG_WEBP) {
1006
            return "image/webp";
0 ignored issues
show
Bug Best Practice introduced by
The expression return 'image/webp' returns the type string which is incompatible with the documented return type CImage.
Loading history...
1007
        }
1008
1009
        return image_type_to_mime_type($this->fileType);
0 ignored issues
show
Bug Best Practice introduced by
The expression return image_type_to_mime_type($this->fileType) returns the type string which is incompatible with the documented return type CImage.
Loading history...
1010
    }
1011
1012
1013
1014
    /**
1015
     * Init new width and height and do some sanity checks on constraints, before any
1016
     * processing can be done.
1017
     *
1018
     * @return $this
1019
     * @throws Exception
1020
     */
1021
    public function initDimensions()
1022
    {
1023
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1024
1025
        // width as %
1026
        if ($this->newWidth
1027
            && $this->newWidth[strlen($this->newWidth)-1] == '%') {
1028
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1029
            $this->log("Setting new width based on % to {$this->newWidth}");
1030
        }
1031
1032
        // height as %
1033
        if ($this->newHeight
1034
            && $this->newHeight[strlen($this->newHeight)-1] == '%') {
1035
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1036
            $this->log("Setting new height based on % to {$this->newHeight}");
1037
        }
1038
1039
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
0 ignored issues
show
Bug Best Practice introduced by
The property aspectRatio does not exist on CImage. Did you maybe forget to declare it?
Loading history...
1040
1041
        // width & height from aspect ratio
1042
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1043
            if ($this->aspectRatio >= 1) {
1044
                $this->newWidth   = $this->width;
1045
                $this->newHeight  = $this->width / $this->aspectRatio;
1046
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1047
1048
            } else {
1049
                $this->newHeight  = $this->height;
1050
                $this->newWidth   = $this->height * $this->aspectRatio;
1051
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1052
            }
1053
1054
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1055
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1056
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1057
1058
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1059
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1060
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1061
        }
1062
1063
        // Change width & height based on dpr
1064
        if ($this->dpr != 1) {
1065
            if (!is_null($this->newWidth)) {
1066
                $this->newWidth  = round($this->newWidth * $this->dpr);
1067
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1068
            }
1069
            if (!is_null($this->newHeight)) {
1070
                $this->newHeight = round($this->newHeight * $this->dpr);
1071
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1072
            }
1073
        }
1074
1075
        // Check values to be within domain
1076
        is_null($this->newWidth)
1077
            or is_numeric($this->newWidth)
1078
            or $this->raiseError('Width not numeric');
1079
1080
        is_null($this->newHeight)
1081
            or is_numeric($this->newHeight)
1082
            or $this->raiseError('Height not numeric');
1083
1084
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1085
1086
        return $this;
1087
    }
1088
1089
1090
1091
    /**
1092
     * Calculate new width and height of image, based on settings.
1093
     *
1094
     * @return $this
1095
     */
1096
    public function calculateNewWidthAndHeight()
1097
    {
1098
        // Crop, use cropped width and height as base for calulations
1099
        $this->log("Calculate new width and height.");
1100
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1101
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1102
1103
        // Check if there is an area to crop off
1104
        if (isset($this->area)) {
1105
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1106
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1107
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1108
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1109
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1110
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1111
            $this->width  = $this->offset['width'];
1112
            $this->height = $this->offset['height'];
1113
            $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']}%.");
1114
            $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.");
1115
        }
1116
1117
        $width  = $this->width;
1118
        $height = $this->height;
1119
1120
        // Check if crop is set
1121
        if ($this->crop) {
1122
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1123
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1124
1125
            if ($this->crop['start_x'] == 'left') {
1126
                $this->crop['start_x'] = 0;
1127
            } elseif ($this->crop['start_x'] == 'right') {
1128
                $this->crop['start_x'] = $this->width - $width;
1129
            } elseif ($this->crop['start_x'] == 'center') {
1130
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1131
            }
1132
1133
            if ($this->crop['start_y'] == 'top') {
1134
                $this->crop['start_y'] = 0;
1135
            } elseif ($this->crop['start_y'] == 'bottom') {
1136
                $this->crop['start_y'] = $this->height - $height;
1137
            } elseif ($this->crop['start_y'] == 'center') {
1138
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1139
            }
1140
1141
            $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.");
1142
        }
1143
1144
        // Calculate new width and height if keeping aspect-ratio.
1145
        if ($this->keepRatio) {
1146
1147
            $this->log("Keep aspect ratio.");
1148
1149
            // Crop-to-fit and both new width and height are set.
1150
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1151
1152
                // Use newWidth and newHeigh as width/height, image should fit in box.
1153
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1154
1155
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1156
1157
                // Both new width and height are set.
1158
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1159
                $ratioWidth  = $width  / $this->newWidth;
1160
                $ratioHeight = $height / $this->newHeight;
1161
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1162
                $this->newWidth  = round($width  / $ratio);
1163
                $this->newHeight = round($height / $ratio);
1164
                $this->log("New width and height was set.");
1165
1166
            } elseif (isset($this->newWidth)) {
1167
1168
                // Use new width as max-width
1169
                $factor = (float)$this->newWidth / (float)$width;
1170
                $this->newHeight = round($factor * $height);
1171
                $this->log("New width was set.");
1172
1173
            } elseif (isset($this->newHeight)) {
1174
1175
                // Use new height as max-hight
1176
                $factor = (float)$this->newHeight / (float)$height;
1177
                $this->newWidth = round($factor * $width);
1178
                $this->log("New height was set.");
1179
1180
            } else {
1181
1182
                // Use existing width and height as new width and height.
1183
                $this->newWidth = $width;
1184
                $this->newHeight = $height;
1185
            }
1186
            
1187
1188
            // Get image dimensions for pre-resize image.
1189
            if ($this->cropToFit || $this->fillToFit) {
1190
1191
                // Get relations of original & target image
1192
                $ratioWidth  = $width  / $this->newWidth;
1193
                $ratioHeight = $height / $this->newHeight;
1194
1195
                if ($this->cropToFit) {
1196
1197
                    // Use newWidth and newHeigh as defined width/height,
1198
                    // image should fit the area.
1199
                    $this->log("Crop to fit.");
1200
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1201
                    $this->cropWidth  = round($width  / $ratio);
1202
                    $this->cropHeight = round($height / $ratio);
1203
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1204
1205
                } elseif ($this->fillToFit) {
1206
1207
                    // Use newWidth and newHeigh as defined width/height,
1208
                    // image should fit the area.
1209
                    $this->log("Fill to fit.");
1210
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1211
                    $this->fillWidth  = round($width  / $ratio);
1212
                    $this->fillHeight = round($height / $ratio);
1213
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1214
                }
1215
            }
1216
        }
1217
1218
        // Crop, ensure to set new width and height
1219
        if ($this->crop) {
1220
            $this->log("Crop.");
1221
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1222
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1223
        }
1224
1225
        // Fill to fit, ensure to set new width and height
1226
        /*if ($this->fillToFit) {
1227
            $this->log("FillToFit.");
1228
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1229
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1230
        }*/
1231
1232
        // No new height or width is set, use existing measures.
1233
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1234
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1235
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1236
1237
        return $this;
1238
    }
1239
1240
1241
1242
    /**
1243
     * Re-calculate image dimensions when original image dimension has changed.
1244
     *
1245
     * @return $this
1246
     */
1247
    public function reCalculateDimensions()
1248
    {
1249
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1250
1251
        $this->newWidth  = $this->newWidthOrig;
1252
        $this->newHeight = $this->newHeightOrig;
1253
        $this->crop      = $this->cropOrig;
1254
1255
        $this->initDimensions()
1256
             ->calculateNewWidthAndHeight();
1257
1258
        return $this;
1259
    }
1260
1261
1262
1263
    /**
1264
     * Set extension for filename to save as.
1265
     *
1266
     * @param string $saveas extension to save image as
1267
     *
1268
     * @return $this
1269
     */
1270
    public function setSaveAsExtension($saveAs = null)
1271
    {
1272
        if (isset($saveAs)) {
1273
            $saveAs = strtolower($saveAs);
1274
            $this->checkFileExtension($saveAs);
1275
            $this->saveAs = $saveAs;
1276
            $this->extension = $saveAs;
1277
        }
1278
1279
        $this->log("Prepare to save image as: " . $this->extension);
1280
1281
        return $this;
1282
    }
1283
1284
1285
1286
    /**
1287
     * Set JPEG quality to use when saving image
1288
     *
1289
     * @param int $quality as the quality to set.
1290
     *
1291
     * @return $this
1292
     */
1293
    public function setJpegQuality($quality = null)
1294
    {
1295
        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 0. 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...
1296
            $this->useQuality = true;
1297
        }
1298
1299
        $this->quality = isset($quality)
1300
            ? $quality
1301
            : self::JPEG_QUALITY_DEFAULT;
1302
1303
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1304
            or $this->raiseError('Quality not in range.');
1305
1306
        $this->log("Setting JPEG quality to {$this->quality}.");
1307
1308
        return $this;
1309
    }
1310
1311
1312
1313
    /**
1314
     * Set PNG compressen algorithm to use when saving image
1315
     *
1316
     * @param int $compress as the algorithm to use.
1317
     *
1318
     * @return $this
1319
     */
1320
    public function setPngCompression($compress = null)
1321
    {
1322
        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 0. 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...
1323
            $this->useCompress = true;
1324
        }
1325
1326
        $this->compress = isset($compress)
1327
            ? $compress
1328
            : self::PNG_COMPRESSION_DEFAULT;
1329
1330
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1331
            or $this->raiseError('Quality not in range.');
1332
1333
        $this->log("Setting PNG compression level to {$this->compress}.");
1334
1335
        return $this;
1336
    }
1337
1338
1339
1340
    /**
1341
     * Use original image if possible, check options which affects image processing.
1342
     *
1343
     * @param boolean $useOrig default is to use original if possible, else set to false.
1344
     *
1345
     * @return $this
1346
     */
1347
    public function useOriginalIfPossible($useOrig = true)
1348
    {
1349
        if ($useOrig
1350
            && ($this->newWidth == $this->width)
1351
            && ($this->newHeight == $this->height)
1352
            && !$this->area
0 ignored issues
show
Bug Best Practice introduced by
The property area does not exist on CImage. Did you maybe forget to declare it?
Loading history...
1353
            && !$this->crop
1354
            && !$this->cropToFit
1355
            && !$this->fillToFit
1356
            && !$this->filters
1357
            && !$this->sharpen
1358
            && !$this->emboss
1359
            && !$this->blur
1360
            && !$this->convolve
1361
            && !$this->palette
1362
            && !$this->useQuality
1363
            && !$this->useCompress
1364
            && !$this->saveAs
1365
            && !$this->rotateBefore
1366
            && !$this->rotateAfter
1367
            && !$this->autoRotate
1368
            && !$this->bgColor
1369
            && ($this->upscale === self::UPSCALE_DEFAULT)
1370
            && !$this->lossy
1371
        ) {
1372
            $this->log("Using original image.");
1373
            $this->output($this->pathToImage);
1374
        }
1375
1376
        return $this;
1377
    }
1378
1379
1380
1381
    /**
1382
     * Generate filename to save file in cache.
1383
     *
1384
     * @param string  $base      as optional basepath for storing file.
1385
     * @param boolean $useSubdir use or skip the subdir part when creating the
1386
     *                           filename.
1387
     * @param string  $prefix    to add as part of filename
1388
     *
1389
     * @return $this
1390
     */
1391
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1392
    {
1393
        $filename     = basename($this->pathToImage);
0 ignored issues
show
Bug introduced by
It seems like $this->pathToImage can also be of type null; however, parameter $path of basename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1393
        $filename     = basename(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
1394
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1395
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1396
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1397
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1398
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1399
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1400
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1401
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1402
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1403
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1404
        $lossy        = $this->lossy        ? "_l"                       : null;
1405
1406
        $saveAs = $this->normalizeFileExtension();
1407
        $saveAs = $saveAs ? "_$saveAs" : null;
1408
1409
        $copyStrat = null;
1410
        if ($this->copyStrategy === self::RESIZE) {
1411
            $copyStrat = "_rs";
1412
        }
1413
1414
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1415
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1416
1417
        $offset = isset($this->offset)
1418
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1419
            : null;
1420
1421
        $crop = $this->crop
1422
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1423
            : null;
1424
1425
        $filters = null;
1426
        if (isset($this->filters)) {
1427
            foreach ($this->filters as $filter) {
1428
                if (is_array($filter)) {
1429
                    $filters .= "_f{$filter['id']}";
1430
                    for ($i=1; $i<=$filter['argc']; $i++) {
1431
                        $filters .= "-".$filter["arg{$i}"];
1432
                    }
1433
                }
1434
            }
1435
        }
1436
1437
        $sharpen = $this->sharpen ? 's' : null;
1438
        $emboss  = $this->emboss  ? 'e' : null;
1439
        $blur    = $this->blur    ? 'b' : null;
1440
        $palette = $this->palette ? 'p' : null;
1441
1442
        $autoRotate = $this->autoRotate ? 'ar' : null;
1443
1444
        $optimize  = $this->jpegOptimize ? 'o' : null;
1445
        $optimize .= $this->pngFilter    ? 'f' : null;
1446
        $optimize .= $this->pngDeflate   ? 'd' : null;
1447
1448
        $convolve = null;
1449
        if ($this->convolve) {
1450
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1451
        }
1452
1453
        $upscale = null;
1454
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1455
            $upscale = '_nu';
1456
        }
1457
1458
        $subdir = null;
1459
        if ($useSubdir === true) {
1460
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
0 ignored issues
show
Bug introduced by
It seems like $this->imageSrc can also be of type null; however, parameter $path of dirname() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1460
            $subdir = str_replace('/', '-', dirname(/** @scrutinizer ignore-type */ $this->imageSrc));
Loading history...
1461
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1462
            $subdir .= '_';
1463
        }
1464
1465
        $file = $prefix . $subdir . $filename . $width . $height
1466
            . $offset . $crop . $cropToFit . $fillToFit
1467
            . $crop_x . $crop_y . $upscale
1468
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1469
            . $optimize . $compress
1470
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1471
            . $convolve . $copyStrat . $lossy . $saveAs;
1472
1473
        return $this->setTarget($file, $base);
1474
    }
1475
1476
1477
1478
    /**
1479
     * Use cached version of image, if possible.
1480
     *
1481
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1482
     *
1483
     * @return $this
1484
     */
1485
    public function useCacheIfPossible($useCache = true)
1486
    {
1487
        if ($useCache && is_readable($this->cacheFileName)) {
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of is_readable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1487
        if ($useCache && is_readable(/** @scrutinizer ignore-type */ $this->cacheFileName)) {
Loading history...
1488
            $fileTime   = filemtime($this->pathToImage);
0 ignored issues
show
Bug introduced by
It seems like $this->pathToImage can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1488
            $fileTime   = filemtime(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
1489
            $cacheTime  = filemtime($this->cacheFileName);
1490
1491
            if ($fileTime <= $cacheTime) {
1492
                if ($this->useCache) {
1493
                    if ($this->verbose) {
1494
                        $this->log("Use cached file.");
1495
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1495
                        $this->log("Cached image filesize: " . filesize(/** @scrutinizer ignore-type */ $this->cacheFileName) . " bytes.");
Loading history...
1496
                    }
1497
                    $this->output($this->cacheFileName, $this->outputFormat);
1498
                } else {
1499
                    $this->log("Cache is valid but ignoring it by intention.");
1500
                }
1501
            } else {
1502
                $this->log("Original file is modified, ignoring cache.");
1503
            }
1504
        } else {
1505
            $this->log("Cachefile does not exists or ignoring it.");
1506
        }
1507
1508
        return $this;
1509
    }
1510
1511
1512
1513
    /**
1514
     * Load image from disk. Try to load image without verbose error message,
1515
     * if fail, load again and display error messages.
1516
     *
1517
     * @param string $src of image.
1518
     * @param string $dir as base directory where images are.
1519
     *
1520
     * @return $this
1521
     *
1522
     */
1523
    public function load($src = null, $dir = null)
1524
    {
1525
        if (isset($src)) {
1526
            $this->setSource($src, $dir);
1527
        }
1528
1529
        $this->loadImageDetails();
1530
1531
        if ($this->fileType === IMG_WEBP) {
1532
            $this->image = imagecreatefromwebp($this->pathToImage);
1533
        } else {
1534
            $imageAsString = file_get_contents($this->pathToImage);
0 ignored issues
show
Bug introduced by
It seems like $this->pathToImage can also be of type null; however, parameter $filename of file_get_contents() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1534
            $imageAsString = file_get_contents(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
1535
            $this->image = imagecreatefromstring($imageAsString);
1536
        }
1537
        if ($this->image === false) {
1538
            throw new Exception("Could not load image.");
1539
        }
1540
1541
        /* Removed v0.7.7
1542
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1543
            $type = $this->getPngType();
1544
            $hasFewColors = imagecolorstotal($this->image);
1545
1546
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1547
                if ($this->verbose) {
1548
                    $this->log("Handle this image as a palette image.");
1549
                }
1550
                $this->palette = true;
1551
            }
1552
        }
1553
        */
1554
1555
        if ($this->verbose) {
1556
            $this->log("### Image successfully loaded from file.");
1557
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1558
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1559
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
0 ignored issues
show
Bug introduced by
It seems like $this->image can also be of type GdImage; however, parameter $im of CImage::colorsTotal() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

1559
            $this->log(" Number of colors in image = " . $this->colorsTotal(/** @scrutinizer ignore-type */ $this->image));
Loading history...
1560
            $index = imagecolortransparent($this->image);
1561
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1562
        }
1563
1564
        return $this;
1565
    }
1566
1567
1568
1569
    /**
1570
     * Get the type of PNG image.
1571
     *
1572
     * @param string $filename to use instead of default.
1573
     *
1574
     * @return int as the type of the png-image
1575
     *
1576
     */
1577
    public function getPngType($filename = null)
1578
    {
1579
        $filename = $filename ? $filename : $this->pathToImage;
1580
1581
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
0 ignored issues
show
Bug introduced by
It seems like $filename can also be of type null; however, parameter $filename of file_get_contents() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1581
        $pngType = ord(file_get_contents(/** @scrutinizer ignore-type */ $filename, false, null, 25, 1));
Loading history...
1582
1583
        if ($this->verbose) {
1584
            $this->log("Checking png type of: " . $filename);
1585
            $this->log($this->getPngTypeAsString($pngType));
1586
        }
1587
1588
        return $pngType;
1589
    }
1590
1591
1592
1593
    /**
1594
     * Get the type of PNG image as a verbose string.
1595
     *
1596
     * @param integer $type     to use, default is to check the type.
1597
     * @param string  $filename to use instead of default.
1598
     *
1599
     * @return int as the type of the png-image
1600
     *
1601
     */
1602
    private function getPngTypeAsString($pngType = null, $filename = null)
1603
    {
1604
        if ($filename || !$pngType) {
1605
            $pngType = $this->getPngType($filename);
1606
        }
1607
1608
        $index = imagecolortransparent($this->image);
1609
        $transparent = null;
1610
        if ($index != -1) {
1611
            $transparent = " (transparent)";
1612
        }
1613
1614
        switch ($pngType) {
1615
1616
            case self::PNG_GREYSCALE:
1617
                $text = "PNG is type 0, Greyscale$transparent";
1618
                break;
1619
1620
            case self::PNG_RGB:
1621
                $text = "PNG is type 2, RGB$transparent";
1622
                break;
1623
1624
            case self::PNG_RGB_PALETTE:
1625
                $text = "PNG is type 3, RGB with palette$transparent";
1626
                break;
1627
1628
            case self::PNG_GREYSCALE_ALPHA:
1629
                $text = "PNG is type 4, Greyscale with alpha channel";
1630
                break;
1631
1632
            case self::PNG_RGB_ALPHA:
1633
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1634
                break;
1635
1636
            default:
1637
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1638
        }
1639
1640
        return $text;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $text returns the type string which is incompatible with the documented return type integer.
Loading history...
1641
    }
1642
1643
1644
1645
1646
    /**
1647
     * Calculate number of colors in an image.
1648
     *
1649
     * @param resource $im the image.
1650
     *
1651
     * @return int
1652
     */
1653
    private function colorsTotal($im)
1654
    {
1655
        if (imageistruecolor($im)) {
1656
            $this->log("Colors as true color.");
1657
            $h = imagesy($im);
1658
            $w = imagesx($im);
1659
            $c = array();
1660
            for ($x=0; $x < $w; $x++) {
1661
                for ($y=0; $y < $h; $y++) {
1662
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1663
                }
1664
            }
1665
            return count($c);
1666
        } else {
1667
            $this->log("Colors as palette.");
1668
            return imagecolorstotal($im);
1669
        }
1670
    }
1671
1672
1673
1674
    /**
1675
     * Preprocess image before rezising it.
1676
     *
1677
     * @return $this
1678
     */
1679
    public function preResize()
1680
    {
1681
        $this->log("### Pre-process before resizing");
1682
1683
        // Rotate image
1684
        if ($this->rotateBefore) {
1685
            $this->log("Rotating image.");
1686
            $this->rotate($this->rotateBefore, $this->bgColor)
1687
                 ->reCalculateDimensions();
1688
        }
1689
1690
        // Auto-rotate image
1691
        if ($this->autoRotate) {
1692
            $this->log("Auto rotating image.");
1693
            $this->rotateExif()
1694
                 ->reCalculateDimensions();
1695
        }
1696
1697
        // Scale the original image before starting
1698
        if (isset($this->scale)) {
1699
            $this->log("Scale by {$this->scale}%");
1700
            $newWidth  = $this->width * $this->scale / 100;
1701
            $newHeight = $this->height * $this->scale / 100;
1702
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1703
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1704
            $this->image = $img;
1705
            $this->width = $newWidth;
1706
            $this->height = $newHeight;
1707
        }
1708
1709
        return $this;
1710
    }
1711
1712
1713
1714
    /**
1715
     * Resize or resample the image while resizing.
1716
     *
1717
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1718
     *
1719
     * @return $this
1720
     */
1721
     public function setCopyResizeStrategy($strategy)
1722
     {
1723
         $this->copyStrategy = $strategy;
1724
         return $this;
1725
     }
1726
1727
1728
1729
    /**
1730
     * Resize and or crop the image.
1731
     *
1732
     * @return void
1733
     */
1734
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1735
    {
1736
        if($this->copyStrategy == self::RESIZE) {
1737
            $this->log("Copy by resize");
1738
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1739
        } else {
1740
            $this->log("Copy by resample");
1741
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1742
        }
1743
    }
1744
1745
1746
1747
    /**
1748
     * Resize and or crop the image.
1749
     *
1750
     * @return $this
1751
     */
1752
    public function resize()
1753
    {
1754
1755
        $this->log("### Starting to Resize()");
1756
        $this->log("Upscale = '$this->upscale'");
1757
1758
        // Only use a specified area of the image, $this->offset is defining the area to use
1759
        if (isset($this->offset)) {
1760
1761
            $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']}");
1762
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1763
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1764
            $this->image = $img;
1765
            $this->width = $this->offset['width'];
1766
            $this->height = $this->offset['height'];
1767
        }
1768
1769
        if ($this->crop) {
1770
1771
            // Do as crop, take only part of image
1772
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1773
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1774
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1775
            $this->image = $img;
1776
            $this->width = $this->crop['width'];
1777
            $this->height = $this->crop['height'];
1778
        }
1779
1780
        if (!$this->upscale) {
1781
            // Consider rewriting the no-upscale code to fit within this if-statement,
1782
            // likely to be more readable code.
1783
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1784
        }
1785
1786
        if ($this->cropToFit) {
1787
1788
            // Resize by crop to fit
1789
            $this->log("Resizing using strategy - Crop to fit");
1790
1791
            if (!$this->upscale 
1792
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1793
                $this->log("Resizing - smaller image, do not upscale.");
1794
1795
                $posX = 0;
1796
                $posY = 0;
1797
                $cropX = 0;
1798
                $cropY = 0;
1799
1800
                if ($this->newWidth > $this->width) {
1801
                    $posX = round(($this->newWidth - $this->width) / 2);
1802
                }
1803
                if ($this->newWidth < $this->width) {
1804
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1805
                }
1806
1807
                if ($this->newHeight > $this->height) {
1808
                    $posY = round(($this->newHeight - $this->height) / 2);
1809
                }
1810
                if ($this->newHeight < $this->height) {
1811
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1812
                }
1813
                $this->log(" cwidth: $this->cropWidth");
1814
                $this->log(" cheight: $this->cropHeight");
1815
                $this->log(" nwidth: $this->newWidth");
1816
                $this->log(" nheight: $this->newHeight");
1817
                $this->log(" width: $this->width");
1818
                $this->log(" height: $this->height");
1819
                $this->log(" posX: $posX");
1820
                $this->log(" posY: $posY");
1821
                $this->log(" cropX: $cropX");
1822
                $this->log(" cropY: $cropY");
1823
1824
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1825
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
0 ignored issues
show
Bug introduced by
It seems like $posX can also be of type double; however, parameter $dst_x of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1825
                imagecopy($imageResized, $this->image, /** @scrutinizer ignore-type */ $posX, $posY, $cropX, $cropY, $this->width, $this->height);
Loading history...
Bug introduced by
It seems like $cropX can also be of type double; however, parameter $src_x of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1825
                imagecopy($imageResized, $this->image, $posX, $posY, /** @scrutinizer ignore-type */ $cropX, $cropY, $this->width, $this->height);
Loading history...
Bug introduced by
It seems like $cropY can also be of type double; however, parameter $src_y of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1825
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, /** @scrutinizer ignore-type */ $cropY, $this->width, $this->height);
Loading history...
Bug introduced by
It seems like $posY can also be of type double; however, parameter $dst_y of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1825
                imagecopy($imageResized, $this->image, $posX, /** @scrutinizer ignore-type */ $posY, $cropX, $cropY, $this->width, $this->height);
Loading history...
1826
            } else {
1827
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1828
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1829
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1830
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1831
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1832
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1833
            }
1834
1835
            $this->image = $imageResized;
1836
            $this->width = $this->newWidth;
1837
            $this->height = $this->newHeight;
1838
1839
        } elseif ($this->fillToFit) {
1840
1841
            // Resize by fill to fit
1842
            $this->log("Resizing using strategy - Fill to fit");
1843
1844
            $posX = 0;
1845
            $posY = 0;
1846
1847
            $ratioOrig = $this->width / $this->height;
1848
            $ratioNew  = $this->newWidth / $this->newHeight;
1849
1850
            // Check ratio for landscape or portrait
1851
            if ($ratioOrig < $ratioNew) {
1852
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1853
            } else {
1854
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1855
            }
1856
1857
            if (!$this->upscale
1858
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1859
            ) {
1860
1861
                $this->log("Resizing - smaller image, do not upscale.");
1862
                $posX = round(($this->newWidth - $this->width) / 2);
1863
                $posY = round(($this->newHeight - $this->height) / 2);
1864
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1865
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1866
1867
            } else {
1868
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1869
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1870
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1871
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1872
            }
1873
1874
            $this->image = $imageResized;
1875
            $this->width = $this->newWidth;
1876
            $this->height = $this->newHeight;
1877
1878
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1879
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
1945
            foreach ($this->filters as $filter) {
1946
                $this->log("Applying filter {$filter['type']}.");
1947
1948
                switch ($filter['argc']) {
1949
1950
                    case 0:
1951
                        imagefilter($this->image, $filter['type']);
1952
                        break;
1953
1954
                    case 1:
1955
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1956
                        break;
1957
1958
                    case 2:
1959
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1960
                        break;
1961
1962
                    case 3:
1963
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1964
                        break;
1965
1966
                    case 4:
1967
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1968
                        break;
1969
                }
1970
            }
1971
        }
1972
1973
        // Convert to palette image
1974
        if ($this->palette) {
1975
            $this->log("Converting to palette image.");
1976
            $this->trueColorToPalette();
1977
        }
1978
1979
        // Blur the image
1980
        if ($this->blur) {
1981
            $this->log("Blur.");
1982
            $this->blurImage();
1983
        }
1984
1985
        // Emboss the image
1986
        if ($this->emboss) {
1987
            $this->log("Emboss.");
1988
            $this->embossImage();
1989
        }
1990
1991
        // Sharpen the image
1992
        if ($this->sharpen) {
1993
            $this->log("Sharpen.");
1994
            $this->sharpenImage();
1995
        }
1996
1997
        // Custom convolution
1998
        if ($this->convolve) {
1999
            //$this->log("Convolve: " . $this->convolve);
2000
            $this->imageConvolution();
2001
        }
2002
2003
        return $this;
2004
    }
2005
2006
2007
2008
    /**
2009
     * Rotate image using angle.
2010
     *
2011
     * @param float $angle        to rotate image.
2012
     * @param int   $anglebgColor to fill image with if needed.
2013
     *
2014
     * @return $this
2015
     */
2016
    public function rotate($angle, $bgColor)
0 ignored issues
show
Unused Code introduced by
The parameter $bgColor is not used and could be removed. ( Ignorable by Annotation )

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

2016
    public function rotate($angle, /** @scrutinizer ignore-unused */ $bgColor)

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

Loading history...
2017
    {
2018
        $this->log("Rotate image " . $angle . " degrees with filler color.");
2019
2020
        $color = $this->getBackgroundColor();
2021
        $this->image = imagerotate($this->image, $angle, $color);
0 ignored issues
show
Bug introduced by
$color of type color is incompatible with the type integer expected by parameter $bgd_color of imagerotate(). ( Ignorable by Annotation )

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

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

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

2167
        array_walk($part, function ($item, /** @scrutinizer ignore-unused */ $key) {

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

Loading history...
2168
            if (!is_numeric($item)) {
2169
                throw new Exception("Argument to convolve expression should be float but is not.");
2170
            }
2171
        });
2172
2173
        return array(
2174
            array(
2175
                array($part[0], $part[1], $part[2]),
2176
                array($part[3], $part[4], $part[5]),
2177
                array($part[6], $part[7], $part[8]),
2178
            ),
2179
            $part[9],
2180
            $part[10],
2181
        );
2182
    }
2183
2184
2185
2186
    /**
2187
     * Add custom expressions (or overwrite existing) for image convolution.
2188
     *
2189
     * @param array $options Key value array with strings to be converted
2190
     *                       to convolution expressions.
2191
     *
2192
     * @return $this
2193
     */
2194
    public function addConvolveExpressions($options)
2195
    {
2196
        $this->convolves = array_merge($this->convolves, $options);
2197
        return $this;
2198
    }
2199
2200
2201
2202
    /**
2203
     * Image convolution.
2204
     *
2205
     * @param string $options A string with 11 float separated by comma.
2206
     *
2207
     * @return $this
2208
     */
2209
    public function imageConvolution($options = null)
2210
    {
2211
        // Use incoming options or use $this.
2212
        $options = $options ? $options : $this->convolve;
2213
2214
        // Treat incoming as string, split by +
2215
        $this->log("Convolution with '$options'");
2216
        $options = explode(":", $options);
2217
2218
        // Check each option if it matches constant value
2219
        foreach ($options as $option) {
2220
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2221
            imageconvolution($this->image, $matrix, $divisor, $offset);
2222
        }
2223
2224
        return $this;
2225
    }
2226
2227
2228
2229
    /**
2230
     * Set default background color between 000000-FFFFFF or if using
2231
     * alpha 00000000-FFFFFF7F.
2232
     *
2233
     * @param string $color as hex value.
2234
     *
2235
     * @return $this
2236
    */
2237
    public function setDefaultBackgroundColor($color)
2238
    {
2239
        $this->log("Setting default background color to '$color'.");
2240
2241
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2242
            throw new Exception(
2243
                "Background color needs a hex value of 6 or 8
2244
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2245
                Current value was: '$color'."
2246
            );
2247
        }
2248
2249
        $red    = hexdec(substr($color, 0, 2));
2250
        $green  = hexdec(substr($color, 2, 2));
2251
        $blue   = hexdec(substr($color, 4, 2));
2252
2253
        $alpha = (strlen($color) == 8)
2254
            ? hexdec(substr($color, 6, 2))
2255
            : null;
2256
2257
        if (($red < 0 || $red > 255)
2258
            || ($green < 0 || $green > 255)
2259
            || ($blue < 0 || $blue > 255)
2260
            || ($alpha < 0 || $alpha > 127)
2261
        ) {
2262
            throw new Exception(
2263
                "Background color out of range. Red, green blue
2264
                should be 00-FF and alpha should be 00-7F.
2265
                Current value was: '$color'."
2266
            );
2267
        }
2268
2269
        $this->bgColor = strtolower($color);
2270
        $this->bgColorDefault = array(
2271
            'red'   => $red,
2272
            'green' => $green,
2273
            'blue'  => $blue,
2274
            'alpha' => $alpha
2275
        );
2276
2277
        return $this;
2278
    }
2279
2280
2281
2282
    /**
2283
     * Get the background color.
2284
     *
2285
     * @param resource $img the image to work with or null if using $this->image.
2286
     *
2287
     * @return color value or null if no background color is set.
2288
    */
2289
    private function getBackgroundColor($img = null)
2290
    {
2291
        $img = isset($img) ? $img : $this->image;
2292
2293
        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...
2294
2295
            $red   = $this->bgColorDefault['red'];
2296
            $green = $this->bgColorDefault['green'];
2297
            $blue  = $this->bgColorDefault['blue'];
2298
            $alpha = $this->bgColorDefault['alpha'];
2299
2300
            if ($alpha) {
2301
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2302
            } else {
2303
                $color = imagecolorallocate($img, $red, $green, $blue);
2304
            }
2305
2306
            return $color;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $color returns the type integer which is incompatible with the documented return type color.
Loading history...
2307
2308
        } else {
2309
            return 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return 0 returns the type integer which is incompatible with the documented return type color.
Loading history...
2310
        }
2311
    }
2312
2313
2314
2315
    /**
2316
     * Create a image and keep transparency for png and gifs.
2317
     *
2318
     * @param int $width of the new image.
2319
     * @param int $height of the new image.
2320
     *
2321
     * @return image resource.
2322
    */
2323
    private function createImageKeepTransparency($width, $height)
2324
    {
2325
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2326
        $img = imagecreatetruecolor($width, $height);
2327
        imagealphablending($img, false);
2328
        imagesavealpha($img, true);
2329
2330
        $index = $this->image
2331
            ? imagecolortransparent($this->image)
2332
            : -1;
2333
2334
        if ($index != -1) {
2335
2336
            imagealphablending($img, true);
2337
            $transparent = imagecolorsforindex($this->image, $index);
2338
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2339
            imagefill($img, 0, 0, $color);
2340
            $index = imagecolortransparent($img, $color);
2341
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2342
2343
        } 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...
2344
2345
            $color = $this->getBackgroundColor($img);
0 ignored issues
show
Bug introduced by
It seems like $img can also be of type GdImage; however, parameter $img of CImage::getBackgroundColor() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

2345
            $color = $this->getBackgroundColor(/** @scrutinizer ignore-type */ $img);
Loading history...
2346
            imagefill($img, 0, 0, $color);
0 ignored issues
show
Bug introduced by
$color of type color is incompatible with the type integer expected by parameter $color of imagefill(). ( Ignorable by Annotation )

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

2346
            imagefill($img, 0, 0, /** @scrutinizer ignore-type */ $color);
Loading history...
2347
            $this->Log("Filling image with background color.");
2348
        }
2349
2350
        return $img;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $img returns the type GdImage|resource which is incompatible with the documented return type image.
Loading history...
2351
    }
2352
2353
2354
2355
    /**
2356
     * Set optimizing  and post-processing options.
2357
     *
2358
     * @param array $options with config for postprocessing with external tools.
2359
     *
2360
     * @return $this
2361
     */
2362
    public function setPostProcessingOptions($options)
2363
    {
2364
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2365
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2366
        } else {
2367
            $this->jpegOptimizeCmd = null;
2368
        }
2369
2370
        if (array_key_exists("png_lossy", $options) 
2371
            && $options['png_lossy'] !== false) {
2372
            $this->pngLossy = $options['png_lossy'];
2373
            $this->pngLossyCmd = $options['png_lossy_cmd'];
2374
        } else {
2375
            $this->pngLossyCmd = null;
2376
        }
2377
2378
        if (isset($options['png_filter']) && $options['png_filter']) {
2379
            $this->pngFilterCmd = $options['png_filter_cmd'];
2380
        } else {
2381
            $this->pngFilterCmd = null;
2382
        }
2383
2384
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2385
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2386
        } else {
2387
            $this->pngDeflateCmd = null;
2388
        }
2389
2390
        return $this;
2391
    }
2392
2393
2394
2395
    /**
2396
     * Find out the type (file extension) for the image to be saved.
2397
     *
2398
     * @return string as image extension.
2399
     */
2400
    protected function getTargetImageExtension()
2401
    {
2402
        // switch on mimetype
2403
        if (isset($this->extension)) {
2404
            return strtolower($this->extension);
2405
        } elseif ($this->fileType === IMG_WEBP) {
2406
            return "webp";
2407
        }
2408
2409
        return substr(image_type_to_extension($this->fileType), 1);
2410
    }
2411
2412
2413
2414
    /**
2415
     * Save image.
2416
     *
2417
     * @param string  $src       as target filename.
2418
     * @param string  $base      as base directory where to store images.
2419
     * @param boolean $overwrite or not, default to always overwrite file.
2420
     *
2421
     * @return $this or false if no folder is set.
2422
     */
2423
    public function save($src = null, $base = null, $overwrite = true)
2424
    {
2425
        if (isset($src)) {
2426
            $this->setTarget($src, $base);
2427
        }
2428
2429
        if ($overwrite === false && is_file($this->cacheFileName)) {
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2429
        if ($overwrite === false && is_file(/** @scrutinizer ignore-type */ $this->cacheFileName)) {
Loading history...
2430
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2431
            return;
2432
        }
2433
2434
        is_writable($this->saveFolder)
2435
            or $this->raiseError('Target directory is not writable.');
2436
2437
        $type = $this->getTargetImageExtension();
2438
        $this->Log("Saving image as " . $type);
2439
        switch($type) {
2440
2441
            case 'jpeg':
2442
            case 'jpg':
2443
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2444
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2445
2446
                // Use JPEG optimize if defined
2447
                if ($this->jpegOptimizeCmd) {
2448
                    if ($this->verbose) {
2449
                        clearstatcache();
2450
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2450
                        $this->log("Filesize before optimize: " . filesize(/** @scrutinizer ignore-type */ $this->cacheFileName) . " bytes.");
Loading history...
2451
                    }
2452
                    $res = array();
2453
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2454
                    exec($cmd, $res);
2455
                    $this->log($cmd);
2456
                    $this->log($res);
2457
                }
2458
                break;
2459
2460
            case 'gif':
2461
                $this->Log("Saving image as GIF to cache.");
2462
                imagegif($this->image, $this->cacheFileName);
2463
                break;
2464
2465
            case 'webp':
2466
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2467
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2468
                break;
2469
2470
            case 'png':
2471
            default:
2472
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2473
2474
                // Turn off alpha blending and set alpha flag
2475
                imagealphablending($this->image, false);
2476
                imagesavealpha($this->image, true);
2477
                imagepng($this->image, $this->cacheFileName, $this->compress);
2478
2479
                // Use external program to process lossy PNG, if defined
2480
                $lossyEnabled = $this->pngLossy === true;
2481
                $lossySoftEnabled = $this->pngLossy === null;
2482
                $lossyActiveEnabled = $this->lossy === true;
2483
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
2484
                    if ($this->verbose) {
2485
                        clearstatcache();
2486
                        $this->log("Lossy enabled: $lossyEnabled");
2487
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
2488
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
2489
                    }
2490
                    $res = array();
2491
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
2492
                    exec($cmd, $res);
2493
                    $this->Log($cmd);
2494
                    $this->Log($res);
2495
                }
2496
2497
                // Use external program to filter PNG, if defined
2498
                if ($this->pngFilterCmd) {
2499
                    if ($this->verbose) {
2500
                        clearstatcache();
2501
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2502
                    }
2503
                    $res = array();
2504
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2505
                    exec($cmd, $res);
2506
                    $this->Log($cmd);
2507
                    $this->Log($res);
2508
                }
2509
2510
                // Use external program to deflate PNG, if defined
2511
                if ($this->pngDeflateCmd) {
2512
                    if ($this->verbose) {
2513
                        clearstatcache();
2514
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2515
                    }
2516
                    $res = array();
2517
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2518
                    exec($cmd, $res);
2519
                    $this->Log($cmd);
2520
                    $this->Log($res);
2521
                }
2522
                break;
2523
        }
2524
2525
        if ($this->verbose) {
2526
            clearstatcache();
2527
            $this->log("Saved image to cache.");
2528
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2529
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2530
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2531
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2532
            $index = imagecolortransparent($this->image);
2533
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2534
        }
2535
2536
        return $this;
2537
    }
2538
2539
2540
2541
    /**
2542
     * Convert image from one colorpsace/color profile to sRGB without
2543
     * color profile.
2544
     *
2545
     * @param string  $src      of image.
2546
     * @param string  $dir      as base directory where images are.
2547
     * @param string  $cache    as base directory where to store images.
2548
     * @param string  $iccFile  filename of colorprofile.
2549
     * @param boolean $useCache or not, default to always use cache.
2550
     *
2551
     * @return string | boolean false if no conversion else the converted
2552
     *                          filename.
2553
     */
2554
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2555
    {
2556
        if ($this->verbose) {
2557
            $this->log("# Converting image to sRGB colorspace.");
2558
        }
2559
2560
        if (!class_exists("Imagick")) {
2561
            $this->log(" Ignoring since Imagemagick is not installed.");
2562
            return false;
2563
        }
2564
2565
        // Prepare
2566
        $this->setSaveFolder($cache)
2567
             ->setSource($src, $dir)
2568
             ->generateFilename(null, false, 'srgb_');
2569
2570
        // Check if the cached version is accurate.
2571
        if ($useCache && is_readable($this->cacheFileName)) {
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of is_readable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2571
        if ($useCache && is_readable(/** @scrutinizer ignore-type */ $this->cacheFileName)) {
Loading history...
2572
            $fileTime  = filemtime($this->pathToImage);
0 ignored issues
show
Bug introduced by
It seems like $this->pathToImage can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2572
            $fileTime  = filemtime(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
2573
            $cacheTime = filemtime($this->cacheFileName);
2574
2575
            if ($fileTime <= $cacheTime) {
2576
                $this->log(" Using cached version: " . $this->cacheFileName);
2577
                return $this->cacheFileName;
2578
            }
2579
        }
2580
2581
        // Only covert if cachedir is writable
2582
        if (is_writable($this->saveFolder)) {
2583
            // Load file and check if conversion is needed
2584
            $image      = new Imagick($this->pathToImage);
2585
            $colorspace = $image->getImageColorspace();
2586
            $this->log(" Current colorspace: " . $colorspace);
2587
2588
            $profiles      = $image->getImageProfiles('*', false);
2589
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2590
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2591
2592
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2593
                $this->log(" Converting to sRGB.");
2594
2595
                $sRGBicc = file_get_contents($iccFile);
2596
                $image->profileImage('icc', $sRGBicc);
2597
2598
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2599
                $image->writeImage($this->cacheFileName);
2600
                return $this->cacheFileName;
2601
            }
2602
        }
2603
2604
        return false;
2605
    }
2606
2607
2608
2609
    /**
2610
     * Create a hard link, as an alias, to the cached file.
2611
     *
2612
     * @param string $alias where to store the link,
2613
     *                      filename without extension.
2614
     *
2615
     * @return $this
2616
     */
2617
    public function linkToCacheFile($alias)
2618
    {
2619
        if ($alias === null) {
0 ignored issues
show
introduced by
The condition $alias === null is always false.
Loading history...
2620
            $this->log("Ignore creating alias.");
2621
            return $this;
2622
        }
2623
2624
        if (is_readable($alias)) {
2625
            unlink($alias);
2626
        }
2627
2628
        $res = link($this->cacheFileName, $alias);
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $target of link() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2628
        $res = link(/** @scrutinizer ignore-type */ $this->cacheFileName, $alias);
Loading history...
2629
2630
        if ($res) {
2631
            $this->log("Created an alias as: $alias");
2632
        } else {
2633
            $this->log("Failed to create the alias: $alias");
2634
        }
2635
2636
        return $this;
2637
    }
2638
2639
2640
2641
    /**
2642
     * Add HTTP header for output together with image.
2643
     *
2644
     * @param string $type  the header type such as "Cache-Control"
2645
     * @param string $value the value to use
2646
     *
2647
     * @return void
2648
     */
2649
    public function addHTTPHeader($type, $value)
2650
    {
2651
        $this->HTTPHeader[$type] = $value;
2652
    }
2653
2654
2655
2656
    /**
2657
     * Output image to browser using caching.
2658
     *
2659
     * @param string $file   to read and output, default is to
2660
     *                       use $this->cacheFileName
2661
     * @param string $format set to json to output file as json
2662
     *                       object with details
2663
     *
2664
     * @return void
2665
     */
2666
    public function output($file = null, $format = null)
2667
    {
2668
        if (is_null($file)) {
2669
            $file = $this->cacheFileName;
2670
        }
2671
2672
        if (is_null($format)) {
2673
            $format = $this->outputFormat;
2674
        }
2675
2676
        $this->log("### Output");
2677
        $this->log("Output format is: $format");
2678
2679
        if (!$this->verbose && $format == 'json') {
2680
            header('Content-type: application/json');
2681
            echo $this->json($file);
2682
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2683
        } elseif ($format == 'ascii') {
2684
            header('Content-type: text/plain');
2685
            echo $this->ascii($file);
2686
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2687
        }
2688
2689
        $this->log("Outputting image: $file");
2690
2691
        // Get image modification time
2692
        clearstatcache();
2693
        $lastModified = filemtime($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2693
        $lastModified = filemtime(/** @scrutinizer ignore-type */ $file);
Loading history...
2694
        $lastModifiedFormat = "D, d M Y H:i:s";
2695
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2696
2697
        if (!$this->verbose) {
2698
            $header = "Last-Modified: $gmdate GMT";
2699
            header($header);
2700
            $this->fastTrackCache->addHeader($header);
2701
            $this->fastTrackCache->setLastModified($lastModified);
2702
        }
2703
2704
        foreach ($this->HTTPHeader as $key => $val) {
2705
            $header = "$key: $val";
2706
            header($header);
2707
            $this->fastTrackCache->addHeader($header);
2708
        }
2709
2710
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
2711
            && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2712
2713
            if ($this->verbose) {
2714
                $this->log("304 not modified");
2715
                $this->verboseOutput();
2716
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2717
            }
2718
2719
            header("HTTP/1.0 304 Not Modified");
2720
            if (CIMAGE_DEBUG) {
2721
                trace(__CLASS__ . " 304");
2722
            }
2723
2724
        } else {
2725
2726
            $this->loadImageDetails($file);
2727
            $mime = $this->getMimeType();
2728
            $size = filesize($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2728
            $size = filesize(/** @scrutinizer ignore-type */ $file);
Loading history...
2729
2730
            if ($this->verbose) {
2731
                $this->log("Last-Modified: " . $gmdate . " GMT");
2732
                $this->log("Content-type: " . $mime);
0 ignored issues
show
Bug introduced by
Are you sure $mime of type CImage can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

2732
                $this->log("Content-type: " . /** @scrutinizer ignore-type */ $mime);
Loading history...
2733
                $this->log("Content-length: " . $size);
2734
                $this->verboseOutput();
2735
2736
                if (is_null($this->verboseFileName)) {
2737
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2738
                }
2739
            }
2740
2741
            $header = "Content-type: $mime";
2742
            header($header);
2743
            $this->fastTrackCache->addHeaderOnOutput($header);
2744
2745
            $header = "Content-length: $size";
2746
            header($header);
2747
            $this->fastTrackCache->addHeaderOnOutput($header);
2748
2749
            $this->fastTrackCache->setSource($file);
2750
            $this->fastTrackCache->writeToCache();
2751
            if (CIMAGE_DEBUG) {
2752
                trace(__CLASS__ . " 200");
2753
            }
2754
            readfile($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of readfile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2754
            readfile(/** @scrutinizer ignore-type */ $file);
Loading history...
2755
        }
2756
2757
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2758
    }
2759
2760
2761
2762
    /**
2763
     * Create a JSON object from the image details.
2764
     *
2765
     * @param string $file the file to output.
2766
     *
2767
     * @return string json-encoded representation of the image.
2768
     */
2769
    public function json($file = null)
2770
    {
2771
        $file = $file ? $file : $this->cacheFileName;
2772
2773
        $details = array();
2774
2775
        clearstatcache();
2776
2777
        $details['src']       = $this->imageSrc;
2778
        $lastModified         = filemtime($this->pathToImage);
0 ignored issues
show
Bug introduced by
It seems like $this->pathToImage can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2778
        $lastModified         = filemtime(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
2779
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2780
2781
        $details['cache']       = basename($this->cacheFileName);
0 ignored issues
show
Bug introduced by
It seems like $this->cacheFileName can also be of type null; however, parameter $path of basename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2781
        $details['cache']       = basename(/** @scrutinizer ignore-type */ $this->cacheFileName);
Loading history...
2782
        $lastModified           = filemtime($this->cacheFileName);
2783
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2784
2785
        $this->load($file);
2786
2787
        $details['filename']    = basename($file);
2788
        $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. ( Ignorable by Annotation )

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

2788
        /** @scrutinizer ignore-call */ 
2789
        $details['mimeType']    = $this->getMimeType($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. Please note the @ignore annotation hint above.

Loading history...
2789
        $details['width']       = $this->width;
2790
        $details['height']      = $this->height;
2791
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2792
        $details['size']        = filesize($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2792
        $details['size']        = filesize(/** @scrutinizer ignore-type */ $file);
Loading history...
2793
        $details['colors'] = $this->colorsTotal($this->image);
0 ignored issues
show
Bug introduced by
It seems like $this->image can also be of type GdImage; however, parameter $im of CImage::colorsTotal() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

2793
        $details['colors'] = $this->colorsTotal(/** @scrutinizer ignore-type */ $this->image);
Loading history...
2794
        $details['includedFiles'] = count(get_included_files());
2795
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2796
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2797
        $details['memoryLimit'] = ini_get('memory_limit');
2798
2799
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2800
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2801
        }
2802
2803
        if ($details['mimeType'] == 'image/png') {
2804
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2805
        }
2806
2807
        $options = null;
2808
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2809
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2810
        }
2811
2812
        return json_encode($details, $options);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type null; however, parameter $flags of json_encode() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2812
        return json_encode($details, /** @scrutinizer ignore-type */ $options);
Loading history...
2813
    }
2814
2815
2816
2817
    /**
2818
     * Set options for creating ascii version of image.
2819
     *
2820
     * @param array $options empty to use default or set options to change.
2821
     *
2822
     * @return void.
0 ignored issues
show
Documentation Bug introduced by
The doc comment void. at position 0 could not be parsed: Unknown type name 'void.' at position 0 in void..
Loading history...
2823
     */
2824
    public function setAsciiOptions($options = array())
2825
    {
2826
        $this->asciiOptions = $options;
2827
    }
2828
2829
2830
2831
    /**
2832
     * Create an ASCII version from the image details.
2833
     *
2834
     * @param string $file the file to output.
2835
     *
2836
     * @return string ASCII representation of the image.
2837
     */
2838
    public function ascii($file = null)
2839
    {
2840
        $file = $file ? $file : $this->cacheFileName;
2841
2842
        $asciiArt = new CAsciiArt();
2843
        $asciiArt->setOptions($this->asciiOptions);
2844
        return $asciiArt->createFromFile($file);
2845
    }
2846
2847
2848
2849
    /**
2850
     * Log an event if verbose mode.
2851
     *
2852
     * @param string $message to log.
2853
     *
2854
     * @return this
2855
     */
2856
    public function log($message)
2857
    {
2858
        if ($this->verbose) {
2859
            $this->log[] = $message;
2860
        }
2861
2862
        return $this;
2863
    }
2864
2865
2866
2867
    /**
2868
     * Do verbose output to a file.
2869
     *
2870
     * @param string $fileName where to write the verbose output.
2871
     *
2872
     * @return void
2873
     */
2874
    public function setVerboseToFile($fileName)
2875
    {
2876
        $this->log("Setting verbose output to file.");
2877
        $this->verboseFileName = $fileName;
2878
    }
2879
2880
2881
2882
    /**
2883
     * Do verbose output and print out the log and the actual images.
2884
     *
2885
     * @return void
2886
     */
2887
    private function verboseOutput()
2888
    {
2889
        $log = null;
2890
        $this->log("### Summary of verbose log");
2891
        $this->log("As JSON: \n" . $this->json());
2892
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2893
        $this->log("Memory limit: " . ini_get('memory_limit'));
2894
2895
        $included = get_included_files();
2896
        $this->log("Included files: " . count($included));
2897
2898
        foreach ($this->log as $val) {
2899
            if (is_array($val)) {
2900
                foreach ($val as $val1) {
2901
                    $log .= htmlentities($val1) . '<br/>';
2902
                }
2903
            } else {
2904
                $log .= htmlentities($val) . '<br/>';
2905
            }
2906
        }
2907
2908
        if (!is_null($this->verboseFileName)) {
2909
            file_put_contents(
2910
                $this->verboseFileName,
2911
                str_replace("<br/>", "\n", $log)
2912
            );
2913
        } else {
2914
            echo <<<EOD
2915
<h1>CImage Verbose Output</h1>
2916
<pre>{$log}</pre>
2917
EOD;
2918
        }
2919
    }
2920
2921
2922
2923
    /**
2924
     * Raise error, enables to implement a selection of error methods.
2925
     *
2926
     * @param string $message the error message to display.
2927
     *
2928
     * @return void
2929
     * @throws Exception
2930
     */
2931
    private function raiseError($message)
2932
    {
2933
        throw new Exception($message);
2934
    }
2935
}
2936