Test Setup Failed
Push — master ( c56327...86737a )
by Mikael
06:48
created

CImage.php (25 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Resize and crop images on the fly, store generated images in a cache.
4
 *
5
 * @author  Mikael Roos [email protected]
6
 * @example http://dbwebb.se/opensource/cimage
7
 * @link    https://github.com/mosbth/cimage
8
 */
9
class CImage
10
{
11
12
    /**
13
     * Constants type of PNG image
14
     */
15
    const PNG_GREYSCALE         = 0;
16
    const PNG_RGB               = 2;
17
    const PNG_RGB_PALETTE       = 3;
18
    const PNG_GREYSCALE_ALPHA   = 4;
19
    const PNG_RGB_ALPHA         = 6;
20
21
22
23
    /**
24
     * Constant for default image quality when not set
25
     */
26
    const JPEG_QUALITY_DEFAULT = 60;
27
28
29
30
    /**
31
     * Quality level for JPEG images.
32
     */
33
    private $quality;
34
35
36
37
    /**
38
     * Is the quality level set from external use (true) or is it default (false)?
39
     */
40
    private $useQuality = false;
41
42
43
44
    /**
45
     * Constant for default image quality when not set
46
     */
47
    const PNG_COMPRESSION_DEFAULT = -1;
48
49
50
51
    /**
52
     * Compression level for PNG images.
53
     */
54
    private $compress;
55
56
57
58
    /**
59
     * Is the compress level set from external use (true) or is it default (false)?
60
     */
61
    private $useCompress = false;
62
63
64
65
66
    /**
67
     * Add HTTP headers for outputing image.
68
     */
69
    private $HTTPHeader = array();
70
71
72
73
    /**
74
     * Default background color, red, green, blue, alpha.
75
     *
76
     * @todo remake when upgrading to PHP 5.5
77
     */
78
    /*
79
    const BACKGROUND_COLOR = array(
80
        'red'   => 0,
81
        'green' => 0,
82
        'blue'  => 0,
83
        'alpha' => null,
84
    );*/
85
86
87
88
    /**
89
     * Default background color to use.
90
     *
91
     * @todo remake when upgrading to PHP 5.5
92
     */
93
    //private $bgColorDefault = self::BACKGROUND_COLOR;
94
    private $bgColorDefault = array(
95
        'red'   => 0,
96
        'green' => 0,
97
        'blue'  => 0,
98
        'alpha' => null,
99
    );
100
101
102
    /**
103
     * Background color to use, specified as part of options.
104
     */
105
    private $bgColor;
106
107
108
109
    /**
110
     * Where to save the target file.
111
     */
112
    private $saveFolder;
113
114
115
116
    /**
117
     * The working image object.
118
     */
119
    private $image;
120
121
122
123
    /**
124
     * Image filename, may include subdirectory, relative from $imageFolder
125
     */
126
    private $imageSrc;
127
128
129
130
    /**
131
     * Actual path to the image, $imageFolder . '/' . $imageSrc
132
     */
133
    private $pathToImage;
134
135
136
137
    /**
138
     * File type for source image, as provided by getimagesize()
139
     */
140
    private $fileType;
141
142
143
144
    /**
145
     * File extension to use when saving image.
146
     */
147
    private $extension;
148
149
150
151
    /**
152
     * Output format, supports null (image) or json.
153
     */
154
    private $outputFormat = null;
155
156
157
158
    /**
159
     * Do lossy output using external postprocessing tools.
160
     */
161
    private $lossy = null;
162
163
164
165
    /**
166
     * Verbose mode to print out a trace and display the created image
167
     */
168
    private $verbose = false;
169
170
171
172
    /**
173
     * Keep a log/trace on what happens
174
     */
175
    private $log = array();
176
177
178
179
    /**
180
     * Handle image as palette image
181
     */
182
    private $palette;
183
184
185
186
    /**
187
     * Target filename, with path, to save resulting image in.
188
     */
189
    private $cacheFileName;
190
191
192
193
    /**
194
     * Set a format to save image as, or null to use original format.
195
     */
196
    private $saveAs;
197
198
199
    /**
200
     * Path to command for lossy optimize, for example pngquant.
201
     */
202
    private $pngLossy;
203
    private $pngLossyCmd;
204
205
206
207
    /**
208
     * Path to command for filter optimize, for example optipng.
209
     */
210
    private $pngFilter;
211
    private $pngFilterCmd;
212
213
214
215
    /**
216
     * Path to command for deflate optimize, for example pngout.
217
     */
218
    private $pngDeflate;
219
    private $pngDeflateCmd;
220
221
222
223
    /**
224
     * Path to command to optimize jpeg images, for example jpegtran or null.
225
     */
226
     private $jpegOptimize;
227
     private $jpegOptimizeCmd;
228
229
230
231
    /**
232
     * Image dimensions, calculated from loaded image.
233
     */
234
    private $width;  // Calculated from source image
235
    private $height; // Calculated from source image
236
237
238
    /**
239
     * New image dimensions, incoming as argument or calculated.
240
     */
241
    private $newWidth;
242
    private $newWidthOrig;  // Save original value
243
    private $newHeight;
244
    private $newHeightOrig; // Save original value
245
246
247
    /**
248
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
249
     */
250
    private $dpr = 1;
251
252
253
    /**
254
     * Always upscale images, even if they are smaller than target image.
255
     */
256
    const UPSCALE_DEFAULT = true;
257
    private $upscale = self::UPSCALE_DEFAULT;
258
259
260
261
    /**
262
     * Array with details on how to crop, incoming as argument and calculated.
263
     */
264
    public $crop;
265
    public $cropOrig; // Save original value
266
267
268
    /**
269
     * String with details on how to do image convolution. String
270
     * should map a key in the $convolvs array or be a string of
271
     * 11 float values separated by comma. The first nine builds
272
     * up the matrix, then divisor and last offset.
273
     */
274
    private $convolve;
275
276
277
    /**
278
     * Custom convolution expressions, matrix 3x3, divisor and offset.
279
     */
280
    private $convolves = array(
281
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
282
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
283
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
284
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
285
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
286
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
287
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
288
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
289
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
290
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
291
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
292
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
293
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
294
    );
295
296
297
    /**
298
     * Resize strategy to fill extra area with background color.
299
     * True or false.
300
     */
301
    private $fillToFit;
302
303
304
305
    /**
306
     * To store value for option scale.
307
     */
308
    private $scale;
309
310
311
312
    /**
313
     * To store value for option.
314
     */
315
    private $rotateBefore;
316
317
318
319
    /**
320
     * To store value for option.
321
     */
322
    private $rotateAfter;
323
324
325
326
    /**
327
     * To store value for option.
328
     */
329
    private $autoRotate;
330
331
332
333
    /**
334
     * To store value for option.
335
     */
336
    private $sharpen;
337
338
339
340
    /**
341
     * To store value for option.
342
     */
343
    private $emboss;
344
345
346
347
    /**
348
     * To store value for option.
349
     */
350
    private $blur;
351
352
353
354
    /**
355
     * Used with option area to set which parts of the image to use.
356
     */
357
    private $offset;
358
359
360
361
    /**
362
     * Calculate target dimension for image when using fill-to-fit resize strategy.
363
     */
364
    private $fillWidth;
365
    private $fillHeight;
366
367
368
369
    /**
370
     * Allow remote file download, default is to disallow remote file download.
371
     */
372
    private $allowRemote = false;
373
374
375
376
    /**
377
     * Path to cache for remote download.
378
     */
379
    private $remoteCache;
380
381
382
383
    /**
384
     * Pattern to recognize a remote file.
385
     */
386
    //private $remotePattern = '#^[http|https]://#';
387
    private $remotePattern = '#^https?://#';
388
389
390
391
    /**
392
     * Use the cache if true, set to false to ignore the cached file.
393
     */
394
    private $useCache = true;
395
396
397
    /**
398
    * Disable the fasttrackCacke to start with, inject an object to enable it.
399
    */
400
    private $fastTrackCache = null;
401
402
403
404
    /*
405
     * Set whitelist for valid hostnames from where remote source can be
406
     * downloaded.
407
     */
408
    private $remoteHostWhitelist = null;
409
410
411
412
    /*
413
     * Do verbose logging to file by setting this to a filename.
414
     */
415
    private $verboseFileName = null;
416
417
418
419
    /*
420
     * Output to ascii can take som options as an array.
421
     */
422
    private $asciiOptions = array();
423
424
425
426
    /*
427
     * Image copy strategy, defaults to RESAMPLE.
428
     */
429
     const RESIZE = 1;
430
     const RESAMPLE = 2;
431
     private $copyStrategy = NULL;
432
433
434 7
435
    /**
436 7
     * Properties, the class is mutable and the method setOptions()
437 7
     * decides (partly) what properties are created.
438 7
     *
439
     * @todo Clean up these and check if and how they are used
440
     */
441
442
    public $keepRatio;
443
    public $cropToFit;
444
    private $cropWidth;
445
    private $cropHeight;
446
    public $crop_x;
447
    public $crop_y;
448
    public $filters;
449
    private $attr; // Calculated from source image
450
451
452
453
454
    /**
455
     * Constructor, can take arguments to init the object.
456
     *
457
     * @param string $imageSrc    filename which may contain subdirectory.
458
     * @param string $imageFolder path to root folder for images.
459
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
460
     * @param string $saveName    name of target file when saveing.
461
     */
462
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
463
    {
464
        $this->setSource($imageSrc, $imageFolder);
465
        $this->setTarget($saveFolder, $saveName);
466
    }
467 2
468
469 2
470 2
    /**
471
     * Inject object and use it, must be available as member.
472
     *
473
     * @param string $property to set as object.
474
     * @param object $object   to set to property.
475
     *
476
     * @return $this
477
     */
478
    public function injectDependency($property, $object)
479
    {
480
        if (!property_exists($this, $property)) {
481
            $this->raiseError("Injecting unknown property.");
482
        }
483
        $this->$property = $object;
484
        return $this;
485
    }
486
487
488
489
    /**
490
     * Set verbose mode.
491
     *
492
     * @param boolean $mode true or false to enable and disable verbose mode,
493
     *                      default is true.
494
     *
495
     * @return $this
496
     */
497
    public function setVerbose($mode = true)
498
    {
499 2
        $this->verbose = $mode;
500
        return $this;
501 2
    }
502 2
503
504 2
505
    /**
506 2
     * Set save folder, base folder for saving cache files.
507
     *
508
     * @todo clean up how $this->saveFolder is used in other methods.
509
     *
510
     * @param string $path where to store cached files.
511
     *
512
     * @return $this
513
     */
514
    public function setSaveFolder($path)
515
    {
516
        $this->saveFolder = $path;
517
        return $this;
518
    }
519 2
520
521 2
522 2
    /**
523
     * Use cache or not.
524 2
     *
525
     * @param boolean $use true or false to use cache.
526 2
     *
527 2
     * @return $this
528 2
     */
529 2
    public function useCache($use = true)
530
    {
531 2
        $this->useCache = $use;
532
        return $this;
533
    }
534
535
536
537
    /**
538
     * Create and save a dummy image. Use dimensions as stated in
539
     * $this->newWidth, or $width or default to 100 (same for height.
540
     *
541
     * @param integer $width  use specified width for image dimension.
542
     * @param integer $height use specified width for image dimension.
543 2
     *
544
     * @return $this
545 2
     */
546 2
    public function createDummyImage($width = null, $height = null)
547 2
    {
548
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
549
        $this->newHeight = $this->newHeight ?: $height ?: 100;
550
551
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
552
553
        return $this;
554
    }
555
556
557
558
    /**
559
     * Allow or disallow remote image download.
560 2
     *
561
     * @param boolean $allow   true or false to enable and disable.
562 2
     * @param string  $cache   path to cache dir.
563 2
     * @param string  $pattern to use to detect if its a remote file.
564
     *
565 2
     * @return $this
566 2
     */
567 2
    public function setRemoteDownload($allow, $cache, $pattern = null)
568
    {
569
        $this->allowRemote = $allow;
570
        $this->remoteCache = $cache;
571
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
572
573
        $this->log(
574
            "Set remote download to: "
575
            . ($this->allowRemote ? "true" : "false")
576
            . " using pattern "
577
            . $this->remotePattern
578
        );
579
580 3
        return $this;
581
    }
582 3
583 1
584 1
585
    /**
586
     * Check if the image resource is a remote file or not.
587 2
     *
588 2
     * @param string $src check if src is remote.
589 2
     *
590
     * @return boolean true if $src is a remote file, else false.
591 2
     */
592
    public function isRemoteSource($src)
593 2
    {
594 2
        $remote = preg_match($this->remotePattern, $src);
595 2
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
596
        return !!$remote;
597
    }
598
599
600
601
    /**
602
     * Set whitelist for valid hostnames from where remote source can be
603
     * downloaded.
604
     *
605
     * @param array $whitelist with regexp hostnames to allow download from.
606
     *
607
     * @return $this
608
     */
609
    public function setRemoteHostWhitelist($whitelist = null)
610
    {
611
        $this->remoteHostWhitelist = $whitelist;
612
        $this->log(
613
            "Setting remote host whitelist to: "
614
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
615
        );
616
        return $this;
617
    }
618
619
620
621
    /**
622
     * Check if the hostname for the remote image, is on a whitelist,
623
     * if the whitelist is defined.
624
     *
625
     * @param string $src the remote source.
626 2
     *
627
     * @return boolean true if hostname on $src is in the whitelist, else false.
628 2
     */
629
    public function isRemoteSourceOnWhitelist($src)
630 2
    {
631
        if (is_null($this->remoteHostWhitelist)) {
632
            $this->log("Remote host on whitelist not configured - allowing.");
633
            return true;
634 2
        }
635
636
        $whitelist = new CWhitelist();
637
        $hostname = parse_url($src, PHP_URL_HOST);
638
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
0 ignored issues
show
It seems like $hostname defined by parse_url($src, PHP_URL_HOST) on line 637 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...
639
640
        $this->log(
641
            "Remote host is on whitelist: "
642
            . ($allow ? "true" : "false")
643
        );
644
        return $allow;
645
    }
646
647
648
649
    /**
650
     * Check if file extension is valid as a file extension.
651
     *
652
     * @param string $extension of image file.
653
     *
654
     * @return $this
655
     */
656
    private function checkFileExtension($extension)
657
    {
658
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
659
660
        in_array(strtolower($extension), $valid)
661
            or $this->raiseError('Not a valid file extension.');
662
663
        return $this;
664
    }
665
666
667
668
    /**
669
     * Normalize the file extension.
670
     *
671
     * @param string $extension of image file or skip to use internal.
672
     *
673
     * @return string $extension as a normalized file extension.
674
     */
675
    private function normalizeFileExtension($extension = null)
676
    {
677
        $extension = strtolower($extension ? $extension : $this->extension);
678
679
        if ($extension == 'jpeg') {
680
                $extension = 'jpg';
681
        }
682
683
        return $extension;
684
    }
685
686
687
688 7
    /**
689
     * Download a remote image and return path to its local copy.
690 7
     *
691 7
     * @param string $src remote path to image.
692 7
     *
693 7
     * @return string as path to downloaded remote source.
694
     */
695
    public function downloadRemoteSource($src)
696 2
    {
697
        if (!$this->isRemoteSourceOnWhitelist($src)) {
698
            throw new Exception("Hostname is not on whitelist for remote sources.");
699
        }
700
701 2
        $remote = new CRemoteImage();
702
703
        if (!is_writable($this->remoteCache)) {
704
            $this->log("The remote cache is not writable.");
705
        }
706 2
707 2
        $remote->setCache($this->remoteCache);
708 2
        $remote->useCache($this->useCache);
709
        $src = $remote->download($src);
710 2
711
        $this->log("Remote HTTP status: " . $remote->getStatus());
712
        $this->log("Remote item is in local cache: $src");
713
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
714
715
        return $src;
716
    }
717
718
719
720
    /**
721
     * Set source file to use as image source.
722
     *
723
     * @param string $src of image.
724 7
     * @param string $dir as optional base directory where images are.
725
     *
726 7
     * @return $this
727 7
     */
728 7
    public function setSource($src, $dir = null)
729
    {
730
        if (!isset($src)) {
731 2
            $this->imageSrc = null;
732
            $this->pathToImage = null;
733
            return $this;
734
        }
735 2
736
        if ($this->allowRemote && $this->isRemoteSource($src)) {
737
            $src = $this->downloadRemoteSource($src);
738 2
            $dir = null;
739 2
        }
740
741 2
        if (!isset($dir)) {
742
            $dir = dirname($src);
743
            $src = basename($src);
744
        }
745
746
        $this->imageSrc     = ltrim($src, '/');
747
        $imageFolder        = rtrim($dir, '/');
748
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
749
750
        return $this;
751 2
    }
752
753 2
754
755
    /**
756
     * Set target file.
757
     *
758
     * @param string $src of target image.
759
     * @param string $dir as optional base directory where images are stored.
760
     *                    Uses $this->saveFolder if null.
761
     *
762
     * @return $this
763
     */
764
    public function setTarget($src = null, $dir = null)
765
    {
766
        if (!isset($src)) {
767
            $this->cacheFileName = null;
768
            return $this;
769
        }
770
771
        if (isset($dir)) {
772
            $this->saveFolder = rtrim($dir, '/');
773
        }
774
775
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
776
777
        // Sanitize filename
778
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
779
        $this->log("The cache file name is: " . $this->cacheFileName);
780
781
        return $this;
782
    }
783
784
785
786
    /**
787
     * Get filename of target file.
788
     *
789
     * @return Boolean|String as filename of target or false if not set.
790
     */
791
    public function getTarget()
792
    {
793
        return $this->cacheFileName;
794
    }
795
796
797
798
    /**
799
     * Set options to use when processing image.
800
     *
801
     * @param array $args used when processing image.
802
     *
803
     * @return $this
804
     */
805
    public function setOptions($args)
806
    {
807
        $this->log("Set new options for processing image.");
808
809
        $defaults = array(
810
            // Options for calculate dimensions
811
            'newWidth'    => null,
812
            'newHeight'   => null,
813
            'aspectRatio' => null,
814
            'keepRatio'   => true,
815
            'cropToFit'   => false,
816
            'fillToFit'   => null,
817
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
818
            'area'        => null, //'0,0,0,0',
819
            'upscale'     => self::UPSCALE_DEFAULT,
820
821
            // Options for caching or using original
822
            'useCache'    => true,
823
            'useOriginal' => true,
824
825
            // Pre-processing, before resizing is done
826
            'scale'        => null,
827
            'rotateBefore' => null,
828
            'autoRotate'  => false,
829
830
            // General options
831
            'bgColor'     => null,
832
833
            // Post-processing, after resizing is done
834
            'palette'     => null,
835
            'filters'     => null,
836
            'sharpen'     => null,
837
            'emboss'      => null,
838
            'blur'        => null,
839
            'convolve'       => null,
840
            'rotateAfter' => null,
841
842
            // Output format
843
            'outputFormat' => null,
844
            'dpr'          => 1,
845
846
            // Postprocessing using external tools
847
            'lossy' => null,
848
        );
849
850
        // Convert crop settings from string to array
851
        if (isset($args['crop']) && !is_array($args['crop'])) {
852
            $pices = explode(',', $args['crop']);
853
            $args['crop'] = array(
854
                'width'   => $pices[0],
855
                'height'  => $pices[1],
856
                'start_x' => $pices[2],
857
                'start_y' => $pices[3],
858
            );
859
        }
860
861
        // Convert area settings from string to array
862
        if (isset($args['area']) && !is_array($args['area'])) {
863
                $pices = explode(',', $args['area']);
864
                $args['area'] = array(
865
                    'top'    => $pices[0],
866
                    'right'  => $pices[1],
867
                    'bottom' => $pices[2],
868
                    'left'   => $pices[3],
869
                );
870
        }
871
872
        // Convert filter settings from array of string to array of array
873
        if (isset($args['filters']) && is_array($args['filters'])) {
874
            foreach ($args['filters'] as $key => $filterStr) {
875
                $parts = explode(',', $filterStr);
876
                $filter = $this->mapFilter($parts[0]);
877
                $filter['str'] = $filterStr;
878
                for ($i=1; $i<=$filter['argc']; $i++) {
879
                    if (isset($parts[$i])) {
880
                        $filter["arg{$i}"] = $parts[$i];
881
                    } else {
882
                        throw new Exception(
883
                            'Missing arg to filter, review how many arguments are needed at
884
                            http://php.net/manual/en/function.imagefilter.php'
885
                        );
886
                    }
887
                }
888
                $args['filters'][$key] = $filter;
889
            }
890
        }
891
892
        // Merge default arguments with incoming and set properties.
893
        //$args = array_merge_recursive($defaults, $args);
894
        $args = array_merge($defaults, $args);
895
        foreach ($defaults as $key => $val) {
896
            $this->{$key} = $args[$key];
897
        }
898
899
        if ($this->bgColor) {
900
            $this->setDefaultBackgroundColor($this->bgColor);
901
        }
902
903
        // Save original values to enable re-calculating
904
        $this->newWidthOrig  = $this->newWidth;
905
        $this->newHeightOrig = $this->newHeight;
906
        $this->cropOrig      = $this->crop;
907
908
        return $this;
909
    }
910
911
912
913
    /**
914
     * Map filter name to PHP filter and id.
915
     *
916
     * @param string $name the name of the filter.
917
     *
918
     * @return array with filter settings
919
     * @throws Exception
920
     */
921
    private function mapFilter($name)
922
    {
923
        $map = array(
924
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
925
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
926
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
927
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
928
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
929
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
930
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
931
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
932
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
933
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
934
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
935
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
936
        );
937
938
        if (isset($map[$name])) {
939
            return $map[$name];
940
        } else {
941
            throw new Exception('No such filter.');
942
        }
943
    }
944
945
946
947
    /**
948
     * Load image details from original image file.
949
     *
950
     * @param string $file the file to load or null to use $this->pathToImage.
951
     *
952
     * @return $this
953
     * @throws Exception
954
     */
955
    public function loadImageDetails($file = null)
956
    {
957
        $file = $file ? $file : $this->pathToImage;
958
959
        is_readable($file)
960
            or $this->raiseError('Image file does not exist.');
961
962
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
963
        if (empty($info)) {
964
            // To support webp
965
            $this->fileType = false;
966
            if (function_exists("exif_imagetype")) {
967
                $this->fileType = exif_imagetype($file);
968
                if ($this->fileType === false) {
969
                    if (function_exists("imagecreatefromwebp")) {
970
                        $webp = imagecreatefromwebp($file);
971
                        if ($webp !== false) {
972
                            $this->width  = imagesx($webp);
973
                            $this->height = imagesy($webp);
974
                            $this->fileType = IMG_WEBP;
975
                        }
976
                    }
977
                }
978
            }
979
        }
980
981
        if (!$this->fileType) {
982
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
983
        }
984
985
        if ($this->verbose) {
986
            $this->log("Loading image details for: {$file}");
987
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
988
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
989
            $this->log(" Image mimetype: " . $this->getMimeType());
990
        }
991
992
        return $this;
993
    }
994
995
996
997
    /**
998
     * Get mime type for image type.
999
     *
1000
     * @return $this
1001
     * @throws Exception
1002
     */
1003
    protected function getMimeType()
1004
    {
1005
        if ($this->fileType === IMG_WEBP) {
1006
            return "image/webp";
1007
        }
1008
1009
        return image_type_to_mime_type($this->fileType);
1010
    }
1011
1012
1013
1014
    /**
1015
     * Init new width and height and do some sanity checks on constraints, before any
1016
     * processing can be done.
1017
     *
1018
     * @return $this
1019
     * @throws Exception
1020
     */
1021
    public function initDimensions()
1022
    {
1023
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1024
1025
        // width as %
1026
        if ($this->newWidth
1027
            && $this->newWidth[strlen($this->newWidth)-1] == '%') {
1028
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1029
            $this->log("Setting new width based on % to {$this->newWidth}");
1030
        }
1031
1032
        // height as %
1033
        if ($this->newHeight
1034
            && $this->newHeight[strlen($this->newHeight)-1] == '%') {
1035
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1036
            $this->log("Setting new height based on % to {$this->newHeight}");
1037
        }
1038
1039
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
0 ignored issues
show
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...
1040
1041
        // width & height from aspect ratio
1042
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1043
            if ($this->aspectRatio >= 1) {
1044
                $this->newWidth   = $this->width;
1045
                $this->newHeight  = $this->width / $this->aspectRatio;
1046
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1047
1048
            } else {
1049
                $this->newHeight  = $this->height;
1050
                $this->newWidth   = $this->height * $this->aspectRatio;
1051
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1052
            }
1053
1054
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1055
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1056
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1057
1058
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1059
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1060
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1061
        }
1062
1063
        // Change width & height based on dpr
1064
        if ($this->dpr != 1) {
1065
            if (!is_null($this->newWidth)) {
1066
                $this->newWidth  = round($this->newWidth * $this->dpr);
1067
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1068
            }
1069
            if (!is_null($this->newHeight)) {
1070
                $this->newHeight = round($this->newHeight * $this->dpr);
1071
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1072
            }
1073
        }
1074
1075
        // Check values to be within domain
1076
        is_null($this->newWidth)
1077
            or is_numeric($this->newWidth)
1078
            or $this->raiseError('Width not numeric');
1079
1080
        is_null($this->newHeight)
1081
            or is_numeric($this->newHeight)
1082
            or $this->raiseError('Height not numeric');
1083
1084
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1085
1086
        return $this;
1087
    }
1088
1089
1090
1091
    /**
1092
     * Calculate new width and height of image, based on settings.
1093
     *
1094
     * @return $this
1095
     */
1096
    public function calculateNewWidthAndHeight()
1097
    {
1098
        // Crop, use cropped width and height as base for calulations
1099
        $this->log("Calculate new width and height.");
1100
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1101
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1102
1103
        // Check if there is an area to crop off
1104
        if (isset($this->area)) {
1105
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
0 ignored issues
show
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...
1106
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1107
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1108
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1109
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1110
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1111
            $this->width  = $this->offset['width'];
1112
            $this->height = $this->offset['height'];
1113
            $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%.");
1114
            $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px.");
1115
        }
1116
1117
        $width  = $this->width;
1118
        $height = $this->height;
1119
1120
        // Check if crop is set
1121
        if ($this->crop) {
1122
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1123
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1124
1125
            if ($this->crop['start_x'] == 'left') {
1126
                $this->crop['start_x'] = 0;
1127
            } elseif ($this->crop['start_x'] == 'right') {
1128
                $this->crop['start_x'] = $this->width - $width;
1129
            } elseif ($this->crop['start_x'] == 'center') {
1130
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1131
            }
1132
1133
            if ($this->crop['start_y'] == 'top') {
1134
                $this->crop['start_y'] = 0;
1135
            } elseif ($this->crop['start_y'] == 'bottom') {
1136
                $this->crop['start_y'] = $this->height - $height;
1137
            } elseif ($this->crop['start_y'] == 'center') {
1138
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1139
            }
1140
1141
            $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px.");
1142
        }
1143
1144
        // Calculate new width and height if keeping aspect-ratio.
1145
        if ($this->keepRatio) {
1146
1147
            $this->log("Keep aspect ratio.");
1148
1149
            // Crop-to-fit and both new width and height are set.
1150
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1151
1152
                // Use newWidth and newHeigh as width/height, image should fit in box.
1153
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1154
1155
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1156
1157
                // Both new width and height are set.
1158
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1159
                $ratioWidth  = $width  / $this->newWidth;
1160
                $ratioHeight = $height / $this->newHeight;
1161
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1162
                $this->newWidth  = round($width  / $ratio);
1163
                $this->newHeight = round($height / $ratio);
1164
                $this->log("New width and height was set.");
1165
1166
            } elseif (isset($this->newWidth)) {
1167
1168
                // Use new width as max-width
1169
                $factor = (float)$this->newWidth / (float)$width;
1170
                $this->newHeight = round($factor * $height);
1171
                $this->log("New width was set.");
1172
1173
            } elseif (isset($this->newHeight)) {
1174
1175
                // Use new height as max-hight
1176
                $factor = (float)$this->newHeight / (float)$height;
1177
                $this->newWidth = round($factor * $width);
1178
                $this->log("New height was set.");
1179
1180
            } else {
1181
1182
                // Use existing width and height as new width and height.
1183
                $this->newWidth = $width;
1184
                $this->newHeight = $height;
1185
            }
1186
            
1187
1188
            // Get image dimensions for pre-resize image.
1189
            if ($this->cropToFit || $this->fillToFit) {
1190
1191
                // Get relations of original & target image
1192
                $ratioWidth  = $width  / $this->newWidth;
1193
                $ratioHeight = $height / $this->newHeight;
1194
1195
                if ($this->cropToFit) {
1196
1197
                    // Use newWidth and newHeigh as defined width/height,
1198
                    // image should fit the area.
1199
                    $this->log("Crop to fit.");
1200
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1201
                    $this->cropWidth  = round($width  / $ratio);
1202
                    $this->cropHeight = round($height / $ratio);
1203
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1204
1205
                } elseif ($this->fillToFit) {
1206
1207
                    // Use newWidth and newHeigh as defined width/height,
1208
                    // image should fit the area.
1209
                    $this->log("Fill to fit.");
1210
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1211
                    $this->fillWidth  = round($width  / $ratio);
1212
                    $this->fillHeight = round($height / $ratio);
1213
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1214
                }
1215
            }
1216
        }
1217
1218
        // Crop, ensure to set new width and height
1219
        if ($this->crop) {
1220
            $this->log("Crop.");
1221
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1222
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1223
        }
1224
1225
        // Fill to fit, ensure to set new width and height
1226
        /*if ($this->fillToFit) {
1227
            $this->log("FillToFit.");
1228
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1229
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1230
        }*/
1231
1232
        // No new height or width is set, use existing measures.
1233
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1234
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1235
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1236
1237
        return $this;
1238
    }
1239
1240
1241
1242
    /**
1243
     * Re-calculate image dimensions when original image dimension has changed.
1244
     *
1245
     * @return $this
1246
     */
1247
    public function reCalculateDimensions()
1248
    {
1249
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1250
1251
        $this->newWidth  = $this->newWidthOrig;
1252
        $this->newHeight = $this->newHeightOrig;
1253
        $this->crop      = $this->cropOrig;
1254
1255
        $this->initDimensions()
1256
             ->calculateNewWidthAndHeight();
1257
1258
        return $this;
1259
    }
1260
1261
1262
1263
    /**
1264
     * Set extension for filename to save as.
1265
     *
1266
     * @param string $saveas extension to save image as
0 ignored issues
show
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...
1267
     *
1268
     * @return $this
1269
     */
1270
    public function setSaveAsExtension($saveAs = null)
1271
    {
1272
        if (isset($saveAs)) {
1273
            $saveAs = strtolower($saveAs);
1274
            $this->checkFileExtension($saveAs);
1275
            $this->saveAs = $saveAs;
1276
            $this->extension = $saveAs;
1277
        }
1278
1279
        $this->log("Prepare to save image as: " . $this->extension);
1280
1281
        return $this;
1282
    }
1283
1284
1285
1286
    /**
1287
     * Set JPEG quality to use when saving image
1288
     *
1289
     * @param int $quality as the quality to set.
1290
     *
1291
     * @return $this
1292
     */
1293
    public function setJpegQuality($quality = null)
1294
    {
1295
        if ($quality) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $quality of type integer|null is loosely compared to true; this is ambiguous if the integer can be 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...
1296
            $this->useQuality = true;
1297
        }
1298
1299
        $this->quality = isset($quality)
1300
            ? $quality
1301
            : self::JPEG_QUALITY_DEFAULT;
1302
1303
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1304 2
            or $this->raiseError('Quality not in range.');
1305
1306 2
        $this->log("Setting JPEG quality to {$this->quality}.");
1307 2
1308 2
        return $this;
1309 2
    }
1310 2
1311 2
1312 2
1313 2
    /**
1314 2
     * Set PNG compressen algorithm to use when saving image
1315 2
     *
1316 2
     * @param int $compress as the algorithm to use.
1317
     *
1318 2
     * @return $this
1319 2
     */
1320
    public function setPngCompression($compress = null)
1321 2
    {
1322 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...
1323
            $this->useCompress = true;
1324
        }
1325
1326 2
        $this->compress = isset($compress)
1327 2
            ? $compress
1328
            : self::PNG_COMPRESSION_DEFAULT;
1329 2
1330 2
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1331 2
            or $this->raiseError('Quality not in range.');
1332
1333 2
        $this->log("Setting PNG compression level to {$this->compress}.");
1334 2
1335 2
        return $this;
1336
    }
1337 2
1338 2
1339
1340
    /**
1341
     * Use original image if possible, check options which affects image processing.
1342
     *
1343
     * @param boolean $useOrig default is to use original if possible, else set to false.
1344
     *
1345
     * @return $this
1346
     */
1347
    public function useOriginalIfPossible($useOrig = true)
1348
    {
1349 2
        if ($useOrig
1350 2
            && ($this->newWidth == $this->width)
1351 2
            && ($this->newHeight == $this->height)
1352 2
            && !$this->area
1353
            && !$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...
1354 2
            && !$this->cropToFit
1355
            && !$this->fillToFit
1356 2
            && !$this->filters
1357 2
            && !$this->sharpen
1358 2
            && !$this->emboss
1359
            && !$this->blur
1360 2
            && !$this->convolve
1361 2
            && !$this->palette
1362
            && !$this->useQuality
1363
            && !$this->useCompress
1364
            && !$this->saveAs
1365 2
            && !$this->rotateBefore
1366 2
            && !$this->rotateAfter
1367
            && !$this->autoRotate
1368
            && !$this->bgColor
1369
            && ($this->upscale === self::UPSCALE_DEFAULT)
1370 2
            && !$this->lossy
1371 2
        ) {
1372
            $this->log("Using original image.");
1373
            $this->output($this->pathToImage);
1374
        }
1375
1376
        return $this;
1377 2
    }
1378 2
1379 2
1380 2
1381 2
    /**
1382 2
     * Generate filename to save file in cache.
1383 2
     *
1384
     * @param string  $base      as optional basepath for storing file.
1385 2
     * @param boolean $useSubdir use or skip the subdir part when creating the
1386
     *                           filename.
1387
     * @param string  $prefix    to add as part of filename
1388
     *
1389
     * @return $this
1390
     */
1391
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1392
    {
1393
        $filename     = basename($this->pathToImage);
1394
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1395
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1396
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1397
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1398
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1399
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1400
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1401
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1402
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1403
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1404
        $lossy        = $this->lossy        ? "_l"                       : null;
1405
1406
        $saveAs = $this->normalizeFileExtension();
1407
        $saveAs = $saveAs ? "_$saveAs" : null;
1408
1409
        $copyStrat = null;
1410
        if ($this->copyStrategy === self::RESIZE) {
1411
            $copyStrat = "_rs";
1412
        }
1413
1414
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1415
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1416
1417
        $offset = isset($this->offset)
1418
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1419
            : null;
1420
1421
        $crop = $this->crop
1422
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1423
            : null;
1424
1425
        $filters = null;
1426
        if (isset($this->filters)) {
1427
            foreach ($this->filters as $filter) {
1428
                if (is_array($filter)) {
1429
                    $filters .= "_f{$filter['id']}";
1430
                    for ($i=1; $i<=$filter['argc']; $i++) {
1431
                        $filters .= "-".$filter["arg{$i}"];
1432
                    }
1433
                }
1434
            }
1435
        }
1436
1437
        $sharpen = $this->sharpen ? 's' : null;
1438
        $emboss  = $this->emboss  ? 'e' : null;
1439
        $blur    = $this->blur    ? 'b' : null;
1440
        $palette = $this->palette ? 'p' : null;
1441
1442
        $autoRotate = $this->autoRotate ? 'ar' : null;
1443
1444
        $optimize  = $this->jpegOptimize ? 'o' : null;
1445
        $optimize .= $this->pngFilter    ? 'f' : null;
1446
        $optimize .= $this->pngDeflate   ? 'd' : null;
1447
1448
        $convolve = null;
1449
        if ($this->convolve) {
1450
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1451
        }
1452
1453
        $upscale = null;
1454
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1455
            $upscale = '_nu';
1456
        }
1457
1458
        $subdir = null;
1459
        if ($useSubdir === true) {
1460
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1461
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1462
            $subdir .= '_';
1463
        }
1464
1465
        $file = $prefix . $subdir . $filename . $width . $height
1466
            . $offset . $crop . $cropToFit . $fillToFit
1467
            . $crop_x . $crop_y . $upscale
1468
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1469
            . $optimize . $compress
1470
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1471
            . $convolve . $copyStrat . $lossy . $saveAs;
1472
1473
        return $this->setTarget($file, $base);
1474
    }
1475
1476
1477
1478
    /**
1479
     * Use cached version of image, if possible.
1480
     *
1481
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1482
     *
1483
     * @return $this
1484
     */
1485
    public function useCacheIfPossible($useCache = true)
1486
    {
1487
        if ($useCache && is_readable($this->cacheFileName)) {
1488
            $fileTime   = filemtime($this->pathToImage);
1489
            $cacheTime  = filemtime($this->cacheFileName);
1490
1491
            if ($fileTime <= $cacheTime) {
1492
                if ($this->useCache) {
1493
                    if ($this->verbose) {
1494
                        $this->log("Use cached file.");
1495
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1496
                    }
1497
                    $this->output($this->cacheFileName, $this->outputFormat);
1498
                } else {
1499
                    $this->log("Cache is valid but ignoring it by intention.");
1500
                }
1501
            } else {
1502
                $this->log("Original file is modified, ignoring cache.");
1503
            }
1504
        } else {
1505
            $this->log("Cachefile does not exists or ignoring it.");
1506
        }
1507
1508
        return $this;
1509
    }
1510
1511
1512
1513
    /**
1514
     * Load image from disk. Try to load image without verbose error message,
1515
     * if fail, load again and display error messages.
1516
     *
1517
     * @param string $src of image.
1518
     * @param string $dir as base directory where images are.
1519
     *
1520
     * @return $this
1521
     *
1522
     */
1523
    public function load($src = null, $dir = null)
1524
    {
1525
        if (isset($src)) {
1526
            $this->setSource($src, $dir);
1527
        }
1528
1529
        $this->loadImageDetails();
1530
1531
        if ($this->fileType === IMG_WEBP) {
1532
            $this->image = imagecreatefromwebp($this->pathToImage);
1533
        } else {
1534
            $imageAsString = file_get_contents($this->pathToImage);
1535
            $this->image = imagecreatefromstring($imageAsString);
1536
        }
1537
        if ($this->image === false) {
1538
            throw new Exception("Could not load image.");
1539
        }
1540
1541
        /* Removed v0.7.7
1542
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1543
            $type = $this->getPngType();
1544
            $hasFewColors = imagecolorstotal($this->image);
1545
1546
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1547
                if ($this->verbose) {
1548
                    $this->log("Handle this image as a palette image.");
1549
                }
1550
                $this->palette = true;
1551
            }
1552
        }
1553
        */
1554
1555
        if ($this->verbose) {
1556
            $this->log("### Image successfully loaded from file.");
1557
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1558
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1559
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1560
            $index = imagecolortransparent($this->image);
1561
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1562
        }
1563
1564
        return $this;
1565
    }
1566
1567
1568
1569
    /**
1570
     * Get the type of PNG image.
1571
     *
1572
     * @param string $filename to use instead of default.
1573
     *
1574
     * @return int as the type of the png-image
1575
     *
1576
     */
1577
    public function getPngType($filename = null)
1578
    {
1579
        $filename = $filename ? $filename : $this->pathToImage;
1580
1581
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1582
1583
        if ($this->verbose) {
1584
            $this->log("Checking png type of: " . $filename);
1585
            $this->log($this->getPngTypeAsString($pngType));
1586
        }
1587
1588
        return $pngType;
1589
    }
1590
1591
1592
1593
    /**
1594
     * Get the type of PNG image as a verbose string.
1595
     *
1596
     * @param integer $type     to use, default is to check the type.
0 ignored issues
show
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...
1597
     * @param string  $filename to use instead of default.
1598
     *
1599
     * @return int as the type of the png-image
1600
     *
1601
     */
1602
    private function getPngTypeAsString($pngType = null, $filename = null)
1603
    {
1604
        if ($filename || !$pngType) {
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...
1605
            $pngType = $this->getPngType($filename);
1606
        }
1607
1608
        $index = imagecolortransparent($this->image);
1609
        $transparent = null;
1610
        if ($index != -1) {
1611
            $transparent = " (transparent)";
1612
        }
1613
1614
        switch ($pngType) {
1615
1616
            case self::PNG_GREYSCALE:
1617
                $text = "PNG is type 0, Greyscale$transparent";
1618
                break;
1619
1620
            case self::PNG_RGB:
1621
                $text = "PNG is type 2, RGB$transparent";
1622
                break;
1623
1624
            case self::PNG_RGB_PALETTE:
1625
                $text = "PNG is type 3, RGB with palette$transparent";
1626
                break;
1627
1628
            case self::PNG_GREYSCALE_ALPHA:
1629
                $text = "PNG is type 4, Greyscale with alpha channel";
1630
                break;
1631
1632
            case self::PNG_RGB_ALPHA:
1633
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1634
                break;
1635
1636
            default:
1637
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1638
        }
1639
1640
        return $text;
1641
    }
1642
1643
1644
1645
1646
    /**
1647
     * Calculate number of colors in an image.
1648
     *
1649
     * @param resource $im the image.
1650
     *
1651
     * @return int
1652
     */
1653
    private function colorsTotal($im)
1654
    {
1655
        if (imageistruecolor($im)) {
1656
            $this->log("Colors as true color.");
1657
            $h = imagesy($im);
1658
            $w = imagesx($im);
1659
            $c = array();
1660
            for ($x=0; $x < $w; $x++) {
1661
                for ($y=0; $y < $h; $y++) {
1662
                    @$c['c'.imagecolorat($im, $x, $y)]++;
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...
1663
                }
1664
            }
1665
            return count($c);
1666
        } else {
1667
            $this->log("Colors as palette.");
1668
            return imagecolorstotal($im);
1669
        }
1670
    }
1671
1672
1673
1674
    /**
1675
     * Preprocess image before rezising it.
1676
     *
1677
     * @return $this
1678
     */
1679
    public function preResize()
1680
    {
1681
        $this->log("### Pre-process before resizing");
1682
1683
        // Rotate image
1684
        if ($this->rotateBefore) {
1685
            $this->log("Rotating image.");
1686
            $this->rotate($this->rotateBefore, $this->bgColor)
1687
                 ->reCalculateDimensions();
1688
        }
1689
1690
        // Auto-rotate image
1691
        if ($this->autoRotate) {
1692
            $this->log("Auto rotating image.");
1693
            $this->rotateExif()
1694
                 ->reCalculateDimensions();
1695
        }
1696
1697
        // Scale the original image before starting
1698
        if (isset($this->scale)) {
1699
            $this->log("Scale by {$this->scale}%");
1700
            $newWidth  = $this->width * $this->scale / 100;
1701
            $newHeight = $this->height * $this->scale / 100;
1702
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1703
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1704
            $this->image = $img;
1705
            $this->width = $newWidth;
1706
            $this->height = $newHeight;
1707
        }
1708
1709
        return $this;
1710
    }
1711
1712
1713
1714
    /**
1715
     * Resize or resample the image while resizing.
1716
     *
1717
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1718
     *
1719
     * @return $this
1720
     */
1721
     public function setCopyResizeStrategy($strategy)
1722
     {
1723
         $this->copyStrategy = $strategy;
1724
         return $this;
1725
     }
1726
1727
1728
1729
    /**
1730
     * Resize and or crop the image.
1731
     *
1732
     * @return void
1733
     */
1734
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1735
    {
1736
        if($this->copyStrategy == self::RESIZE) {
1737
            $this->log("Copy by resize");
1738
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1739
        } else {
1740
            $this->log("Copy by resample");
1741
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1742
        }
1743
    }
1744
1745
1746
1747
    /**
1748
     * Resize and or crop the image.
1749
     *
1750
     * @return $this
1751
     */
1752
    public function resize()
1753
    {
1754
1755
        $this->log("### Starting to Resize()");
1756
        $this->log("Upscale = '$this->upscale'");
1757
1758
        // Only use a specified area of the image, $this->offset is defining the area to use
1759
        if (isset($this->offset)) {
1760
1761
            $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}");
1762
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1763
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1764
            $this->image = $img;
1765
            $this->width = $this->offset['width'];
1766
            $this->height = $this->offset['height'];
1767
        }
1768
1769
        if ($this->crop) {
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...
1770
1771
            // Do as crop, take only part of image
1772
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1773
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1774
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1775
            $this->image = $img;
1776
            $this->width = $this->crop['width'];
1777
            $this->height = $this->crop['height'];
1778
        }
1779
1780
        if (!$this->upscale) {
1781
            // Consider rewriting the no-upscale code to fit within this if-statement,
1782
            // likely to be more readable code.
1783
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1784
        }
1785
1786
        if ($this->cropToFit) {
1787
1788
            // Resize by crop to fit
1789
            $this->log("Resizing using strategy - Crop to fit");
1790
1791
            if (!$this->upscale 
1792
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1793
                $this->log("Resizing - smaller image, do not upscale.");
1794
1795
                $posX = 0;
1796
                $posY = 0;
1797
                $cropX = 0;
1798
                $cropY = 0;
1799
1800
                if ($this->newWidth > $this->width) {
1801
                    $posX = round(($this->newWidth - $this->width) / 2);
1802
                }
1803
                if ($this->newWidth < $this->width) {
1804
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1805
                }
1806
1807
                if ($this->newHeight > $this->height) {
1808
                    $posY = round(($this->newHeight - $this->height) / 2);
1809
                }
1810
                if ($this->newHeight < $this->height) {
1811
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1812
                }
1813
                $this->log(" cwidth: $this->cropWidth");
1814
                $this->log(" cheight: $this->cropHeight");
1815
                $this->log(" nwidth: $this->newWidth");
1816
                $this->log(" nheight: $this->newHeight");
1817
                $this->log(" width: $this->width");
1818
                $this->log(" height: $this->height");
1819
                $this->log(" posX: $posX");
1820
                $this->log(" posY: $posY");
1821
                $this->log(" cropX: $cropX");
1822
                $this->log(" cropY: $cropY");
1823
1824
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1825
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1826
            } else {
1827
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1828
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1829
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1830
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1831
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1832
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1833
            }
1834
1835
            $this->image = $imageResized;
1836
            $this->width = $this->newWidth;
1837
            $this->height = $this->newHeight;
1838
1839
        } elseif ($this->fillToFit) {
1840
1841
            // Resize by fill to fit
1842
            $this->log("Resizing using strategy - Fill to fit");
1843
1844
            $posX = 0;
1845
            $posY = 0;
1846
1847
            $ratioOrig = $this->width / $this->height;
1848
            $ratioNew  = $this->newWidth / $this->newHeight;
1849
1850
            // Check ratio for landscape or portrait
1851
            if ($ratioOrig < $ratioNew) {
1852
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1853
            } else {
1854
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1855
            }
1856
1857
            if (!$this->upscale
1858
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1859
            ) {
1860
1861
                $this->log("Resizing - smaller image, do not upscale.");
1862
                $posX = round(($this->newWidth - $this->width) / 2);
1863
                $posY = round(($this->newHeight - $this->height) / 2);
1864
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1865
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1866
1867
            } else {
1868
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1869
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1870
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1871
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1872
            }
1873
1874
            $this->image = $imageResized;
1875
            $this->width = $this->newWidth;
1876
            $this->height = $this->newHeight;
1877
1878
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1879
1880
            // Resize it
1881
            $this->log("Resizing, new height and/or width");
1882
1883
            if (!$this->upscale
1884
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1885
            ) {
1886
                $this->log("Resizing - smaller image, do not upscale.");
1887
1888
                if (!$this->keepRatio) {
1889
                    $this->log("Resizing - stretch to fit selected.");
1890
1891
                    $posX = 0;
1892
                    $posY = 0;
1893
                    $cropX = 0;
1894
                    $cropY = 0;
1895
1896
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1897
                        $posX = round(($this->newWidth - $this->width) / 2);
1898
                        $posY = round(($this->newHeight - $this->height) / 2);
1899
                    } elseif ($this->newWidth > $this->width) {
1900
                        $posX = round(($this->newWidth - $this->width) / 2);
1901
                        $cropY = round(($this->height - $this->newHeight) / 2);
1902
                    } elseif ($this->newHeight > $this->height) {
1903
                        $posY = round(($this->newHeight - $this->height) / 2);
1904
                        $cropX = round(($this->width - $this->newWidth) / 2);
1905
                    }
1906
1907
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1908
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1909
                    $this->image = $imageResized;
1910
                    $this->width = $this->newWidth;
1911
                    $this->height = $this->newHeight;
1912
                }
1913
            } else {
1914
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1915
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1916
                $this->image = $imageResized;
1917
                $this->width = $this->newWidth;
1918
                $this->height = $this->newHeight;
1919
            }
1920
        }
1921
1922
        return $this;
1923
    }
1924
1925
1926
1927
    /**
1928
     * Postprocess image after rezising image.
1929
     *
1930
     * @return $this
1931
     */
1932
    public function postResize()
1933
    {
1934
        $this->log("### Post-process after resizing");
1935
1936
        // Rotate image
1937
        if ($this->rotateAfter) {
1938
            $this->log("Rotating image.");
1939
            $this->rotate($this->rotateAfter, $this->bgColor);
1940
        }
1941
1942
        // Apply filters
1943
        if (isset($this->filters) && is_array($this->filters)) {
1944
1945
            foreach ($this->filters as $filter) {
1946
                $this->log("Applying filter {$filter['type']}.");
1947
1948
                switch ($filter['argc']) {
1949
1950
                    case 0:
1951
                        imagefilter($this->image, $filter['type']);
1952
                        break;
1953
1954
                    case 1:
1955
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1956
                        break;
1957
1958
                    case 2:
1959
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1960
                        break;
1961
1962
                    case 3:
1963
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1964
                        break;
1965
1966
                    case 4:
1967
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1968
                        break;
1969
                }
1970
            }
1971
        }
1972
1973
        // Convert to palette image
1974
        if ($this->palette) {
1975
            $this->log("Converting to palette image.");
1976
            $this->trueColorToPalette();
1977
        }
1978
1979
        // Blur the image
1980
        if ($this->blur) {
1981
            $this->log("Blur.");
1982
            $this->blurImage();
1983
        }
1984
1985
        // Emboss the image
1986
        if ($this->emboss) {
1987
            $this->log("Emboss.");
1988
            $this->embossImage();
1989
        }
1990
1991
        // Sharpen the image
1992
        if ($this->sharpen) {
1993
            $this->log("Sharpen.");
1994
            $this->sharpenImage();
1995
        }
1996
1997
        // Custom convolution
1998
        if ($this->convolve) {
1999
            //$this->log("Convolve: " . $this->convolve);
2000
            $this->imageConvolution();
2001
        }
2002
2003
        return $this;
2004
    }
2005
2006
2007
2008
    /**
2009
     * Rotate image using angle.
2010
     *
2011
     * @param float $angle        to rotate image.
2012
     * @param int   $anglebgColor to fill image with if needed.
0 ignored issues
show
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...
2013
     *
2014
     * @return $this
2015
     */
2016
    public function rotate($angle, $bgColor)
0 ignored issues
show
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...
2017
    {
2018
        $this->log("Rotate image " . $angle . " degrees with filler color.");
2019
2020
        $color = $this->getBackgroundColor();
2021
        $this->image = imagerotate($this->image, $angle, $color);
2022
2023
        $this->width  = imagesx($this->image);
2024
        $this->height = imagesy($this->image);
2025
2026
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2027
2028
        return $this;
2029
    }
2030
2031
2032
2033
    /**
2034
     * Rotate image using information in EXIF.
2035
     *
2036
     * @return $this
2037
     */
2038
    public function rotateExif()
2039
    {
2040
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2041
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2042
            return $this;
2043
        }
2044
2045
        $exif = exif_read_data($this->pathToImage);
2046
2047
        if (!empty($exif['Orientation'])) {
2048
            switch ($exif['Orientation']) {
2049
                case 3:
2050
                    $this->log("Autorotate 180.");
2051
                    $this->rotate(180, $this->bgColor);
2052
                    break;
2053
2054
                case 6:
2055
                    $this->log("Autorotate -90.");
2056
                    $this->rotate(-90, $this->bgColor);
2057
                    break;
2058
2059
                case 8:
2060
                    $this->log("Autorotate 90.");
2061
                    $this->rotate(90, $this->bgColor);
2062
                    break;
2063
2064
                default:
2065
                    $this->log("Autorotate ignored, unknown value as orientation.");
2066
            }
2067
        } else {
2068
            $this->log("Autorotate ignored, no orientation in EXIF.");
2069
        }
2070
2071
        return $this;
2072
    }
2073
2074
2075
2076
    /**
2077
     * Convert true color image to palette image, keeping alpha.
2078
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2079
     *
2080
     * @return void
2081
     */
2082
    public function trueColorToPalette()
2083
    {
2084
        $img = imagecreatetruecolor($this->width, $this->height);
2085
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2086
        imagecolortransparent($img, $bga);
2087
        imagefill($img, 0, 0, $bga);
2088
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2089
        imagetruecolortopalette($img, false, 255);
2090
        imagesavealpha($img, true);
2091
2092
        if (imageistruecolor($this->image)) {
2093
            $this->log("Matching colors with true color image.");
2094
            imagecolormatch($this->image, $img);
2095
        }
2096
2097
        $this->image = $img;
2098
    }
2099
2100
2101
2102
    /**
2103
     * Sharpen image using image convolution.
2104
     *
2105
     * @return $this
2106
     */
2107
    public function sharpenImage()
2108
    {
2109
        $this->imageConvolution('sharpen');
2110
        return $this;
2111
    }
2112
2113
2114
2115
    /**
2116
     * Emboss image using image convolution.
2117
     *
2118
     * @return $this
2119
     */
2120
    public function embossImage()
2121
    {
2122
        $this->imageConvolution('emboss');
2123
        return $this;
2124
    }
2125
2126
2127
2128
    /**
2129
     * Blur image using image convolution.
2130
     *
2131
     * @return $this
2132
     */
2133
    public function blurImage()
2134
    {
2135
        $this->imageConvolution('blur');
2136
        return $this;
2137
    }
2138
2139
2140
2141
    /**
2142
     * Create convolve expression and return arguments for image convolution.
2143
     *
2144
     * @param string $expression constant string which evaluates to a list of
2145
     *                           11 numbers separated by komma or such a list.
2146
     *
2147
     * @return array as $matrix (3x3), $divisor and $offset
2148
     */
2149
    public function createConvolveArguments($expression)
2150
    {
2151
        // Check of matching constant
2152
        if (isset($this->convolves[$expression])) {
2153
            $expression = $this->convolves[$expression];
2154
        }
2155
2156
        $part = explode(',', $expression);
2157
        $this->log("Creating convolution expressen: $expression");
2158
2159
        // Expect list of 11 numbers, split by , and build up arguments
2160
        if (count($part) != 11) {
2161
            throw new Exception(
2162
                "Missmatch in argument convolve. Expected comma-separated string with
2163
                11 float values. Got $expression."
2164
            );
2165
        }
2166
2167
        array_walk($part, function ($item, $key) {
0 ignored issues
show
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...
2168
            if (!is_numeric($item)) {
2169
                throw new Exception("Argument to convolve expression should be float but is not.");
2170
            }
2171
        });
2172
2173
        return array(
2174
            array(
2175
                array($part[0], $part[1], $part[2]),
2176
                array($part[3], $part[4], $part[5]),
2177
                array($part[6], $part[7], $part[8]),
2178
            ),
2179
            $part[9],
2180
            $part[10],
2181 2
        );
2182
    }
2183 2
2184
2185 2
2186
    /**
2187 2
     * Add custom expressions (or overwrite existing) for image convolution.
2188 2
     *
2189 2
     * @param array $options Key value array with strings to be converted
2190 2
     *                       to convolution expressions.
2191
     *
2192 2
     * @return $this
2193
     */
2194
    public function addConvolveExpressions($options)
2195 2
    {
2196
        $this->convolves = array_merge($this->convolves, $options);
2197
        return $this;
2198 2
    }
2199
2200
2201
2202
    /**
2203
     * Image convolution.
2204
     *
2205
     * @param string $options A string with 11 float separated by comma.
2206
     *
2207
     * @return $this
2208
     */
2209
    public function imageConvolution($options = null)
2210
    {
2211
        // Use incoming options or use $this.
2212
        $options = $options ? $options : $this->convolve;
2213
2214
        // Treat incoming as string, split by +
2215 2
        $this->log("Convolution with '$options'");
2216
        $options = explode(":", $options);
2217 2
2218 2
        // Check each option if it matches constant value
2219 2
        foreach ($options as $option) {
2220 2
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2221
            imageconvolution($this->image, $matrix, $divisor, $offset);
2222 2
        }
2223 2
2224 2
        return $this;
2225
    }
2226 2
2227
2228
2229
    /**
2230
     * Set default background color between 000000-FFFFFF or if using
2231
     * alpha 00000000-FFFFFF7F.
2232
     *
2233
     * @param string $color as hex value.
2234
     *
2235 2
     * @return $this
2236
    */
2237 2
    public function setDefaultBackgroundColor($color)
2238 2
    {
2239 2
        $this->log("Setting default background color to '$color'.");
2240 2
2241
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2242 2
            throw new Exception(
2243
                "Background color needs a hex value of 6 or 8
2244
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2245
                Current value was: '$color'."
2246
            );
2247
        }
2248
2249
        $red    = hexdec(substr($color, 0, 2));
2250
        $green  = hexdec(substr($color, 2, 2));
2251
        $blue   = hexdec(substr($color, 4, 2));
2252
2253
        $alpha = (strlen($color) == 8)
2254
            ? hexdec(substr($color, 6, 2))
2255
            : null;
2256
2257
        if (($red < 0 || $red > 255)
2258
            || ($green < 0 || $green > 255)
2259
            || ($blue < 0 || $blue > 255)
2260
            || ($alpha < 0 || $alpha > 127)
2261
        ) {
2262
            throw new Exception(
2263
                "Background color out of range. Red, green blue
2264
                should be 00-FF and alpha should be 00-7F.
2265
                Current value was: '$color'."
2266
            );
2267
        }
2268
2269
        $this->bgColor = strtolower($color);
2270
        $this->bgColorDefault = array(
2271
            'red'   => $red,
2272
            'green' => $green,
2273
            'blue'  => $blue,
2274
            'alpha' => $alpha
2275
        );
2276
2277
        return $this;
2278
    }
2279
2280
2281
2282
    /**
2283
     * Get the background color.
2284 2
     *
2285
     * @param resource $img the image to work with or null if using $this->image.
2286
     *
2287 2
     * @return color value or null if no background color is set.
2288
    */
2289
    private function getBackgroundColor($img = null)
2290 2
    {
2291
        $img = isset($img) ? $img : $this->image;
2292
2293
        if ($this->bgColorDefault) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->bgColorDefault of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
2344
2345
            $color = $this->getBackgroundColor($img);
2346
            imagefill($img, 0, 0, $color);
2347 2
            $this->Log("Filling image with background color.");
2348 2
        }
2349 2
2350
        return $img;
2351
    }
2352 2
2353 2
2354 2
2355
    /**
2356
     * Set optimizing  and post-processing options.
2357 2
     *
2358
     * @param array $options with config for postprocessing with external tools.
2359
     *
2360
     * @return $this
2361
     */
2362
    public function setPostProcessingOptions($options)
2363
    {
2364
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2365
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2366
        } else {
2367
            $this->jpegOptimizeCmd = null;
2368
        }
2369
2370 2
        if (array_key_exists("png_lossy", $options) 
2371
            && $options['png_lossy'] !== false) {
2372
            $this->pngLossy = $options['png_lossy'];
2373
            $this->pngLossyCmd = $options['png_lossy_cmd'];
2374
        } else {
2375
            $this->pngLossyCmd = null;
2376
        }
2377
2378
        if (isset($options['png_filter']) && $options['png_filter']) {
2379
            $this->pngFilterCmd = $options['png_filter_cmd'];
2380
        } else {
2381 2
            $this->pngFilterCmd = null;
2382 2
        }
2383
2384 2
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2385
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2386
        } else {
2387
            $this->pngDeflateCmd = null;
2388
        }
2389
2390
        return $this;
2391
    }
2392
2393
2394
2395 2
    /**
2396
     * Find out the type (file extension) for the image to be saved.
2397
     *
2398
     * @return string as image extension.
2399
     */
2400
    protected function getTargetImageExtension()
2401
    {
2402
        // switch on mimetype
2403
        if (isset($this->extension)) {
2404
            return strtolower($this->extension);
2405
        } elseif ($this->fileType === IMG_WEBP) {
2406
            return "webp";
2407
        }
2408
2409
        return substr(image_type_to_extension($this->fileType), 1);
2410
    }
2411
2412
2413
2414
    /**
2415
     * Save image.
2416
     *
2417
     * @param string  $src       as target filename.
2418
     * @param string  $base      as base directory where to store images.
2419
     * @param boolean $overwrite or not, default to always overwrite file.
2420
     *
2421
     * @return $this or false if no folder is set.
2422
     */
2423
    public function save($src = null, $base = null, $overwrite = true)
2424
    {
2425
        if (isset($src)) {
2426
            $this->setTarget($src, $base);
2427
        }
2428
2429
        if ($overwrite === false && is_file($this->cacheFileName)) {
2430
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2431
            return;
2432
        }
2433
2434
        is_writable($this->saveFolder)
2435
            or $this->raiseError('Target directory is not writable.');
2436
2437
        $type = $this->getTargetImageExtension();
2438
        $this->Log("Saving image as " . $type);
2439
        switch($type) {
2440
2441
            case 'jpeg':
2442
            case 'jpg':
2443
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2444
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2445
2446
                // Use JPEG optimize if defined
2447
                if ($this->jpegOptimizeCmd) {
2448
                    if ($this->verbose) {
2449
                        clearstatcache();
2450
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2451
                    }
2452
                    $res = array();
2453
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2454
                    exec($cmd, $res);
2455
                    $this->log($cmd);
2456
                    $this->log($res);
0 ignored issues
show
$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...
2457
                }
2458
                break;
2459
2460
            case 'gif':
2461
                $this->Log("Saving image as GIF to cache.");
2462
                imagegif($this->image, $this->cacheFileName);
2463
                break;
2464
2465
            case 'webp':
2466
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2467
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2468
                break;
2469
2470
            case 'png':
2471
            default:
2472
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2473
2474
                // Turn off alpha blending and set alpha flag
2475
                imagealphablending($this->image, false);
2476
                imagesavealpha($this->image, true);
2477
                imagepng($this->image, $this->cacheFileName, $this->compress);
2478
2479
                // Use external program to process lossy PNG, if defined
2480
                $lossyEnabled = $this->pngLossy === true;
2481
                $lossySoftEnabled = $this->pngLossy === null;
2482
                $lossyActiveEnabled = $this->lossy === true;
2483
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
2484
                    if ($this->verbose) {
2485
                        clearstatcache();
2486
                        $this->log("Lossy enabled: $lossyEnabled");
2487
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
2488
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
2489
                    }
2490
                    $res = array();
2491
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
2492
                    exec($cmd, $res);
2493
                    $this->Log($cmd);
2494
                    $this->Log($res);
0 ignored issues
show
$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...
2495
                }
2496
2497
                // Use external program to filter PNG, if defined
2498
                if ($this->pngFilterCmd) {
2499
                    if ($this->verbose) {
2500
                        clearstatcache();
2501
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2502
                    }
2503
                    $res = array();
2504
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2505
                    exec($cmd, $res);
2506
                    $this->Log($cmd);
2507
                    $this->Log($res);
0 ignored issues
show
$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...
2508
                }
2509
2510
                // Use external program to deflate PNG, if defined
2511
                if ($this->pngDeflateCmd) {
2512
                    if ($this->verbose) {
2513
                        clearstatcache();
2514
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2515
                    }
2516
                    $res = array();
2517
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2518
                    exec($cmd, $res);
2519
                    $this->Log($cmd);
2520
                    $this->Log($res);
0 ignored issues
show
$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...
2521
                }
2522
                break;
2523
        }
2524
2525
        if ($this->verbose) {
2526
            clearstatcache();
2527
            $this->log("Saved image to cache.");
2528
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2529
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2530
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2531
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2532
            $index = imagecolortransparent($this->image);
2533
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2534
        }
2535
2536
        return $this;
2537
    }
2538
2539
2540
2541
    /**
2542
     * Convert image from one colorpsace/color profile to sRGB without
2543
     * color profile.
2544
     *
2545
     * @param string  $src      of image.
2546
     * @param string  $dir      as base directory where images are.
2547
     * @param string  $cache    as base directory where to store images.
2548
     * @param string  $iccFile  filename of colorprofile.
2549
     * @param boolean $useCache or not, default to always use cache.
2550
     *
2551
     * @return string | boolean false if no conversion else the converted
2552
     *                          filename.
2553
     */
2554
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2555
    {
2556
        if ($this->verbose) {
2557
            $this->log("# Converting image to sRGB colorspace.");
2558
        }
2559
2560
        if (!class_exists("Imagick")) {
2561
            $this->log(" Ignoring since Imagemagick is not installed.");
2562
            return false;
2563
        }
2564
2565
        // Prepare
2566
        $this->setSaveFolder($cache)
2567
             ->setSource($src, $dir)
2568
             ->generateFilename(null, false, 'srgb_');
2569
2570
        // Check if the cached version is accurate.
2571
        if ($useCache && is_readable($this->cacheFileName)) {
2572
            $fileTime  = filemtime($this->pathToImage);
2573
            $cacheTime = filemtime($this->cacheFileName);
2574
2575
            if ($fileTime <= $cacheTime) {
2576
                $this->log(" Using cached version: " . $this->cacheFileName);
2577
                return $this->cacheFileName;
2578
            }
2579
        }
2580
2581
        // Only covert if cachedir is writable
2582
        if (is_writable($this->saveFolder)) {
2583
            // Load file and check if conversion is needed
2584
            $image      = new Imagick($this->pathToImage);
2585
            $colorspace = $image->getImageColorspace();
2586
            $this->log(" Current colorspace: " . $colorspace);
2587
2588
            $profiles      = $image->getImageProfiles('*', false);
2589
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2590
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2591
2592
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2593
                $this->log(" Converting to sRGB.");
2594
2595
                $sRGBicc = file_get_contents($iccFile);
2596
                $image->profileImage('icc', $sRGBicc);
2597
2598
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2599
                $image->writeImage($this->cacheFileName);
2600
                return $this->cacheFileName;
2601
            }
2602
        }
2603
2604
        return false;
2605
    }
2606
2607
2608
2609
    /**
2610
     * Create a hard link, as an alias, to the cached file.
2611
     *
2612
     * @param string $alias where to store the link,
2613
     *                      filename without extension.
2614
     *
2615
     * @return $this
2616
     */
2617
    public function linkToCacheFile($alias)
2618
    {
2619
        if ($alias === null) {
2620
            $this->log("Ignore creating alias.");
2621
            return $this;
2622
        }
2623
2624
        if (is_readable($alias)) {
2625
            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 1196
  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 1196

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...
2626
        }
2627 7
2628
        $res = link($this->cacheFileName, $alias);
2629 7
2630
        if ($res) {
2631
            $this->log("Created an alias as: $alias");
2632
        } else {
2633 7
            $this->log("Failed to create the alias: $alias");
2634
        }
2635
2636
        return $this;
2637
    }
2638
2639
2640
2641
    /**
2642
     * Add HTTP header for output together with image.
2643
     *
2644
     * @param string $type  the header type such as "Cache-Control"
2645
     * @param string $value the value to use
2646
     *
2647
     * @return void
2648
     */
2649
    public function addHTTPHeader($type, $value)
2650
    {
2651
        $this->HTTPHeader[$type] = $value;
2652
    }
2653
2654
2655
2656
    /**
2657
     * Output image to browser using caching.
2658
     *
2659
     * @param string $file   to read and output, default is to
2660
     *                       use $this->cacheFileName
2661
     * @param string $format set to json to output file as json
2662
     *                       object with details
2663
     *
2664
     * @return void
2665
     */
2666
    public function output($file = null, $format = null)
2667
    {
2668
        if (is_null($file)) {
2669
            $file = $this->cacheFileName;
2670
        }
2671
2672
        if (is_null($format)) {
2673
            $format = $this->outputFormat;
2674
        }
2675
2676
        $this->log("### Output");
2677
        $this->log("Output format is: $format");
2678
2679
        if (!$this->verbose && $format == 'json') {
2680
            header('Content-type: application/json');
2681
            echo $this->json($file);
2682
            exit;
2683
        } elseif ($format == 'ascii') {
2684
            header('Content-type: text/plain');
2685
            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 2826
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2843
  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 2844
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2685
  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 2826
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2843
  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 2844
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2685

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...
2686
            exit;
2687
        }
2688
2689
        $this->log("Outputting image: $file");
2690
2691
        // Get image modification time
2692
        clearstatcache();
2693
        $lastModified = filemtime($file);
2694
        $lastModifiedFormat = "D, d M Y H:i:s";
2695
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2696
2697
        if (!$this->verbose) {
2698
            $header = "Last-Modified: $gmdate GMT";
2699
            header($header);
2700
            $this->fastTrackCache->addHeader($header);
2701
            $this->fastTrackCache->setLastModified($lastModified);
2702
        }
2703
2704
        foreach ($this->HTTPHeader as $key => $val) {
2705
            $header = "$key: $val";
2706
            header($header);
2707
            $this->fastTrackCache->addHeader($header);
2708
        }
2709
2710
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
2711
            && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2712
2713
            if ($this->verbose) {
2714
                $this->log("304 not modified");
2715
                $this->verboseOutput();
2716
                exit;
2717
            }
2718
2719
            header("HTTP/1.0 304 Not Modified");
2720
            if (CIMAGE_DEBUG) {
2721
                trace(__CLASS__ . " 304");
2722
            }
2723
2724
        } else {
2725
2726
            $this->loadImageDetails($file);
2727
            $mime = $this->getMimeType();
2728
            $size = filesize($file);
2729
2730
            if ($this->verbose) {
2731
                $this->log("Last-Modified: " . $gmdate . " GMT");
2732
                $this->log("Content-type: " . $mime);
2733
                $this->log("Content-length: " . $size);
2734
                $this->verboseOutput();
2735
2736
                if (is_null($this->verboseFileName)) {
2737
                    exit;
2738
                }
2739
            }
2740
2741
            $header = "Content-type: $mime";
2742
            header($header);
2743
            $this->fastTrackCache->addHeaderOnOutput($header);
2744
2745
            $header = "Content-length: $size";
2746
            header($header);
2747
            $this->fastTrackCache->addHeaderOnOutput($header);
2748
2749
            $this->fastTrackCache->setSource($file);
2750
            $this->fastTrackCache->writeToCache();
2751
            if (CIMAGE_DEBUG) {
2752
                trace(__CLASS__ . " 200");
2753
            }
2754
            readfile($file);
2755
        }
2756
2757
        exit;
2758
    }
2759
2760
2761
2762
    /**
2763
     * Create a JSON object from the image details.
2764
     *
2765
     * @param string $file the file to output.
2766
     *
2767
     * @return string json-encoded representation of the image.
2768
     */
2769
    public function json($file = null)
2770
    {
2771
        $file = $file ? $file : $this->cacheFileName;
2772
2773
        $details = array();
2774
2775
        clearstatcache();
2776
2777
        $details['src']       = $this->imageSrc;
2778
        $lastModified         = filemtime($this->pathToImage);
2779
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2780
2781
        $details['cache']       = basename($this->cacheFileName);
2782
        $lastModified           = filemtime($this->cacheFileName);
2783
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2784
2785
        $this->load($file);
2786
2787
        $details['filename']    = basename($file);
2788
        $details['mimeType']    = $this->getMimeType($this->fileType);
0 ignored issues
show
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...
2789
        $details['width']       = $this->width;
2790
        $details['height']      = $this->height;
2791
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2792
        $details['size']        = filesize($file);
2793
        $details['colors'] = $this->colorsTotal($this->image);
2794
        $details['includedFiles'] = count(get_included_files());
2795
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2796
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2797
        $details['memoryLimit'] = ini_get('memory_limit');
2798
2799
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2800
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2801
        }
2802
2803
        if ($details['mimeType'] == 'image/png') {
2804
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2805
        }
2806
2807
        $options = null;
2808
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2809
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2810
        }
2811
2812
        return json_encode($details, $options);
2813
    }
2814
2815
2816
2817
    /**
2818
     * Set options for creating ascii version of image.
2819
     *
2820
     * @param array $options empty to use default or set options to change.
2821
     *
2822
     * @return void.
0 ignored issues
show
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...
2823
     */
2824
    public function setAsciiOptions($options = array())
2825
    {
2826
        $this->asciiOptions = $options;
2827
    }
2828
2829
2830
2831
    /**
2832
     * Create an ASCII version from the image details.
2833
     *
2834
     * @param string $file the file to output.
2835
     *
2836
     * @return string ASCII representation of the image.
2837
     */
2838
    public function ascii($file = null)
2839
    {
2840
        $file = $file ? $file : $this->cacheFileName;
2841
2842
        $asciiArt = new CAsciiArt();
2843
        $asciiArt->setOptions($this->asciiOptions);
2844
        return $asciiArt->createFromFile($file);
2845
    }
2846
2847
2848
2849
    /**
2850
     * Log an event if verbose mode.
2851
     *
2852
     * @param string $message to log.
2853
     *
2854
     * @return this
2855
     */
2856
    public function log($message)
2857
    {
2858
        if ($this->verbose) {
2859
            $this->log[] = $message;
2860
        }
2861
2862
        return $this;
2863
    }
2864
2865
2866
2867
    /**
2868
     * Do verbose output to a file.
2869
     *
2870
     * @param string $fileName where to write the verbose output.
2871
     *
2872
     * @return void
2873
     */
2874
    public function setVerboseToFile($fileName)
2875
    {
2876
        $this->log("Setting verbose output to file.");
2877
        $this->verboseFileName = $fileName;
2878
    }
2879
2880
2881
2882
    /**
2883
     * Do verbose output and print out the log and the actual images.
2884
     *
2885
     * @return void
2886
     */
2887
    private function verboseOutput()
2888
    {
2889
        $log = null;
2890
        $this->log("### Summary of verbose log");
2891
        $this->log("As JSON: \n" . $this->json());
2892
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2893
        $this->log("Memory limit: " . ini_get('memory_limit'));
2894
2895
        $included = get_included_files();
2896
        $this->log("Included files: " . count($included));
2897
2898
        foreach ($this->log as $val) {
2899
            if (is_array($val)) {
2900
                foreach ($val as $val1) {
2901
                    $log .= htmlentities($val1) . '<br/>';
2902
                }
2903
            } else {
2904
                $log .= htmlentities($val) . '<br/>';
2905
            }
2906
        }
2907
2908
        if (!is_null($this->verboseFileName)) {
2909
            file_put_contents(
2910
                $this->verboseFileName,
2911
                str_replace("<br/>", "\n", $log)
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 1184
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in CImage.php on line 1273
  5. CImage::$extension is assigned
    in CImage.php on line 1276
  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 1279
  7. CImage::$log is assigned
    in CImage.php on line 2859
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2898
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2904
  10. $log is passed through str_replace()
    in CImage.php on line 2911
  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 1184
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in CImage.php on line 1273
  5. CImage::$extension is assigned
    in CImage.php on line 1276
  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 1279
  7. CImage::$log is assigned
    in CImage.php on line 2859
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2898
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2904
  10. $log is passed through str_replace()
    in CImage.php on line 2911

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...
2912
            );
2913
        } else {
2914
            echo <<<EOD
2915
<h1>CImage Verbose Output</h1>
2916
<pre>{$log}</pre>
2917
EOD;
2918
        }
2919
    }
2920
2921
2922
2923
    /**
2924
     * Raise error, enables to implement a selection of error methods.
2925
     *
2926
     * @param string $message the error message to display.
2927
     *
2928
     * @return void
2929
     * @throws Exception
2930
     */
2931
    private function raiseError($message)
2932
    {
2933
        throw new Exception($message);
2934
    }
2935
}
2936