Completed
Push — master ( 9f6cba...c5de59 )
by Mikael
05:05 queued 02:06
created

CImage   D

Complexity

Total Complexity 387

Size/Duplication

Total Lines 2923
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 14.89%

Importance

Changes 38
Bugs 6 Features 12
Metric Value
c 38
b 6
f 12
dl 0
loc 2923
ccs 168
cts 1128
cp 0.1489
rs 4.4102
wmc 387
lcom 1
cbo 3

64 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A injectDependency() 0 8 2
A setVerbose() 0 5 1
A setSaveFolder() 0 5 1
A useCache() 0 5 1
B createDummyImage() 0 9 5
A setRemoteDownload() 0 15 3
A isRemoteSource() 0 6 2
A setRemoteHostWhitelist() 0 9 2
A isRemoteSourceOnWhitelist() 0 17 3
A checkFileExtension() 0 9 2
A normalizeFileExtension() 0 10 3
A downloadRemoteSource() 0 22 3
B setSource() 0 24 5
A setTarget() 0 19 3
A getTarget() 0 4 1
C setOptions() 0 105 12
A mapFilter() 0 23 2
D loadImageDetails() 0 39 10
A getMimeType() 0 8 2
F initDimensions() 0 65 20
F calculateNewWidthAndHeight() 0 143 32
A reCalculateDimensions() 0 13 1
A setSaveAsExtension() 0 13 2
B setJpegQuality() 0 17 6
B setPngCompression() 0 17 6
D useOriginalIfPossible() 0 30 22
F generateFilename() 0 83 33
B useCacheIfPossible() 0 25 6
C load() 0 43 7
A getPngType() 0 13 3
D getPngTypeAsString() 0 40 9
A colorsTotal() 0 18 4
B preResize() 0 32 4
A setCopyResizeStrategy() 0 5 1
A imageCopyResampled() 0 10 2
F resize() 0 172 27
C postResize() 0 73 15
A rotate() 0 14 1
B rotateExif() 0 35 6
A trueColorToPalette() 0 17 2
A sharpenImage() 0 5 1
A embossImage() 0 5 1
A blurImage() 0 5 1
B createConvolveArguments() 0 34 4
A addConvolveExpressions() 0 5 1
A imageConvolution() 0 17 3
C setDefaultBackgroundColor() 0 42 12
B getBackgroundColor() 0 23 4
B createImageKeepTransparency() 0 29 4
D setPostProcessingOptions() 0 30 9
A getTargetImageExtension() 0 11 3
F save() 0 115 23
C convert2sRGBColorSpace() 0 52 10
A linkToCacheFile() 0 21 4
A addHTTPHeader() 0 4 1
D output() 0 93 15
B json() 0 45 6
A setAsciiOptions() 0 4 1
A ascii() 0 8 2
A log() 0 8 2
A setVerboseToFile() 0 5 1
B verboseOutput() 0 33 5
A raiseError() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like CImage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CImage, and based on these observations, apply Extract Interface, too.

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
     * Do lossy output using external postprocessing tools.
160
     */
161
    private $lossy = null;
162
163
164
165
    /**
166
     * Verbose mode to print out a trace and display the created image
167
     */
168
    private $verbose = false;
169
170
171
172
    /**
173
     * Keep a log/trace on what happens
174
     */
175
    private $log = array();
176
177
178
179
    /**
180
     * Handle image as palette image
181
     */
182
    private $palette;
183
184
185
186
    /**
187
     * Target filename, with path, to save resulting image in.
188
     */
189
    private $cacheFileName;
190
191
192
193
    /**
194
     * Set a format to save image as, or null to use original format.
195
     */
196
    private $saveAs;
197
198
199
    /**
200
     * Path to command for lossy optimize, for example pngquant.
201
     */
202
    private $pngLossy;
203
    private $pngLossyCmd;
204
205
206
207
    /**
208
     * Path to command for filter optimize, for example optipng.
209
     */
210
    private $pngFilter;
211
    private $pngFilterCmd;
212
213
214
215
    /**
216
     * Path to command for deflate optimize, for example pngout.
217
     */
218
    private $pngDeflate;
219
    private $pngDeflateCmd;
220
221
222
223
    /**
224
     * Path to command to optimize jpeg images, for example jpegtran or null.
225
     */
226
     private $jpegOptimize;
227
     private $jpegOptimizeCmd;
228
229
230
231
    /**
232
     * Image dimensions, calculated from loaded image.
233
     */
234
    private $width;  // Calculated from source image
235
    private $height; // Calculated from source image
236
237
238
    /**
239
     * New image dimensions, incoming as argument or calculated.
240
     */
241
    private $newWidth;
242
    private $newWidthOrig;  // Save original value
243
    private $newHeight;
244
    private $newHeightOrig; // Save original value
245
246
247
    /**
248
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
249
     */
250
    private $dpr = 1;
251
252
253
    /**
254
     * Always upscale images, even if they are smaller than target image.
255
     */
256
    const UPSCALE_DEFAULT = true;
257
    private $upscale = self::UPSCALE_DEFAULT;
258
259
260
261
    /**
262
     * Array with details on how to crop, incoming as argument and calculated.
263
     */
264
    public $crop;
265
    public $cropOrig; // Save original value
266
267
268
    /**
269
     * String with details on how to do image convolution. String
270
     * should map a key in the $convolvs array or be a string of
271
     * 11 float values separated by comma. The first nine builds
272
     * up the matrix, then divisor and last offset.
273
     */
274
    private $convolve;
275
276
277
    /**
278
     * Custom convolution expressions, matrix 3x3, divisor and offset.
279
     */
280
    private $convolves = array(
281
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
282
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
283
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
284
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
285
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
286
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
287
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
288
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
289
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
290
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
291
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
292
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
293
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
294
    );
295
296
297
    /**
298
     * Resize strategy to fill extra area with background color.
299
     * True or false.
300
     */
301
    private $fillToFit;
302
303
304
305
    /**
306
     * To store value for option scale.
307
     */
308
    private $scale;
309
310
311
312
    /**
313
     * To store value for option.
314
     */
315
    private $rotateBefore;
316
317
318
319
    /**
320
     * To store value for option.
321
     */
322
    private $rotateAfter;
323
324
325
326
    /**
327
     * To store value for option.
328
     */
329
    private $autoRotate;
330
331
332
333
    /**
334
     * To store value for option.
335
     */
336
    private $sharpen;
337
338
339
340
    /**
341
     * To store value for option.
342
     */
343
    private $emboss;
344
345
346
347
    /**
348
     * To store value for option.
349
     */
350
    private $blur;
351
352
353
354
    /**
355
     * Used with option area to set which parts of the image to use.
356
     */
357
    private $offset;
358
359
360
361
    /**
362
     * Calculate target dimension for image when using fill-to-fit resize strategy.
363
     */
364
    private $fillWidth;
365
    private $fillHeight;
366
367
368
369
    /**
370
     * Allow remote file download, default is to disallow remote file download.
371
     */
372
    private $allowRemote = false;
373
374
375
376
    /**
377
     * Path to cache for remote download.
378
     */
379
    private $remoteCache;
380
381
382
383
    /**
384
     * Pattern to recognize a remote file.
385
     */
386
    //private $remotePattern = '#^[http|https]://#';
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...
387
    private $remotePattern = '#^https?://#';
388
389
390
391
    /**
392
     * Use the cache if true, set to false to ignore the cached file.
393
     */
394
    private $useCache = true;
395
396
397
    /**
398
    * Disable the fasttrackCacke to start with, inject an object to enable it.
399
    */
400
    private $fastTrackCache = null;
401
402
403
404
    /*
405
     * Set whitelist for valid hostnames from where remote source can be
406
     * downloaded.
407
     */
408
    private $remoteHostWhitelist = null;
409
410
411
412
    /*
413
     * Do verbose logging to file by setting this to a filename.
414
     */
415
    private $verboseFileName = null;
416
417
418
419
    /*
420
     * Output to ascii can take som options as an array.
421
     */
422
    private $asciiOptions = array();
423
424
425
426
    /*
427
     * Image copy strategy, defaults to RESAMPLE.
428
     */
429
     const RESIZE = 1;
430
     const RESAMPLE = 2;
431
     private $copyStrategy = NULL;
432
433
434 7
435
    /**
436 7
     * Properties, the class is mutable and the method setOptions()
437 7
     * decides (partly) what properties are created.
438 7
     *
439
     * @todo Clean up these and check if and how they are used
440
     */
441
442
    public $keepRatio;
443
    public $cropToFit;
444
    private $cropWidth;
445
    private $cropHeight;
446
    public $crop_x;
447
    public $crop_y;
448
    public $filters;
449
    private $attr; // Calculated from source image
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...
450
451
452
453
454
    /**
455
     * Constructor, can take arguments to init the object.
456
     *
457
     * @param string $imageSrc    filename which may contain subdirectory.
458
     * @param string $imageFolder path to root folder for images.
459
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
460
     * @param string $saveName    name of target file when saveing.
461
     */
462
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
463
    {
464
        $this->setSource($imageSrc, $imageFolder);
465
        $this->setTarget($saveFolder, $saveName);
466
    }
467 2
468
469 2
470 2
    /**
471
     * Inject object and use it, must be available as member.
472
     *
473
     * @param string $property to set as object.
474
     * @param object $object   to set to property.
475
     *
476
     * @return $this
477
     */
478
    public function injectDependency($property, $object)
479
    {
480
        if (!property_exists($this, $property)) {
481
            $this->raiseError("Injecting unknown property.");
482
        }
483
        $this->$property = $object;
484
        return $this;
485
    }
486
487
488
489
    /**
490
     * Set verbose mode.
491
     *
492
     * @param boolean $mode true or false to enable and disable verbose mode,
493
     *                      default is true.
494
     *
495
     * @return $this
496
     */
497
    public function setVerbose($mode = true)
498
    {
499 2
        $this->verbose = $mode;
500
        return $this;
501 2
    }
502 2
503
504 2
505
    /**
506 2
     * Set save folder, base folder for saving cache files.
507
     *
508
     * @todo clean up how $this->saveFolder is used in other methods.
509
     *
510
     * @param string $path where to store cached files.
511
     *
512
     * @return $this
513
     */
514
    public function setSaveFolder($path)
515
    {
516
        $this->saveFolder = $path;
517
        return $this;
518
    }
519 2
520
521 2
522 2
    /**
523
     * Use cache or not.
524 2
     *
525
     * @param boolean $use true or false to use cache.
526 2
     *
527 2
     * @return $this
528 2
     */
529 2
    public function useCache($use = true)
530
    {
531 2
        $this->useCache = $use;
532
        return $this;
533
    }
534
535
536
537
    /**
538
     * Create and save a dummy image. Use dimensions as stated in
539
     * $this->newWidth, or $width or default to 100 (same for height.
540
     *
541
     * @param integer $width  use specified width for image dimension.
542
     * @param integer $height use specified width for image dimension.
543 2
     *
544
     * @return $this
545 2
     */
546 2
    public function createDummyImage($width = null, $height = null)
547 2
    {
548
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
549
        $this->newHeight = $this->newHeight ?: $height ?: 100;
550
551
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
552
553
        return $this;
554
    }
555
556
557
558
    /**
559
     * Allow or disallow remote image download.
560 2
     *
561
     * @param boolean $allow   true or false to enable and disable.
562 2
     * @param string  $cache   path to cache dir.
563 2
     * @param string  $pattern to use to detect if its a remote file.
564
     *
565 2
     * @return $this
566 2
     */
567 2
    public function setRemoteDownload($allow, $cache, $pattern = null)
568
    {
569
        $this->allowRemote = $allow;
570
        $this->remoteCache = $cache;
571
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
572
573
        $this->log(
574
            "Set remote download to: "
575
            . ($this->allowRemote ? "true" : "false")
576
            . " using pattern "
577
            . $this->remotePattern
578
        );
579
580 3
        return $this;
581
    }
582 3
583 1
584 1
585
    /**
586
     * Check if the image resource is a remote file or not.
587 2
     *
588 2
     * @param string $src check if src is remote.
589 2
     *
590
     * @return boolean true if $src is a remote file, else false.
591 2
     */
592
    public function isRemoteSource($src)
593 2
    {
594 2
        $remote = preg_match($this->remotePattern, $src);
595 2
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
596
        return !!$remote;
597
    }
598
599
600
601
    /**
602
     * Set whitelist for valid hostnames from where remote source can be
603
     * downloaded.
604
     *
605
     * @param array $whitelist with regexp hostnames to allow download from.
606
     *
607
     * @return $this
608
     */
609
    public function setRemoteHostWhitelist($whitelist = null)
610
    {
611
        $this->remoteHostWhitelist = $whitelist;
612
        $this->log(
613
            "Setting remote host whitelist to: "
614
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
615
        );
616
        return $this;
617
    }
618
619
620
621
    /**
622
     * Check if the hostname for the remote image, is on a whitelist,
623
     * if the whitelist is defined.
624
     *
625
     * @param string $src the remote source.
626 2
     *
627
     * @return boolean true if hostname on $src is in the whitelist, else false.
628 2
     */
629
    public function isRemoteSourceOnWhitelist($src)
630 2
    {
631
        if (is_null($this->remoteHostWhitelist)) {
632
            $this->log("Remote host on whitelist not configured - allowing.");
633
            return true;
634 2
        }
635
636
        $whitelist = new CWhitelist();
637
        $hostname = parse_url($src, PHP_URL_HOST);
638
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
0 ignored issues
show
Security Bug introduced by
It seems like $hostname defined by parse_url($src, PHP_URL_HOST) on line 637 can also be of type false; however, CWhitelist::check() does only seem to accept string, did you maybe forget to handle an error condition?

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

Consider the follow example

<?php

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

    return false;
}

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

Loading history...
639
640
        $this->log(
641
            "Remote host is on whitelist: "
642
            . ($allow ? "true" : "false")
643
        );
644
        return $allow;
645
    }
646
647
648
649
    /**
650
     * Check if file extension is valid as a file extension.
651
     *
652
     * @param string $extension of image file.
653
     *
654
     * @return $this
655
     */
656
    private function checkFileExtension($extension)
657
    {
658
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
659
660
        in_array(strtolower($extension), $valid)
661
            or $this->raiseError('Not a valid file extension.');
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...
662
663
        return $this;
664
    }
665
666
667
668
    /**
669
     * Normalize the file extension.
670
     *
671
     * @param string $extension of image file or skip to use internal.
672
     *
673
     * @return string $extension as a normalized file extension.
674
     */
675
    private function normalizeFileExtension($extension = null)
676
    {
677
        $extension = strtolower($extension ? $extension : $this->extension);
678
679
        if ($extension == 'jpeg') {
680
                $extension = 'jpg';
681
        }
682
683
        return $extension;
684
    }
685
686
687
688 7
    /**
689
     * Download a remote image and return path to its local copy.
690 7
     *
691 7
     * @param string $src remote path to image.
692 7
     *
693 7
     * @return string as path to downloaded remote source.
694
     */
695
    public function downloadRemoteSource($src)
696 2
    {
697
        if (!$this->isRemoteSourceOnWhitelist($src)) {
698
            throw new Exception("Hostname is not on whitelist for remote sources.");
699
        }
700
701 2
        $remote = new CRemoteImage();
702
703
        if (!is_writable($this->remoteCache)) {
704
            $this->log("The remote cache is not writable.");
705
        }
706 2
707 2
        $remote->setCache($this->remoteCache);
708 2
        $remote->useCache($this->useCache);
709
        $src = $remote->download($src);
710 2
711
        $this->log("Remote HTTP status: " . $remote->getStatus());
712
        $this->log("Remote item is in local cache: $src");
713
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
714
715
        return $src;
716
    }
717
718
719
720
    /**
721
     * Set source file to use as image source.
722
     *
723
     * @param string $src of image.
724 7
     * @param string $dir as optional base directory where images are.
725
     *
726 7
     * @return $this
727 7
     */
728 7
    public function setSource($src, $dir = null)
729
    {
730
        if (!isset($src)) {
731 2
            $this->imageSrc = null;
732
            $this->pathToImage = null;
733
            return $this;
734
        }
735 2
736
        if ($this->allowRemote && $this->isRemoteSource($src)) {
737
            $src = $this->downloadRemoteSource($src);
738 2
            $dir = null;
739 2
        }
740
741 2
        if (!isset($dir)) {
742
            $dir = dirname($src);
743
            $src = basename($src);
744
        }
745
746
        $this->imageSrc     = ltrim($src, '/');
747
        $imageFolder        = rtrim($dir, '/');
748
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
749
750
        return $this;
751 2
    }
752
753 2
754
755
    /**
756
     * Set target file.
757
     *
758
     * @param string $src of target image.
759
     * @param string $dir as optional base directory where images are stored.
760
     *                    Uses $this->saveFolder if null.
761
     *
762
     * @return $this
763
     */
764
    public function setTarget($src = null, $dir = null)
765
    {
766
        if (!isset($src)) {
767
            $this->cacheFileName = null;
768
            return $this;
769
        }
770
771
        if (isset($dir)) {
772
            $this->saveFolder = rtrim($dir, '/');
773
        }
774
775
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
776
777
        // Sanitize filename
778
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
779
        $this->log("The cache file name is: " . $this->cacheFileName);
780
781
        return $this;
782
    }
783
784
785
786
    /**
787
     * Get filename of target file.
788
     *
789
     * @return Boolean|String as filename of target or false if not set.
790
     */
791
    public function getTarget()
792
    {
793
        return $this->cacheFileName;
794
    }
795
796
797
798
    /**
799
     * Set options to use when processing image.
800
     *
801
     * @param array $args used when processing image.
802
     *
803
     * @return $this
804
     */
805
    public function setOptions($args)
806
    {
807
        $this->log("Set new options for processing image.");
808
809
        $defaults = array(
810
            // Options for calculate dimensions
811
            'newWidth'    => null,
812
            'newHeight'   => null,
813
            'aspectRatio' => null,
814
            'keepRatio'   => true,
815
            'cropToFit'   => false,
816
            'fillToFit'   => null,
817
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
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...
818
            'area'        => null, //'0,0,0,0',
819
            'upscale'     => self::UPSCALE_DEFAULT,
820
821
            // Options for caching or using original
822
            'useCache'    => true,
823
            'useOriginal' => true,
824
825
            // Pre-processing, before resizing is done
826
            'scale'        => null,
827
            'rotateBefore' => null,
828
            'autoRotate'  => false,
829
830
            // General options
831
            'bgColor'     => null,
832
833
            // Post-processing, after resizing is done
834
            'palette'     => null,
835
            'filters'     => null,
836
            'sharpen'     => null,
837
            'emboss'      => null,
838
            'blur'        => null,
839
            'convolve'       => null,
840
            'rotateAfter' => null,
841
842
            // Output format
843
            'outputFormat' => null,
844
            'dpr'          => 1,
845
846
            // Postprocessing using external tools
847
            'lossy' => null,
848
        );
849
850
        // Convert crop settings from string to array
851
        if (isset($args['crop']) && !is_array($args['crop'])) {
852
            $pices = explode(',', $args['crop']);
853
            $args['crop'] = array(
854
                'width'   => $pices[0],
855
                'height'  => $pices[1],
856
                'start_x' => $pices[2],
857
                'start_y' => $pices[3],
858
            );
859
        }
860
861
        // Convert area settings from string to array
862
        if (isset($args['area']) && !is_array($args['area'])) {
863
                $pices = explode(',', $args['area']);
864
                $args['area'] = array(
865
                    'top'    => $pices[0],
866
                    'right'  => $pices[1],
867
                    'bottom' => $pices[2],
868
                    'left'   => $pices[3],
869
                );
870
        }
871
872
        // Convert filter settings from array of string to array of array
873
        if (isset($args['filters']) && is_array($args['filters'])) {
874
            foreach ($args['filters'] as $key => $filterStr) {
875
                $parts = explode(',', $filterStr);
876
                $filter = $this->mapFilter($parts[0]);
877
                $filter['str'] = $filterStr;
878
                for ($i=1; $i<=$filter['argc']; $i++) {
879
                    if (isset($parts[$i])) {
880
                        $filter["arg{$i}"] = $parts[$i];
881
                    } else {
882
                        throw new Exception(
883
                            'Missing arg to filter, review how many arguments are needed at
884
                            http://php.net/manual/en/function.imagefilter.php'
885
                        );
886
                    }
887
                }
888
                $args['filters'][$key] = $filter;
889
            }
890
        }
891
892
        // Merge default arguments with incoming and set properties.
893
        //$args = array_merge_recursive($defaults, $args);
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...
894
        $args = array_merge($defaults, $args);
895
        foreach ($defaults as $key => $val) {
896
            $this->{$key} = $args[$key];
897
        }
898
899
        if ($this->bgColor) {
900
            $this->setDefaultBackgroundColor($this->bgColor);
901
        }
902
903
        // Save original values to enable re-calculating
904
        $this->newWidthOrig  = $this->newWidth;
905
        $this->newHeightOrig = $this->newHeight;
906
        $this->cropOrig      = $this->crop;
907
908
        return $this;
909
    }
910
911
912
913
    /**
914
     * Map filter name to PHP filter and id.
915
     *
916
     * @param string $name the name of the filter.
917
     *
918
     * @return array with filter settings
919
     * @throws Exception
920
     */
921
    private function mapFilter($name)
922
    {
923
        $map = array(
924
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
925
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
926
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
927
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
928
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
929
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
930
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
931
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
932
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
933
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
934
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
935
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
936
        );
937
938
        if (isset($map[$name])) {
939
            return $map[$name];
940
        } else {
941
            throw new Exception('No such filter.');
942
        }
943
    }
944
945
946
947
    /**
948
     * Load image details from original image file.
949
     *
950
     * @param string $file the file to load or null to use $this->pathToImage.
951
     *
952
     * @return $this
953
     * @throws Exception
954
     */
955
    public function loadImageDetails($file = null)
956
    {
957
        $file = $file ? $file : $this->pathToImage;
958
959
        is_readable($file)
960
            or $this->raiseError('Image file does not exist.');
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...
961
962
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
963
        if (empty($info)) {
964
            // To support webp
965
            $this->fileType = false;
966
            if (function_exists("exif_imagetype")) {
967
                $this->fileType = exif_imagetype($file);
968
                if ($this->fileType === false) {
969
                    if (function_exists("imagecreatefromwebp")) {
970
                        $webp = imagecreatefromwebp($file);
971
                        if ($webp !== false) {
972
                            $this->width  = imagesx($webp);
973
                            $this->height = imagesy($webp);
974
                            $this->fileType = IMG_WEBP;
975
                        }
976
                    }
977
                }
978
            }
979
        }
980
981
        if (!$this->fileType) {
982
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
983
        }
984
985
        if ($this->verbose) {
986
            $this->log("Loading image details for: {$file}");
987
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
988
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
989
            $this->log(" Image mimetype: " . $this->getMimeType());
990
        }
991
992
        return $this;
993
    }
994
995
996
997
    /**
998
     * Get mime type for image type.
999
     *
1000
     * @return $this
1001
     * @throws Exception
1002
     */
1003
    protected function getMimeType()
1004
    {
1005
        if ($this->fileType === IMG_WEBP) {
1006
            return "image/webp";
1007
        }
1008
1009
        return image_type_to_mime_type($this->fileType);
1010
    }
1011
1012
1013
1014
    /**
1015
     * Init new width and height and do some sanity checks on constraints, before any
1016
     * processing can be done.
1017
     *
1018
     * @return $this
1019
     * @throws Exception
1020
     */
1021
    public function initDimensions()
1022
    {
1023
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1024
1025
        // width as %
1026
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
1027
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1028
            $this->log("Setting new width based on % to {$this->newWidth}");
1029
        }
1030
1031
        // height as %
1032
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
1033
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1034
            $this->log("Setting new height based on % to {$this->newHeight}");
1035
        }
1036
1037
        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...
1038
1039
        // width & height from aspect ratio
1040
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1041
            if ($this->aspectRatio >= 1) {
1042
                $this->newWidth   = $this->width;
1043
                $this->newHeight  = $this->width / $this->aspectRatio;
1044
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1045
1046
            } else {
1047
                $this->newHeight  = $this->height;
1048
                $this->newWidth   = $this->height * $this->aspectRatio;
1049
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1050
            }
1051
1052
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1053
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1054
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1055
1056
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1057
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1058
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1059
        }
1060
1061
        // Change width & height based on dpr
1062
        if ($this->dpr != 1) {
1063
            if (!is_null($this->newWidth)) {
1064
                $this->newWidth  = round($this->newWidth * $this->dpr);
1065
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1066
            }
1067
            if (!is_null($this->newHeight)) {
1068
                $this->newHeight = round($this->newHeight * $this->dpr);
1069
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1070
            }
1071
        }
1072
1073
        // Check values to be within domain
1074
        is_null($this->newWidth)
1075
            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...
1076
            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...
1077
1078
        is_null($this->newHeight)
1079
            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...
1080
            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...
1081
1082
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1083
1084
        return $this;
1085
    }
1086
1087
1088
1089
    /**
1090
     * Calculate new width and height of image, based on settings.
1091
     *
1092
     * @return $this
1093
     */
1094
    public function calculateNewWidthAndHeight()
1095
    {
1096
        // Crop, use cropped width and height as base for calulations
1097
        $this->log("Calculate new width and height.");
1098
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1099
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1100
1101
        // Check if there is an area to crop off
1102
        if (isset($this->area)) {
1103
            $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...
1104
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1105
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1106
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1107
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1108
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1109
            $this->width  = $this->offset['width'];
1110
            $this->height = $this->offset['height'];
1111
            $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']}%.");
1112
            $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.");
1113
        }
1114
1115
        $width  = $this->width;
1116
        $height = $this->height;
1117
1118
        // Check if crop is set
1119
        if ($this->crop) {
1120
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1121
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1122
1123
            if ($this->crop['start_x'] == 'left') {
1124
                $this->crop['start_x'] = 0;
1125
            } elseif ($this->crop['start_x'] == 'right') {
1126
                $this->crop['start_x'] = $this->width - $width;
1127
            } elseif ($this->crop['start_x'] == 'center') {
1128
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1129
            }
1130
1131
            if ($this->crop['start_y'] == 'top') {
1132
                $this->crop['start_y'] = 0;
1133
            } elseif ($this->crop['start_y'] == 'bottom') {
1134
                $this->crop['start_y'] = $this->height - $height;
1135
            } elseif ($this->crop['start_y'] == 'center') {
1136
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1137
            }
1138
1139
            $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.");
1140
        }
1141
1142
        // Calculate new width and height if keeping aspect-ratio.
1143
        if ($this->keepRatio) {
1144
1145
            $this->log("Keep aspect ratio.");
1146
1147
            // Crop-to-fit and both new width and height are set.
1148
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1149
1150
                // Use newWidth and newHeigh as width/height, image should fit in box.
1151
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1152
1153
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1154
1155
                // Both new width and height are set.
1156
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1157
                $ratioWidth  = $width  / $this->newWidth;
1158
                $ratioHeight = $height / $this->newHeight;
1159
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1160
                $this->newWidth  = round($width  / $ratio);
1161
                $this->newHeight = round($height / $ratio);
1162
                $this->log("New width and height was set.");
1163
1164
            } elseif (isset($this->newWidth)) {
1165
1166
                // Use new width as max-width
1167
                $factor = (float)$this->newWidth / (float)$width;
1168
                $this->newHeight = round($factor * $height);
1169
                $this->log("New width was set.");
1170
1171
            } elseif (isset($this->newHeight)) {
1172
1173
                // Use new height as max-hight
1174
                $factor = (float)$this->newHeight / (float)$height;
1175
                $this->newWidth = round($factor * $width);
1176
                $this->log("New height was set.");
1177
1178
            } else {
1179
1180
                // Use existing width and height as new width and height.
1181
                $this->newWidth = $width;
1182
                $this->newHeight = $height;
1183
            }
1184
            
1185
1186
            // Get image dimensions for pre-resize image.
1187
            if ($this->cropToFit || $this->fillToFit) {
1188
1189
                // Get relations of original & target image
1190
                $ratioWidth  = $width  / $this->newWidth;
1191
                $ratioHeight = $height / $this->newHeight;
1192
1193
                if ($this->cropToFit) {
1194
1195
                    // Use newWidth and newHeigh as defined width/height,
1196
                    // image should fit the area.
1197
                    $this->log("Crop to fit.");
1198
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1199
                    $this->cropWidth  = round($width  / $ratio);
1200
                    $this->cropHeight = round($height / $ratio);
1201
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1202
1203
                } elseif ($this->fillToFit) {
1204
1205
                    // Use newWidth and newHeigh as defined width/height,
1206
                    // image should fit the area.
1207
                    $this->log("Fill to fit.");
1208
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1209
                    $this->fillWidth  = round($width  / $ratio);
1210
                    $this->fillHeight = round($height / $ratio);
1211
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1212
                }
1213
            }
1214
        }
1215
1216
        // Crop, ensure to set new width and height
1217
        if ($this->crop) {
1218
            $this->log("Crop.");
1219
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1220
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1221
        }
1222
1223
        // Fill to fit, ensure to set new width and height
1224
        /*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...
1225
            $this->log("FillToFit.");
1226
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1227
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1228
        }*/
1229
1230
        // No new height or width is set, use existing measures.
1231
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1232
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1233
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1234
1235
        return $this;
1236
    }
1237
1238
1239
1240
    /**
1241
     * Re-calculate image dimensions when original image dimension has changed.
1242
     *
1243
     * @return $this
1244
     */
1245
    public function reCalculateDimensions()
1246
    {
1247
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1248
1249
        $this->newWidth  = $this->newWidthOrig;
1250
        $this->newHeight = $this->newHeightOrig;
1251
        $this->crop      = $this->cropOrig;
1252
1253
        $this->initDimensions()
1254
             ->calculateNewWidthAndHeight();
1255
1256
        return $this;
1257
    }
1258
1259
1260
1261
    /**
1262
     * Set extension for filename to save as.
1263
     *
1264
     * @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...
1265
     *
1266
     * @return $this
1267
     */
1268
    public function setSaveAsExtension($saveAs = null)
1269
    {
1270
        if (isset($saveAs)) {
1271
            $saveAs = strtolower($saveAs);
1272
            $this->checkFileExtension($saveAs);
1273
            $this->saveAs = $saveAs;
1274
            $this->extension = $saveAs;
1275
        }
1276
1277
        $this->log("Prepare to save image as: " . $this->extension);
1278
1279
        return $this;
1280
    }
1281
1282
1283
1284
    /**
1285
     * Set JPEG quality to use when saving image
1286
     *
1287
     * @param int $quality as the quality to set.
1288
     *
1289
     * @return $this
1290
     */
1291
    public function setJpegQuality($quality = null)
1292
    {
1293
        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...
1294
            $this->useQuality = true;
1295
        }
1296
1297
        $this->quality = isset($quality)
1298
            ? $quality
1299
            : self::JPEG_QUALITY_DEFAULT;
1300
1301
        (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...
1302
            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...
1303
1304 2
        $this->log("Setting JPEG quality to {$this->quality}.");
1305
1306 2
        return $this;
1307 2
    }
1308 2
1309 2
1310 2
1311 2
    /**
1312 2
     * Set PNG compressen algorithm to use when saving image
1313 2
     *
1314 2
     * @param int $compress as the algorithm to use.
1315 2
     *
1316 2
     * @return $this
1317
     */
1318 2
    public function setPngCompression($compress = null)
1319 2
    {
1320
        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...
1321 2
            $this->useCompress = true;
1322 2
        }
1323
1324
        $this->compress = isset($compress)
1325
            ? $compress
1326 2
            : self::PNG_COMPRESSION_DEFAULT;
1327 2
1328
        (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...
1329 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...
1330 2
1331 2
        $this->log("Setting PNG compression level to {$this->compress}.");
1332
1333 2
        return $this;
1334 2
    }
1335 2
1336
1337 2
1338 2
    /**
1339
     * Use original image if possible, check options which affects image processing.
1340
     *
1341
     * @param boolean $useOrig default is to use original if possible, else set to false.
1342
     *
1343
     * @return $this
1344
     */
1345
    public function useOriginalIfPossible($useOrig = true)
1346
    {
1347
        if ($useOrig
1348
            && ($this->newWidth == $this->width)
1349 2
            && ($this->newHeight == $this->height)
1350 2
            && !$this->area
1351 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...
1352 2
            && !$this->cropToFit
1353
            && !$this->fillToFit
1354 2
            && !$this->filters
1355
            && !$this->sharpen
1356 2
            && !$this->emboss
1357 2
            && !$this->blur
1358 2
            && !$this->convolve
1359
            && !$this->palette
1360 2
            && !$this->useQuality
1361 2
            && !$this->useCompress
1362
            && !$this->saveAs
1363
            && !$this->rotateBefore
1364
            && !$this->rotateAfter
1365 2
            && !$this->autoRotate
1366 2
            && !$this->bgColor
1367
            && ($this->upscale === self::UPSCALE_DEFAULT)
1368
        ) {
1369
            $this->log("Using original image.");
1370 2
            $this->output($this->pathToImage);
1371 2
        }
1372
1373
        return $this;
1374
    }
1375
1376
1377 2
1378 2
    /**
1379 2
     * Generate filename to save file in cache.
1380 2
     *
1381 2
     * @param string  $base      as optional basepath for storing file.
1382 2
     * @param boolean $useSubdir use or skip the subdir part when creating the
1383 2
     *                           filename.
1384
     * @param string  $prefix    to add as part of filename
1385 2
     *
1386
     * @return $this
1387
     */
1388
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1389
    {
1390
        $filename     = basename($this->pathToImage);
1391
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1392
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1393
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1394
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1395
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1396
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1397
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1398
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1399
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1400
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1401
1402
        $saveAs = $this->normalizeFileExtension();
1403
        $saveAs = $saveAs ? "_$saveAs" : null;
1404
1405
        $copyStrat = null;
1406
        if ($this->copyStrategy === self::RESIZE) {
1407
            $copyStrat = "_rs";
1408
        }
1409
1410
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1411
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1412
1413
        $offset = isset($this->offset)
1414
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1415
            : null;
1416
1417
        $crop = $this->crop
1418
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1419
            : null;
1420
1421
        $filters = null;
1422
        if (isset($this->filters)) {
1423
            foreach ($this->filters as $filter) {
1424
                if (is_array($filter)) {
1425
                    $filters .= "_f{$filter['id']}";
1426
                    for ($i=1; $i<=$filter['argc']; $i++) {
1427
                        $filters .= "-".$filter["arg{$i}"];
1428
                    }
1429
                }
1430
            }
1431
        }
1432
1433
        $sharpen = $this->sharpen ? 's' : null;
1434
        $emboss  = $this->emboss  ? 'e' : null;
1435
        $blur    = $this->blur    ? 'b' : null;
1436
        $palette = $this->palette ? 'p' : null;
1437
1438
        $autoRotate = $this->autoRotate ? 'ar' : null;
1439
1440
        $optimize  = $this->jpegOptimize ? 'o' : null;
1441
        $optimize .= $this->pngFilter    ? 'f' : null;
1442
        $optimize .= $this->pngDeflate   ? 'd' : null;
1443
1444
        $convolve = null;
1445
        if ($this->convolve) {
1446
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1447
        }
1448
1449
        $upscale = null;
1450
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1451
            $upscale = '_nu';
1452
        }
1453
1454
        $subdir = null;
1455
        if ($useSubdir === true) {
1456
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1457
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1458
            $subdir .= '_';
1459
        }
1460
1461
        $file = $prefix . $subdir . $filename . $width . $height
1462
            . $offset . $crop . $cropToFit . $fillToFit
1463
            . $crop_x . $crop_y . $upscale
1464
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1465
            . $optimize . $compress
1466
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1467
            . $convolve . $copyStrat . $saveAs;
1468
1469
        return $this->setTarget($file, $base);
1470
    }
1471
1472
1473
1474
    /**
1475
     * Use cached version of image, if possible.
1476
     *
1477
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1478
     *
1479
     * @return $this
1480
     */
1481
    public function useCacheIfPossible($useCache = true)
1482
    {
1483
        if ($useCache && is_readable($this->cacheFileName)) {
1484
            $fileTime   = filemtime($this->pathToImage);
1485
            $cacheTime  = filemtime($this->cacheFileName);
1486
1487
            if ($fileTime <= $cacheTime) {
1488
                if ($this->useCache) {
1489
                    if ($this->verbose) {
1490
                        $this->log("Use cached file.");
1491
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1492
                    }
1493
                    $this->output($this->cacheFileName, $this->outputFormat);
1494
                } else {
1495
                    $this->log("Cache is valid but ignoring it by intention.");
1496
                }
1497
            } else {
1498
                $this->log("Original file is modified, ignoring cache.");
1499
            }
1500
        } else {
1501
            $this->log("Cachefile does not exists or ignoring it.");
1502
        }
1503
1504
        return $this;
1505
    }
1506
1507
1508
1509
    /**
1510
     * Load image from disk. Try to load image without verbose error message,
1511
     * if fail, load again and display error messages.
1512
     *
1513
     * @param string $src of image.
1514
     * @param string $dir as base directory where images are.
1515
     *
1516
     * @return $this
1517
     *
1518
     */
1519
    public function load($src = null, $dir = null)
1520
    {
1521
        if (isset($src)) {
1522
            $this->setSource($src, $dir);
1523
        }
1524
1525
        $this->loadImageDetails();
1526
1527
        if ($this->fileType === IMG_WEBP) {
1528
            $this->image = imagecreatefromwebp($this->pathToImage);
1529
        } else {
1530
            $imageAsString = file_get_contents($this->pathToImage);
1531
            $this->image = imagecreatefromstring($imageAsString);
1532
        }
1533
        if ($this->image === false) {
1534
            throw new Exception("Could not load image.");
1535
        }
1536
1537
        /* 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...
1538
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1539
            $type = $this->getPngType();
1540
            $hasFewColors = imagecolorstotal($this->image);
1541
1542
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1543
                if ($this->verbose) {
1544
                    $this->log("Handle this image as a palette image.");
1545
                }
1546
                $this->palette = true;
1547
            }
1548
        }
1549
        */
1550
1551
        if ($this->verbose) {
1552
            $this->log("### Image successfully loaded from file.");
1553
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1554
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1555
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1556
            $index = imagecolortransparent($this->image);
1557
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1558
        }
1559
1560
        return $this;
1561
    }
1562
1563
1564
1565
    /**
1566
     * Get the type of PNG image.
1567
     *
1568
     * @param string $filename to use instead of default.
1569
     *
1570
     * @return int as the type of the png-image
1571
     *
1572
     */
1573
    public function getPngType($filename = null)
1574
    {
1575
        $filename = $filename ? $filename : $this->pathToImage;
1576
1577
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1578
1579
        if ($this->verbose) {
1580
            $this->log("Checking png type of: " . $filename);
1581
            $this->log($this->getPngTypeAsString($pngType));
1582
        }
1583
1584
        return $pngType;
1585
    }
1586
1587
1588
1589
    /**
1590
     * Get the type of PNG image as a verbose string.
1591
     *
1592
     * @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...
1593
     * @param string  $filename to use instead of default.
1594
     *
1595
     * @return int as the type of the png-image
1596
     *
1597
     */
1598
    private function getPngTypeAsString($pngType = null, $filename = null)
1599
    {
1600
        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...
1601
            $pngType = $this->getPngType($filename);
1602
        }
1603
1604
        $index = imagecolortransparent($this->image);
1605
        $transparent = null;
1606
        if ($index != -1) {
1607
            $transparent = " (transparent)";
1608
        }
1609
1610
        switch ($pngType) {
1611
1612
            case self::PNG_GREYSCALE:
1613
                $text = "PNG is type 0, Greyscale$transparent";
1614
                break;
1615
1616
            case self::PNG_RGB:
1617
                $text = "PNG is type 2, RGB$transparent";
1618
                break;
1619
1620
            case self::PNG_RGB_PALETTE:
1621
                $text = "PNG is type 3, RGB with palette$transparent";
1622
                break;
1623
1624
            case self::PNG_GREYSCALE_ALPHA:
1625
                $text = "PNG is type 4, Greyscale with alpha channel";
1626
                break;
1627
1628
            case self::PNG_RGB_ALPHA:
1629
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1630
                break;
1631
1632
            default:
1633
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1634
        }
1635
1636
        return $text;
1637
    }
1638
1639
1640
1641
1642
    /**
1643
     * Calculate number of colors in an image.
1644
     *
1645
     * @param resource $im the image.
1646
     *
1647
     * @return int
1648
     */
1649
    private function colorsTotal($im)
1650
    {
1651
        if (imageistruecolor($im)) {
1652
            $this->log("Colors as true color.");
1653
            $h = imagesy($im);
1654
            $w = imagesx($im);
1655
            $c = array();
1656
            for ($x=0; $x < $w; $x++) {
1657
                for ($y=0; $y < $h; $y++) {
1658
                    @$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...
1659
                }
1660
            }
1661
            return count($c);
1662
        } else {
1663
            $this->log("Colors as palette.");
1664
            return imagecolorstotal($im);
1665
        }
1666
    }
1667
1668
1669
1670
    /**
1671
     * Preprocess image before rezising it.
1672
     *
1673
     * @return $this
1674
     */
1675
    public function preResize()
1676
    {
1677
        $this->log("### Pre-process before resizing");
1678
1679
        // Rotate image
1680
        if ($this->rotateBefore) {
1681
            $this->log("Rotating image.");
1682
            $this->rotate($this->rotateBefore, $this->bgColor)
1683
                 ->reCalculateDimensions();
1684
        }
1685
1686
        // Auto-rotate image
1687
        if ($this->autoRotate) {
1688
            $this->log("Auto rotating image.");
1689
            $this->rotateExif()
1690
                 ->reCalculateDimensions();
1691
        }
1692
1693
        // Scale the original image before starting
1694
        if (isset($this->scale)) {
1695
            $this->log("Scale by {$this->scale}%");
1696
            $newWidth  = $this->width * $this->scale / 100;
1697
            $newHeight = $this->height * $this->scale / 100;
1698
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1699
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1700
            $this->image = $img;
1701
            $this->width = $newWidth;
1702
            $this->height = $newHeight;
1703
        }
1704
1705
        return $this;
1706
    }
1707
1708
1709
1710
    /**
1711
     * Resize or resample the image while resizing.
1712
     *
1713
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1714
     *
1715
     * @return $this
1716
     */
1717
     public function setCopyResizeStrategy($strategy)
1718
     {
1719
         $this->copyStrategy = $strategy;
1720
         return $this;
1721
     }
1722
1723
1724
1725
    /**
1726
     * Resize and or crop the image.
1727
     *
1728
     * @return void
1729
     */
1730
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1731
    {
1732
        if($this->copyStrategy == self::RESIZE) {
1733
            $this->log("Copy by resize");
1734
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1735
        } else {
1736
            $this->log("Copy by resample");
1737
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1738
        }
1739
    }
1740
1741
1742
1743
    /**
1744
     * Resize and or crop the image.
1745
     *
1746
     * @return $this
1747
     */
1748
    public function resize()
1749
    {
1750
1751
        $this->log("### Starting to Resize()");
1752
        $this->log("Upscale = '$this->upscale'");
1753
1754
        // Only use a specified area of the image, $this->offset is defining the area to use
1755
        if (isset($this->offset)) {
1756
1757
            $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']}");
1758
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1759
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1760
            $this->image = $img;
1761
            $this->width = $this->offset['width'];
1762
            $this->height = $this->offset['height'];
1763
        }
1764
1765
        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...
1766
1767
            // Do as crop, take only part of image
1768
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1769
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1770
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1771
            $this->image = $img;
1772
            $this->width = $this->crop['width'];
1773
            $this->height = $this->crop['height'];
1774
        }
1775
1776
        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...
1777
            // Consider rewriting the no-upscale code to fit within this if-statement,
1778
            // likely to be more readable code.
1779
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1780
        }
1781
1782
        if ($this->cropToFit) {
1783
1784
            // Resize by crop to fit
1785
            $this->log("Resizing using strategy - Crop to fit");
1786
1787
            if (!$this->upscale 
1788
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1789
                $this->log("Resizing - smaller image, do not upscale.");
1790
1791
                $posX = 0;
1792
                $posY = 0;
1793
                $cropX = 0;
1794
                $cropY = 0;
1795
1796
                if ($this->newWidth > $this->width) {
1797
                    $posX = round(($this->newWidth - $this->width) / 2);
1798
                }
1799
                if ($this->newWidth < $this->width) {
1800
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1801
                }
1802
1803
                if ($this->newHeight > $this->height) {
1804
                    $posY = round(($this->newHeight - $this->height) / 2);
1805
                }
1806
                if ($this->newHeight < $this->height) {
1807
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1808
                }
1809
                $this->log(" cwidth: $this->cropWidth");
1810
                $this->log(" cheight: $this->cropHeight");
1811
                $this->log(" nwidth: $this->newWidth");
1812
                $this->log(" nheight: $this->newHeight");
1813
                $this->log(" width: $this->width");
1814
                $this->log(" height: $this->height");
1815
                $this->log(" posX: $posX");
1816
                $this->log(" posY: $posY");
1817
                $this->log(" cropX: $cropX");
1818
                $this->log(" cropY: $cropY");
1819
1820
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1821
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1822
            } else {
1823
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1824
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1825
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1826
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1827
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1828
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1829
            }
1830
1831
            $this->image = $imageResized;
1832
            $this->width = $this->newWidth;
1833
            $this->height = $this->newHeight;
1834
1835
        } elseif ($this->fillToFit) {
1836
1837
            // Resize by fill to fit
1838
            $this->log("Resizing using strategy - Fill to fit");
1839
1840
            $posX = 0;
1841
            $posY = 0;
1842
1843
            $ratioOrig = $this->width / $this->height;
1844
            $ratioNew  = $this->newWidth / $this->newHeight;
1845
1846
            // Check ratio for landscape or portrait
1847
            if ($ratioOrig < $ratioNew) {
1848
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1849
            } else {
1850
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1851
            }
1852
1853
            if (!$this->upscale
1854
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1855
            ) {
1856
1857
                $this->log("Resizing - smaller image, do not upscale.");
1858
                $posX = round(($this->newWidth - $this->width) / 2);
1859
                $posY = round(($this->newHeight - $this->height) / 2);
1860
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1861
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1862
1863
            } else {
1864
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1865
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1866
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1867
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1868
            }
1869
1870
            $this->image = $imageResized;
1871
            $this->width = $this->newWidth;
1872
            $this->height = $this->newHeight;
1873
1874
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1875
1876
            // Resize it
1877
            $this->log("Resizing, new height and/or width");
1878
1879
            if (!$this->upscale
1880
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1881
            ) {
1882
                $this->log("Resizing - smaller image, do not upscale.");
1883
1884
                if (!$this->keepRatio) {
1885
                    $this->log("Resizing - stretch to fit selected.");
1886
1887
                    $posX = 0;
1888
                    $posY = 0;
1889
                    $cropX = 0;
1890
                    $cropY = 0;
1891
1892
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1893
                        $posX = round(($this->newWidth - $this->width) / 2);
1894
                        $posY = round(($this->newHeight - $this->height) / 2);
1895
                    } elseif ($this->newWidth > $this->width) {
1896
                        $posX = round(($this->newWidth - $this->width) / 2);
1897
                        $cropY = round(($this->height - $this->newHeight) / 2);
1898
                    } elseif ($this->newHeight > $this->height) {
1899
                        $posY = round(($this->newHeight - $this->height) / 2);
1900
                        $cropX = round(($this->width - $this->newWidth) / 2);
1901
                    }
1902
1903
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1904
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1905
                    $this->image = $imageResized;
1906
                    $this->width = $this->newWidth;
1907
                    $this->height = $this->newHeight;
1908
                }
1909
            } else {
1910
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1911
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1912
                $this->image = $imageResized;
1913
                $this->width = $this->newWidth;
1914
                $this->height = $this->newHeight;
1915
            }
1916
        }
1917
1918
        return $this;
1919
    }
1920
1921
1922
1923
    /**
1924
     * Postprocess image after rezising image.
1925
     *
1926
     * @return $this
1927
     */
1928
    public function postResize()
1929
    {
1930
        $this->log("### Post-process after resizing");
1931
1932
        // Rotate image
1933
        if ($this->rotateAfter) {
1934
            $this->log("Rotating image.");
1935
            $this->rotate($this->rotateAfter, $this->bgColor);
1936
        }
1937
1938
        // Apply filters
1939
        if (isset($this->filters) && is_array($this->filters)) {
1940
1941
            foreach ($this->filters as $filter) {
1942
                $this->log("Applying filter {$filter['type']}.");
1943
1944
                switch ($filter['argc']) {
1945
1946
                    case 0:
1947
                        imagefilter($this->image, $filter['type']);
1948
                        break;
1949
1950
                    case 1:
1951
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1952
                        break;
1953
1954
                    case 2:
1955
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1956
                        break;
1957
1958
                    case 3:
1959
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1960
                        break;
1961
1962
                    case 4:
1963
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1964
                        break;
1965
                }
1966
            }
1967
        }
1968
1969
        // Convert to palette image
1970
        if ($this->palette) {
1971
            $this->log("Converting to palette image.");
1972
            $this->trueColorToPalette();
1973
        }
1974
1975
        // Blur the image
1976
        if ($this->blur) {
1977
            $this->log("Blur.");
1978
            $this->blurImage();
1979
        }
1980
1981
        // Emboss the image
1982
        if ($this->emboss) {
1983
            $this->log("Emboss.");
1984
            $this->embossImage();
1985
        }
1986
1987
        // Sharpen the image
1988
        if ($this->sharpen) {
1989
            $this->log("Sharpen.");
1990
            $this->sharpenImage();
1991
        }
1992
1993
        // Custom convolution
1994
        if ($this->convolve) {
1995
            //$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...
1996
            $this->imageConvolution();
1997
        }
1998
1999
        return $this;
2000
    }
2001
2002
2003
2004
    /**
2005
     * Rotate image using angle.
2006
     *
2007
     * @param float $angle        to rotate image.
2008
     * @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...
2009
     *
2010
     * @return $this
2011
     */
2012
    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...
2013
    {
2014
        $this->log("Rotate image " . $angle . " degrees with filler color.");
2015
2016
        $color = $this->getBackgroundColor();
2017
        $this->image = imagerotate($this->image, $angle, $color);
2018
2019
        $this->width  = imagesx($this->image);
2020
        $this->height = imagesy($this->image);
2021
2022
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2023
2024
        return $this;
2025
    }
2026
2027
2028
2029
    /**
2030
     * Rotate image using information in EXIF.
2031
     *
2032
     * @return $this
2033
     */
2034
    public function rotateExif()
2035
    {
2036
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2037
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2038
            return $this;
2039
        }
2040
2041
        $exif = exif_read_data($this->pathToImage);
2042
2043
        if (!empty($exif['Orientation'])) {
2044
            switch ($exif['Orientation']) {
2045
                case 3:
2046
                    $this->log("Autorotate 180.");
2047
                    $this->rotate(180, $this->bgColor);
2048
                    break;
2049
2050
                case 6:
2051
                    $this->log("Autorotate -90.");
2052
                    $this->rotate(-90, $this->bgColor);
2053
                    break;
2054
2055
                case 8:
2056
                    $this->log("Autorotate 90.");
2057
                    $this->rotate(90, $this->bgColor);
2058
                    break;
2059
2060
                default:
2061
                    $this->log("Autorotate ignored, unknown value as orientation.");
2062
            }
2063
        } else {
2064
            $this->log("Autorotate ignored, no orientation in EXIF.");
2065
        }
2066
2067
        return $this;
2068
    }
2069
2070
2071
2072
    /**
2073
     * Convert true color image to palette image, keeping alpha.
2074
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2075
     *
2076
     * @return void
2077
     */
2078
    public function trueColorToPalette()
2079
    {
2080
        $img = imagecreatetruecolor($this->width, $this->height);
2081
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2082
        imagecolortransparent($img, $bga);
2083
        imagefill($img, 0, 0, $bga);
2084
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2085
        imagetruecolortopalette($img, false, 255);
2086
        imagesavealpha($img, true);
2087
2088
        if (imageistruecolor($this->image)) {
2089
            $this->log("Matching colors with true color image.");
2090
            imagecolormatch($this->image, $img);
2091
        }
2092
2093
        $this->image = $img;
2094
    }
2095
2096
2097
2098
    /**
2099
     * Sharpen image using image convolution.
2100
     *
2101
     * @return $this
2102
     */
2103
    public function sharpenImage()
2104
    {
2105
        $this->imageConvolution('sharpen');
2106
        return $this;
2107
    }
2108
2109
2110
2111
    /**
2112
     * Emboss image using image convolution.
2113
     *
2114
     * @return $this
2115
     */
2116
    public function embossImage()
2117
    {
2118
        $this->imageConvolution('emboss');
2119
        return $this;
2120
    }
2121
2122
2123
2124
    /**
2125
     * Blur image using image convolution.
2126
     *
2127
     * @return $this
2128
     */
2129
    public function blurImage()
2130
    {
2131
        $this->imageConvolution('blur');
2132
        return $this;
2133
    }
2134
2135
2136
2137
    /**
2138
     * Create convolve expression and return arguments for image convolution.
2139
     *
2140
     * @param string $expression constant string which evaluates to a list of
2141
     *                           11 numbers separated by komma or such a list.
2142
     *
2143
     * @return array as $matrix (3x3), $divisor and $offset
2144
     */
2145
    public function createConvolveArguments($expression)
2146
    {
2147
        // Check of matching constant
2148
        if (isset($this->convolves[$expression])) {
2149
            $expression = $this->convolves[$expression];
2150
        }
2151
2152
        $part = explode(',', $expression);
2153
        $this->log("Creating convolution expressen: $expression");
2154
2155
        // Expect list of 11 numbers, split by , and build up arguments
2156
        if (count($part) != 11) {
2157
            throw new Exception(
2158
                "Missmatch in argument convolve. Expected comma-separated string with
2159
                11 float values. Got $expression."
2160
            );
2161
        }
2162
2163
        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...
2164
            if (!is_numeric($item)) {
2165
                throw new Exception("Argument to convolve expression should be float but is not.");
2166
            }
2167
        });
2168
2169
        return array(
2170
            array(
2171
                array($part[0], $part[1], $part[2]),
2172
                array($part[3], $part[4], $part[5]),
2173
                array($part[6], $part[7], $part[8]),
2174
            ),
2175
            $part[9],
2176
            $part[10],
2177
        );
2178
    }
2179
2180
2181 2
2182
    /**
2183 2
     * Add custom expressions (or overwrite existing) for image convolution.
2184
     *
2185 2
     * @param array $options Key value array with strings to be converted
2186
     *                       to convolution expressions.
2187 2
     *
2188 2
     * @return $this
2189 2
     */
2190 2
    public function addConvolveExpressions($options)
2191
    {
2192 2
        $this->convolves = array_merge($this->convolves, $options);
2193
        return $this;
2194
    }
2195 2
2196
2197
2198 2
    /**
2199
     * Image convolution.
2200
     *
2201
     * @param string $options A string with 11 float separated by comma.
2202
     *
2203
     * @return $this
2204
     */
2205
    public function imageConvolution($options = null)
2206
    {
2207
        // Use incoming options or use $this.
2208
        $options = $options ? $options : $this->convolve;
2209
2210
        // Treat incoming as string, split by +
2211
        $this->log("Convolution with '$options'");
2212
        $options = explode(":", $options);
2213
2214
        // Check each option if it matches constant value
2215 2
        foreach ($options as $option) {
2216
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2217 2
            imageconvolution($this->image, $matrix, $divisor, $offset);
2218 2
        }
2219 2
2220 2
        return $this;
2221
    }
2222 2
2223 2
2224 2
2225
    /**
2226 2
     * Set default background color between 000000-FFFFFF or if using
2227
     * alpha 00000000-FFFFFF7F.
2228
     *
2229
     * @param string $color as hex value.
2230
     *
2231
     * @return $this
2232
    */
2233
    public function setDefaultBackgroundColor($color)
2234
    {
2235 2
        $this->log("Setting default background color to '$color'.");
2236
2237 2
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2238 2
            throw new Exception(
2239 2
                "Background color needs a hex value of 6 or 8
2240 2
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2241
                Current value was: '$color'."
2242 2
            );
2243
        }
2244
2245
        $red    = hexdec(substr($color, 0, 2));
2246
        $green  = hexdec(substr($color, 2, 2));
2247
        $blue   = hexdec(substr($color, 4, 2));
2248
2249
        $alpha = (strlen($color) == 8)
2250
            ? hexdec(substr($color, 6, 2))
2251
            : null;
2252
2253
        if (($red < 0 || $red > 255)
2254
            || ($green < 0 || $green > 255)
2255
            || ($blue < 0 || $blue > 255)
2256
            || ($alpha < 0 || $alpha > 127)
2257
        ) {
2258
            throw new Exception(
2259
                "Background color out of range. Red, green blue
2260
                should be 00-FF and alpha should be 00-7F.
2261
                Current value was: '$color'."
2262
            );
2263
        }
2264
2265
        $this->bgColor = strtolower($color);
2266
        $this->bgColorDefault = array(
2267
            'red'   => $red,
2268
            'green' => $green,
2269
            'blue'  => $blue,
2270
            'alpha' => $alpha
2271
        );
2272
2273
        return $this;
2274
    }
2275
2276
2277
2278
    /**
2279
     * Get the background color.
2280
     *
2281
     * @param resource $img the image to work with or null if using $this->image.
2282
     *
2283
     * @return color value or null if no background color is set.
2284 2
    */
2285
    private function getBackgroundColor($img = null)
2286
    {
2287 2
        $img = isset($img) ? $img : $this->image;
2288
2289
        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...
2290 2
2291
            $red   = $this->bgColorDefault['red'];
2292
            $green = $this->bgColorDefault['green'];
2293
            $blue  = $this->bgColorDefault['blue'];
2294
            $alpha = $this->bgColorDefault['alpha'];
2295
2296
            if ($alpha) {
2297
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2298
            } else {
2299
                $color = imagecolorallocate($img, $red, $green, $blue);
2300
            }
2301
2302
            return $color;
2303
2304
        } else {
2305 2
            return 0;
2306
        }
2307 2
    }
2308
2309
2310
2311 2
    /**
2312
     * Create a image and keep transparency for png and gifs.
2313
     *
2314
     * @param int $width of the new image.
2315
     * @param int $height of the new image.
2316 2
     *
2317
     * @return image resource.
2318
    */
2319 2
    private function createImageKeepTransparency($width, $height)
2320 2
    {
2321
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2322
        $img = imagecreatetruecolor($width, $height);
2323 2
        imagealphablending($img, false);
2324 2
        imagesavealpha($img, true);
2325
2326
        $index = $this->image
2327
            ? imagecolortransparent($this->image)
2328
            : -1;
2329
2330
        if ($index != -1) {
2331
2332
            imagealphablending($img, true);
2333
            $transparent = imagecolorsforindex($this->image, $index);
2334
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2335
            imagefill($img, 0, 0, $color);
2336
            $index = imagecolortransparent($img, $color);
2337
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2338
2339
        } 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...
2340
2341
            $color = $this->getBackgroundColor($img);
2342 2
            imagefill($img, 0, 0, $color);
2343
            $this->Log("Filling image with background color.");
2344
        }
2345
2346
        return $img;
2347 2
    }
2348 2
2349 2
2350
2351
    /**
2352 2
     * Set optimizing  and post-processing options.
2353 2
     *
2354 2
     * @param array $options with config for postprocessing with external tools.
2355
     *
2356
     * @return $this
2357 2
     */
2358
    public function setPostProcessingOptions($options)
2359
    {
2360
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2361
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2362
        } else {
2363
            $this->jpegOptimizeCmd = null;
2364
        }
2365
2366
        if (array_key_exists("png_lossy", $options) 
2367
            && $options['png_lossy'] !== false) {
2368
            $this->pngLossy = $options['png_lossy'];
2369
            $this->pngLossyCmd = $options['png_lossy_cmd'];
2370 2
        } else {
2371
            $this->pngLossyCmd = null;
2372
        }
2373
2374
        if (isset($options['png_filter']) && $options['png_filter']) {
2375
            $this->pngFilterCmd = $options['png_filter_cmd'];
2376
        } else {
2377
            $this->pngFilterCmd = null;
2378
        }
2379
2380
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2381 2
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2382 2
        } else {
2383
            $this->pngDeflateCmd = null;
2384 2
        }
2385
2386
        return $this;
2387
    }
2388
2389
2390
2391
    /**
2392
     * Find out the type (file extension) for the image to be saved.
2393
     *
2394
     * @return string as image extension.
2395 2
     */
2396
    protected function getTargetImageExtension()
2397
    {
2398
        // switch on mimetype
2399
        if (isset($this->extension)) {
2400
            return strtolower($this->extension);
2401
        } elseif ($this->fileType === IMG_WEBP) {
2402
            return "webp";
2403
        }
2404
2405
        return substr(image_type_to_extension($this->fileType), 1);
2406
    }
2407
2408
2409
2410
    /**
2411
     * Save image.
2412
     *
2413
     * @param string  $src       as target filename.
2414
     * @param string  $base      as base directory where to store images.
2415
     * @param boolean $overwrite or not, default to always overwrite file.
2416
     *
2417
     * @return $this or false if no folder is set.
2418
     */
2419
    public function save($src = null, $base = null, $overwrite = true)
2420
    {
2421
        if (isset($src)) {
2422
            $this->setTarget($src, $base);
2423
        }
2424
2425
        if ($overwrite === false && is_file($this->cacheFileName)) {
2426
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2427
            return;
2428
        }
2429
2430
        is_writable($this->saveFolder)
2431
            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...
2432
2433
        $type = $this->getTargetImageExtension();
2434
        $this->Log("Saving image as " . $type);
2435
        switch($type) {
2436
2437
            case 'jpeg':
2438
            case 'jpg':
2439
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2440
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2441
2442
                // Use JPEG optimize if defined
2443
                if ($this->jpegOptimizeCmd) {
2444
                    if ($this->verbose) {
2445
                        clearstatcache();
2446
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2447
                    }
2448
                    $res = array();
2449
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2450
                    exec($cmd, $res);
2451
                    $this->log($cmd);
2452
                    $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...
2453
                }
2454
                break;
2455
2456
            case 'gif':
2457
                $this->Log("Saving image as GIF to cache.");
2458
                imagegif($this->image, $this->cacheFileName);
2459
                break;
2460
2461
            case 'webp':
2462
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2463
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2464
                break;
2465
2466
            case 'png':
2467
            default:
2468
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2469
2470
                // Turn off alpha blending and set alpha flag
2471
                imagealphablending($this->image, false);
2472
                imagesavealpha($this->image, true);
2473
                imagepng($this->image, $this->cacheFileName, $this->compress);
2474
2475
                // Use external program to process lossy PNG, if defined
2476
                $lossyEnabled = $this->pngLossy === true;
2477
                $lossySoftEnabled = $this->pngLossy === null;
2478
                $lossyActiveEnabled = $this->lossy === true;
2479
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
2480
                    if ($this->verbose) {
2481
                        clearstatcache();
2482
                        $this->log("Lossy enabled: $lossyEnabled");
2483
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
2484
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
2485
                    }
2486
                    $res = array();
2487
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
2488
                    exec($cmd, $res);
2489
                    $this->Log($cmd);
2490
                    $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...
2491
                }
2492
2493
                // Use external program to filter PNG, if defined
2494
                if ($this->pngFilterCmd) {
2495
                    if ($this->verbose) {
2496
                        clearstatcache();
2497
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2498
                    }
2499
                    $res = array();
2500
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2501
                    exec($cmd, $res);
2502
                    $this->Log($cmd);
2503
                    $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...
2504
                }
2505
2506
                // Use external program to deflate PNG, if defined
2507
                if ($this->pngDeflateCmd) {
2508
                    if ($this->verbose) {
2509
                        clearstatcache();
2510
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2511
                    }
2512
                    $res = array();
2513
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2514
                    exec($cmd, $res);
2515
                    $this->Log($cmd);
2516
                    $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...
2517
                }
2518
                break;
2519
        }
2520
2521
        if ($this->verbose) {
2522
            clearstatcache();
2523
            $this->log("Saved image to cache.");
2524
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2525
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2526
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2527
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2528
            $index = imagecolortransparent($this->image);
2529
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2530
        }
2531
2532
        return $this;
2533
    }
2534
2535
2536
2537
    /**
2538
     * Convert image from one colorpsace/color profile to sRGB without
2539
     * color profile.
2540
     *
2541
     * @param string  $src      of image.
2542
     * @param string  $dir      as base directory where images are.
2543
     * @param string  $cache    as base directory where to store images.
2544
     * @param string  $iccFile  filename of colorprofile.
2545
     * @param boolean $useCache or not, default to always use cache.
2546
     *
2547
     * @return string | boolean false if no conversion else the converted
2548
     *                          filename.
2549
     */
2550
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2551
    {
2552
        if ($this->verbose) {
2553
            $this->log("# Converting image to sRGB colorspace.");
2554
        }
2555
2556
        if (!class_exists("Imagick")) {
2557
            $this->log(" Ignoring since Imagemagick is not installed.");
2558
            return false;
2559
        }
2560
2561
        // Prepare
2562
        $this->setSaveFolder($cache)
2563
             ->setSource($src, $dir)
2564
             ->generateFilename(null, false, 'srgb_');
2565
2566
        // Check if the cached version is accurate.
2567
        if ($useCache && is_readable($this->cacheFileName)) {
2568
            $fileTime  = filemtime($this->pathToImage);
2569
            $cacheTime = filemtime($this->cacheFileName);
2570
2571
            if ($fileTime <= $cacheTime) {
2572
                $this->log(" Using cached version: " . $this->cacheFileName);
2573
                return $this->cacheFileName;
2574
            }
2575
        }
2576
2577
        // Only covert if cachedir is writable
2578
        if (is_writable($this->saveFolder)) {
2579
            // Load file and check if conversion is needed
2580
            $image      = new Imagick($this->pathToImage);
2581
            $colorspace = $image->getImageColorspace();
2582
            $this->log(" Current colorspace: " . $colorspace);
2583
2584
            $profiles      = $image->getImageProfiles('*', false);
2585
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2586
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2587
2588
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2589
                $this->log(" Converting to sRGB.");
2590
2591
                $sRGBicc = file_get_contents($iccFile);
2592
                $image->profileImage('icc', $sRGBicc);
2593
2594
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2595
                $image->writeImage($this->cacheFileName);
2596
                return $this->cacheFileName;
2597
            }
2598
        }
2599
2600
        return false;
2601
    }
2602
2603
2604
2605
    /**
2606
     * Create a hard link, as an alias, to the cached file.
2607
     *
2608
     * @param string $alias where to store the link,
2609
     *                      filename without extension.
2610
     *
2611
     * @return $this
2612
     */
2613
    public function linkToCacheFile($alias)
2614
    {
2615
        if ($alias === null) {
2616
            $this->log("Ignore creating alias.");
2617
            return $this;
2618
        }
2619
2620
        if (is_readable($alias)) {
2621
            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 871
  3. $aliasTarget is assigned
    in webroot/img.php on line 878
  4. $aliasTarget is passed to CImage::linkToCacheFile()
    in webroot/img.php on line 1180
  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 871
  3. $aliasTarget is assigned
    in webroot/img.php on line 878
  4. $aliasTarget is passed to CImage::linkToCacheFile()
    in webroot/img.php on line 1180

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

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

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