Completed
Push — master ( 1e5de9...21e538 )
by Mikael
02:31
created

CImage::getMimeType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 8
ccs 0
cts 8
cp 0
crap 6
rs 9.4285
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 7
    private $attr; // Calculated from source image
0 ignored issues
show
Unused Code introduced by
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 7
437 7
438 7
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 2
        }
468
        $this->$property = $object;
469 2
        return $this;
470 2
    }
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 2
    public function setSaveFolder($path)
500
    {
501 2
        $this->saveFolder = $path;
502 2
        return $this;
503
    }
504 2
505
506 2
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 2
520
521 2
522 2
    /**
523
     * Create and save a dummy image. Use dimensions as stated in
524 2
     * $this->newWidth, or $width or default to 100 (same for height.
525
     *
526 2
     * @param integer $width  use specified width for image dimension.
527 2
     * @param integer $height use specified width for image dimension.
528 2
     *
529 2
     * @return $this
530
     */
531 2
    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 2
    /**
544
     * Allow or disallow remote image download.
545 2
     *
546 2
     * @param boolean $allow   true or false to enable and disable.
547 2
     * @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 2
            . ($this->allowRemote ? "true" : "false")
561
            . " using pattern "
562 2
            . $this->remotePattern
563 2
        );
564
565 2
        return $this;
566 2
    }
567 2
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 3
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
581
        return !!$remote;
582 3
    }
583 1
584 1
585
586
    /**
587 2
     * Set whitelist for valid hostnames from where remote source can be
588 2
     * downloaded.
589 2
     *
590
     * @param array $whitelist with regexp hostnames to allow download from.
591 2
     *
592
     * @return $this
593 2
     */
594 2
    public function setRemoteHostWhitelist($whitelist = null)
595 2
    {
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
Security Bug introduced by
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 2
            "Remote host is on whitelist: "
627
            . ($allow ? "true" : "false")
628 2
        );
629
        return $allow;
630 2
    }
631
632
633
634 2
    /**
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 7
        if (!is_writable($this->remoteCache)) {
689
            $this->log("The remote cache is not writable.");
690 7
        }
691 7
692 7
        $remote->setCache($this->remoteCache);
693 7
        $remote->useCache($this->useCache);
694
        $src = $remote->download($src);
695
696 2
        $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 2
    }
702
703
704
705
    /**
706 2
     * Set source file to use as image source.
707 2
     *
708 2
     * @param string $src of image.
709
     * @param string $dir as optional base directory where images are.
710 2
     *
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 7
        }
725
726 7
        if (!isset($dir)) {
727 7
            $dir = dirname($src);
728 7
            $src = basename($src);
729
        }
730
731 2
        $this->imageSrc     = ltrim($src, '/');
732
        $imageFolder        = rtrim($dir, '/');
733
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
734
735 2
        return $this;
736
    }
737
738 2
739 2
740
    /**
741 2
     * 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 2
        if (!isset($src)) {
752
            $this->cacheFileName = null;
753 2
            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($file);
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
Bug introduced by
The property aspectRatio does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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
Bug introduced by
The property area does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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
Documentation introduced by
There is no parameter named $saveas. Did you maybe mean $saveAs?

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

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

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

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

Loading history...
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 2
        }
1305
1306 2
        $this->compress = isset($compress)
1307 2
            ? $compress
1308 2
            : self::PNG_COMPRESSION_DEFAULT;
1309 2
1310 2
        (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 2
            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 2
1313 2
        $this->log("Setting PNG compression level to {$this->compress}.");
1314 2
1315 2
        return $this;
1316 2
    }
1317
1318 2
1319 2
1320
    /**
1321 2
     * Use original image if possible, check options which affects image processing.
1322 2
     *
1323
     * @param boolean $useOrig default is to use original if possible, else set to false.
1324
     *
1325
     * @return $this
1326 2
     */
1327 2
    public function useOriginalIfPossible($useOrig = true)
1328
    {
1329 2
        if ($useOrig
1330 2
            && ($this->newWidth == $this->width)
1331 2
            && ($this->newHeight == $this->height)
1332
            && !$this->area
1333 2
            && !$this->crop
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->crop of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1334 2
            && !$this->cropToFit
1335 2
            && !$this->fillToFit
1336
            && !$this->filters
1337 2
            && !$this->sharpen
1338 2
            && !$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 2
            && ($this->upscale === self::UPSCALE_DEFAULT)
1350 2
        ) {
1351 2
            $this->log("Using original image.");
1352 2
            $this->output($this->pathToImage);
1353
        }
1354 2
1355
        return $this;
1356 2
    }
1357 2
1358 2
1359
1360 2
    /**
1361 2
     * 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 2
     *                           filename.
1366 2
     * @param string  $prefix    to add as part of filename
1367
     *
1368
     * @return $this
1369
     */
1370 2
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1371 2
    {
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 2
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1378 2
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1379 2
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1380 2
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1381 2
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1382 2
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1383 2
1384
        $saveAs = $this->normalizeFileExtension();
1385 2
        $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
        if ($this->fileType === IMG_WEBP) {
1510
            $this->image = imagecreatefromwebp($this->pathToImage);
1511
        } else {
1512
            $imageAsString = file_get_contents($this->pathToImage);
1513
            $this->image = imagecreatefromstring($imageAsString);
1514
        }
1515
        if ($this->image === false) {
1516
            throw new Exception("Could not load image.");
1517
        }
1518
1519
        /* 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...
1520
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1521
            $type = $this->getPngType();
1522
            $hasFewColors = imagecolorstotal($this->image);
1523
1524
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1525
                if ($this->verbose) {
1526
                    $this->log("Handle this image as a palette image.");
1527
                }
1528
                $this->palette = true;
1529
            }
1530
        }
1531
        */
1532
1533
        if ($this->verbose) {
1534
            $this->log("### Image successfully loaded from file.");
1535
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1536
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1537
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1538
            $index = imagecolortransparent($this->image);
1539
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1540
        }
1541
1542
        return $this;
1543
    }
1544
1545
1546
1547
    /**
1548
     * Get the type of PNG image.
1549
     *
1550
     * @param string $filename to use instead of default.
1551
     *
1552
     * @return int as the type of the png-image
1553
     *
1554
     */
1555
    public function getPngType($filename = null)
1556
    {
1557
        $filename = $filename ? $filename : $this->pathToImage;
1558
1559
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1560
1561
        if ($this->verbose) {
1562
            $this->log("Checking png type of: " . $filename);
1563
            $this->log($this->getPngTypeAsString($pngType));
1564
        }
1565
1566
        return $pngType;
1567
    }
1568
1569
1570
1571
    /**
1572
     * Get the type of PNG image as a verbose string.
1573
     *
1574
     * @param integer $type     to use, default is to check the type.
0 ignored issues
show
Bug introduced by
There is no parameter named $type. Was it maybe removed?

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

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

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

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

Loading history...
1575
     * @param string  $filename to use instead of default.
1576
     *
1577
     * @return int as the type of the png-image
1578
     *
1579
     */
1580
    private function getPngTypeAsString($pngType = null, $filename = null)
1581
    {
1582
        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...
1583
            $pngType = $this->getPngType($filename);
1584
        }
1585
1586
        $index = imagecolortransparent($this->image);
1587
        $transparent = null;
1588
        if ($index != -1) {
1589
            $transparent = " (transparent)";
1590
        }
1591
1592
        switch ($pngType) {
1593
1594
            case self::PNG_GREYSCALE:
1595
                $text = "PNG is type 0, Greyscale$transparent";
1596
                break;
1597
1598
            case self::PNG_RGB:
1599
                $text = "PNG is type 2, RGB$transparent";
1600
                break;
1601
1602
            case self::PNG_RGB_PALETTE:
1603
                $text = "PNG is type 3, RGB with palette$transparent";
1604
                break;
1605
1606
            case self::PNG_GREYSCALE_ALPHA:
1607
                $text = "PNG is type 4, Greyscale with alpha channel";
1608
                break;
1609
1610
            case self::PNG_RGB_ALPHA:
1611
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1612
                break;
1613
1614
            default:
1615
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1616
        }
1617
1618
        return $text;
1619
    }
1620
1621
1622
1623
1624
    /**
1625
     * Calculate number of colors in an image.
1626
     *
1627
     * @param resource $im the image.
1628
     *
1629
     * @return int
1630
     */
1631
    private function colorsTotal($im)
1632
    {
1633
        if (imageistruecolor($im)) {
1634
            $this->log("Colors as true color.");
1635
            $h = imagesy($im);
1636
            $w = imagesx($im);
1637
            $c = array();
1638
            for ($x=0; $x < $w; $x++) {
1639
                for ($y=0; $y < $h; $y++) {
1640
                    @$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...
1641
                }
1642
            }
1643
            return count($c);
1644
        } else {
1645
            $this->log("Colors as palette.");
1646
            return imagecolorstotal($im);
1647
        }
1648
    }
1649
1650
1651
1652
    /**
1653
     * Preprocess image before rezising it.
1654
     *
1655
     * @return $this
1656
     */
1657
    public function preResize()
1658
    {
1659
        $this->log("### Pre-process before resizing");
1660
1661
        // Rotate image
1662
        if ($this->rotateBefore) {
1663
            $this->log("Rotating image.");
1664
            $this->rotate($this->rotateBefore, $this->bgColor)
1665
                 ->reCalculateDimensions();
1666
        }
1667
1668
        // Auto-rotate image
1669
        if ($this->autoRotate) {
1670
            $this->log("Auto rotating image.");
1671
            $this->rotateExif()
1672
                 ->reCalculateDimensions();
1673
        }
1674
1675
        // Scale the original image before starting
1676
        if (isset($this->scale)) {
1677
            $this->log("Scale by {$this->scale}%");
1678
            $newWidth  = $this->width * $this->scale / 100;
1679
            $newHeight = $this->height * $this->scale / 100;
1680
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1681
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1682
            $this->image = $img;
1683
            $this->width = $newWidth;
1684
            $this->height = $newHeight;
1685
        }
1686
1687
        return $this;
1688
    }
1689
1690
1691
1692
    /**
1693
     * Resize or resample the image while resizing.
1694
     *
1695
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1696
     *
1697
     * @return $this
1698
     */
1699
     public function setCopyResizeStrategy($strategy)
1700
     {
1701
         $this->copyStrategy = $strategy;
1702
         return $this;
1703
     }
1704
1705
1706
1707
    /**
1708
     * Resize and or crop the image.
1709
     *
1710
     * @return void
1711
     */
1712
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1713
    {
1714
        if($this->copyStrategy == self::RESIZE) {
1715
            $this->log("Copy by resize");
1716
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1717
        } else {
1718
            $this->log("Copy by resample");
1719
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1720
        }
1721
    }
1722
1723
1724
1725
    /**
1726
     * Resize and or crop the image.
1727
     *
1728
     * @return $this
1729
     */
1730
    public function resize()
1731
    {
1732
1733
        $this->log("### Starting to Resize()");
1734
        $this->log("Upscale = '$this->upscale'");
1735
1736
        // Only use a specified area of the image, $this->offset is defining the area to use
1737
        if (isset($this->offset)) {
1738
1739
            $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']}");
1740
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1741
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1742
            $this->image = $img;
1743
            $this->width = $this->offset['width'];
1744
            $this->height = $this->offset['height'];
1745
        }
1746
1747
        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...
1748
1749
            // Do as crop, take only part of image
1750
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1751
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1752
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1753
            $this->image = $img;
1754
            $this->width = $this->crop['width'];
1755
            $this->height = $this->crop['height'];
1756
        }
1757
1758
        if (!$this->upscale) {
0 ignored issues
show
Unused Code introduced by
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...
1759
            // Consider rewriting the no-upscale code to fit within this if-statement,
1760
            // likely to be more readable code.
1761
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1762
        }
1763
1764
        if ($this->cropToFit) {
1765
1766
            // Resize by crop to fit
1767
            $this->log("Resizing using strategy - Crop to fit");
1768
1769
            if (!$this->upscale 
1770
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1771
                $this->log("Resizing - smaller image, do not upscale.");
1772
1773
                $posX = 0;
1774
                $posY = 0;
1775
                $cropX = 0;
1776
                $cropY = 0;
1777
1778
                if ($this->newWidth > $this->width) {
1779
                    $posX = round(($this->newWidth - $this->width) / 2);
1780
                }
1781
                if ($this->newWidth < $this->width) {
1782
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1783
                }
1784
1785
                if ($this->newHeight > $this->height) {
1786
                    $posY = round(($this->newHeight - $this->height) / 2);
1787
                }
1788
                if ($this->newHeight < $this->height) {
1789
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1790
                }
1791
                $this->log(" cwidth: $this->cropWidth");
1792
                $this->log(" cheight: $this->cropHeight");
1793
                $this->log(" nwidth: $this->newWidth");
1794
                $this->log(" nheight: $this->newHeight");
1795
                $this->log(" width: $this->width");
1796
                $this->log(" height: $this->height");
1797
                $this->log(" posX: $posX");
1798
                $this->log(" posY: $posY");
1799
                $this->log(" cropX: $cropX");
1800
                $this->log(" cropY: $cropY");
1801
1802
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1803
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1804
            } else {
1805
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1806
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1807
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1808
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1809
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1810
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1811
            }
1812
1813
            $this->image = $imageResized;
1814
            $this->width = $this->newWidth;
1815
            $this->height = $this->newHeight;
1816
1817
        } elseif ($this->fillToFit) {
1818
1819
            // Resize by fill to fit
1820
            $this->log("Resizing using strategy - Fill to fit");
1821
1822
            $posX = 0;
1823
            $posY = 0;
1824
1825
            $ratioOrig = $this->width / $this->height;
1826
            $ratioNew  = $this->newWidth / $this->newHeight;
1827
1828
            // Check ratio for landscape or portrait
1829
            if ($ratioOrig < $ratioNew) {
1830
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1831
            } else {
1832
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1833
            }
1834
1835
            if (!$this->upscale
1836
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1837
            ) {
1838
1839
                $this->log("Resizing - smaller image, do not upscale.");
1840
                $posX = round(($this->newWidth - $this->width) / 2);
1841
                $posY = round(($this->newHeight - $this->height) / 2);
1842
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1843
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1844
1845
            } else {
1846
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1847
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1848
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1849
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1850
            }
1851
1852
            $this->image = $imageResized;
1853
            $this->width = $this->newWidth;
1854
            $this->height = $this->newHeight;
1855
1856
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1857
1858
            // Resize it
1859
            $this->log("Resizing, new height and/or width");
1860
1861
            if (!$this->upscale
1862
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1863
            ) {
1864
                $this->log("Resizing - smaller image, do not upscale.");
1865
1866
                if (!$this->keepRatio) {
1867
                    $this->log("Resizing - stretch to fit selected.");
1868
1869
                    $posX = 0;
1870
                    $posY = 0;
1871
                    $cropX = 0;
1872
                    $cropY = 0;
1873
1874
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1875
                        $posX = round(($this->newWidth - $this->width) / 2);
1876
                        $posY = round(($this->newHeight - $this->height) / 2);
1877
                    } elseif ($this->newWidth > $this->width) {
1878
                        $posX = round(($this->newWidth - $this->width) / 2);
1879
                        $cropY = round(($this->height - $this->newHeight) / 2);
1880
                    } elseif ($this->newHeight > $this->height) {
1881
                        $posY = round(($this->newHeight - $this->height) / 2);
1882
                        $cropX = round(($this->width - $this->newWidth) / 2);
1883
                    }
1884
1885
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1886
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1887
                    $this->image = $imageResized;
1888
                    $this->width = $this->newWidth;
1889
                    $this->height = $this->newHeight;
1890
                }
1891
            } else {
1892
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1893
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1894
                $this->image = $imageResized;
1895
                $this->width = $this->newWidth;
1896
                $this->height = $this->newHeight;
1897
            }
1898
        }
1899
1900
        return $this;
1901
    }
1902
1903
1904
1905
    /**
1906
     * Postprocess image after rezising image.
1907
     *
1908
     * @return $this
1909
     */
1910
    public function postResize()
1911
    {
1912
        $this->log("### Post-process after resizing");
1913
1914
        // Rotate image
1915
        if ($this->rotateAfter) {
1916
            $this->log("Rotating image.");
1917
            $this->rotate($this->rotateAfter, $this->bgColor);
1918
        }
1919
1920
        // Apply filters
1921
        if (isset($this->filters) && is_array($this->filters)) {
1922
1923
            foreach ($this->filters as $filter) {
1924
                $this->log("Applying filter {$filter['type']}.");
1925
1926
                switch ($filter['argc']) {
1927
1928
                    case 0:
1929
                        imagefilter($this->image, $filter['type']);
1930
                        break;
1931
1932
                    case 1:
1933
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1934
                        break;
1935
1936
                    case 2:
1937
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1938
                        break;
1939
1940
                    case 3:
1941
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1942
                        break;
1943
1944
                    case 4:
1945
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1946
                        break;
1947
                }
1948
            }
1949
        }
1950
1951
        // Convert to palette image
1952
        if ($this->palette) {
1953
            $this->log("Converting to palette image.");
1954
            $this->trueColorToPalette();
1955
        }
1956
1957
        // Blur the image
1958
        if ($this->blur) {
1959
            $this->log("Blur.");
1960
            $this->blurImage();
1961
        }
1962
1963
        // Emboss the image
1964
        if ($this->emboss) {
1965
            $this->log("Emboss.");
1966
            $this->embossImage();
1967
        }
1968
1969
        // Sharpen the image
1970
        if ($this->sharpen) {
1971
            $this->log("Sharpen.");
1972
            $this->sharpenImage();
1973
        }
1974
1975
        // Custom convolution
1976
        if ($this->convolve) {
1977
            //$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...
1978
            $this->imageConvolution();
1979
        }
1980
1981
        return $this;
1982
    }
1983
1984
1985
1986
    /**
1987
     * Rotate image using angle.
1988
     *
1989
     * @param float $angle        to rotate image.
1990
     * @param int   $anglebgColor to fill image with if needed.
0 ignored issues
show
Documentation introduced by
There is no parameter named $anglebgColor. Did you maybe mean $bgColor?

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

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

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

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

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

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

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2427
                }
2428
                break;
2429
2430
            case 'gif':
2431
                $this->Log("Saving image as GIF to cache.");
2432
                imagegif($this->image, $this->cacheFileName);
2433
                break;
2434
2435
            case 'webp':
2436
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2437
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2438
                break;
2439
2440
            case 'png':
2441
            default:
2442
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2443
2444
                // Turn off alpha blending and set alpha flag
2445
                imagealphablending($this->image, false);
2446
                imagesavealpha($this->image, true);
2447
                imagepng($this->image, $this->cacheFileName, $this->compress);
2448
2449
                // Use external program to filter PNG, if defined
2450
                if ($this->pngFilterCmd) {
2451
                    if ($this->verbose) {
2452
                        clearstatcache();
2453
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2454
                    }
2455
                    $res = array();
2456
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2457
                    exec($cmd, $res);
2458
                    $this->Log($cmd);
2459
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2460
                }
2461
2462
                // Use external program to deflate PNG, if defined
2463
                if ($this->pngDeflateCmd) {
2464
                    if ($this->verbose) {
2465
                        clearstatcache();
2466
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2467
                    }
2468
                    $res = array();
2469
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2470
                    exec($cmd, $res);
2471
                    $this->Log($cmd);
2472
                    $this->Log($res);
0 ignored issues
show
Documentation introduced by
$res is of type null|array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2473
                }
2474
                break;
2475
        }
2476
2477
        if ($this->verbose) {
2478
            clearstatcache();
2479
            $this->log("Saved image to cache.");
2480
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2481
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2482
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2483
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2484
            $index = imagecolortransparent($this->image);
2485
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2486
        }
2487
2488
        return $this;
2489
    }
2490
2491
2492
2493
    /**
2494
     * Convert image from one colorpsace/color profile to sRGB without
2495
     * color profile.
2496
     *
2497
     * @param string  $src      of image.
2498
     * @param string  $dir      as base directory where images are.
2499
     * @param string  $cache    as base directory where to store images.
2500
     * @param string  $iccFile  filename of colorprofile.
2501
     * @param boolean $useCache or not, default to always use cache.
2502
     *
2503
     * @return string | boolean false if no conversion else the converted
2504
     *                          filename.
2505
     */
2506
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2507
    {
2508
        if ($this->verbose) {
2509
            $this->log("# Converting image to sRGB colorspace.");
2510
        }
2511
2512
        if (!class_exists("Imagick")) {
2513
            $this->log(" Ignoring since Imagemagick is not installed.");
2514
            return false;
2515
        }
2516
2517
        // Prepare
2518
        $this->setSaveFolder($cache)
2519
             ->setSource($src, $dir)
2520
             ->generateFilename(null, false, 'srgb_');
2521
2522
        // Check if the cached version is accurate.
2523
        if ($useCache && is_readable($this->cacheFileName)) {
2524
            $fileTime  = filemtime($this->pathToImage);
2525
            $cacheTime = filemtime($this->cacheFileName);
2526
2527
            if ($fileTime <= $cacheTime) {
2528
                $this->log(" Using cached version: " . $this->cacheFileName);
2529
                return $this->cacheFileName;
2530
            }
2531
        }
2532
2533
        // Only covert if cachedir is writable
2534
        if (is_writable($this->saveFolder)) {
2535
            // Load file and check if conversion is needed
2536
            $image      = new Imagick($this->pathToImage);
2537
            $colorspace = $image->getImageColorspace();
2538
            $this->log(" Current colorspace: " . $colorspace);
2539
2540
            $profiles      = $image->getImageProfiles('*', false);
2541
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2542
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2543
2544
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2545
                $this->log(" Converting to sRGB.");
2546
2547
                $sRGBicc = file_get_contents($iccFile);
2548
                $image->profileImage('icc', $sRGBicc);
2549
2550
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2551
                $image->writeImage($this->cacheFileName);
2552
                return $this->cacheFileName;
2553
            }
2554
        }
2555
2556
        return false;
2557
    }
2558
2559
2560
2561
    /**
2562
     * Create a hard link, as an alias, to the cached file.
2563
     *
2564
     * @param string $alias where to store the link,
2565
     *                      filename without extension.
2566
     *
2567
     * @return $this
2568
     */
2569
    public function linkToCacheFile($alias)
2570
    {
2571
        if ($alias === null) {
2572
            $this->log("Ignore creating alias.");
2573
            return $this;
2574
        }
2575
2576
        if (is_readable($alias)) {
2577
            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...
2578
        }
2579
2580
        $res = link($this->cacheFileName, $alias);
2581
2582
        if ($res) {
2583
            $this->log("Created an alias as: $alias");
2584
        } else {
2585
            $this->log("Failed to create the alias: $alias");
2586
        }
2587
2588
        return $this;
2589
    }
2590
2591
2592
2593
    /**
2594
     * Add HTTP header for output together with image.
2595
     *
2596
     * @param string $type  the header type such as "Cache-Control"
2597
     * @param string $value the value to use
2598
     *
2599
     * @return void
2600
     */
2601
    public function addHTTPHeader($type, $value)
2602
    {
2603
        $this->HTTPHeader[$type] = $value;
2604
    }
2605
2606
2607
2608
    /**
2609
     * Output image to browser using caching.
2610
     *
2611
     * @param string $file   to read and output, default is to
2612
     *                       use $this->cacheFileName
2613
     * @param string $format set to json to output file as json
2614
     *                       object with details
2615
     *
2616
     * @return void
2617
     */
2618
    public function output($file = null, $format = null)
0 ignored issues
show
Coding Style introduced by
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...
2619
    {
2620
        if (is_null($file)) {
2621
            $file = $this->cacheFileName;
2622
        }
2623
2624
        if (is_null($format)) {
2625
            $format = $this->outputFormat;
2626
        }
2627 7
2628
        $this->log("### Output");
2629 7
        $this->log("Output format is: $format");
2630
2631
        if (!$this->verbose && $format == 'json') {
2632
            header('Content-type: application/json');
2633 7
            echo $this->json($file);
2634
            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...
2635
        } elseif ($format == 'ascii') {
2636
            header('Content-type: text/plain');
2637
            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 2778
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2795
  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 2796
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2637
  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 2778
  7. Tainted property CImage::$asciiOptions is read, and $this->asciiOptions is passed to CAsciiArt::setOptions()
    in CImage.php on line 2795
  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 2796
  15. CImage::ascii() returns tainted data
    in CImage.php on line 2637

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...
2638
            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...
2639
        }
2640
2641
        $this->log("Outputting image: $file");
2642
2643
        // Get image modification time
2644
        clearstatcache();
2645
        $lastModified = filemtime($file);
2646
        $lastModifiedFormat = "D, d M Y H:i:s";
2647
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2648
2649
        if (!$this->verbose) {
2650
            $header = "Last-Modified: $gmdate GMT";
2651
            header($header);
2652
            $this->fastTrackCache->addHeader($header);
2653
            $this->fastTrackCache->setLastModified($lastModified);
2654
        }
2655
2656
        foreach ($this->HTTPHeader as $key => $val) {
2657
            $header = "$key: $val";
2658
            header($header);
2659
            $this->fastTrackCache->addHeader($header);
2660
        }
2661
2662
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
2663
            && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2664
2665
            if ($this->verbose) {
2666
                $this->log("304 not modified");
2667
                $this->verboseOutput();
2668
                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...
2669
            }
2670
2671
            header("HTTP/1.0 304 Not Modified");
2672
            if (CIMAGE_DEBUG) {
2673
                trace(__CLASS__ . " 304");
2674
            }
2675
2676
        } else {
2677
2678
            $this->loadImageDetails($file);
2679
            $mime = $this->getMimeType();
2680
            $size = filesize($file);
2681
2682
            if ($this->verbose) {
2683
                $this->log("Last-Modified: " . $gmdate . " GMT");
2684
                $this->log("Content-type: " . $mime);
2685
                $this->log("Content-length: " . $size);
2686
                $this->verboseOutput();
2687
2688
                if (is_null($this->verboseFileName)) {
2689
                    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...
2690
                }
2691
            }
2692
2693
            $header = "Content-type: $mime";
2694
            header($header);
2695
            $this->fastTrackCache->addHeaderOnOutput($header);
2696
2697
            $header = "Content-length: $size";
2698
            header($header);
2699
            $this->fastTrackCache->addHeaderOnOutput($header);
2700
2701
            $this->fastTrackCache->setSource($file);
2702
            $this->fastTrackCache->writeToCache();
2703
            if (CIMAGE_DEBUG) {
2704
                trace(__CLASS__ . " 200");
2705
            }
2706
            readfile($file);
2707
        }
2708
2709
        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...
2710
    }
2711
2712
2713
2714
    /**
2715
     * Create a JSON object from the image details.
2716
     *
2717
     * @param string $file the file to output.
2718
     *
2719
     * @return string json-encoded representation of the image.
2720
     */
2721
    public function json($file = null)
0 ignored issues
show
Coding Style introduced by
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...
2722
    {
2723
        $file = $file ? $file : $this->cacheFileName;
2724
2725
        $details = array();
2726
2727
        clearstatcache();
2728
2729
        $details['src']       = $this->imageSrc;
2730
        $lastModified         = filemtime($this->pathToImage);
2731
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2732
2733
        $details['cache']       = basename($this->cacheFileName);
2734
        $lastModified           = filemtime($this->cacheFileName);
2735
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2736
2737
        $this->load($file);
2738
2739
        $details['filename']    = basename($file);
2740
        $details['mimeType']    = $this->getMimeType($this->fileType);
0 ignored issues
show
Unused Code introduced by
The call to CImage::getMimeType() has too many arguments starting with $this->fileType.

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

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

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

Loading history...
2741
        $details['width']       = $this->width;
2742
        $details['height']      = $this->height;
2743
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2744
        $details['size']        = filesize($file);
2745
        $details['colors'] = $this->colorsTotal($this->image);
2746
        $details['includedFiles'] = count(get_included_files());
2747
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2748
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2749
        $details['memoryLimit'] = ini_get('memory_limit');
2750
2751
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2752
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2753
        }
2754
2755
        if ($details['mimeType'] == 'image/png') {
2756
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2757
        }
2758
2759
        $options = null;
2760
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2761
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2762
        }
2763
2764
        return json_encode($details, $options);
2765
    }
2766
2767
2768
2769
    /**
2770
     * Set options for creating ascii version of image.
2771
     *
2772
     * @param array $options empty to use default or set options to change.
2773
     *
2774
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

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

Loading history...
2775
     */
2776
    public function setAsciiOptions($options = array())
2777
    {
2778
        $this->asciiOptions = $options;
2779
    }
2780
2781
2782
2783
    /**
2784
     * Create an ASCII version from the image details.
2785
     *
2786
     * @param string $file the file to output.
2787
     *
2788
     * @return string ASCII representation of the image.
2789
     */
2790
    public function ascii($file = null)
2791
    {
2792
        $file = $file ? $file : $this->cacheFileName;
2793
2794
        $asciiArt = new CAsciiArt();
2795
        $asciiArt->setOptions($this->asciiOptions);
2796
        return $asciiArt->createFromFile($file);
2797
    }
2798
2799
2800
2801
    /**
2802
     * Log an event if verbose mode.
2803
     *
2804
     * @param string $message to log.
2805
     *
2806
     * @return this
2807
     */
2808
    public function log($message)
2809
    {
2810
        if ($this->verbose) {
2811
            $this->log[] = $message;
2812
        }
2813
2814
        return $this;
2815
    }
2816
2817
2818
2819
    /**
2820
     * Do verbose output to a file.
2821
     *
2822
     * @param string $fileName where to write the verbose output.
2823
     *
2824
     * @return void
2825
     */
2826
    public function setVerboseToFile($fileName)
2827
    {
2828
        $this->log("Setting verbose output to file.");
2829
        $this->verboseFileName = $fileName;
2830
    }
2831
2832
2833
2834
    /**
2835
     * Do verbose output and print out the log and the actual images.
2836
     *
2837
     * @return void
2838
     */
2839
    private function verboseOutput()
2840
    {
2841
        $log = null;
2842
        $this->log("### Summary of verbose log");
2843
        $this->log("As JSON: \n" . $this->json());
2844
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2845
        $this->log("Memory limit: " . ini_get('memory_limit'));
2846
2847
        $included = get_included_files();
2848
        $this->log("Included files: " . count($included));
2849
2850
        foreach ($this->log as $val) {
2851
            if (is_array($val)) {
2852
                foreach ($val as $val1) {
2853
                    $log .= htmlentities($val1) . '<br/>';
2854
                }
2855
            } else {
2856
                $log .= htmlentities($val) . '<br/>';
2857
            }
2858
        }
2859
2860
        if (!is_null($this->verboseFileName)) {
2861
            file_put_contents(
2862
                $this->verboseFileName,
2863
                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 2811
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2850
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2856
  10. $log is passed through str_replace()
    in CImage.php on line 2863
  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 2811
  8. Tainted property CImage::$log is read, and $val is assigned
    in CImage.php on line 2850
  9. $val is escaped by htmlentities() for html (no single-quotes) context(s), and $log is assigned
    in CImage.php on line 2856
  10. $log is passed through str_replace()
    in CImage.php on line 2863

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