Completed
Push — 0715 ( b5de49...bbfd89 )
by Mikael
05:02 queued 02:13
created

CImage.php (55 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
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
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
    /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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
     * Verbose mode to print out a trace and display the created image
160
     */
161
    private $verbose = false;
162
163
164
165
    /**
166
     * Keep a log/trace on what happens
167
     */
168
    private $log = array();
169
170
171
172
    /**
173
     * Handle image as palette image
174
     */
175
    private $palette;
176
177
178
179
    /**
180
     * Target filename, with path, to save resulting image in.
181
     */
182
    private $cacheFileName;
183
184
185
186
    /**
187
     * Set a format to save image as, or null to use original format.
188
     */
189
    private $saveAs;
190
191
192
    /**
193
     * Path to command for filter optimize, for example optipng or null.
194
     */
195
    private $pngFilter;
196
    private $pngFilterCmd;
197
198
199
200
    /**
201
     * Path to command for deflate optimize, for example pngout or null.
202
     */
203
    private $pngDeflate;
204
    private $pngDeflateCmd;
205
206
207
208
    /**
209
     * Path to command to optimize jpeg images, for example jpegtran or null.
210
     */
211
     private $jpegOptimize;
212
     private $jpegOptimizeCmd;
213
214
215
216
    /**
217
     * Image dimensions, calculated from loaded image.
218
     */
219
    private $width;  // Calculated from source image
220
    private $height; // Calculated from source image
221
222
223
    /**
224
     * New image dimensions, incoming as argument or calculated.
225
     */
226
    private $newWidth;
227
    private $newWidthOrig;  // Save original value
228
    private $newHeight;
229
    private $newHeightOrig; // Save original value
230
231
232
    /**
233
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
234
     */
235
    private $dpr = 1;
236
237
238
    /**
239
     * Always upscale images, even if they are smaller than target image.
240
     */
241
    const UPSCALE_DEFAULT = true;
242
    private $upscale = self::UPSCALE_DEFAULT;
243
244
245
246
    /**
247
     * Array with details on how to crop, incoming as argument and calculated.
248
     */
249
    public $crop;
250
    public $cropOrig; // Save original value
251
252
253
    /**
254
     * String with details on how to do image convolution. String
255
     * should map a key in the $convolvs array or be a string of
256
     * 11 float values separated by comma. The first nine builds
257
     * up the matrix, then divisor and last offset.
258
     */
259
    private $convolve;
260
261
262
    /**
263
     * Custom convolution expressions, matrix 3x3, divisor and offset.
264
     */
265
    private $convolves = array(
266
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
267
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
268
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
269
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
270
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
271
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
272
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
273
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
274
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
275
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
276
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
277
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
278
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
279
    );
280
281
282
    /**
283
     * Resize strategy to fill extra area with background color.
284
     * True or false.
285
     */
286
    private $fillToFit;
287
288
289
290
    /**
291
     * To store value for option scale.
292
     */
293
    private $scale;
294
295
296
297
    /**
298
     * To store value for option.
299
     */
300
    private $rotateBefore;
301
302
303
304
    /**
305
     * To store value for option.
306
     */
307
    private $rotateAfter;
308
309
310
311
    /**
312
     * To store value for option.
313
     */
314
    private $autoRotate;
315
316
317
318
    /**
319
     * To store value for option.
320
     */
321
    private $sharpen;
322
323
324
325
    /**
326
     * To store value for option.
327
     */
328
    private $emboss;
329
330
331
332
    /**
333
     * To store value for option.
334
     */
335
    private $blur;
336
337
338
339
    /**
340
     * Used with option area to set which parts of the image to use.
341
     */
342
    private $offset;
343
344
345
346
    /**
347
     * Calculate target dimension for image when using fill-to-fit resize strategy.
348
     */
349
    private $fillWidth;
350
    private $fillHeight;
351
352
353
354
    /**
355
     * Allow remote file download, default is to disallow remote file download.
356
     */
357
    private $allowRemote = false;
358
359
360
361
    /**
362
     * Path to cache for remote download.
363
     */
364
    private $remoteCache;
365
366
367
368
    /**
369
     * Pattern to recognize a remote file.
370
     */
371
    //private $remotePattern = '#^[http|https]://#';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
372
    private $remotePattern = '#^https?://#';
373
374
375
376
    /**
377
     * Use the cache if true, set to false to ignore the cached file.
378
     */
379
    private $useCache = true;
380
381
382
    /**
383
    * Disable the fasttrackCacke to start with, inject an object to enable it.
384
    */
385
    private $fastTrackCache = null;
386
387
388
389
    /*
390
     * Set whitelist for valid hostnames from where remote source can be
391
     * downloaded.
392
     */
393
    private $remoteHostWhitelist = null;
394
395
396
397
    /*
398
     * Do verbose logging to file by setting this to a filename.
399
     */
400
    private $verboseFileName = null;
401
402
403
404
    /*
405
     * Output to ascii can take som options as an array.
406
     */
407
    private $asciiOptions = array();
408
409
410
411
    /*
412
     * Image copy strategy, defaults to RESAMPLE.
413
     */
414
     const RESIZE = 1;
415
     const RESAMPLE = 2;
416
     private $copyStrategy = NULL;
417
418
419
420
    /**
421
     * Properties, the class is mutable and the method setOptions()
422
     * decides (partly) what properties are created.
423
     *
424
     * @todo Clean up these and check if and how they are used
425
     */
426
427
    public $keepRatio;
428
    public $cropToFit;
429
    private $cropWidth;
430
    private $cropHeight;
431
    public $crop_x;
432
    public $crop_y;
433
    public $filters;
434
    private $attr; // Calculated from source image
0 ignored issues
show
The property $attr is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

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

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
647
648
        return $this;
649
    }
650
651
652
653
    /**
654
     * Normalize the file extension.
655
     *
656
     * @param string $extension of image file or skip to use internal.
657
     *
658
     * @return string $extension as a normalized file extension.
659
     */
660
    private function normalizeFileExtension($extension = null)
661
    {
662
        $extension = strtolower($extension ? $extension : $this->extension);
663
664
        if ($extension == 'jpeg') {
665
                $extension = 'jpg';
666
        }
667
668
        return $extension;
669
    }
670
671
672
673
    /**
674
     * Download a remote image and return path to its local copy.
675
     *
676
     * @param string $src remote path to image.
677
     *
678
     * @return string as path to downloaded remote source.
679
     */
680
    public function downloadRemoteSource($src)
681
    {
682
        if (!$this->isRemoteSourceOnWhitelist($src)) {
683
            throw new Exception("Hostname is not on whitelist for remote sources.");
684
        }
685
686
        $remote = new CRemoteImage();
687
688
        if (!is_writable($this->remoteCache)) {
689
            $this->log("The remote cache is not writable.");
690
        }
691
692
        $remote->setCache($this->remoteCache);
693
        $remote->useCache($this->useCache);
694
        $src = $remote->download($src);
695
696
        $this->log("Remote HTTP status: " . $remote->getStatus());
697
        $this->log("Remote item is in local cache: $src");
698
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
699
700
        return $src;
701
    }
702
703
704
705
    /**
706
     * Set source file to use as image source.
707
     *
708
     * @param string $src of image.
709
     * @param string $dir as optional base directory where images are.
710
     *
711
     * @return $this
712
     */
713
    public function setSource($src, $dir = null)
714
    {
715
        if (!isset($src)) {
716
            $this->imageSrc = null;
717
            $this->pathToImage = null;
718
            return $this;
719
        }
720
721
        if ($this->allowRemote && $this->isRemoteSource($src)) {
722
            $src = $this->downloadRemoteSource($src);
723
            $dir = null;
724
        }
725
726
        if (!isset($dir)) {
727
            $dir = dirname($src);
728
            $src = basename($src);
729
        }
730
731
        $this->imageSrc     = ltrim($src, '/');
732
        $imageFolder        = rtrim($dir, '/');
733
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
734
735
        return $this;
736
    }
737
738
739
740
    /**
741
     * Set target file.
742
     *
743
     * @param string $src of target image.
744
     * @param string $dir as optional base directory where images are stored.
745
     *                    Uses $this->saveFolder if null.
746
     *
747
     * @return $this
748
     */
749
    public function setTarget($src = null, $dir = null)
750
    {
751
        if (!isset($src)) {
752
            $this->cacheFileName = null;
753
            return $this;
754
        }
755
756
        if (isset($dir)) {
757
            $this->saveFolder = rtrim($dir, '/');
758
        }
759
760
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
761
762
        // Sanitize filename
763
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
764
        $this->log("The cache file name is: " . $this->cacheFileName);
765
766
        return $this;
767
    }
768
769
770
771
    /**
772
     * Get filename of target file.
773
     *
774
     * @return Boolean|String as filename of target or false if not set.
775
     */
776
    public function getTarget()
777
    {
778
        return $this->cacheFileName;
779
    }
780
781
782
783
    /**
784
     * Set options to use when processing image.
785
     *
786
     * @param array $args used when processing image.
787
     *
788
     * @return $this
789
     */
790
    public function setOptions($args)
791
    {
792
        $this->log("Set new options for processing image.");
793
794
        $defaults = array(
795
            // Options for calculate dimensions
796
            'newWidth'    => null,
797
            'newHeight'   => null,
798
            'aspectRatio' => null,
799
            'keepRatio'   => true,
800
            'cropToFit'   => false,
801
            'fillToFit'   => null,
802
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
0 ignored issues
show
Unused Code Comprehensibility introduced by
78% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
803
            'area'        => null, //'0,0,0,0',
804
            'upscale'     => self::UPSCALE_DEFAULT,
805
806
            // Options for caching or using original
807
            'useCache'    => true,
808
            'useOriginal' => true,
809
810
            // Pre-processing, before resizing is done
811
            'scale'        => null,
812
            'rotateBefore' => null,
813
            'autoRotate'  => false,
814
815
            // General options
816
            'bgColor'     => null,
817
818
            // Post-processing, after resizing is done
819
            'palette'     => null,
820
            'filters'     => null,
821
            'sharpen'     => null,
822
            'emboss'      => null,
823
            'blur'        => null,
824
            'convolve'       => null,
825
            'rotateAfter' => null,
826
827
            // Output format
828
            'outputFormat' => null,
829
            'dpr'          => 1,
830
        );
831
832
        // Convert crop settings from string to array
833
        if (isset($args['crop']) && !is_array($args['crop'])) {
834
            $pices = explode(',', $args['crop']);
835
            $args['crop'] = array(
836
                'width'   => $pices[0],
837
                'height'  => $pices[1],
838
                'start_x' => $pices[2],
839
                'start_y' => $pices[3],
840
            );
841
        }
842
843
        // Convert area settings from string to array
844
        if (isset($args['area']) && !is_array($args['area'])) {
845
                $pices = explode(',', $args['area']);
846
                $args['area'] = array(
847
                    'top'    => $pices[0],
848
                    'right'  => $pices[1],
849
                    'bottom' => $pices[2],
850
                    'left'   => $pices[3],
851
                );
852
        }
853
854
        // Convert filter settings from array of string to array of array
855
        if (isset($args['filters']) && is_array($args['filters'])) {
856
            foreach ($args['filters'] as $key => $filterStr) {
857
                $parts = explode(',', $filterStr);
858
                $filter = $this->mapFilter($parts[0]);
859
                $filter['str'] = $filterStr;
860
                for ($i=1; $i<=$filter['argc']; $i++) {
861
                    if (isset($parts[$i])) {
862
                        $filter["arg{$i}"] = $parts[$i];
863
                    } else {
864
                        throw new Exception(
865
                            'Missing arg to filter, review how many arguments are needed at
866
                            http://php.net/manual/en/function.imagefilter.php'
867
                        );
868
                    }
869
                }
870
                $args['filters'][$key] = $filter;
871
            }
872
        }
873
874
        // Merge default arguments with incoming and set properties.
875
        //$args = array_merge_recursive($defaults, $args);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
876
        $args = array_merge($defaults, $args);
877
        foreach ($defaults as $key => $val) {
878
            $this->{$key} = $args[$key];
879
        }
880
881
        if ($this->bgColor) {
882
            $this->setDefaultBackgroundColor($this->bgColor);
883
        }
884
885
        // Save original values to enable re-calculating
886
        $this->newWidthOrig  = $this->newWidth;
887
        $this->newHeightOrig = $this->newHeight;
888
        $this->cropOrig      = $this->crop;
889
890
        return $this;
891
    }
892
893
894
895
    /**
896
     * Map filter name to PHP filter and id.
897
     *
898
     * @param string $name the name of the filter.
899
     *
900
     * @return array with filter settings
901
     * @throws Exception
902
     */
903
    private function mapFilter($name)
904
    {
905
        $map = array(
906
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
907
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
908
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
909
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
910
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
911
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
912
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
913
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
914
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
915
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
916
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
917
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
918
        );
919
920
        if (isset($map[$name])) {
921
            return $map[$name];
922
        } else {
923
            throw new Exception('No such filter.');
924
        }
925
    }
926
927
928
929
    /**
930
     * Load image details from original image file.
931
     *
932
     * @param string $file the file to load or null to use $this->pathToImage.
933
     *
934
     * @return $this
935
     * @throws Exception
936
     */
937
    public function loadImageDetails($file = null)
938
    {
939
        $file = $file ? $file : $this->pathToImage;
940
941
        is_readable($file)
942
            or $this->raiseError('Image file does not exist.');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
943
944
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($this->pathToImage);
945
        if (empty($info)) {
946
            // To support webp
947
            $this->fileType = false;
948
            if (function_exists("exif_imagetype")) {
949
                $this->fileType = exif_imagetype($file);
950
                if ($this->fileType === false) {
951
                    if (function_exists("imagecreatefromwebp")) {
952
                        $webp = imagecreatefromwebp($file);
953
                        if ($webp !== false) {
954
                            $this->width  = imagesx($webp);
955
                            $this->height = imagesy($webp);
956
                            $this->fileType = IMG_WEBP;
957
                        }
958
                    }
959
                }
960
            }
961
        }
962
963
        if (!$this->fileType) {
964
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
965
        }
966
967
        if ($this->verbose) {
968
            $this->log("Loading image details for: {$file}");
969
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
970
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
971
            $this->log(" Image mimetype: " . $this->getMimeType());
972
        }
973
974
        return $this;
975
    }
976
977
978
979
    /**
980
     * Get mime type for image type.
981
     *
982
     * @return $this
983
     * @throws Exception
984
     */
985
    protected function getMimeType()
986
    {
987
        if ($this->fileType === IMG_WEBP) {
988
            return "image/webp";
989
        }
990
991
        return image_type_to_mime_type($this->fileType);
992
    }
993
994
995
996
    /**
997
     * Init new width and height and do some sanity checks on constraints, before any
998
     * processing can be done.
999
     *
1000
     * @return $this
1001
     * @throws Exception
1002
     */
1003
    public function initDimensions()
1004
    {
1005
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1006
1007
        // width as %
1008
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
1009
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1010
            $this->log("Setting new width based on % to {$this->newWidth}");
1011
        }
1012
1013
        // height as %
1014
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
1015
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1016
            $this->log("Setting new height based on % to {$this->newHeight}");
1017
        }
1018
1019
        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...
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1020
1021
        // width & height from aspect ratio
1022
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1023
            if ($this->aspectRatio >= 1) {
1024
                $this->newWidth   = $this->width;
1025
                $this->newHeight  = $this->width / $this->aspectRatio;
1026
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1027
1028
            } else {
1029
                $this->newHeight  = $this->height;
1030
                $this->newWidth   = $this->height * $this->aspectRatio;
1031
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1032
            }
1033
1034
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1035
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1036
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1037
1038
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1039
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1040
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1041
        }
1042
1043
        // Change width & height based on dpr
1044
        if ($this->dpr != 1) {
1045
            if (!is_null($this->newWidth)) {
1046
                $this->newWidth  = round($this->newWidth * $this->dpr);
1047
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1048
            }
1049
            if (!is_null($this->newHeight)) {
1050
                $this->newHeight = round($this->newHeight * $this->dpr);
1051
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1052
            }
1053
        }
1054
1055
        // Check values to be within domain
1056
        is_null($this->newWidth)
1057
            or is_numeric($this->newWidth)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1058
            or $this->raiseError('Width not numeric');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1059
1060
        is_null($this->newHeight)
1061
            or is_numeric($this->newHeight)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1062
            or $this->raiseError('Height not numeric');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1063
1064
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1065
1066
        return $this;
1067
    }
1068
1069
1070
1071
    /**
1072
     * Calculate new width and height of image, based on settings.
1073
     *
1074
     * @return $this
1075
     */
1076
    public function calculateNewWidthAndHeight()
1077
    {
1078
        // Crop, use cropped width and height as base for calulations
1079
        $this->log("Calculate new width and height.");
1080
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1081
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1082
1083
        // Check if there is an area to crop off
1084
        if (isset($this->area)) {
1085
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
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...
1086
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1087
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1088
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1089
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1090
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1091
            $this->width  = $this->offset['width'];
1092
            $this->height = $this->offset['height'];
1093
            $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%.");
1094
            $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px.");
1095
        }
1096
1097
        $width  = $this->width;
1098
        $height = $this->height;
1099
1100
        // Check if crop is set
1101
        if ($this->crop) {
1102
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1103
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1104
1105
            if ($this->crop['start_x'] == 'left') {
1106
                $this->crop['start_x'] = 0;
1107
            } elseif ($this->crop['start_x'] == 'right') {
1108
                $this->crop['start_x'] = $this->width - $width;
1109
            } elseif ($this->crop['start_x'] == 'center') {
1110
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1111
            }
1112
1113
            if ($this->crop['start_y'] == 'top') {
1114
                $this->crop['start_y'] = 0;
1115
            } elseif ($this->crop['start_y'] == 'bottom') {
1116
                $this->crop['start_y'] = $this->height - $height;
1117
            } elseif ($this->crop['start_y'] == 'center') {
1118
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1119
            }
1120
1121
            $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px.");
1122
        }
1123
1124
        // Calculate new width and height if keeping aspect-ratio.
1125
        if ($this->keepRatio) {
1126
1127
            $this->log("Keep aspect ratio.");
1128
1129
            // Crop-to-fit and both new width and height are set.
1130
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1131
1132
                // Use newWidth and newHeigh as width/height, image should fit in box.
1133
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1134
1135
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1136
1137
                // Both new width and height are set.
1138
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1139
                $ratioWidth  = $width  / $this->newWidth;
1140
                $ratioHeight = $height / $this->newHeight;
1141
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1142
                $this->newWidth  = round($width  / $ratio);
1143
                $this->newHeight = round($height / $ratio);
1144
                $this->log("New width and height was set.");
1145
1146
            } elseif (isset($this->newWidth)) {
1147
1148
                // Use new width as max-width
1149
                $factor = (float)$this->newWidth / (float)$width;
1150
                $this->newHeight = round($factor * $height);
1151
                $this->log("New width was set.");
1152
1153
            } elseif (isset($this->newHeight)) {
1154
1155
                // Use new height as max-hight
1156
                $factor = (float)$this->newHeight / (float)$height;
1157
                $this->newWidth = round($factor * $width);
1158
                $this->log("New height was set.");
1159
1160
            } else {
1161
1162
                // Use existing width and height as new width and height.
1163
                $this->newWidth = $width;
1164
                $this->newHeight = $height;
1165
            }
1166
            
1167
1168
            // Get image dimensions for pre-resize image.
1169
            if ($this->cropToFit || $this->fillToFit) {
1170
1171
                // Get relations of original & target image
1172
                $ratioWidth  = $width  / $this->newWidth;
1173
                $ratioHeight = $height / $this->newHeight;
1174
1175
                if ($this->cropToFit) {
1176
1177
                    // Use newWidth and newHeigh as defined width/height,
1178
                    // image should fit the area.
1179
                    $this->log("Crop to fit.");
1180
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1181
                    $this->cropWidth  = round($width  / $ratio);
1182
                    $this->cropHeight = round($height / $ratio);
1183
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1184
1185
                } elseif ($this->fillToFit) {
1186
1187
                    // Use newWidth and newHeigh as defined width/height,
1188
                    // image should fit the area.
1189
                    $this->log("Fill to fit.");
1190
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1191
                    $this->fillWidth  = round($width  / $ratio);
1192
                    $this->fillHeight = round($height / $ratio);
1193
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1194
                }
1195
            }
1196
        }
1197
1198
        // Crop, ensure to set new width and height
1199
        if ($this->crop) {
1200
            $this->log("Crop.");
1201
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1202
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1203
        }
1204
1205
        // Fill to fit, ensure to set new width and height
1206
        /*if ($this->fillToFit) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1207
            $this->log("FillToFit.");
1208
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1209
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1210
        }*/
1211
1212
        // No new height or width is set, use existing measures.
1213
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1214
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1215
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1216
1217
        return $this;
1218
    }
1219
1220
1221
1222
    /**
1223
     * Re-calculate image dimensions when original image dimension has changed.
1224
     *
1225
     * @return $this
1226
     */
1227
    public function reCalculateDimensions()
1228
    {
1229
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1230
1231
        $this->newWidth  = $this->newWidthOrig;
1232
        $this->newHeight = $this->newHeightOrig;
1233
        $this->crop      = $this->cropOrig;
1234
1235
        $this->initDimensions()
1236
             ->calculateNewWidthAndHeight();
1237
1238
        return $this;
1239
    }
1240
1241
1242
1243
    /**
1244
     * Set extension for filename to save as.
1245
     *
1246
     * @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...
1247
     *
1248
     * @return $this
1249
     */
1250
    public function setSaveAsExtension($saveAs = null)
1251
    {
1252
        if (isset($saveAs)) {
1253
            $saveAs = strtolower($saveAs);
1254
            $this->checkFileExtension($saveAs);
1255
            $this->saveAs = $saveAs;
1256
            $this->extension = $saveAs;
1257
        }
1258
1259
        $this->log("Prepare to save image as: " . $this->extension);
1260
1261
        return $this;
1262
    }
1263
1264
1265
1266
    /**
1267
     * Set JPEG quality to use when saving image
1268
     *
1269
     * @param int $quality as the quality to set.
1270
     *
1271
     * @return $this
1272
     */
1273
    public function setJpegQuality($quality = null)
1274
    {
1275
        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...
1276
            $this->useQuality = true;
1277
        }
1278
1279
        $this->quality = isset($quality)
1280
            ? $quality
1281
            : self::JPEG_QUALITY_DEFAULT;
1282
1283
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1284
            or $this->raiseError('Quality not in range.');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1285
1286
        $this->log("Setting JPEG quality to {$this->quality}.");
1287
1288
        return $this;
1289
    }
1290
1291
1292
1293
    /**
1294
     * Set PNG compressen algorithm to use when saving image
1295
     *
1296
     * @param int $compress as the algorithm to use.
1297
     *
1298
     * @return $this
1299
     */
1300
    public function setPngCompression($compress = null)
1301
    {
1302
        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...
1303
            $this->useCompress = true;
1304
        }
1305
1306
        $this->compress = isset($compress)
1307
            ? $compress
1308
            : self::PNG_COMPRESSION_DEFAULT;
1309
1310
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1311
            or $this->raiseError('Quality not in range.');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1312
1313
        $this->log("Setting PNG compression level to {$this->compress}.");
1314
1315
        return $this;
1316
    }
1317
1318
1319
1320
    /**
1321
     * Use original image if possible, check options which affects image processing.
1322
     *
1323
     * @param boolean $useOrig default is to use original if possible, else set to false.
1324
     *
1325
     * @return $this
1326
     */
1327
    public function useOriginalIfPossible($useOrig = true)
1328
    {
1329
        if ($useOrig
1330
            && ($this->newWidth == $this->width)
1331
            && ($this->newHeight == $this->height)
1332
            && !$this->area
1333
            && !$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...
1334
            && !$this->cropToFit
1335
            && !$this->fillToFit
1336
            && !$this->filters
1337
            && !$this->sharpen
1338
            && !$this->emboss
1339
            && !$this->blur
1340
            && !$this->convolve
1341
            && !$this->palette
1342
            && !$this->useQuality
1343
            && !$this->useCompress
1344
            && !$this->saveAs
1345
            && !$this->rotateBefore
1346
            && !$this->rotateAfter
1347
            && !$this->autoRotate
1348
            && !$this->bgColor
1349
            && ($this->upscale === self::UPSCALE_DEFAULT)
1350
        ) {
1351
            $this->log("Using original image.");
1352
            $this->output($this->pathToImage);
1353
        }
1354
1355
        return $this;
1356
    }
1357
1358
1359
1360
    /**
1361
     * Generate filename to save file in cache.
1362
     *
1363
     * @param string  $base      as optional basepath for storing file.
1364
     * @param boolean $useSubdir use or skip the subdir part when creating the
1365
     *                           filename.
1366
     * @param string  $prefix    to add as part of filename
1367
     *
1368
     * @return $this
1369
     */
1370
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1371
    {
1372
        $filename     = basename($this->pathToImage);
1373
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1374
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1375
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1376
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1377
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1378
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1379
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1380
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1381
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1382
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1383
1384
        $saveAs = $this->normalizeFileExtension();
1385
        $saveAs = $saveAs ? "_$saveAs" : null;
1386
1387
        $copyStrat = null;
1388
        if ($this->copyStrategy === self::RESIZE) {
1389
            $copyStrat = "_rs";
1390
        }
1391
1392
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1393
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1394
1395
        $offset = isset($this->offset)
1396
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1397
            : null;
1398
1399
        $crop = $this->crop
1400
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1401
            : null;
1402
1403
        $filters = null;
1404
        if (isset($this->filters)) {
1405
            foreach ($this->filters as $filter) {
1406
                if (is_array($filter)) {
1407
                    $filters .= "_f{$filter['id']}";
1408
                    for ($i=1; $i<=$filter['argc']; $i++) {
1409
                        $filters .= "-".$filter["arg{$i}"];
1410
                    }
1411
                }
1412
            }
1413
        }
1414
1415
        $sharpen = $this->sharpen ? 's' : null;
1416
        $emboss  = $this->emboss  ? 'e' : null;
1417
        $blur    = $this->blur    ? 'b' : null;
1418
        $palette = $this->palette ? 'p' : null;
1419
1420
        $autoRotate = $this->autoRotate ? 'ar' : null;
1421
1422
        $optimize  = $this->jpegOptimize ? 'o' : null;
1423
        $optimize .= $this->pngFilter    ? 'f' : null;
1424
        $optimize .= $this->pngDeflate   ? 'd' : null;
1425
1426
        $convolve = null;
1427
        if ($this->convolve) {
1428
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1429
        }
1430
1431
        $upscale = null;
1432
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1433
            $upscale = '_nu';
1434
        }
1435
1436
        $subdir = null;
1437
        if ($useSubdir === true) {
1438
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1439
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1440
            $subdir .= '_';
1441
        }
1442
1443
        $file = $prefix . $subdir . $filename . $width . $height
1444
            . $offset . $crop . $cropToFit . $fillToFit
1445
            . $crop_x . $crop_y . $upscale
1446
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1447
            . $optimize . $compress
1448
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1449
            . $convolve . $copyStrat . $saveAs;
1450
1451
        return $this->setTarget($file, $base);
1452
    }
1453
1454
1455
1456
    /**
1457
     * Use cached version of image, if possible.
1458
     *
1459
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1460
     *
1461
     * @return $this
1462
     */
1463
    public function useCacheIfPossible($useCache = true)
1464
    {
1465
        if ($useCache && is_readable($this->cacheFileName)) {
1466
            $fileTime   = filemtime($this->pathToImage);
1467
            $cacheTime  = filemtime($this->cacheFileName);
1468
1469
            if ($fileTime <= $cacheTime) {
1470
                if ($this->useCache) {
1471
                    if ($this->verbose) {
1472
                        $this->log("Use cached file.");
1473
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1474
                    }
1475
                    $this->output($this->cacheFileName, $this->outputFormat);
1476
                } else {
1477
                    $this->log("Cache is valid but ignoring it by intention.");
1478
                }
1479
            } else {
1480
                $this->log("Original file is modified, ignoring cache.");
1481
            }
1482
        } else {
1483
            $this->log("Cachefile does not exists or ignoring it.");
1484
        }
1485
1486
        return $this;
1487
    }
1488
1489
1490
1491
    /**
1492
     * Load image from disk. Try to load image without verbose error message,
1493
     * if fail, load again and display error messages.
1494
     *
1495
     * @param string $src of image.
1496
     * @param string $dir as base directory where images are.
1497
     *
1498
     * @return $this
1499
     *
1500
     */
1501
    public function load($src = null, $dir = null)
1502
    {
1503
        if (isset($src)) {
1504
            $this->setSource($src, $dir);
1505
        }
1506
1507
        $this->loadImageDetails();
1508
1509
        $imageAsString = file_get_contents($this->pathToImage);
1510
        $this->image = imagecreatefromstring($imageAsString);
1511
        if ($this->image === false) {
1512
            throw new Exception("Could not load image.");
1513
        }
1514
1515
        /* Removed v0.7.7
0 ignored issues
show
Unused Code Comprehensibility introduced by
51% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1516
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1517
            $type = $this->getPngType();
1518
            $hasFewColors = imagecolorstotal($this->image);
1519
1520
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1521
                if ($this->verbose) {
1522
                    $this->log("Handle this image as a palette image.");
1523
                }
1524
                $this->palette = true;
1525
            }
1526
        }
1527
        */
1528
1529
        if ($this->verbose) {
1530
            $this->log("### Image successfully loaded from file.");
1531
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1532
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1533
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1534
            $index = imagecolortransparent($this->image);
1535
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1536
        }
1537
1538
        return $this;
1539
    }
1540
1541
1542
1543
    /**
1544
     * Get the type of PNG image.
1545
     *
1546
     * @param string $filename to use instead of default.
1547
     *
1548
     * @return int as the type of the png-image
1549
     *
1550
     */
1551
    public function getPngType($filename = null)
1552
    {
1553
        $filename = $filename ? $filename : $this->pathToImage;
1554
1555
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1556
1557
        if ($this->verbose) {
1558
            $this->log("Checking png type of: " . $filename);
1559
            $this->log($this->getPngTypeAsString($pngType));
1560
        }
1561
1562
        return $pngType;
1563
    }
1564
1565
1566
1567
    /**
1568
     * Get the type of PNG image as a verbose string.
1569
     *
1570
     * @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...
1571
     * @param string  $filename to use instead of default.
1572
     *
1573
     * @return int as the type of the png-image
1574
     *
1575
     */
1576
    private function getPngTypeAsString($pngType = null, $filename = null)
1577
    {
1578
        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...
1579
            $pngType = $this->getPngType($filename);
1580
        }
1581
1582
        $index = imagecolortransparent($this->image);
1583
        $transparent = null;
1584
        if ($index != -1) {
1585
            $transparent = " (transparent)";
1586
        }
1587
1588
        switch ($pngType) {
1589
1590
            case self::PNG_GREYSCALE:
1591
                $text = "PNG is type 0, Greyscale$transparent";
1592
                break;
1593
1594
            case self::PNG_RGB:
1595
                $text = "PNG is type 2, RGB$transparent";
1596
                break;
1597
1598
            case self::PNG_RGB_PALETTE:
1599
                $text = "PNG is type 3, RGB with palette$transparent";
1600
                break;
1601
1602
            case self::PNG_GREYSCALE_ALPHA:
1603
                $text = "PNG is type 4, Greyscale with alpha channel";
1604
                break;
1605
1606
            case self::PNG_RGB_ALPHA:
1607
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1608
                break;
1609
1610
            default:
1611
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1612
        }
1613
1614
        return $text;
1615
    }
1616
1617
1618
1619
1620
    /**
1621
     * Calculate number of colors in an image.
1622
     *
1623
     * @param resource $im the image.
1624
     *
1625
     * @return int
1626
     */
1627
    private function colorsTotal($im)
1628
    {
1629
        if (imageistruecolor($im)) {
1630
            $this->log("Colors as true color.");
1631
            $h = imagesy($im);
1632
            $w = imagesx($im);
1633
            $c = array();
1634
            for ($x=0; $x < $w; $x++) {
1635
                for ($y=0; $y < $h; $y++) {
1636
                    @$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...
1637
                }
1638
            }
1639
            return count($c);
1640
        } else {
1641
            $this->log("Colors as palette.");
1642
            return imagecolorstotal($im);
1643
        }
1644
    }
1645
1646
1647
1648
    /**
1649
     * Preprocess image before rezising it.
1650
     *
1651
     * @return $this
1652
     */
1653
    public function preResize()
1654
    {
1655
        $this->log("### Pre-process before resizing");
1656
1657
        // Rotate image
1658
        if ($this->rotateBefore) {
1659
            $this->log("Rotating image.");
1660
            $this->rotate($this->rotateBefore, $this->bgColor)
1661
                 ->reCalculateDimensions();
1662
        }
1663
1664
        // Auto-rotate image
1665
        if ($this->autoRotate) {
1666
            $this->log("Auto rotating image.");
1667
            $this->rotateExif()
1668
                 ->reCalculateDimensions();
1669
        }
1670
1671
        // Scale the original image before starting
1672
        if (isset($this->scale)) {
1673
            $this->log("Scale by {$this->scale}%");
1674
            $newWidth  = $this->width * $this->scale / 100;
1675
            $newHeight = $this->height * $this->scale / 100;
1676
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1677
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1678
            $this->image = $img;
1679
            $this->width = $newWidth;
1680
            $this->height = $newHeight;
1681
        }
1682
1683
        return $this;
1684
    }
1685
1686
1687
1688
    /**
1689
     * Resize or resample the image while resizing.
1690
     *
1691
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1692
     *
1693
     * @return $this
1694
     */
1695
     public function setCopyResizeStrategy($strategy)
1696
     {
1697
         $this->copyStrategy = $strategy;
1698
         return $this;
1699
     }
1700
1701
1702
1703
    /**
1704
     * Resize and or crop the image.
1705
     *
1706
     * @return void
1707
     */
1708
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1709
    {
1710
        if($this->copyStrategy == self::RESIZE) {
1711
            $this->log("Copy by resize");
1712
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1713
        } else {
1714
            $this->log("Copy by resample");
1715
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1716
        }
1717
    }
1718
1719
1720
1721
    /**
1722
     * Resize and or crop the image.
1723
     *
1724
     * @return $this
1725
     */
1726
    public function resize()
1727
    {
1728
1729
        $this->log("### Starting to Resize()");
1730
        $this->log("Upscale = '$this->upscale'");
1731
1732
        // Only use a specified area of the image, $this->offset is defining the area to use
1733
        if (isset($this->offset)) {
1734
1735
            $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']}");
1736
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1737
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1738
            $this->image = $img;
1739
            $this->width = $this->offset['width'];
1740
            $this->height = $this->offset['height'];
1741
        }
1742
1743
        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...
1744
1745
            // Do as crop, take only part of image
1746
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1747
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1748
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1749
            $this->image = $img;
1750
            $this->width = $this->crop['width'];
1751
            $this->height = $this->crop['height'];
1752
        }
1753
1754
        if (!$this->upscale) {
0 ignored issues
show
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
1755
            // Consider rewriting the no-upscale code to fit within this if-statement,
1756
            // likely to be more readable code.
1757
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1758
        }
1759
1760
        if ($this->cropToFit) {
1761
1762
            // Resize by crop to fit
1763
            $this->log("Resizing using strategy - Crop to fit");
1764
1765
            if (!$this->upscale 
1766
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1767
                $this->log("Resizing - smaller image, do not upscale.");
1768
1769
                $posX = 0;
1770
                $posY = 0;
1771
                $cropX = 0;
1772
                $cropY = 0;
1773
1774
                if ($this->newWidth > $this->width) {
1775
                    $posX = round(($this->newWidth - $this->width) / 2);
1776
                }
1777
                if ($this->newWidth < $this->width) {
1778
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1779
                }
1780
1781
                if ($this->newHeight > $this->height) {
1782
                    $posY = round(($this->newHeight - $this->height) / 2);
1783
                }
1784
                if ($this->newHeight < $this->height) {
1785
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1786
                }
1787
                $this->log(" cwidth: $this->cropWidth");
1788
                $this->log(" cheight: $this->cropHeight");
1789
                $this->log(" nwidth: $this->newWidth");
1790
                $this->log(" nheight: $this->newHeight");
1791
                $this->log(" width: $this->width");
1792
                $this->log(" height: $this->height");
1793
                $this->log(" posX: $posX");
1794
                $this->log(" posY: $posY");
1795
                $this->log(" cropX: $cropX");
1796
                $this->log(" cropY: $cropY");
1797
1798
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1799
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1800
            } else {
1801
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1802
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1803
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1804
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1805
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1806
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1807
            }
1808
1809
            $this->image = $imageResized;
1810
            $this->width = $this->newWidth;
1811
            $this->height = $this->newHeight;
1812
1813
        } elseif ($this->fillToFit) {
1814
1815
            // Resize by fill to fit
1816
            $this->log("Resizing using strategy - Fill to fit");
1817
1818
            $posX = 0;
1819
            $posY = 0;
1820
1821
            $ratioOrig = $this->width / $this->height;
1822
            $ratioNew  = $this->newWidth / $this->newHeight;
1823
1824
            // Check ratio for landscape or portrait
1825
            if ($ratioOrig < $ratioNew) {
1826
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1827
            } else {
1828
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1829
            }
1830
1831
            if (!$this->upscale
1832
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1833
            ) {
1834
1835
                $this->log("Resizing - smaller image, do not upscale.");
1836
                $posX = round(($this->newWidth - $this->width) / 2);
1837
                $posY = round(($this->newHeight - $this->height) / 2);
1838
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1839
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1840
1841
            } else {
1842
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1843
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1844
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1845
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1846
            }
1847
1848
            $this->image = $imageResized;
1849
            $this->width = $this->newWidth;
1850
            $this->height = $this->newHeight;
1851
1852
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1853
1854
            // Resize it
1855
            $this->log("Resizing, new height and/or width");
1856
1857
            if (!$this->upscale
1858
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1859
            ) {
1860
                $this->log("Resizing - smaller image, do not upscale.");
1861
1862
                if (!$this->keepRatio) {
1863
                    $this->log("Resizing - stretch to fit selected.");
1864
1865
                    $posX = 0;
1866
                    $posY = 0;
1867
                    $cropX = 0;
1868
                    $cropY = 0;
1869
1870
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1871
                        $posX = round(($this->newWidth - $this->width) / 2);
1872
                        $posY = round(($this->newHeight - $this->height) / 2);
1873
                    } elseif ($this->newWidth > $this->width) {
1874
                        $posX = round(($this->newWidth - $this->width) / 2);
1875
                        $cropY = round(($this->height - $this->newHeight) / 2);
1876
                    } elseif ($this->newHeight > $this->height) {
1877
                        $posY = round(($this->newHeight - $this->height) / 2);
1878
                        $cropX = round(($this->width - $this->newWidth) / 2);
1879
                    }
1880
1881
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1882
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1883
                    $this->image = $imageResized;
1884
                    $this->width = $this->newWidth;
1885
                    $this->height = $this->newHeight;
1886
                }
1887
            } else {
1888
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1889
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1890
                $this->image = $imageResized;
1891
                $this->width = $this->newWidth;
1892
                $this->height = $this->newHeight;
1893
            }
1894
        }
1895
1896
        return $this;
1897
    }
1898
1899
1900
1901
    /**
1902
     * Postprocess image after rezising image.
1903
     *
1904
     * @return $this
1905
     */
1906
    public function postResize()
1907
    {
1908
        $this->log("### Post-process after resizing");
1909
1910
        // Rotate image
1911
        if ($this->rotateAfter) {
1912
            $this->log("Rotating image.");
1913
            $this->rotate($this->rotateAfter, $this->bgColor);
1914
        }
1915
1916
        // Apply filters
1917
        if (isset($this->filters) && is_array($this->filters)) {
1918
1919
            foreach ($this->filters as $filter) {
1920
                $this->log("Applying filter {$filter['type']}.");
1921
1922
                switch ($filter['argc']) {
1923
1924
                    case 0:
1925
                        imagefilter($this->image, $filter['type']);
1926
                        break;
1927
1928
                    case 1:
1929
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1930
                        break;
1931
1932
                    case 2:
1933
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1934
                        break;
1935
1936
                    case 3:
1937
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1938
                        break;
1939
1940
                    case 4:
1941
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1942
                        break;
1943
                }
1944
            }
1945
        }
1946
1947
        // Convert to palette image
1948
        if ($this->palette) {
1949
            $this->log("Converting to palette image.");
1950
            $this->trueColorToPalette();
1951
        }
1952
1953
        // Blur the image
1954
        if ($this->blur) {
1955
            $this->log("Blur.");
1956
            $this->blurImage();
1957
        }
1958
1959
        // Emboss the image
1960
        if ($this->emboss) {
1961
            $this->log("Emboss.");
1962
            $this->embossImage();
1963
        }
1964
1965
        // Sharpen the image
1966
        if ($this->sharpen) {
1967
            $this->log("Sharpen.");
1968
            $this->sharpenImage();
1969
        }
1970
1971
        // Custom convolution
1972
        if ($this->convolve) {
1973
            //$this->log("Convolve: " . $this->convolve);
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1974
            $this->imageConvolution();
1975
        }
1976
1977
        return $this;
1978
    }
1979
1980
1981
1982
    /**
1983
     * Rotate image using angle.
1984
     *
1985
     * @param float $angle        to rotate image.
1986
     * @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...
1987
     *
1988
     * @return $this
1989
     */
1990
    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...
1991
    {
1992
        $this->log("Rotate image " . $angle . " degrees with filler color.");
1993
1994
        $color = $this->getBackgroundColor();
1995
        $this->image = imagerotate($this->image, $angle, $color);
1996
1997
        $this->width  = imagesx($this->image);
1998
        $this->height = imagesy($this->image);
1999
2000
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2001
2002
        return $this;
2003
    }
2004
2005
2006
2007
    /**
2008
     * Rotate image using information in EXIF.
2009
     *
2010
     * @return $this
2011
     */
2012
    public function rotateExif()
2013
    {
2014
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2015
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2016
            return $this;
2017
        }
2018
2019
        $exif = exif_read_data($this->pathToImage);
2020
2021
        if (!empty($exif['Orientation'])) {
2022
            switch ($exif['Orientation']) {
2023
                case 3:
2024
                    $this->log("Autorotate 180.");
2025
                    $this->rotate(180, $this->bgColor);
2026
                    break;
2027
2028
                case 6:
2029
                    $this->log("Autorotate -90.");
2030
                    $this->rotate(-90, $this->bgColor);
2031
                    break;
2032
2033
                case 8:
2034
                    $this->log("Autorotate 90.");
2035
                    $this->rotate(90, $this->bgColor);
2036
                    break;
2037
2038
                default:
2039
                    $this->log("Autorotate ignored, unknown value as orientation.");
2040
            }
2041
        } else {
2042
            $this->log("Autorotate ignored, no orientation in EXIF.");
2043
        }
2044
2045
        return $this;
2046
    }
2047
2048
2049
2050
    /**
2051
     * Convert true color image to palette image, keeping alpha.
2052
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2053
     *
2054
     * @return void
2055
     */
2056
    public function trueColorToPalette()
2057
    {
2058
        $img = imagecreatetruecolor($this->width, $this->height);
2059
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2060
        imagecolortransparent($img, $bga);
2061
        imagefill($img, 0, 0, $bga);
2062
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2063
        imagetruecolortopalette($img, false, 255);
2064
        imagesavealpha($img, true);
2065
2066
        if (imageistruecolor($this->image)) {
2067
            $this->log("Matching colors with true color image.");
2068
            imagecolormatch($this->image, $img);
2069
        }
2070
2071
        $this->image = $img;
2072
    }
2073
2074
2075
2076
    /**
2077
     * Sharpen image using image convolution.
2078
     *
2079
     * @return $this
2080
     */
2081
    public function sharpenImage()
2082
    {
2083
        $this->imageConvolution('sharpen');
2084
        return $this;
2085
    }
2086
2087
2088
2089
    /**
2090
     * Emboss image using image convolution.
2091
     *
2092
     * @return $this
2093
     */
2094
    public function embossImage()
2095
    {
2096
        $this->imageConvolution('emboss');
2097
        return $this;
2098
    }
2099
2100
2101
2102
    /**
2103
     * Blur image using image convolution.
2104
     *
2105
     * @return $this
2106
     */
2107
    public function blurImage()
2108
    {
2109
        $this->imageConvolution('blur');
2110
        return $this;
2111
    }
2112
2113
2114
2115
    /**
2116
     * Create convolve expression and return arguments for image convolution.
2117
     *
2118
     * @param string $expression constant string which evaluates to a list of
2119
     *                           11 numbers separated by komma or such a list.
2120
     *
2121
     * @return array as $matrix (3x3), $divisor and $offset
2122
     */
2123
    public function createConvolveArguments($expression)
2124
    {
2125
        // Check of matching constant
2126
        if (isset($this->convolves[$expression])) {
2127
            $expression = $this->convolves[$expression];
2128
        }
2129
2130
        $part = explode(',', $expression);
2131
        $this->log("Creating convolution expressen: $expression");
2132
2133
        // Expect list of 11 numbers, split by , and build up arguments
2134
        if (count($part) != 11) {
2135
            throw new Exception(
2136
                "Missmatch in argument convolve. Expected comma-separated string with
2137
                11 float values. Got $expression."
2138
            );
2139
        }
2140
2141
        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...
2142
            if (!is_numeric($item)) {
2143
                throw new Exception("Argument to convolve expression should be float but is not.");
2144
            }
2145
        });
2146
2147
        return array(
2148
            array(
2149
                array($part[0], $part[1], $part[2]),
2150
                array($part[3], $part[4], $part[5]),
2151
                array($part[6], $part[7], $part[8]),
2152
            ),
2153
            $part[9],
2154
            $part[10],
2155
        );
2156
    }
2157
2158
2159
2160
    /**
2161
     * Add custom expressions (or overwrite existing) for image convolution.
2162
     *
2163
     * @param array $options Key value array with strings to be converted
2164
     *                       to convolution expressions.
2165
     *
2166
     * @return $this
2167
     */
2168
    public function addConvolveExpressions($options)
2169
    {
2170
        $this->convolves = array_merge($this->convolves, $options);
2171
        return $this;
2172
    }
2173
2174
2175
2176
    /**
2177
     * Image convolution.
2178
     *
2179
     * @param string $options A string with 11 float separated by comma.
2180
     *
2181
     * @return $this
2182
     */
2183
    public function imageConvolution($options = null)
2184
    {
2185
        // Use incoming options or use $this.
2186
        $options = $options ? $options : $this->convolve;
2187
2188
        // Treat incoming as string, split by +
2189
        $this->log("Convolution with '$options'");
2190
        $options = explode(":", $options);
2191
2192
        // Check each option if it matches constant value
2193
        foreach ($options as $option) {
2194
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2195
            imageconvolution($this->image, $matrix, $divisor, $offset);
2196
        }
2197
2198
        return $this;
2199
    }
2200
2201
2202
2203
    /**
2204
     * Set default background color between 000000-FFFFFF or if using
2205
     * alpha 00000000-FFFFFF7F.
2206
     *
2207
     * @param string $color as hex value.
2208
     *
2209
     * @return $this
2210
    */
2211
    public function setDefaultBackgroundColor($color)
2212
    {
2213
        $this->log("Setting default background color to '$color'.");
2214
2215
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2216
            throw new Exception(
2217
                "Background color needs a hex value of 6 or 8
2218
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2219
                Current value was: '$color'."
2220
            );
2221
        }
2222
2223
        $red    = hexdec(substr($color, 0, 2));
2224
        $green  = hexdec(substr($color, 2, 2));
2225
        $blue   = hexdec(substr($color, 4, 2));
2226
2227
        $alpha = (strlen($color) == 8)
2228
            ? hexdec(substr($color, 6, 2))
2229
            : null;
2230
2231
        if (($red < 0 || $red > 255)
2232
            || ($green < 0 || $green > 255)
2233
            || ($blue < 0 || $blue > 255)
2234
            || ($alpha < 0 || $alpha > 127)
2235
        ) {
2236
            throw new Exception(
2237
                "Background color out of range. Red, green blue
2238
                should be 00-FF and alpha should be 00-7F.
2239
                Current value was: '$color'."
2240
            );
2241
        }
2242
2243
        $this->bgColor = strtolower($color);
2244
        $this->bgColorDefault = array(
2245
            'red'   => $red,
2246
            'green' => $green,
2247
            'blue'  => $blue,
2248
            'alpha' => $alpha
2249
        );
2250
2251
        return $this;
2252
    }
2253
2254
2255
2256
    /**
2257
     * Get the background color.
2258
     *
2259
     * @param resource $img the image to work with or null if using $this->image.
2260
     *
2261
     * @return color value or null if no background color is set.
2262
    */
2263
    private function getBackgroundColor($img = null)
2264
    {
2265
        $img = isset($img) ? $img : $this->image;
2266
2267
        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...
2268
2269
            $red   = $this->bgColorDefault['red'];
2270
            $green = $this->bgColorDefault['green'];
2271
            $blue  = $this->bgColorDefault['blue'];
2272
            $alpha = $this->bgColorDefault['alpha'];
2273
2274
            if ($alpha) {
2275
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2276
            } else {
2277
                $color = imagecolorallocate($img, $red, $green, $blue);
2278
            }
2279
2280
            return $color;
2281
2282
        } else {
2283
            return 0;
2284
        }
2285
    }
2286
2287
2288
2289
    /**
2290
     * Create a image and keep transparency for png and gifs.
2291
     *
2292
     * @param int $width of the new image.
2293
     * @param int $height of the new image.
2294
     *
2295
     * @return image resource.
2296
    */
2297
    private function createImageKeepTransparency($width, $height)
2298
    {
2299
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2300
        $img = imagecreatetruecolor($width, $height);
2301
        imagealphablending($img, false);
2302
        imagesavealpha($img, true);
2303
2304
        $index = $this->image
2305
            ? imagecolortransparent($this->image)
2306
            : -1;
2307
2308
        if ($index != -1) {
2309
2310
            imagealphablending($img, true);
2311
            $transparent = imagecolorsforindex($this->image, $index);
2312
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2313
            imagefill($img, 0, 0, $color);
2314
            $index = imagecolortransparent($img, $color);
2315
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2316
2317
        } 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...
2318
2319
            $color = $this->getBackgroundColor($img);
2320
            imagefill($img, 0, 0, $color);
2321
            $this->Log("Filling image with background color.");
2322
        }
2323
2324
        return $img;
2325
    }
2326
2327
2328
2329
    /**
2330
     * Set optimizing  and post-processing options.
2331
     *
2332
     * @param array $options with config for postprocessing with external tools.
2333
     *
2334
     * @return $this
2335
     */
2336
    public function setPostProcessingOptions($options)
2337
    {
2338
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2339
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2340
        } else {
2341
            $this->jpegOptimizeCmd = null;
2342
        }
2343
2344
        if (isset($options['png_filter']) && $options['png_filter']) {
2345
            $this->pngFilterCmd = $options['png_filter_cmd'];
2346
        } else {
2347
            $this->pngFilterCmd = null;
2348
        }
2349
2350
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2351
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2352
        } else {
2353
            $this->pngDeflateCmd = null;
2354
        }
2355
2356
        return $this;
2357
    }
2358
2359
2360
2361
    /**
2362
     * Find out the type (file extension) for the image to be saved.
2363
     *
2364
     * @return string as image extension.
2365
     */
2366
    protected function getTargetImageExtension()
2367
    {
2368
        // switch on mimetype
2369
        if (isset($this->extension)) {
2370
            return strtolower($this->extension);
2371
        } else {
2372
            return substr(image_type_to_extension($this->fileType), 1);
2373
        }
2374
    }
2375
2376
2377
2378
    /**
2379
     * Save image.
2380
     *
2381
     * @param string  $src       as target filename.
2382
     * @param string  $base      as base directory where to store images.
2383
     * @param boolean $overwrite or not, default to always overwrite file.
2384
     *
2385
     * @return $this or false if no folder is set.
2386
     */
2387
    public function save($src = null, $base = null, $overwrite = true)
2388
    {
2389
        if (isset($src)) {
2390
            $this->setTarget($src, $base);
2391
        }
2392
2393
        if ($overwrite === false && is_file($this->cacheFileName)) {
2394
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2395
            return;
2396
        }
2397
2398
        is_writable($this->saveFolder)
2399
            or $this->raiseError('Target directory is not writable.');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
2400
2401
        $type = $this->getTargetImageExtension();
2402
        $this->Log("Saving image as " . $type);
2403
        switch($type) {
2404
2405
            case 'jpeg':
2406
            case 'jpg':
2407
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2408
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2409
2410
                // Use JPEG optimize if defined
2411
                if ($this->jpegOptimizeCmd) {
2412
                    if ($this->verbose) {
2413
                        clearstatcache();
2414
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2415
                    }
2416
                    $res = array();
2417
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2418
                    exec($cmd, $res);
2419
                    $this->log($cmd);
2420
                    $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...
2421
                }
2422
                break;
2423
2424
            case 'gif':
2425
                $this->Log("Saving image as GIF to cache.");
2426
                imagegif($this->image, $this->cacheFileName);
2427
                break;
2428
2429
            case 'webp':
2430
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2431
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2432
                break;
2433
2434
            case 'png':
2435
            default:
2436
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2437
2438
                // Turn off alpha blending and set alpha flag
2439
                imagealphablending($this->image, false);
2440
                imagesavealpha($this->image, true);
2441
                imagepng($this->image, $this->cacheFileName, $this->compress);
2442
2443
                // Use external program to filter PNG, if defined
2444
                if ($this->pngFilterCmd) {
2445
                    if ($this->verbose) {
2446
                        clearstatcache();
2447
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2448
                    }
2449
                    $res = array();
2450
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2451
                    exec($cmd, $res);
2452
                    $this->Log($cmd);
2453
                    $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...
2454
                }
2455
2456
                // Use external program to deflate PNG, if defined
2457
                if ($this->pngDeflateCmd) {
2458
                    if ($this->verbose) {
2459
                        clearstatcache();
2460
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2461
                    }
2462
                    $res = array();
2463
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2464
                    exec($cmd, $res);
2465
                    $this->Log($cmd);
2466
                    $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...
2467
                }
2468
                break;
2469
        }
2470
2471
        if ($this->verbose) {
2472
            clearstatcache();
2473
            $this->log("Saved image to cache.");
2474
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2475
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2476
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2477
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2478
            $index = imagecolortransparent($this->image);
2479
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2480
        }
2481
2482
        return $this;
2483
    }
2484
2485
2486
2487
    /**
2488
     * Convert image from one colorpsace/color profile to sRGB without
2489
     * color profile.
2490
     *
2491
     * @param string  $src      of image.
2492
     * @param string  $dir      as base directory where images are.
2493
     * @param string  $cache    as base directory where to store images.
2494
     * @param string  $iccFile  filename of colorprofile.
2495
     * @param boolean $useCache or not, default to always use cache.
2496
     *
2497
     * @return string | boolean false if no conversion else the converted
2498
     *                          filename.
2499
     */
2500
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2501
    {
2502
        if ($this->verbose) {
2503
            $this->log("# Converting image to sRGB colorspace.");
2504
        }
2505
2506
        if (!class_exists("Imagick")) {
2507
            $this->log(" Ignoring since Imagemagick is not installed.");
2508
            return false;
2509
        }
2510
2511
        // Prepare
2512
        $this->setSaveFolder($cache)
2513
             ->setSource($src, $dir)
2514
             ->generateFilename(null, false, 'srgb_');
2515
2516
        // Check if the cached version is accurate.
2517
        if ($useCache && is_readable($this->cacheFileName)) {
2518
            $fileTime  = filemtime($this->pathToImage);
2519
            $cacheTime = filemtime($this->cacheFileName);
2520
2521
            if ($fileTime <= $cacheTime) {
2522
                $this->log(" Using cached version: " . $this->cacheFileName);
2523
                return $this->cacheFileName;
2524
            }
2525
        }
2526
2527
        // Only covert if cachedir is writable
2528
        if (is_writable($this->saveFolder)) {
2529
            // Load file and check if conversion is needed
2530
            $image      = new Imagick($this->pathToImage);
2531
            $colorspace = $image->getImageColorspace();
2532
            $this->log(" Current colorspace: " . $colorspace);
2533
2534
            $profiles      = $image->getImageProfiles('*', false);
2535
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2536
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2537
2538
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2539
                $this->log(" Converting to sRGB.");
2540
2541
                $sRGBicc = file_get_contents($iccFile);
2542
                $image->profileImage('icc', $sRGBicc);
2543
2544
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2545
                $image->writeImage($this->cacheFileName);
2546
                return $this->cacheFileName;
2547
            }
2548
        }
2549
2550
        return false;
2551
    }
2552
2553
2554
2555
    /**
2556
     * Create a hard link, as an alias, to the cached file.
2557
     *
2558
     * @param string $alias where to store the link,
2559
     *                      filename without extension.
2560
     *
2561
     * @return $this
2562
     */
2563
    public function linkToCacheFile($alias)
2564
    {
2565
        if ($alias === null) {
2566
            $this->log("Ignore creating alias.");
2567
            return $this;
2568
        }
2569
2570
        if (is_readable($alias)) {
2571
            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 99
  1. Read from $_GET
    in functions.php on line 99
  2. get() returns tainted data, and $alias is assigned
    in webroot/img.php on line 859
  3. $aliasTarget is assigned
    in webroot/img.php on line 866
  4. $aliasTarget is passed to CImage::linkToCacheFile()
    in webroot/img.php on line 1159
  2. Path: Read from $_GET in functions.php on line 103
  1. Read from $_GET
    in functions.php on line 103
  2. get() returns tainted data, and $alias is assigned
    in webroot/img.php on line 859
  3. $aliasTarget is assigned
    in webroot/img.php on line 866
  4. $aliasTarget is passed to CImage::linkToCacheFile()
    in webroot/img.php on line 1159

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...
2572
        }
2573
2574
        $res = link($this->cacheFileName, $alias);
2575
2576
        if ($res) {
2577
            $this->log("Created an alias as: $alias");
2578
        } else {
2579
            $this->log("Failed to create the alias: $alias");
2580
        }
2581
2582
        return $this;
2583
    }
2584
2585
2586
2587
    /**
2588
     * Add HTTP header for output together with image.
2589
     *
2590
     * @param string $type  the header type such as "Cache-Control"
2591
     * @param string $value the value to use
2592
     *
2593
     * @return void
2594
     */
2595
    public function addHTTPHeader($type, $value)
2596
    {
2597
        $this->HTTPHeader[$type] = $value;
2598
    }
2599
2600
2601
2602
    /**
2603
     * Output image to browser using caching.
2604
     *
2605
     * @param string $file   to read and output, default is to
2606
     *                       use $this->cacheFileName
2607
     * @param string $format set to json to output file as json
2608
     *                       object with details
2609
     *
2610
     * @return void
2611
     */
2612
    public function output($file = null, $format = null)
0 ignored issues
show
output uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
2613
    {
2614
        if (is_null($file)) {
2615
            $file = $this->cacheFileName;
2616
        }
2617
2618
        if (is_null($format)) {
2619
            $format = $this->outputFormat;
2620
        }
2621
2622
        $this->log("Output format is: $format");
2623
2624
        if (!$this->verbose && $format == 'json') {
2625
            header('Content-type: application/json');
2626
            echo $this->json($file);
2627
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2628
        } elseif ($format == 'ascii') {
2629
            header('Content-type: text/plain');
2630
            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 99
  1. Read from $_GET
    in functions.php on line 99
  2. get() returns tainted data, and $options is assigned
    in webroot/img.php on line 775
  3. $options is passed through explode(), and $options is assigned
    in webroot/img.php on line 776
  4. $defaultOptions is assigned
    in webroot/img.php on line 787
  5. $defaultOptions is passed to CImage::setAsciiOptions()
    in webroot/img.php on line 799
  6. CImage::$asciiOptions is assigned
    in CImage.php on line 2772
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2789
  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 2790
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2630
  2. Path: Read from $_GET in functions.php on line 103
  1. Read from $_GET
    in functions.php on line 103
  2. get() returns tainted data, and $options is assigned
    in webroot/img.php on line 775
  3. $options is passed through explode(), and $options is assigned
    in webroot/img.php on line 776
  4. $defaultOptions is assigned
    in webroot/img.php on line 787
  5. $defaultOptions is passed to CImage::setAsciiOptions()
    in webroot/img.php on line 799
  6. CImage::$asciiOptions is assigned
    in CImage.php on line 2772
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2789
  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 2790
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2630

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...
2631
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2632
        }
2633
2634
        $this->log("Outputting image: $file");
2635
2636
        // Get image modification time
2637
        clearstatcache();
2638
        $lastModified = filemtime($file);
2639
        $lastModifiedFormat = "D, d M Y H:i:s";
2640
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2641
2642
        if (!$this->verbose) {
2643
            $header = "Last-Modified: $gmdate GMT";
2644
            header($header);
2645
            $this->fastTrackCache->addHeader($header);
2646
            $this->fastTrackCache->setLastModified($lastModified);
2647
        }
2648
2649
        foreach ($this->HTTPHeader as $key => $val) {
2650
            $header = "$key: $val";
2651
            header($header);
2652
            $this->fastTrackCache->addHeader($header);
2653
        }
2654
2655
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2656
2657
            if ($this->verbose) {
2658
                $this->log("304 not modified");
2659
                $this->verboseOutput();
2660
                exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2661
            }
2662
2663
            header("HTTP/1.0 304 Not Modified");
2664
            if (CIMAGE_DEBUG) {
2665
                trace(__CLASS__ . " 304");
2666
            }
2667
2668
        } else {
2669
2670
            // Get details on image
2671
            $info = getimagesize($file);
2672
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
2673
            $mime = $info['mime'];
2674
            $size = filesize($file);
2675
2676
            if ($this->verbose) {
2677
                $this->log("Last-Modified: " . $gmdate . " GMT");
2678
                $this->log("Content-type: " . $mime);
2679
                $this->log("Content-length: " . $size);
2680
                $this->verboseOutput();
2681
2682
                if (is_null($this->verboseFileName)) {
2683
                    exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2684
                }
2685
            }
2686
2687
            $header = "Content-type: $mime";
2688
            header($header);
2689
            $this->fastTrackCache->addHeaderOnOutput($header);
2690
2691
            $header = "Content-length: $size";
2692
            header($header);
2693
            $this->fastTrackCache->addHeaderOnOutput($header);
2694
2695
            $this->fastTrackCache->setSource($file);
2696
            $this->fastTrackCache->writeToCache();
2697
            if (CIMAGE_DEBUG) {
2698
                trace(__CLASS__ . " 200");
2699
            }
2700
            readfile($file);
2701
        }
2702
2703
        exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2704
    }
2705
2706
2707
2708
    /**
2709
     * Create a JSON object from the image details.
2710
     *
2711
     * @param string $file the file to output.
2712
     *
2713
     * @return string json-encoded representation of the image.
2714
     */
2715
    public function json($file = null)
0 ignored issues
show
json uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
2716
    {
2717
        $file = $file ? $file : $this->cacheFileName;
2718
2719
        $details = array();
2720
2721
        clearstatcache();
2722
2723
        $details['src']       = $this->imageSrc;
2724
        $lastModified         = filemtime($this->pathToImage);
2725
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2726
2727
        $details['cache']       = basename($this->cacheFileName);
2728
        $lastModified           = filemtime($this->cacheFileName);
2729
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2730
2731
        $this->load($file);
2732
2733
        $details['filename']    = basename($file);
2734
        $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...
2735
        $details['width']       = $this->width;
2736
        $details['height']      = $this->height;
2737
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2738
        $details['size']        = filesize($file);
2739
        $details['colors'] = $this->colorsTotal($this->image);
2740
        $details['includedFiles'] = count(get_included_files());
2741
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2742
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2743
        $details['memoryLimit'] = ini_get('memory_limit');
2744
2745
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2746
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2747
        }
2748
2749
        if ($details['mimeType'] == 'image/png') {
2750
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2751
        }
2752
2753
        $options = null;
2754
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2755
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2756
        }
2757
2758
        return json_encode($details, $options);
2759
    }
2760
2761
2762
2763
    /**
2764
     * Set options for creating ascii version of image.
2765
     *
2766
     * @param array $options empty to use default or set options to change.
2767
     *
2768
     * @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...
2769
     */
2770
    public function setAsciiOptions($options = array())
2771
    {
2772
        $this->asciiOptions = $options;
2773
    }
2774
2775
2776
2777
    /**
2778
     * Create an ASCII version from the image details.
2779
     *
2780
     * @param string $file the file to output.
2781
     *
2782
     * @return string ASCII representation of the image.
2783
     */
2784
    public function ascii($file = null)
2785
    {
2786
        $file = $file ? $file : $this->cacheFileName;
2787
2788
        $asciiArt = new CAsciiArt();
2789
        $asciiArt->setOptions($this->asciiOptions);
2790
        return $asciiArt->createFromFile($file);
2791
    }
2792
2793
2794
2795
    /**
2796
     * Log an event if verbose mode.
2797
     *
2798
     * @param string $message to log.
2799
     *
2800
     * @return this
2801
     */
2802
    public function log($message)
2803
    {
2804
        if ($this->verbose) {
2805
            $this->log[] = $message;
2806
        }
2807
2808
        return $this;
2809
    }
2810
2811
2812
2813
    /**
2814
     * Do verbose output to a file.
2815
     *
2816
     * @param string $fileName where to write the verbose output.
2817
     *
2818
     * @return void
2819
     */
2820
    public function setVerboseToFile($fileName)
2821
    {
2822
        $this->log("Setting verbose output to file.");
2823
        $this->verboseFileName = $fileName;
2824
    }
2825
2826
2827
2828
    /**
2829
     * Do verbose output and print out the log and the actual images.
2830
     *
2831
     * @return void
2832
     */
2833
    private function verboseOutput()
2834
    {
2835
        $log = null;
2836
        $this->log("As JSON: \n" . $this->json());
2837
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2838
        $this->log("Memory limit: " . ini_get('memory_limit'));
2839
2840
        $included = get_included_files();
2841
        $this->log("Included files: " . count($included));
2842
2843
        foreach ($this->log as $val) {
2844
            if (is_array($val)) {
2845
                foreach ($val as $val1) {
2846
                    $log .= htmlentities($val1) . '<br/>';
2847
                }
2848
            } else {
2849
                $log .= htmlentities($val) . '<br/>';
2850
            }
2851
        }
2852
2853
        if (!is_null($this->verboseFileName)) {
2854
            file_put_contents(
2855
                $this->verboseFileName,
2856
                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 99
  1. Read from $_GET
    in functions.php on line 99
  2. get() returns tainted data, and $saveAs is assigned
    in webroot/img.php on line 646
  3. $saveAs is passed to CImage::setSaveAsExtension()
    in webroot/img.php on line 1147
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in CImage.php on line 1253
  5. CImage::$extension is assigned
    in CImage.php on line 1256
  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 1259
  7. CImage::$log is assigned
    in CImage.php on line 2805
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2843
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2849
  10. $log is passed through str_replace()
    in CImage.php on line 2856
  2. Path: Read from $_GET in functions.php on line 103
  1. Read from $_GET
    in functions.php on line 103
  2. get() returns tainted data, and $saveAs is assigned
    in webroot/img.php on line 646
  3. $saveAs is passed to CImage::setSaveAsExtension()
    in webroot/img.php on line 1147
  4. $saveAs is passed through strtolower(), and $saveAs is assigned
    in CImage.php on line 1253
  5. CImage::$extension is assigned
    in CImage.php on line 1256
  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 1259
  7. CImage::$log is assigned
    in CImage.php on line 2805
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2843
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2849
  10. $log is passed through str_replace()
    in CImage.php on line 2856

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...
2857
            );
2858
        } else {
2859
            echo <<<EOD
2860
<h1>CImage Verbose Output</h1>
2861
<pre>{$log}</pre>
2862
EOD;
2863
        }
2864
    }
2865
2866
2867
2868
    /**
2869
     * Raise error, enables to implement a selection of error methods.
2870
     *
2871
     * @param string $message the error message to display.
2872
     *
2873
     * @return void
2874
     * @throws Exception
2875
     */
2876
    private function raiseError($message)
2877
    {
2878
        throw new Exception($message);
2879
    }
2880
}
2881