CImage::generateFilename()   F
last analyzed

Complexity

Conditions 35
Paths > 20000

Size

Total Lines 85

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1260

Importance

Changes 0
Metric Value
cc 35
nc 1610612736
nop 3
dl 0
loc 85
rs 0
c 0
b 0
f 0
ccs 0
cts 34
cp 0
crap 1260

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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
     * Use interlaced progressive mode for JPEG images.
428
     */
429
    private $interlace = false;
430
431
432
433
    /*
434 7
     * Image copy strategy, defaults to RESAMPLE.
435
     */
436 7
     const RESIZE = 1;
437 7
     const RESAMPLE = 2;
438 7
     private $copyStrategy = NULL;
439
440
441
442
    /**
443
     * Properties, the class is mutable and the method setOptions()
444
     * decides (partly) what properties are created.
445
     *
446
     * @todo Clean up these and check if and how they are used
447
     */
448
449
    public $keepRatio;
450
    public $cropToFit;
451
    private $cropWidth;
452
    private $cropHeight;
453
    public $crop_x;
454
    public $crop_y;
455
    public $filters;
456
    private $attr; // Calculated from source image
457
458
459
460
461
    /**
462
     * Constructor, can take arguments to init the object.
463
     *
464
     * @param string $imageSrc    filename which may contain subdirectory.
465
     * @param string $imageFolder path to root folder for images.
466
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
467 2
     * @param string $saveName    name of target file when saveing.
468
     */
469 2
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
470 2
    {
471
        $this->setSource($imageSrc, $imageFolder);
472
        $this->setTarget($saveFolder, $saveName);
473
    }
474
475
476
477
    /**
478
     * Inject object and use it, must be available as member.
479
     *
480
     * @param string $property to set as object.
481
     * @param object $object   to set to property.
482
     *
483
     * @return $this
484
     */
485
    public function injectDependency($property, $object)
486
    {
487
        if (!property_exists($this, $property)) {
488
            $this->raiseError("Injecting unknown property.");
489
        }
490
        $this->$property = $object;
491
        return $this;
492
    }
493
494
495
496
    /**
497
     * Set verbose mode.
498
     *
499 2
     * @param boolean $mode true or false to enable and disable verbose mode,
500
     *                      default is true.
501 2
     *
502 2
     * @return $this
503
     */
504 2
    public function setVerbose($mode = true)
505
    {
506 2
        $this->verbose = $mode;
507
        return $this;
508
    }
509
510
511
512
    /**
513
     * Set save folder, base folder for saving cache files.
514
     *
515
     * @todo clean up how $this->saveFolder is used in other methods.
516
     *
517
     * @param string $path where to store cached files.
518
     *
519 2
     * @return $this
520
     */
521 2
    public function setSaveFolder($path)
522 2
    {
523
        $this->saveFolder = $path;
524 2
        return $this;
525
    }
526 2
527 2
528 2
529 2
    /**
530
     * Use cache or not.
531 2
     *
532
     * @param boolean $use true or false to use cache.
533
     *
534
     * @return $this
535
     */
536
    public function useCache($use = true)
537
    {
538
        $this->useCache = $use;
539
        return $this;
540
    }
541
542
543 2
544
    /**
545 2
     * Create and save a dummy image. Use dimensions as stated in
546 2
     * $this->newWidth, or $width or default to 100 (same for height.
547 2
     *
548
     * @param integer $width  use specified width for image dimension.
549
     * @param integer $height use specified width for image dimension.
550
     *
551
     * @return $this
552
     */
553
    public function createDummyImage($width = null, $height = null)
554
    {
555
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
556
        $this->newHeight = $this->newHeight ?: $height ?: 100;
557
558
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
559
560 2
        return $this;
561
    }
562 2
563 2
564
565 2
    /**
566 2
     * Allow or disallow remote image download.
567 2
     *
568
     * @param boolean $allow   true or false to enable and disable.
569
     * @param string  $cache   path to cache dir.
570
     * @param string  $pattern to use to detect if its a remote file.
571
     *
572
     * @return $this
573
     */
574
    public function setRemoteDownload($allow, $cache, $pattern = null)
575
    {
576
        $this->allowRemote = $allow;
577
        $this->remoteCache = $cache;
578
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
579
580 3
        $this->log(
581
            "Set remote download to: "
582 3
            . ($this->allowRemote ? "true" : "false")
583 1
            . " using pattern "
584 1
            . $this->remotePattern
585
        );
586
587 2
        return $this;
588 2
    }
589 2
590
591 2
592
    /**
593 2
     * Check if the image resource is a remote file or not.
594 2
     *
595 2
     * @param string $src check if src is remote.
596
     *
597
     * @return boolean true if $src is a remote file, else false.
598
     */
599
    public function isRemoteSource($src)
600
    {
601
        $remote = preg_match($this->remotePattern, $src);
602
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
603
        return !!$remote;
604
    }
605
606
607
608
    /**
609
     * Set whitelist for valid hostnames from where remote source can be
610
     * downloaded.
611
     *
612
     * @param array $whitelist with regexp hostnames to allow download from.
613
     *
614
     * @return $this
615
     */
616
    public function setRemoteHostWhitelist($whitelist = null)
617
    {
618
        $this->remoteHostWhitelist = $whitelist;
619
        $this->log(
620
            "Setting remote host whitelist to: "
621
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
622
        );
623
        return $this;
624
    }
625
626 2
627
628 2
    /**
629
     * Check if the hostname for the remote image, is on a whitelist,
630 2
     * if the whitelist is defined.
631
     *
632
     * @param string $src the remote source.
633
     *
634 2
     * @return boolean true if hostname on $src is in the whitelist, else false.
635
     */
636
    public function isRemoteSourceOnWhitelist($src)
637
    {
638
        if (is_null($this->remoteHostWhitelist)) {
639
            $this->log("Remote host on whitelist not configured - allowing.");
640
            return true;
641
        }
642
643
        $whitelist = new CWhitelist();
644
        $hostname = parse_url($src, PHP_URL_HOST);
645
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
0 ignored issues
show
Security Bug introduced by
It seems like $hostname defined by parse_url($src, PHP_URL_HOST) on line 644 can also be of type false; however, CWhitelist::check() does only seem to accept string, did you maybe forget to handle an error condition?

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

Consider the follow example

<?php

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

    return false;
}

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

Loading history...
646
647
        $this->log(
648
            "Remote host is on whitelist: "
649
            . ($allow ? "true" : "false")
650
        );
651
        return $allow;
652
    }
653
654
655
656
    /**
657
     * Check if file extension is valid as a file extension.
658
     *
659
     * @param string $extension of image file.
660
     *
661
     * @return $this
662
     */
663
    private function checkFileExtension($extension)
664
    {
665
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
666
667
        in_array(strtolower($extension), $valid)
668
            or $this->raiseError('Not a valid file extension.');
669
670
        return $this;
671
    }
672
673
674
675
    /**
676
     * Normalize the file extension.
677
     *
678
     * @param string $extension of image file or skip to use internal.
679
     *
680
     * @return string $extension as a normalized file extension.
681
     */
682
    private function normalizeFileExtension($extension = null)
683
    {
684
        $extension = strtolower($extension ? $extension : $this->extension);
685
686
        if ($extension == 'jpeg') {
687
                $extension = 'jpg';
688 7
        }
689
690 7
        return $extension;
691 7
    }
692 7
693 7
694
695
    /**
696 2
     * Download a remote image and return path to its local copy.
697
     *
698
     * @param string $src remote path to image.
699
     *
700
     * @return string as path to downloaded remote source.
701 2
     */
702
    public function downloadRemoteSource($src)
703
    {
704
        if (!$this->isRemoteSourceOnWhitelist($src)) {
705
            throw new Exception("Hostname is not on whitelist for remote sources.");
706 2
        }
707 2
708 2
        $remote = new CRemoteImage();
709
710 2
        if (!is_writable($this->remoteCache)) {
711
            $this->log("The remote cache is not writable.");
712
        }
713
714
        $remote->setCache($this->remoteCache);
715
        $remote->useCache($this->useCache);
716
        $src = $remote->download($src);
717
718
        $this->log("Remote HTTP status: " . $remote->getStatus());
719
        $this->log("Remote item is in local cache: $src");
720
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
721
722
        return $src;
723
    }
724 7
725
726 7
727 7
    /**
728 7
     * Set source file to use as image source.
729
     *
730
     * @param string $src of image.
731 2
     * @param string $dir as optional base directory where images are.
732
     *
733
     * @return $this
734
     */
735 2
    public function setSource($src, $dir = null)
736
    {
737
        if (!isset($src)) {
738 2
            $this->imageSrc = null;
739 2
            $this->pathToImage = null;
740
            return $this;
741 2
        }
742
743
        if ($this->allowRemote && $this->isRemoteSource($src)) {
744
            $src = $this->downloadRemoteSource($src);
745
            $dir = null;
746
        }
747
748
        if (!isset($dir)) {
749
            $dir = dirname($src);
750
            $src = basename($src);
751 2
        }
752
753 2
        $this->imageSrc     = ltrim($src, '/');
754
        $imageFolder        = rtrim($dir, '/');
755
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
756
757
        return $this;
758
    }
759
760
761
762
    /**
763
     * Set target file.
764
     *
765
     * @param string $src of target image.
766
     * @param string $dir as optional base directory where images are stored.
767
     *                    Uses $this->saveFolder if null.
768
     *
769
     * @return $this
770
     */
771
    public function setTarget($src = null, $dir = null)
772
    {
773
        if (!isset($src)) {
774
            $this->cacheFileName = null;
775
            return $this;
776
        }
777
778
        if (isset($dir)) {
779
            $this->saveFolder = rtrim($dir, '/');
780
        }
781
782
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
783
784
        // Sanitize filename
785
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
786
        $this->log("The cache file name is: " . $this->cacheFileName);
787
788
        return $this;
789
    }
790
791
792
793
    /**
794
     * Get filename of target file.
795
     *
796
     * @return Boolean|String as filename of target or false if not set.
797
     */
798
    public function getTarget()
799
    {
800
        return $this->cacheFileName;
801
    }
802
803
804
805
    /**
806
     * Set options to use when processing image.
807
     *
808
     * @param array $args used when processing image.
809
     *
810
     * @return $this
811
     */
812
    public function setOptions($args)
813
    {
814
        $this->log("Set new options for processing image.");
815
816
        $defaults = array(
817
            // Options for calculate dimensions
818
            'newWidth'    => null,
819
            'newHeight'   => null,
820
            'aspectRatio' => null,
821
            'keepRatio'   => true,
822
            'cropToFit'   => false,
823
            'fillToFit'   => null,
824
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
825
            'area'        => null, //'0,0,0,0',
826
            'upscale'     => self::UPSCALE_DEFAULT,
827
828
            // Options for caching or using original
829
            'useCache'    => true,
830
            'useOriginal' => true,
831
832
            // Pre-processing, before resizing is done
833
            'scale'        => null,
834
            'rotateBefore' => null,
835
            'autoRotate'  => false,
836
837
            // General options
838
            'bgColor'     => null,
839
840
            // Post-processing, after resizing is done
841
            'palette'     => null,
842
            'filters'     => null,
843
            'sharpen'     => null,
844
            'emboss'      => null,
845
            'blur'        => null,
846
            'convolve'       => null,
847
            'rotateAfter' => null,
848
            'interlace' => null,
849
850
            // Output format
851
            'outputFormat' => null,
852
            'dpr'          => 1,
853
854
            // Postprocessing using external tools
855
            'lossy' => null,
856
        );
857
858
        // Convert crop settings from string to array
859
        if (isset($args['crop']) && !is_array($args['crop'])) {
860
            $pices = explode(',', $args['crop']);
861
            $args['crop'] = array(
862
                'width'   => $pices[0],
863
                'height'  => $pices[1],
864
                'start_x' => $pices[2],
865
                'start_y' => $pices[3],
866
            );
867
        }
868
869
        // Convert area settings from string to array
870
        if (isset($args['area']) && !is_array($args['area'])) {
871
                $pices = explode(',', $args['area']);
872
                $args['area'] = array(
873
                    'top'    => $pices[0],
874
                    'right'  => $pices[1],
875
                    'bottom' => $pices[2],
876
                    'left'   => $pices[3],
877
                );
878
        }
879
880
        // Convert filter settings from array of string to array of array
881
        if (isset($args['filters']) && is_array($args['filters'])) {
882
            foreach ($args['filters'] as $key => $filterStr) {
883
                $parts = explode(',', $filterStr);
884
                $filter = $this->mapFilter($parts[0]);
885
                $filter['str'] = $filterStr;
886
                for ($i=1; $i<=$filter['argc']; $i++) {
887
                    if (isset($parts[$i])) {
888
                        $filter["arg{$i}"] = $parts[$i];
889
                    } else {
890
                        throw new Exception(
891
                            'Missing arg to filter, review how many arguments are needed at
892
                            http://php.net/manual/en/function.imagefilter.php'
893
                        );
894
                    }
895
                }
896
                $args['filters'][$key] = $filter;
897
            }
898
        }
899
900
        // Merge default arguments with incoming and set properties.
901
        //$args = array_merge_recursive($defaults, $args);
902
        $args = array_merge($defaults, $args);
903
        foreach ($defaults as $key => $val) {
904
            $this->{$key} = $args[$key];
905
        }
906
907
        if ($this->bgColor) {
908
            $this->setDefaultBackgroundColor($this->bgColor);
909
        }
910
911
        // Save original values to enable re-calculating
912
        $this->newWidthOrig  = $this->newWidth;
913
        $this->newHeightOrig = $this->newHeight;
914
        $this->cropOrig      = $this->crop;
915
916
        return $this;
917
    }
918
919
920
921
    /**
922
     * Map filter name to PHP filter and id.
923
     *
924
     * @param string $name the name of the filter.
925
     *
926
     * @return array with filter settings
927
     * @throws Exception
928
     */
929
    private function mapFilter($name)
930
    {
931
        $map = array(
932
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
933
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
934
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
935
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
936
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
937
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
938
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
939
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
940
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
941
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
942
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
943
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
944
        );
945
946
        if (isset($map[$name])) {
947
            return $map[$name];
948
        } else {
949
            throw new Exception('No such filter.');
950
        }
951
    }
952
953
954
955
    /**
956
     * Load image details from original image file.
957
     *
958
     * @param string $file the file to load or null to use $this->pathToImage.
959
     *
960
     * @return $this
961
     * @throws Exception
962
     */
963
    public function loadImageDetails($file = null)
964
    {
965
        $file = $file ? $file : $this->pathToImage;
966
967
        is_readable($file)
968
            or $this->raiseError('Image file does not exist.');
969
970
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
971
        if (empty($info)) {
972
            // To support webp
973
            $this->fileType = false;
974
            if (function_exists("exif_imagetype")) {
975
                $this->fileType = exif_imagetype($file);
976
                if ($this->fileType === false) {
977
                    if (function_exists("imagecreatefromwebp")) {
978
                        $webp = imagecreatefromwebp($file);
979
                        if ($webp !== false) {
980
                            $this->width  = imagesx($webp);
981
                            $this->height = imagesy($webp);
982
                            $this->fileType = IMG_WEBP;
983
                        }
984
                    }
985
                }
986
            }
987
        }
988
989
        if (!$this->fileType) {
990
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
991
        }
992
993
        if ($this->verbose) {
994
            $this->log("Loading image details for: {$file}");
995
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
996
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
997
            $this->log(" Image mimetype: " . $this->getMimeType());
998
        }
999
1000
        return $this;
1001
    }
1002
1003
1004
1005
    /**
1006
     * Get mime type for image type.
1007
     *
1008
     * @return $this
1009
     * @throws Exception
1010
     */
1011
    protected function getMimeType()
1012
    {
1013
        if ($this->fileType === IMG_WEBP) {
1014
            return "image/webp";
1015
        }
1016
1017
        return image_type_to_mime_type($this->fileType);
1018
    }
1019
1020
1021
1022
    /**
1023
     * Init new width and height and do some sanity checks on constraints, before any
1024
     * processing can be done.
1025
     *
1026
     * @return $this
1027
     * @throws Exception
1028
     */
1029
    public function initDimensions()
1030
    {
1031
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1032
1033
        // width as %
1034
        if ($this->newWidth
1035
            && $this->newWidth[strlen($this->newWidth)-1] == '%') {
1036
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1037
            $this->log("Setting new width based on % to {$this->newWidth}");
1038
        }
1039
1040
        // height as %
1041
        if ($this->newHeight
1042
            && $this->newHeight[strlen($this->newHeight)-1] == '%') {
1043
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1044
            $this->log("Setting new height based on % to {$this->newHeight}");
1045
        }
1046
1047
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
0 ignored issues
show
Bug introduced by
The property aspectRatio does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1048
1049
        // width & height from aspect ratio
1050
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1051
            if ($this->aspectRatio >= 1) {
1052
                $this->newWidth   = $this->width;
1053
                $this->newHeight  = $this->width / $this->aspectRatio;
1054
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1055
1056
            } else {
1057
                $this->newHeight  = $this->height;
1058
                $this->newWidth   = $this->height * $this->aspectRatio;
1059
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1060
            }
1061
1062
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1063
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1064
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1065
1066
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1067
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1068
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1069
        }
1070
1071
        // Change width & height based on dpr
1072
        if ($this->dpr != 1) {
1073
            if (!is_null($this->newWidth)) {
1074
                $this->newWidth  = round($this->newWidth * $this->dpr);
1075
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1076
            }
1077
            if (!is_null($this->newHeight)) {
1078
                $this->newHeight = round($this->newHeight * $this->dpr);
1079
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1080
            }
1081
        }
1082
1083
        // Check values to be within domain
1084
        is_null($this->newWidth)
1085
            or is_numeric($this->newWidth)
1086
            or $this->raiseError('Width not numeric');
1087
1088
        is_null($this->newHeight)
1089
            or is_numeric($this->newHeight)
1090
            or $this->raiseError('Height not numeric');
1091
1092
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1093
1094
        return $this;
1095
    }
1096
1097
1098
1099
    /**
1100
     * Calculate new width and height of image, based on settings.
1101
     *
1102
     * @return $this
1103
     */
1104
    public function calculateNewWidthAndHeight()
1105
    {
1106
        // Crop, use cropped width and height as base for calulations
1107
        $this->log("Calculate new width and height.");
1108
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1109
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1110
1111
        // Check if there is an area to crop off
1112
        if (isset($this->area)) {
1113
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
0 ignored issues
show
Bug introduced by
The property area does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1114
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1115
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1116
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1117
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1118
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1119
            $this->width  = $this->offset['width'];
1120
            $this->height = $this->offset['height'];
1121
            $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']}%.");
1122
            $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.");
1123
        }
1124
1125
        $width  = $this->width;
1126
        $height = $this->height;
1127
1128
        // Check if crop is set
1129
        if ($this->crop) {
1130
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1131
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1132
1133
            if ($this->crop['start_x'] == 'left') {
1134
                $this->crop['start_x'] = 0;
1135
            } elseif ($this->crop['start_x'] == 'right') {
1136
                $this->crop['start_x'] = $this->width - $width;
1137
            } elseif ($this->crop['start_x'] == 'center') {
1138
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1139
            }
1140
1141
            if ($this->crop['start_y'] == 'top') {
1142
                $this->crop['start_y'] = 0;
1143
            } elseif ($this->crop['start_y'] == 'bottom') {
1144
                $this->crop['start_y'] = $this->height - $height;
1145
            } elseif ($this->crop['start_y'] == 'center') {
1146
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1147
            }
1148
1149
            $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.");
1150
        }
1151
1152
        // Calculate new width and height if keeping aspect-ratio.
1153
        if ($this->keepRatio) {
1154
1155
            $this->log("Keep aspect ratio.");
1156
1157
            // Crop-to-fit and both new width and height are set.
1158
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1159
1160
                // Use newWidth and newHeigh as width/height, image should fit in box.
1161
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1162
1163
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1164
1165
                // Both new width and height are set.
1166
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1167
                $ratioWidth  = $width  / $this->newWidth;
1168
                $ratioHeight = $height / $this->newHeight;
1169
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1170
                $this->newWidth  = round($width  / $ratio);
1171
                $this->newHeight = round($height / $ratio);
1172
                $this->log("New width and height was set.");
1173
1174
            } elseif (isset($this->newWidth)) {
1175
1176
                // Use new width as max-width
1177
                $factor = (float)$this->newWidth / (float)$width;
1178
                $this->newHeight = round($factor * $height);
1179
                $this->log("New width was set.");
1180
1181
            } elseif (isset($this->newHeight)) {
1182
1183
                // Use new height as max-hight
1184
                $factor = (float)$this->newHeight / (float)$height;
1185
                $this->newWidth = round($factor * $width);
1186
                $this->log("New height was set.");
1187
1188
            } else {
1189
1190
                // Use existing width and height as new width and height.
1191
                $this->newWidth = $width;
1192
                $this->newHeight = $height;
1193
            }
1194
1195
1196
            // Get image dimensions for pre-resize image.
1197
            if ($this->cropToFit || $this->fillToFit) {
1198
1199
                // Get relations of original & target image
1200
                $ratioWidth  = $width  / $this->newWidth;
1201
                $ratioHeight = $height / $this->newHeight;
1202
1203
                if ($this->cropToFit) {
1204
1205
                    // Use newWidth and newHeigh as defined width/height,
1206
                    // image should fit the area.
1207
                    $this->log("Crop to fit.");
1208
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1209
                    $this->cropWidth  = round($width  / $ratio);
1210
                    $this->cropHeight = round($height / $ratio);
1211
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1212
1213
                } elseif ($this->fillToFit) {
1214
1215
                    // Use newWidth and newHeigh as defined width/height,
1216
                    // image should fit the area.
1217
                    $this->log("Fill to fit.");
1218
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1219
                    $this->fillWidth  = round($width  / $ratio);
1220
                    $this->fillHeight = round($height / $ratio);
1221
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1222
                }
1223
            }
1224
        }
1225
1226
        // Crop, ensure to set new width and height
1227
        if ($this->crop) {
1228
            $this->log("Crop.");
1229
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1230
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1231
        }
1232
1233
        // Fill to fit, ensure to set new width and height
1234
        /*if ($this->fillToFit) {
1235
            $this->log("FillToFit.");
1236
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1237
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1238
        }*/
1239
1240
        // No new height or width is set, use existing measures.
1241
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1242
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1243
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1244
1245
        return $this;
1246
    }
1247
1248
1249
1250
    /**
1251
     * Re-calculate image dimensions when original image dimension has changed.
1252
     *
1253
     * @return $this
1254
     */
1255
    public function reCalculateDimensions()
1256
    {
1257
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1258
1259
        $this->newWidth  = $this->newWidthOrig;
1260
        $this->newHeight = $this->newHeightOrig;
1261
        $this->crop      = $this->cropOrig;
1262
1263
        $this->initDimensions()
1264
             ->calculateNewWidthAndHeight();
1265
1266
        return $this;
1267
    }
1268
1269
1270
1271
    /**
1272
     * Set extension for filename to save as.
1273
     *
1274
     * @param string $saveas extension to save image as
0 ignored issues
show
Documentation introduced by
There is no parameter named $saveas. Did you maybe mean $saveAs?

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

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

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

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

Loading history...
1275
     *
1276
     * @return $this
1277
     */
1278
    public function setSaveAsExtension($saveAs = null)
1279
    {
1280
        if (isset($saveAs)) {
1281
            $saveAs = strtolower($saveAs);
1282
            $this->checkFileExtension($saveAs);
1283
            $this->saveAs = $saveAs;
1284
            $this->extension = $saveAs;
1285
        }
1286
1287
        $this->log("Prepare to save image as: " . $this->extension);
1288
1289
        return $this;
1290
    }
1291
1292
1293
1294
    /**
1295
     * Set JPEG quality to use when saving image
1296
     *
1297
     * @param int $quality as the quality to set.
1298
     *
1299
     * @return $this
1300
     */
1301
    public function setJpegQuality($quality = null)
1302
    {
1303
        if ($quality) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $quality of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1304 2
            $this->useQuality = true;
1305
        }
1306 2
1307 2
        $this->quality = isset($quality)
1308 2
            ? $quality
1309 2
            : self::JPEG_QUALITY_DEFAULT;
1310 2
1311 2
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1312 2
            or $this->raiseError('Quality not in range.');
1313 2
1314 2
        $this->log("Setting JPEG quality to {$this->quality}.");
1315 2
1316 2
        return $this;
1317
    }
1318 2
1319 2
1320
1321 2
    /**
1322 2
     * Set PNG compressen algorithm to use when saving image
1323
     *
1324
     * @param int $compress as the algorithm to use.
1325
     *
1326 2
     * @return $this
1327 2
     */
1328
    public function setPngCompression($compress = null)
1329 2
    {
1330 2
        if ($compress) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $compress of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

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

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

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

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

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

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

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

Loading history...
1606
     * @param string  $filename to use instead of default.
1607
     *
1608
     * @return int as the type of the png-image
1609
     *
1610
     */
1611
    private function getPngTypeAsString($pngType = null, $filename = null)
1612
    {
1613
        if ($filename || !$pngType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1614
            $pngType = $this->getPngType($filename);
1615
        }
1616
1617
        $index = imagecolortransparent($this->image);
1618
        $transparent = null;
1619
        if ($index != -1) {
1620
            $transparent = " (transparent)";
1621
        }
1622
1623
        switch ($pngType) {
1624
1625
            case self::PNG_GREYSCALE:
1626
                $text = "PNG is type 0, Greyscale$transparent";
1627
                break;
1628
1629
            case self::PNG_RGB:
1630
                $text = "PNG is type 2, RGB$transparent";
1631
                break;
1632
1633
            case self::PNG_RGB_PALETTE:
1634
                $text = "PNG is type 3, RGB with palette$transparent";
1635
                break;
1636
1637
            case self::PNG_GREYSCALE_ALPHA:
1638
                $text = "PNG is type 4, Greyscale with alpha channel";
1639
                break;
1640
1641
            case self::PNG_RGB_ALPHA:
1642
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1643
                break;
1644
1645
            default:
1646
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1647
        }
1648
1649
        return $text;
1650
    }
1651
1652
1653
1654
1655
    /**
1656
     * Calculate number of colors in an image.
1657
     *
1658
     * @param resource $im the image.
1659
     *
1660
     * @return int
1661
     */
1662
    private function colorsTotal($im)
1663
    {
1664
        if (imageistruecolor($im)) {
1665
            $this->log("Colors as true color.");
1666
            $h = imagesy($im);
1667
            $w = imagesx($im);
1668
            $c = array();
1669
            for ($x=0; $x < $w; $x++) {
1670
                for ($y=0; $y < $h; $y++) {
1671
                    @$c['c'.imagecolorat($im, $x, $y)]++;
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1672
                }
1673
            }
1674
            return count($c);
1675
        } else {
1676
            $this->log("Colors as palette.");
1677
            return imagecolorstotal($im);
1678
        }
1679
    }
1680
1681
1682
1683
    /**
1684
     * Preprocess image before rezising it.
1685
     *
1686
     * @return $this
1687
     */
1688
    public function preResize()
1689
    {
1690
        $this->log("### Pre-process before resizing");
1691
1692
        // Rotate image
1693
        if ($this->rotateBefore) {
1694
            $this->log("Rotating image.");
1695
            $this->rotate($this->rotateBefore, $this->bgColor)
1696
                 ->reCalculateDimensions();
1697
        }
1698
1699
        // Auto-rotate image
1700
        if ($this->autoRotate) {
1701
            $this->log("Auto rotating image.");
1702
            $this->rotateExif()
1703
                 ->reCalculateDimensions();
1704
        }
1705
1706
        // Scale the original image before starting
1707
        if (isset($this->scale)) {
1708
            $this->log("Scale by {$this->scale}%");
1709
            $newWidth  = $this->width * $this->scale / 100;
1710
            $newHeight = $this->height * $this->scale / 100;
1711
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1712
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1713
            $this->image = $img;
1714
            $this->width = $newWidth;
1715
            $this->height = $newHeight;
1716
        }
1717
1718
        return $this;
1719
    }
1720
1721
1722
1723
    /**
1724
     * Resize or resample the image while resizing.
1725
     *
1726
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1727
     *
1728
     * @return $this
1729
     */
1730
     public function setCopyResizeStrategy($strategy)
1731
     {
1732
         $this->copyStrategy = $strategy;
1733
         return $this;
1734
     }
1735
1736
1737
1738
    /**
1739
     * Resize and or crop the image.
1740
     *
1741
     * @return void
1742
     */
1743
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1744
    {
1745
        if($this->copyStrategy == self::RESIZE) {
1746
            $this->log("Copy by resize");
1747
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1748
        } else {
1749
            $this->log("Copy by resample");
1750
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1751
        }
1752
    }
1753
1754
1755
1756
    /**
1757
     * Resize and or crop the image.
1758
     *
1759
     * @return $this
1760
     */
1761
    public function resize()
1762
    {
1763
1764
        $this->log("### Starting to Resize()");
1765
        $this->log("Upscale = '$this->upscale'");
1766
1767
        // Only use a specified area of the image, $this->offset is defining the area to use
1768
        if (isset($this->offset)) {
1769
1770
            $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']}");
1771
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1772
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1773
            $this->image = $img;
1774
            $this->width = $this->offset['width'];
1775
            $this->height = $this->offset['height'];
1776
        }
1777
1778
        if ($this->crop) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->crop 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...
1779
1780
            // Do as crop, take only part of image
1781
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1782
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1783
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1784
            $this->image = $img;
1785
            $this->width = $this->crop['width'];
1786
            $this->height = $this->crop['height'];
1787
        }
1788
1789
        if (!$this->upscale) {
1790
            // Consider rewriting the no-upscale code to fit within this if-statement,
1791
            // likely to be more readable code.
1792
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1793
        }
1794
1795
        if ($this->cropToFit) {
1796
1797
            // Resize by crop to fit
1798
            $this->log("Resizing using strategy - Crop to fit");
1799
1800
            if (!$this->upscale
1801
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1802
                $this->log("Resizing - smaller image, do not upscale.");
1803
1804
                $posX = 0;
1805
                $posY = 0;
1806
                $cropX = 0;
1807
                $cropY = 0;
1808
1809
                if ($this->newWidth > $this->width) {
1810
                    $posX = round(($this->newWidth - $this->width) / 2);
1811
                }
1812
                if ($this->newWidth < $this->width) {
1813
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1814
                }
1815
1816
                if ($this->newHeight > $this->height) {
1817
                    $posY = round(($this->newHeight - $this->height) / 2);
1818
                }
1819
                if ($this->newHeight < $this->height) {
1820
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1821
                }
1822
                $this->log(" cwidth: $this->cropWidth");
1823
                $this->log(" cheight: $this->cropHeight");
1824
                $this->log(" nwidth: $this->newWidth");
1825
                $this->log(" nheight: $this->newHeight");
1826
                $this->log(" width: $this->width");
1827
                $this->log(" height: $this->height");
1828
                $this->log(" posX: $posX");
1829
                $this->log(" posY: $posY");
1830
                $this->log(" cropX: $cropX");
1831
                $this->log(" cropY: $cropY");
1832
1833
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1834
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1835
            } else {
1836
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1837
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1838
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1839
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1840
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1841
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1842
            }
1843
1844
            $this->image = $imageResized;
1845
            $this->width = $this->newWidth;
1846
            $this->height = $this->newHeight;
1847
1848
        } elseif ($this->fillToFit) {
1849
1850
            // Resize by fill to fit
1851
            $this->log("Resizing using strategy - Fill to fit");
1852
1853
            $posX = 0;
1854
            $posY = 0;
1855
1856
            $ratioOrig = $this->width / $this->height;
1857
            $ratioNew  = $this->newWidth / $this->newHeight;
1858
1859
            // Check ratio for landscape or portrait
1860
            if ($ratioOrig < $ratioNew) {
1861
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1862
            } else {
1863
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1864
            }
1865
1866
            if (!$this->upscale
1867
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1868
            ) {
1869
1870
                $this->log("Resizing - smaller image, do not upscale.");
1871
                $posX = round(($this->newWidth - $this->width) / 2);
1872
                $posY = round(($this->newHeight - $this->height) / 2);
1873
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1874
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1875
1876
            } else {
1877
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1878
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1879
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1880
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1881
            }
1882
1883
            $this->image = $imageResized;
1884
            $this->width = $this->newWidth;
1885
            $this->height = $this->newHeight;
1886
1887
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1888
1889
            // Resize it
1890
            $this->log("Resizing, new height and/or width");
1891
1892
            if (!$this->upscale
1893
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1894
            ) {
1895
                $this->log("Resizing - smaller image, do not upscale.");
1896
1897
                if (!$this->keepRatio) {
1898
                    $this->log("Resizing - stretch to fit selected.");
1899
1900
                    $posX = 0;
1901
                    $posY = 0;
1902
                    $cropX = 0;
1903
                    $cropY = 0;
1904
1905
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1906
                        $posX = round(($this->newWidth - $this->width) / 2);
1907
                        $posY = round(($this->newHeight - $this->height) / 2);
1908
                    } elseif ($this->newWidth > $this->width) {
1909
                        $posX = round(($this->newWidth - $this->width) / 2);
1910
                        $cropY = round(($this->height - $this->newHeight) / 2);
1911
                    } elseif ($this->newHeight > $this->height) {
1912
                        $posY = round(($this->newHeight - $this->height) / 2);
1913
                        $cropX = round(($this->width - $this->newWidth) / 2);
1914
                    }
1915
1916
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1917
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1918
                    $this->image = $imageResized;
1919
                    $this->width = $this->newWidth;
1920
                    $this->height = $this->newHeight;
1921
                }
1922
            } else {
1923
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1924
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1925
                $this->image = $imageResized;
1926
                $this->width = $this->newWidth;
1927
                $this->height = $this->newHeight;
1928
            }
1929
        }
1930
1931
        return $this;
1932
    }
1933
1934
1935
1936
    /**
1937
     * Postprocess image after rezising image.
1938
     *
1939
     * @return $this
1940
     */
1941
    public function postResize()
1942
    {
1943
        $this->log("### Post-process after resizing");
1944
1945
        // Rotate image
1946
        if ($this->rotateAfter) {
1947
            $this->log("Rotating image.");
1948
            $this->rotate($this->rotateAfter, $this->bgColor);
1949
        }
1950
1951
        // Apply filters
1952
        if (isset($this->filters) && is_array($this->filters)) {
1953
1954
            foreach ($this->filters as $filter) {
1955
                $this->log("Applying filter {$filter['type']}.");
1956
1957
                switch ($filter['argc']) {
1958
1959
                    case 0:
1960
                        imagefilter($this->image, $filter['type']);
1961
                        break;
1962
1963
                    case 1:
1964
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1965
                        break;
1966
1967
                    case 2:
1968
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1969
                        break;
1970
1971
                    case 3:
1972
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1973
                        break;
1974
1975
                    case 4:
1976
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1977
                        break;
1978
                }
1979
            }
1980
        }
1981
1982
        // Convert to palette image
1983
        if ($this->palette) {
1984
            $this->log("Converting to palette image.");
1985
            $this->trueColorToPalette();
1986
        }
1987
1988
        // Blur the image
1989
        if ($this->blur) {
1990
            $this->log("Blur.");
1991
            $this->blurImage();
1992
        }
1993
1994
        // Emboss the image
1995
        if ($this->emboss) {
1996
            $this->log("Emboss.");
1997
            $this->embossImage();
1998
        }
1999
2000
        // Sharpen the image
2001
        if ($this->sharpen) {
2002
            $this->log("Sharpen.");
2003
            $this->sharpenImage();
2004
        }
2005
2006
        // Custom convolution
2007
        if ($this->convolve) {
2008
            //$this->log("Convolve: " . $this->convolve);
2009
            $this->imageConvolution();
2010
        }
2011
2012
        return $this;
2013
    }
2014
2015
2016
2017
    /**
2018
     * Rotate image using angle.
2019
     *
2020
     * @param float $angle        to rotate image.
2021
     * @param int   $anglebgColor to fill image with if needed.
0 ignored issues
show
Documentation introduced by
There is no parameter named $anglebgColor. Did you maybe mean $bgColor?

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

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

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

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

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

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

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

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

Loading history...
2177
            if (!is_numeric($item)) {
2178
                throw new Exception("Argument to convolve expression should be float but is not.");
2179
            }
2180
        });
2181 2
2182
        return array(
2183 2
            array(
2184
                array($part[0], $part[1], $part[2]),
2185 2
                array($part[3], $part[4], $part[5]),
2186
                array($part[6], $part[7], $part[8]),
2187 2
            ),
2188 2
            $part[9],
2189 2
            $part[10],
2190 2
        );
2191
    }
2192 2
2193
2194
2195 2
    /**
2196
     * Add custom expressions (or overwrite existing) for image convolution.
2197
     *
2198 2
     * @param array $options Key value array with strings to be converted
2199
     *                       to convolution expressions.
2200
     *
2201
     * @return $this
2202
     */
2203
    public function addConvolveExpressions($options)
2204
    {
2205
        $this->convolves = array_merge($this->convolves, $options);
2206
        return $this;
2207
    }
2208
2209
2210
2211
    /**
2212
     * Image convolution.
2213
     *
2214
     * @param string $options A string with 11 float separated by comma.
2215 2
     *
2216
     * @return $this
2217 2
     */
2218 2
    public function imageConvolution($options = null)
2219 2
    {
2220 2
        // Use incoming options or use $this.
2221
        $options = $options ? $options : $this->convolve;
2222 2
2223 2
        // Treat incoming as string, split by +
2224 2
        $this->log("Convolution with '$options'");
2225
        $options = explode(":", $options);
2226 2
2227
        // Check each option if it matches constant value
2228
        foreach ($options as $option) {
2229
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2230
            imageconvolution($this->image, $matrix, $divisor, $offset);
2231
        }
2232
2233
        return $this;
2234
    }
2235 2
2236
2237 2
2238 2
    /**
2239 2
     * Set default background color between 000000-FFFFFF or if using
2240 2
     * alpha 00000000-FFFFFF7F.
2241
     *
2242 2
     * @param string $color as hex value.
2243
     *
2244
     * @return $this
2245
    */
2246
    public function setDefaultBackgroundColor($color)
2247
    {
2248
        $this->log("Setting default background color to '$color'.");
2249
2250
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2251
            throw new Exception(
2252
                "Background color needs a hex value of 6 or 8
2253
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2254
                Current value was: '$color'."
2255
            );
2256
        }
2257
2258
        $red    = hexdec(substr($color, 0, 2));
2259
        $green  = hexdec(substr($color, 2, 2));
2260
        $blue   = hexdec(substr($color, 4, 2));
2261
2262
        $alpha = (strlen($color) == 8)
2263
            ? hexdec(substr($color, 6, 2))
2264
            : null;
2265
2266
        if (($red < 0 || $red > 255)
2267
            || ($green < 0 || $green > 255)
2268
            || ($blue < 0 || $blue > 255)
2269
            || ($alpha < 0 || $alpha > 127)
2270
        ) {
2271
            throw new Exception(
2272
                "Background color out of range. Red, green blue
2273
                should be 00-FF and alpha should be 00-7F.
2274
                Current value was: '$color'."
2275
            );
2276
        }
2277
2278
        $this->bgColor = strtolower($color);
2279
        $this->bgColorDefault = array(
2280
            'red'   => $red,
2281
            'green' => $green,
2282
            'blue'  => $blue,
2283
            'alpha' => $alpha
2284 2
        );
2285
2286
        return $this;
2287 2
    }
2288
2289
2290 2
2291
    /**
2292
     * Get the background color.
2293
     *
2294
     * @param resource $img the image to work with or null if using $this->image.
2295
     *
2296
     * @return color value or null if no background color is set.
2297
    */
2298
    private function getBackgroundColor($img = null)
2299
    {
2300
        $img = isset($img) ? $img : $this->image;
2301
2302
        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...
2303
2304
            $red   = $this->bgColorDefault['red'];
2305 2
            $green = $this->bgColorDefault['green'];
2306
            $blue  = $this->bgColorDefault['blue'];
2307 2
            $alpha = $this->bgColorDefault['alpha'];
2308
2309
            if ($alpha) {
2310
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2311 2
            } else {
2312
                $color = imagecolorallocate($img, $red, $green, $blue);
2313
            }
2314
2315
            return $color;
2316 2
2317
        } else {
2318
            return 0;
2319 2
        }
2320 2
    }
2321
2322
2323 2
2324 2
    /**
2325
     * Create a image and keep transparency for png and gifs.
2326
     *
2327
     * @param int $width of the new image.
2328
     * @param int $height of the new image.
2329
     *
2330
     * @return image resource.
2331
    */
2332
    private function createImageKeepTransparency($width, $height)
2333
    {
2334
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2335
        $img = imagecreatetruecolor($width, $height);
2336
        imagealphablending($img, false);
2337
        imagesavealpha($img, true);
2338
2339
        $index = $this->image
2340
            ? imagecolortransparent($this->image)
2341
            : -1;
2342 2
2343
        if ($index != -1) {
2344
2345
            imagealphablending($img, true);
2346
            $transparent = imagecolorsforindex($this->image, $index);
2347 2
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2348 2
            imagefill($img, 0, 0, $color);
2349 2
            $index = imagecolortransparent($img, $color);
2350
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2351
2352 2
        } 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...
2353 2
2354 2
            $color = $this->getBackgroundColor($img);
2355
            imagefill($img, 0, 0, $color);
2356
            $this->Log("Filling image with background color.");
2357 2
        }
2358
2359
        return $img;
2360
    }
2361
2362
2363
2364
    /**
2365
     * Set optimizing  and post-processing options.
2366
     *
2367
     * @param array $options with config for postprocessing with external tools.
2368
     *
2369
     * @return $this
2370 2
     */
2371
    public function setPostProcessingOptions($options)
2372
    {
2373
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2374
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2375
        } else {
2376
            $this->jpegOptimizeCmd = null;
2377
        }
2378
2379
        if (array_key_exists("png_lossy", $options)
2380
            && $options['png_lossy'] !== false) {
2381 2
            $this->pngLossy = $options['png_lossy'];
2382 2
            $this->pngLossyCmd = $options['png_lossy_cmd'];
2383
        } else {
2384 2
            $this->pngLossyCmd = null;
2385
        }
2386
2387
        if (isset($options['png_filter']) && $options['png_filter']) {
2388
            $this->pngFilterCmd = $options['png_filter_cmd'];
2389
        } else {
2390
            $this->pngFilterCmd = null;
2391
        }
2392
2393
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2394
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2395 2
        } else {
2396
            $this->pngDeflateCmd = null;
2397
        }
2398
2399
        return $this;
2400
    }
2401
2402
2403
2404
    /**
2405
     * Find out the type (file extension) for the image to be saved.
2406
     *
2407
     * @return string as image extension.
2408
     */
2409
    protected function getTargetImageExtension()
2410
    {
2411
        // switch on mimetype
2412
        if (isset($this->extension)) {
2413
            return strtolower($this->extension);
2414
        } elseif ($this->fileType === IMG_WEBP) {
2415
            return "webp";
2416
        }
2417
2418
        return substr(image_type_to_extension($this->fileType), 1);
2419
    }
2420
2421
2422
2423
    /**
2424
     * Save image.
2425
     *
2426
     * @param string  $src       as target filename.
2427
     * @param string  $base      as base directory where to store images.
2428
     * @param boolean $overwrite or not, default to always overwrite file.
2429
     *
2430
     * @return $this or false if no folder is set.
2431
     */
2432
    public function save($src = null, $base = null, $overwrite = true)
2433
    {
2434
        if (isset($src)) {
2435
            $this->setTarget($src, $base);
2436
        }
2437
2438
        if ($overwrite === false && is_file($this->cacheFileName)) {
2439
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2440
            return;
2441
        }
2442
2443
        is_writable($this->saveFolder)
2444
            or $this->raiseError('Target directory is not writable.');
2445
2446
        $type = $this->getTargetImageExtension();
2447
        $this->Log("Saving image as " . $type);
2448
        switch($type) {
2449
2450
            case 'jpeg':
2451
            case 'jpg':
2452
                // Set as interlaced progressive JPEG
2453
                if ($this->interlace) {
2454
                    $this->Log("Set JPEG image to be interlaced.");
2455
                    $res = imageinterlace($this->image, true);
0 ignored issues
show
Unused Code introduced by
$res is not used, you could remove the assignment.

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

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

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

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

Loading history...
2456
                }
2457
2458
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2459
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2460
2461
                // Use JPEG optimize if defined
2462
                if ($this->jpegOptimizeCmd) {
2463
                    if ($this->verbose) {
2464
                        clearstatcache();
2465
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2466
                    }
2467
                    $res = array();
2468
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2469
                    exec($cmd, $res);
2470
                    $this->log($cmd);
2471
                    $this->log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2472
                }
2473
                break;
2474
2475
            case 'gif':
2476
                $this->Log("Saving image as GIF to cache.");
2477
                imagegif($this->image, $this->cacheFileName);
2478
                break;
2479
2480
            case 'webp':
2481
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2482
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2483
                break;
2484
2485
            case 'png':
2486
            default:
2487
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2488
2489
                // Turn off alpha blending and set alpha flag
2490
                imagealphablending($this->image, false);
2491
                imagesavealpha($this->image, true);
2492
                imagepng($this->image, $this->cacheFileName, $this->compress);
2493
2494
                // Use external program to process lossy PNG, if defined
2495
                $lossyEnabled = $this->pngLossy === true;
2496
                $lossySoftEnabled = $this->pngLossy === null;
2497
                $lossyActiveEnabled = $this->lossy === true;
2498
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
2499
                    if ($this->verbose) {
2500
                        clearstatcache();
2501
                        $this->log("Lossy enabled: $lossyEnabled");
2502
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
2503
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
2504
                    }
2505
                    $res = array();
2506
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
2507
                    exec($cmd, $res);
2508
                    $this->Log($cmd);
2509
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2510
                }
2511
2512
                // Use external program to filter PNG, if defined
2513
                if ($this->pngFilterCmd) {
2514
                    if ($this->verbose) {
2515
                        clearstatcache();
2516
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2517
                    }
2518
                    $res = array();
2519
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2520
                    exec($cmd, $res);
2521
                    $this->Log($cmd);
2522
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2523
                }
2524
2525
                // Use external program to deflate PNG, if defined
2526
                if ($this->pngDeflateCmd) {
2527
                    if ($this->verbose) {
2528
                        clearstatcache();
2529
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2530
                    }
2531
                    $res = array();
2532
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2533
                    exec($cmd, $res);
2534
                    $this->Log($cmd);
2535
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

2 paths for user data to reach this point

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

General Strategies to prevent injection

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

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

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

$sanitized = (integer) $tainted;
Loading history...
2641
        }
2642
2643
        $res = link($this->cacheFileName, $alias);
2644
2645
        if ($res) {
2646
            $this->log("Created an alias as: $alias");
2647
        } else {
2648
            $this->log("Failed to create the alias: $alias");
2649
        }
2650
2651
        return $this;
2652
    }
2653
2654
2655
2656
    /**
2657
     * Add HTTP header for output together with image.
2658
     *
2659
     * @param string $type  the header type such as "Cache-Control"
2660
     * @param string $value the value to use
2661
     *
2662
     * @return void
2663
     */
2664
    public function addHTTPHeader($type, $value)
2665
    {
2666
        $this->HTTPHeader[$type] = $value;
2667
    }
2668
2669
2670
2671
    /**
2672
     * Output image to browser using caching.
2673
     *
2674
     * @param string $file   to read and output, default is to
2675
     *                       use $this->cacheFileName
2676
     * @param string $format set to json to output file as json
2677
     *                       object with details
2678
     *
2679
     * @return void
2680
     */
2681
    public function output($file = null, $format = null)
2682
    {
2683
        if (is_null($file)) {
2684
            $file = $this->cacheFileName;
2685
        }
2686
2687
        if (is_null($format)) {
2688
            $format = $this->outputFormat;
2689
        }
2690
2691
        $this->log("### Output");
2692
        $this->log("Output format is: $format");
2693
2694
        if (!$this->verbose && $format == 'json') {
2695
            header('Content-type: application/json');
2696
            echo $this->json($file);
2697
            exit;
2698
        } elseif ($format == 'ascii') {
2699
            header('Content-type: text/plain');
2700
            echo $this->ascii($file);
0 ignored issues
show
Security Cross-Site Scripting introduced by
$this->ascii($file) can contain request data and is used in output context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET in functions.php on line 83
  1. Read from $_GET
    in functions.php on line 83
  2. get() returns tainted data, and $options is assigned
    in webroot/img.php on line 791
  3. $options is passed through explode(), and $options is assigned
    in webroot/img.php on line 792
  4. $defaultOptions is assigned
    in webroot/img.php on line 803
  5. $defaultOptions is passed to CImage::setAsciiOptions()
    in webroot/img.php on line 815
  6. CImage::$asciiOptions is assigned
    in CImage.php on line 2841
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2858
  8. $options is passed through array_merge(), and $default is assigned
    in CAsciiArt.php on line 88
  9. $default['customCharacterSet'] is passed to CAsciiArt::addCharacterSet()
    in CAsciiArt.php on line 91
  10. CAsciiArt::$characterSet is assigned
    in CAsciiArt.php on line 67
  11. Tainted property CAsciiArt::$characterSet is read, and CAsciiArt::$characters is assigned
    in CAsciiArt.php on line 96
  12. Tainted property CAsciiArt::$characters is read, and $char is assigned
    in CAsciiArt.php on line 209
  13. CAsciiArt::luminance2character() returns tainted data, and $ascii is assigned
    in CAsciiArt.php on line 126
  14. CAsciiArt::createFromFile() returns tainted data
    in CImage.php on line 2859
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2700
  2. Path: Read from $_GET in functions.php on line 87
  1. Read from $_GET
    in functions.php on line 87
  2. get() returns tainted data, and $options is assigned
    in webroot/img.php on line 791
  3. $options is passed through explode(), and $options is assigned
    in webroot/img.php on line 792
  4. $defaultOptions is assigned
    in webroot/img.php on line 803
  5. $defaultOptions is passed to CImage::setAsciiOptions()
    in webroot/img.php on line 815
  6. CImage::$asciiOptions is assigned
    in CImage.php on line 2841
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2858
  8. $options is passed through array_merge(), and $default is assigned
    in CAsciiArt.php on line 88
  9. $default['customCharacterSet'] is passed to CAsciiArt::addCharacterSet()
    in CAsciiArt.php on line 91
  10. CAsciiArt::$characterSet is assigned
    in CAsciiArt.php on line 67
  11. Tainted property CAsciiArt::$characterSet is read, and CAsciiArt::$characters is assigned
    in CAsciiArt.php on line 96
  12. Tainted property CAsciiArt::$characters is read, and $char is assigned
    in CAsciiArt.php on line 209
  13. CAsciiArt::luminance2character() returns tainted data, and $ascii is assigned
    in CAsciiArt.php on line 126
  14. CAsciiArt::createFromFile() returns tainted data
    in CImage.php on line 2859
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2700

Preventing Cross-Site-Scripting Attacks

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

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

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

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

General Strategies to prevent injection

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

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

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

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

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

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

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

Loading history...
2804
        $details['width']       = $this->width;
2805
        $details['height']      = $this->height;
2806
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2807
        $details['size']        = filesize($file);
2808
        $details['colors'] = $this->colorsTotal($this->image);
2809
        $details['includedFiles'] = count(get_included_files());
2810
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2811
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2812
        $details['memoryLimit'] = ini_get('memory_limit');
2813
2814
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2815
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2816
        }
2817
2818
        if ($details['mimeType'] == 'image/png') {
2819
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2820
        }
2821
2822
        $options = null;
2823
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2824
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2825
        }
2826
2827
        return json_encode($details, $options);
2828
    }
2829
2830
2831
2832
    /**
2833
     * Set options for creating ascii version of image.
2834
     *
2835
     * @param array $options empty to use default or set options to change.
2836
     *
2837
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

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

Loading history...
2838
     */
2839
    public function setAsciiOptions($options = array())
2840
    {
2841
        $this->asciiOptions = $options;
2842
    }
2843
2844
2845
2846
    /**
2847
     * Create an ASCII version from the image details.
2848
     *
2849
     * @param string $file the file to output.
2850
     *
2851
     * @return string ASCII representation of the image.
2852
     */
2853
    public function ascii($file = null)
2854
    {
2855
        $file = $file ? $file : $this->cacheFileName;
2856
2857
        $asciiArt = new CAsciiArt();
2858
        $asciiArt->setOptions($this->asciiOptions);
2859
        return $asciiArt->createFromFile($file);
2860
    }
2861
2862
2863
2864
    /**
2865
     * Log an event if verbose mode.
2866
     *
2867
     * @param string $message to log.
2868
     *
2869
     * @return this
2870
     */
2871
    public function log($message)
2872
    {
2873
        if ($this->verbose) {
2874
            $this->log[] = $message;
2875
        }
2876
2877
        return $this;
2878
    }
2879
2880
2881
2882
    /**
2883
     * Do verbose output to a file.
2884
     *
2885
     * @param string $fileName where to write the verbose output.
2886
     *
2887
     * @return void
2888
     */
2889
    public function setVerboseToFile($fileName)
2890
    {
2891
        $this->log("Setting verbose output to file.");
2892
        $this->verboseFileName = $fileName;
2893
    }
2894
2895
2896
2897
    /**
2898
     * Do verbose output and print out the log and the actual images.
2899
     *
2900
     * @return void
2901
     */
2902
    private function verboseOutput()
2903
    {
2904
        $log = null;
2905
        $this->log("### Summary of verbose log");
2906
        $this->log("As JSON: \n" . $this->json());
2907
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2908
        $this->log("Memory limit: " . ini_get('memory_limit'));
2909
2910
        $included = get_included_files();
2911
        $this->log("Included files: " . count($included));
2912
2913
        foreach ($this->log as $val) {
2914
            if (is_array($val)) {
2915
                foreach ($val as $val1) {
2916
                    $log .= htmlentities($val1) . '<br/>';
2917
                }
2918
            } else {
2919
                $log .= htmlentities($val) . '<br/>';
2920
            }
2921
        }
2922
2923
        if (!is_null($this->verboseFileName)) {
2924
            file_put_contents(
2925
                $this->verboseFileName,
2926
                str_replace("<br/>", "\n", $log)
0 ignored issues
show
Security File Manipulation introduced by
str_replace('<br/>', ' ', $log) can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET in functions.php on line 83
  1. Read from $_GET
    in functions.php on line 83
  2. get() returns tainted data, and $saveAs is assigned
    in webroot/img.php on line 662
  3. $saveAs is passed to CImage::setSaveAsExtension()
    in webroot/img.php on line 1198
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in CImage.php on line 1281
  5. CImage::$extension is assigned
    in CImage.php on line 1284
  6. Tainted property CImage::$extension is read, and 'Prepare to save image as: ' . $this->extension is passed to CImage::log()
    in CImage.php on line 1287
  7. CImage::$log is assigned
    in CImage.php on line 2874
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2913
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2919
  10. $log is passed through str_replace()
    in CImage.php on line 2926
  2. Path: Read from $_GET in functions.php on line 87
  1. Read from $_GET
    in functions.php on line 87
  2. get() returns tainted data, and $saveAs is assigned
    in webroot/img.php on line 662
  3. $saveAs is passed to CImage::setSaveAsExtension()
    in webroot/img.php on line 1198
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in CImage.php on line 1281
  5. CImage::$extension is assigned
    in CImage.php on line 1284
  6. Tainted property CImage::$extension is read, and 'Prepare to save image as: ' . $this->extension is passed to CImage::log()
    in CImage.php on line 1287
  7. CImage::$log is assigned
    in CImage.php on line 2874
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2913
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2919
  10. $log is passed through str_replace()
    in CImage.php on line 2926

General Strategies to prevent injection

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

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

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

$sanitized = (integer) $tainted;
Loading history...
2927
            );
2928
        } else {
2929
            echo <<<EOD
2930
<h1>CImage Verbose Output</h1>
2931
<pre>{$log}</pre>
2932
EOD;
2933
        }
2934
    }
2935
2936
2937
2938
    /**
2939
     * Raise error, enables to implement a selection of error methods.
2940
     *
2941
     * @param string $message the error message to display.
2942
     *
2943
     * @return void
2944
     * @throws Exception
2945
     */
2946
    private function raiseError($message)
2947
    {
2948
        throw new Exception($message);
2949
    }
2950
}
2951