CImage::ascii()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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