Issues (1933)

a/vendor/mos/cimage/webroot/imgp.php (97 issues)

1
<?php
2
/**
3
 * Resize and crop images on the fly, store generated images in a cache.
4
 *
5
 * This version is a all-in-one version of img.php, it is not dependant an any other file
6
 * so you can simply copy it to any place you want it.
7
 *
8
 * @author  Mikael Roos [email protected]
9
 * @example http://dbwebb.se/opensource/cimage
10
 * @link    https://github.com/mosbth/cimage
11
 *
12
 */
13
define("CIMAGE_BUNDLE", true);
14
15
16
/**
17
 * Change configuration details in the array below or create a separate file
18
 * where you store the configuration details.
19
 *
20
 * The configuration file should be named the same name as this file and then
21
 * add '_config.php'. If this file is named 'img.php' then name the
22
 * config file should be named 'img_config.php'.
23
 *
24
 * The settings below are only a few of the available ones. Check the file in
25
 * webroot/img_config.php for a complete list of configuration options.
26
 */
27
$config = array(
28
29
    //'mode'         => 'production',               // 'production', 'development', 'strict'
30
    //'image_path'   =>  __DIR__ . '/img/',
31
    //'cache_path'   =>  __DIR__ . '/../cache/',
32
    //'alias_path'   =>  __DIR__ . '/img/alias/',
33
    //'remote_allow' => true,
34
    //'password'     => false,                      // "secret-password",
35
36
);
37
38
39
40
// Version of cimage and img.php
41
define("CIMAGE_VERSION", "v0.7.23 (2020-05-06)");
42
43
// For CRemoteImage
44
define("CIMAGE_USER_AGENT", "CImage/" . CIMAGE_VERSION);
45
46
// Image type IMG_WEBP is only defined from 5.6.25
47
if (!defined("IMG_WEBP")) {
48
    define("IMG_WEBP", -1);
49
}
50
51
52
53
/**
54
 * General functions to use in img.php.
55
 */
56
57
58
59
/**
60
 * Trace and log execution to logfile, useful for debugging and development.
61
 *
62
 * @param string $msg message to log to file.
63
 *
64
 * @return void
65
 */
66
function trace($msg)
67
{
68
    $file = CIMAGE_DEBUG_FILE;
69
    if (!is_writable($file)) {
70
        return;
71
    }
72
73
    $timer = number_format((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 6);
74
    $details  = "{$timer}ms";
75
    $details .= ":" . round(memory_get_peak_usage()/1024/1024, 3) . "MB";
76
    $details .= ":" . count(get_included_files());
77
    file_put_contents($file, "$details:$msg\n", FILE_APPEND);
78
}
79
80
81
82
/**
83
 * Display error message.
84
 *
85
 * @param string $msg to display.
86
 * @param int $type of HTTP error to display.
87
 *
88
 * @return void
89
 */
90
function errorPage($msg, $type = 500)
91
{
92
    global $mode;
93
94
    switch ($type) {
95
        case 403:
96
            $header = "403 Forbidden";
97
            break;
98
        case 404:
99
            $header = "404 Not Found";
100
            break;
101
        default:
102
            $header = "500 Internal Server Error";
103
    }
104
105
    if ($mode == "strict") {
106
        $header = "404 Not Found";
107
    }
108
109
    header("HTTP/1.0 $header");
110
111
    if ($mode == "development") {
112
        die("[img.php] $msg");
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
113
    }
114
115
    error_log("[img.php] $msg");
116
    die("HTTP/1.0 $header");
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
117
}
118
119
120
121
/**
122
 * Get input from query string or return default value if not set.
123
 *
124
 * @param mixed $key     as string or array of string values to look for in $_GET.
125
 * @param mixed $default value to return when $key is not set in $_GET.
126
 *
127
 * @return mixed value from $_GET or default value.
128
 */
129
function get($key, $default = null)
130
{
131
    if (is_array($key)) {
132
        foreach ($key as $val) {
133
            if (isset($_GET[$val])) {
134
                return $_GET[$val];
135
            }
136
        }
137
    } elseif (isset($_GET[$key])) {
138
        return $_GET[$key];
139
    }
140
    return $default;
141
}
142
143
144
145
/**
146
 * Get input from query string and set to $defined if defined or else $undefined.
147
 *
148
 * @param mixed $key       as string or array of string values to look for in $_GET.
149
 * @param mixed $defined   value to return when $key is set in $_GET.
150
 * @param mixed $undefined value to return when $key is not set in $_GET.
151
 *
152
 * @return mixed value as $defined or $undefined.
153
 */
154
function getDefined($key, $defined, $undefined)
155
{
156
    return get($key) === null ? $undefined : $defined;
157
}
158
159
160
161
/**
162
 * Get value from config array or default if key is not set in config array.
163
 *
164
 * @param string $key    the key in the config array.
165
 * @param mixed $default value to be default if $key is not set in config.
166
 *
167
 * @return mixed value as $config[$key] or $default.
168
 */
169
function getConfig($key, $default)
170
{
171
    global $config;
172
    return isset($config[$key])
173
        ? $config[$key]
174
        : $default;
175
}
176
177
178
179
/**
180
 * Log when verbose mode, when used without argument it returns the result.
181
 *
182
 * @param string $msg to log.
183
 *
184
 * @return void or array.
185
 */
186
function verbose($msg = null)
187
{
188
    global $verbose, $verboseFile;
189
    static $log = array();
190
191
    if (!($verbose || $verboseFile)) {
192
        return;
193
    }
194
195
    if (is_null($msg)) {
196
        return $log;
197
    }
198
199
    $log[] = $msg;
200
}
201
202
203
204
/**
205
 * Log when verbose mode, when used without argument it returns the result.
206
 *
207
 * @param string $msg to log.
208
 *
209
 * @return void or array.
210
 */
211
function checkExternalCommand($what, $enabled, $commandString)
212
{
213
    $no = $enabled ? null : 'NOT';
214
    $text = "Post processing $what is $no enabled.<br>";
215
216
    list($command) = explode(" ", $commandString);
217
    $no = is_executable($command) ? null : 'NOT';
218
    $text .= "The command for $what is $no an executable.<br>";
219
220
    return $text;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $text returns the type string which is incompatible with the documented return type void.
Loading history...
221
}
222
223
224
225
/**
226
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
227
 *
228
 */
229
class CHttpGet
230
{
231
    private $request  = array();
232
    private $response = array();
233
234
235
236
    /**
237
    * Constructor
238
    *
239
    */
240
    public function __construct()
241
    {
242
        $this->request['header'] = array();
243
    }
244
245
246
247
    /**
248
     * Build an encoded url.
249
     *
250
     * @param string $baseUrl This is the original url which will be merged.
251
     * @param string $merge   Thse parts should be merged into the baseUrl,
252
     *                        the format is as parse_url.
253
     *
254
     * @return string $url as the modified url.
255
     */
256
    public function buildUrl($baseUrl, $merge)
257
    {
258
        $parts = parse_url($baseUrl);
259
        $parts = array_merge($parts, $merge);
0 ignored issues
show
$merge of type string is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
        $parts = array_merge($parts, /** @scrutinizer ignore-type */ $merge);
Loading history...
260
261
        $url  = $parts['scheme'];
262
        $url .= "://";
263
        $url .= $parts['host'];
264
        $url .= isset($parts['port'])
265
            ? ":" . $parts['port']
266
            : "" ;
267
        $url .= $parts['path'];
268
269
        return $url;
270
    }
271
272
273
274
    /**
275
     * Set the url for the request.
276
     *
277
     * @param string $url
278
     *
279
     * @return $this
280
     */
281
    public function setUrl($url)
282
    {
283
        $parts = parse_url($url);
284
        
285
        $path = "";
286
        if (isset($parts['path'])) {
287
            $pathParts = explode('/', $parts['path']);
288
            unset($pathParts[0]);
289
            foreach ($pathParts as $value) {
290
                $path .= "/" . rawurlencode($value);
291
            }
292
        }
293
        $url = $this->buildUrl($url, array("path" => $path));
0 ignored issues
show
array('path' => $path) of type array<string,string> is incompatible with the type string expected by parameter $merge of CHttpGet::buildUrl(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

293
        $url = $this->buildUrl($url, /** @scrutinizer ignore-type */ array("path" => $path));
Loading history...
294
295
        $this->request['url'] = $url;
296
        return $this;
297
    }
298
299
300
301
    /**
302
     * Set custom header field for the request.
303
     *
304
     * @param string $field
305
     * @param string $value
306
     *
307
     * @return $this
308
     */
309
    public function setHeader($field, $value)
310
    {
311
        $this->request['header'][] = "$field: $value";
312
        return $this;
313
    }
314
315
316
317
    /**
318
     * Set header fields for the request.
319
     *
320
     * @param string $field
321
     * @param string $value
322
     *
323
     * @return $this
324
     */
325
    public function parseHeader()
326
    {
327
        //$header = explode("\r\n", rtrim($this->response['headerRaw'], "\r\n"));
328
        
329
        $rawHeaders = rtrim($this->response['headerRaw'], "\r\n");
330
        # Handle multiple responses e.g. with redirections (proxies too)
331
        $headerGroups = explode("\r\n\r\n", $rawHeaders);
332
        # We're only interested in the last one
333
        $header = explode("\r\n", end($headerGroups));
334
335
        $output = array();
336
337
        if ('HTTP' === substr($header[0], 0, 4)) {
338
            list($output['version'], $output['status']) = explode(' ', $header[0]);
339
            unset($header[0]);
340
        }
341
342
        foreach ($header as $entry) {
343
            $pos = strpos($entry, ':');
344
            $output[trim(substr($entry, 0, $pos))] = trim(substr($entry, $pos + 1));
345
        }
346
347
        $this->response['header'] = $output;
348
        return $this;
349
    }
350
351
352
353
    /**
354
     * Perform the request.
355
     *
356
     * @param boolean $debug set to true to dump headers.
357
     *
358
     * @throws Exception when curl fails to retrieve url.
359
     *
360
     * @return boolean
361
     */
362
    public function doGet($debug = false)
363
    {
364
        $options = array(
365
            CURLOPT_URL             => $this->request['url'],
366
            CURLOPT_HEADER          => 1,
367
            CURLOPT_HTTPHEADER      => $this->request['header'],
368
            CURLOPT_AUTOREFERER     => true,
369
            CURLOPT_RETURNTRANSFER  => true,
370
            CURLINFO_HEADER_OUT     => $debug,
371
            CURLOPT_CONNECTTIMEOUT  => 5,
372
            CURLOPT_TIMEOUT         => 5,
373
            CURLOPT_FOLLOWLOCATION  => true,
374
            CURLOPT_MAXREDIRS       => 2,
375
        );
376
377
        $ch = curl_init();
378
        curl_setopt_array($ch, $options);
379
        $response = curl_exec($ch);
380
381
        if (!$response) {
382
            throw new Exception("Failed retrieving url, details follows: " . curl_error($ch));
383
        }
384
385
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
386
        $this->response['headerRaw'] = substr($response, 0, $headerSize);
0 ignored issues
show
It seems like $response can also be of type true; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

386
        $this->response['headerRaw'] = substr(/** @scrutinizer ignore-type */ $response, 0, $headerSize);
Loading history...
387
        $this->response['body']      = substr($response, $headerSize);
388
389
        $this->parseHeader();
390
391
        if ($debug) {
392
            $info = curl_getinfo($ch);
393
            echo "Request header<br><pre>", var_dump($info['request_header']), "</pre>";
0 ignored issues
show
Are you sure the usage of var_dump($info['request_header']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Security Debugging Code introduced by
var_dump($info['request_header']) looks like debug code. Are you sure you do not want to remove it?
Loading history...
Are you sure var_dump($info['request_header']) of type void can be used in echo? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

393
            echo "Request header<br><pre>", /** @scrutinizer ignore-type */ var_dump($info['request_header']), "</pre>";
Loading history...
394
            echo "Response header (raw)<br><pre>", var_dump($this->response['headerRaw']), "</pre>";
0 ignored issues
show
Are you sure the usage of var_dump($this->response['headerRaw']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Are you sure var_dump($this->response['headerRaw']) of type void can be used in echo? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

394
            echo "Response header (raw)<br><pre>", /** @scrutinizer ignore-type */ var_dump($this->response['headerRaw']), "</pre>";
Loading history...
395
            echo "Response header (parsed)<br><pre>", var_dump($this->response['header']), "</pre>";
0 ignored issues
show
Are you sure var_dump($this->response['header']) of type void can be used in echo? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

395
            echo "Response header (parsed)<br><pre>", /** @scrutinizer ignore-type */ var_dump($this->response['header']), "</pre>";
Loading history...
Are you sure the usage of var_dump($this->response['header']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
396
        }
397
398
        curl_close($ch);
399
        return true;
400
    }
401
402
403
404
    /**
405
     * Get HTTP code of response.
406
     *
407
     * @return integer as HTTP status code or null if not available.
408
     */
409
    public function getStatus()
410
    {
411
        return isset($this->response['header']['status'])
412
            ? (int) $this->response['header']['status']
413
            : null;
414
    }
415
416
417
418
    /**
419
     * Get file modification time of response.
420
     *
421
     * @return int as timestamp.
422
     */
423
    public function getLastModified()
424
    {
425
        return isset($this->response['header']['Last-Modified'])
426
            ? strtotime($this->response['header']['Last-Modified'])
427
            : null;
428
    }
429
430
431
432
    /**
433
     * Get content type.
434
     *
435
     * @return string as the content type or null if not existing or invalid.
436
     */
437
    public function getContentType()
438
    {
439
        $type = isset($this->response['header']['Content-Type'])
440
            ? $this->response['header']['Content-Type']
441
            : null;
442
443
        return preg_match('#[a-z]+/[a-z]+#', $type)
0 ignored issues
show
It seems like $type can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

443
        return preg_match('#[a-z]+/[a-z]+#', /** @scrutinizer ignore-type */ $type)
Loading history...
444
            ? $type
445
            : null;
446
    }
447
448
449
450
    /**
451
     * Get file modification time of response.
452
     *
453
     * @param mixed $default as default value (int seconds) if date is
454
     *                       missing in response header.
455
     *
456
     * @return int as timestamp or $default if Date is missing in
457
     *             response header.
458
     */
459
    public function getDate($default = false)
460
    {
461
        return isset($this->response['header']['Date'])
0 ignored issues
show
Bug Best Practice introduced by
The expression return IssetNode ? strto...r']['Date']) : $default could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
462
            ? strtotime($this->response['header']['Date'])
463
            : $default;
464
    }
465
466
467
468
    /**
469
     * Get max age of cachable item.
470
     *
471
     * @param mixed $default as default value if date is missing in response
472
     *                       header.
473
     *
474
     * @return int as timestamp or false if not available.
475
     */
476
    public function getMaxAge($default = false)
477
    {
478
        $cacheControl = isset($this->response['header']['Cache-Control'])
479
            ? $this->response['header']['Cache-Control']
480
            : null;
481
482
        $maxAge = null;
483
        if ($cacheControl) {
484
            // max-age=2592000
485
            $part = explode('=', $cacheControl);
486
            $maxAge = ($part[0] == "max-age")
487
                ? (int) $part[1]
488
                : null;
489
        }
490
491
        if ($maxAge) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $maxAge of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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...
492
            return $maxAge;
493
        }
494
495
        $expire = isset($this->response['header']['Expires'])
496
            ? strtotime($this->response['header']['Expires'])
497
            : null;
498
499
        return $expire ? $expire : $default;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $expire ? $expire : $default could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
500
    }
501
502
503
504
    /**
505
     * Get body of response.
506
     *
507
     * @return string as body.
508
     */
509
    public function getBody()
510
    {
511
        return $this->response['body'];
512
    }
513
}
514
515
516
517
/**
518
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
519
 *
520
 */
521
class CRemoteImage
522
{
523
    /**
524
     * Path to cache files.
525
     */
526
    private $saveFolder = null;
527
528
529
530
    /**
531
     * Use cache or not.
532
     */
533
    private $useCache = true;
534
535
536
537
    /**
538
     * HTTP object to aid in download file.
539
     */
540
    private $http;
541
542
543
544
    /**
545
     * Status of the HTTP request.
546
     */
547
    private $status;
548
549
550
551
    /**
552
     * Defalt age for cached items 60*60*24*7.
553
     */
554
    private $defaultMaxAge = 604800;
555
556
557
558
    /**
559
     * Url of downloaded item.
560
     */
561
    private $url;
562
563
564
565
    /**
566
     * Base name of cache file for downloaded item and name of image.
567
     */
568
    private $fileName;
569
570
571
572
    /**
573
     * Filename for json-file with details of cached item.
574
     */
575
    private $fileJson;
576
577
578
579
    /**
580
     * Cache details loaded from file.
581
     */
582
    private $cache;
583
584
585
586
    /**
587
     * Get status of last HTTP request.
588
     *
589
     * @return int as status
590
     */
591
    public function getStatus()
592
    {
593
        return $this->status;
594
    }
595
596
597
598
    /**
599
     * Get JSON details for cache item.
600
     *
601
     * @return array with json details on cache.
602
     */
603
    public function getDetails()
604
    {
605
        return $this->cache;
606
    }
607
608
609
610
    /**
611
     * Set the path to the cache directory.
612
     *
613
     * @param boolean $use true to use the cache and false to ignore cache.
614
     *
615
     * @return $this
616
     */
617
    public function setCache($path)
618
    {
619
        $this->saveFolder = rtrim($path, "/") . "/";
620
        return $this;
621
    }
622
623
624
625
    /**
626
     * Check if cache is writable or throw exception.
627
     *
628
     * @return $this
629
     *
630
     * @throws Exception if cahce folder is not writable.
631
     */
632
    public function isCacheWritable()
633
    {
634
        if (!is_writable($this->saveFolder)) {
635
            throw new Exception("Cache folder is not writable for downloaded files.");
636
        }
637
        return $this;
638
    }
639
640
641
642
    /**
643
     * Decide if the cache should be used or not before trying to download
644
     * a remote file.
645
     *
646
     * @param boolean $use true to use the cache and false to ignore cache.
647
     *
648
     * @return $this
649
     */
650
    public function useCache($use = true)
651
    {
652
        $this->useCache = $use;
653
        return $this;
654
    }
655
656
657
658
    /**
659
     * Set header fields.
660
     *
661
     * @return $this
662
     */
663
    public function setHeaderFields()
664
    {
665
        $cimageVersion = "CImage";
666
        if (defined("CIMAGE_USER_AGENT")) {
667
            $cimageVersion = CIMAGE_USER_AGENT;
668
        }
669
        
670
        $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)");
671
        $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif");
672
673
        if ($this->useCache) {
674
            $this->http->setHeader("Cache-Control", "max-age=0");
675
        } else {
676
            $this->http->setHeader("Cache-Control", "no-cache");
677
            $this->http->setHeader("Pragma", "no-cache");
678
        }
679
    }
680
681
682
683
    /**
684
     * Save downloaded resource to cache.
685
     *
686
     * @return string as path to saved file or false if not saved.
687
     */
688
    public function save()
689
    {
690
        $this->cache = array();
691
        $date         = $this->http->getDate(time());
692
        $maxAge       = $this->http->getMaxAge($this->defaultMaxAge);
693
        $lastModified = $this->http->getLastModified();
694
        $type         = $this->http->getContentType();
695
696
        $this->cache['Date']           = gmdate("D, d M Y H:i:s T", $date);
697
        $this->cache['Max-Age']        = $maxAge;
698
        $this->cache['Content-Type']   = $type;
699
        $this->cache['Url']            = $this->url;
700
701
        if ($lastModified) {
702
            $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified);
703
        }
704
705
        // Save only if body is a valid image
706
        $body = $this->http->getBody();
707
        $img = imagecreatefromstring($body);
708
709
        if ($img !== false) {
710
            file_put_contents($this->fileName, $body);
711
            file_put_contents($this->fileJson, json_encode($this->cache));
712
            return $this->fileName;
713
        }
714
715
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
716
    }
717
718
719
720
    /**
721
     * Got a 304 and updates cache with new age.
722
     *
723
     * @return string as path to cached file.
724
     */
725
    public function updateCacheDetails()
726
    {
727
        $date         = $this->http->getDate(time());
728
        $maxAge       = $this->http->getMaxAge($this->defaultMaxAge);
729
        $lastModified = $this->http->getLastModified();
730
731
        $this->cache['Date']    = gmdate("D, d M Y H:i:s T", $date);
732
        $this->cache['Max-Age'] = $maxAge;
733
734
        if ($lastModified) {
735
            $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified);
736
        }
737
738
        file_put_contents($this->fileJson, json_encode($this->cache));
739
        return $this->fileName;
740
    }
741
742
743
744
    /**
745
     * Download a remote file and keep a cache of downloaded files.
746
     *
747
     * @param string $url a remote url.
748
     *
749
     * @throws Exception when status code does not match 200 or 304.
750
     *
751
     * @return string as path to downloaded file or false if failed.
752
     */
753
    public function download($url)
754
    {
755
        $this->http = new CHttpGet();
756
        $this->url = $url;
757
758
        // First check if the cache is valid and can be used
759
        $this->loadCacheDetails();
760
761
        if ($this->useCache) {
762
            $src = $this->getCachedSource();
763
            if ($src) {
764
                $this->status = 1;
765
                return $src;
766
            }
767
        }
768
769
        // Do a HTTP request to download item
770
        $this->setHeaderFields();
771
        $this->http->setUrl($this->url);
772
        $this->http->doGet();
773
774
        $this->status = $this->http->getStatus();
775
        if ($this->status === 200) {
776
            $this->isCacheWritable();
777
            return $this->save();
778
        } elseif ($this->status === 304) {
779
            $this->isCacheWritable();
780
            return $this->updateCacheDetails();
781
        }
782
783
        throw new Exception("Unknown statuscode when downloading remote image: " . $this->status);
784
    }
785
786
787
788
    /**
789
     * Get the path to the cached image file if the cache is valid.
790
     *
791
     * @return $this
792
     */
793
    public function loadCacheDetails()
794
    {
795
        $cacheFile = md5($this->url);
796
        $this->fileName = $this->saveFolder . $cacheFile;
797
        $this->fileJson = $this->fileName . ".json";
798
        if (is_readable($this->fileJson)) {
799
            $this->cache = json_decode(file_get_contents($this->fileJson), true);
800
        }
801
    }
802
803
804
805
    /**
806
     * Get the path to the cached image file if the cache is valid.
807
     *
808
     * @return string as the path ot the image file or false if no cache.
809
     */
810
    public function getCachedSource()
811
    {
812
        $imageExists = is_readable($this->fileName);
813
814
        // Is cache valid?
815
        $date   = strtotime($this->cache['Date']);
816
        $maxAge = $this->cache['Max-Age'];
817
        $now    = time();
818
        
819
        if ($imageExists && $date + $maxAge > $now) {
820
            return $this->fileName;
821
        }
822
823
        // Prepare for a 304 if available
824
        if ($imageExists && isset($this->cache['Last-Modified'])) {
825
            $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']);
826
        }
827
828
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
829
    }
830
}
831
832
833
834
/**
835
 * Act as whitelist (or blacklist).
836
 *
837
 */
838
class CWhitelist
839
{
840
    /**
841
     * Array to contain the whitelist options.
842
     */
843
    private $whitelist = array();
844
845
846
847
    /**
848
     * Set the whitelist from an array of strings, each item in the
849
     * whitelist should be a regexp without the surrounding / or #.
850
     *
851
     * @param array $whitelist with all valid options,
852
     *                         default is to clear the whitelist.
853
     *
854
     * @return $this
855
     */
856
    public function set($whitelist = array())
857
    {
858
        if (!is_array($whitelist)) {
0 ignored issues
show
The condition is_array($whitelist) is always true.
Loading history...
859
            throw new Exception("Whitelist is not of a supported format.");
860
        }
861
862
        $this->whitelist = $whitelist;
863
        return $this;
864
    }
865
866
867
868
    /**
869
     * Check if item exists in the whitelist.
870
     *
871
     * @param string $item      string to check.
872
     * @param array  $whitelist optional with all valid options, default is null.
873
     *
874
     * @return boolean true if item is in whitelist, else false.
875
     */
876
    public function check($item, $whitelist = null)
877
    {
878
        if ($whitelist !== null) {
879
            $this->set($whitelist);
880
        }
881
        
882
        if (empty($item) or empty($this->whitelist)) {
883
            return false;
884
        }
885
        
886
        foreach ($this->whitelist as $regexp) {
887
            if (preg_match("#$regexp#", $item)) {
888
                return true;
889
            }
890
        }
891
892
        return false;
893
    }
894
}
895
896
897
898
/**
899
 * Create an ASCII version of an image.
900
 *
901
 */
902
class CAsciiArt
903
{
904
    /**
905
     * Character set to use.
906
     */
907
    private $characterSet = array(
908
        'one' => "#0XT|:,.' ",
909
        'two' => "@%#*+=-:. ",
910
        'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
911
    );
912
913
914
915
    /**
916
     * Current character set.
917
     */
918
    private $characters = null;
919
920
921
922
    /**
923
     * Length of current character set.
924
     */
925
    private $charCount = null;
926
927
928
929
    /**
930
     * Scale of the area to swap to a character.
931
     */
932
    private $scale = null;
933
934
935
936
    /**
937
     * Strategy to calculate luminance.
938
     */
939
    private $luminanceStrategy = null;
940
941
942
943
    /**
944
     * Constructor which sets default options.
945
     */
946
    public function __construct()
947
    {
948
        $this->setOptions();
949
    }
950
951
952
953
    /**
954
     * Add a custom character set.
955
     *
956
     * @param string $key   for the character set.
957
     * @param string $value for the character set.
958
     *
959
     * @return $this
960
     */
961
    public function addCharacterSet($key, $value)
962
    {
963
        $this->characterSet[$key] = $value;
964
        return $this;
965
    }
966
967
968
969
    /**
970
     * Set options for processing, defaults are available.
971
     *
972
     * @param array $options to use as default settings.
973
     *
974
     * @return $this
975
     */
976
    public function setOptions($options = array())
977
    {
978
        $default = array(
979
            "characterSet" => 'two',
980
            "scale" => 14,
981
            "luminanceStrategy" => 3,
982
            "customCharacterSet" => null,
983
        );
984
        $default = array_merge($default, $options);
985
        
986
        if (!is_null($default['customCharacterSet'])) {
987
            $this->addCharacterSet('custom', $default['customCharacterSet']);
988
            $default['characterSet'] = 'custom';
989
        }
990
        
991
        $this->scale = $default['scale'];
992
        $this->characters = $this->characterSet[$default['characterSet']];
993
        $this->charCount = strlen($this->characters);
994
        $this->luminanceStrategy = $default['luminanceStrategy'];
995
        
996
        return $this;
997
    }
998
999
1000
1001
    /**
1002
     * Create an Ascii image from an image file.
1003
     *
1004
     * @param string $filename of the image to use.
1005
     *
1006
     * @return string $ascii with the ASCII image.
1007
     */
1008
    public function createFromFile($filename)
1009
    {
1010
        $img = imagecreatefromstring(file_get_contents($filename));
1011
        list($width, $height) = getimagesize($filename);
1012
1013
        $ascii = null;
1014
        $incY = $this->scale;
1015
        $incX = $this->scale / 2;
1016
        
1017
        for ($y = 0; $y < $height - 1; $y += $incY) {
1018
            for ($x = 0; $x < $width - 1; $x += $incX) {
1019
                $toX = min($x + $this->scale / 2, $width - 1);
1020
                $toY = min($y + $this->scale, $height - 1);
1021
                $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY);
0 ignored issues
show
$img of type GdImage|resource is incompatible with the type string expected by parameter $img of CAsciiArt::luminanceAreaAverage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1021
                $luminance = $this->luminanceAreaAverage(/** @scrutinizer ignore-type */ $img, $x, $y, $toX, $toY);
Loading history...
1022
                $ascii .= $this->luminance2character($luminance);
1023
            }
1024
            $ascii .= PHP_EOL;
1025
        }
1026
1027
        return $ascii;
1028
    }
1029
1030
1031
1032
    /**
1033
     * Get the luminance from a region of an image using average color value.
1034
     *
1035
     * @param string  $img the image.
1036
     * @param integer $x1  the area to get pixels from.
1037
     * @param integer $y1  the area to get pixels from.
1038
     * @param integer $x2  the area to get pixels from.
1039
     * @param integer $y2  the area to get pixels from.
1040
     *
1041
     * @return integer $luminance with a value between 0 and 100.
1042
     */
1043
    public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2)
1044
    {
1045
        $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1);
1046
        $luminance = 0;
1047
        
1048
        for ($x = $x1; $x <= $x2; $x++) {
1049
            for ($y = $y1; $y <= $y2; $y++) {
1050
                $rgb   = imagecolorat($img, $x, $y);
0 ignored issues
show
$img of type string is incompatible with the type GdImage|resource expected by parameter $image of imagecolorat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1050
                $rgb   = imagecolorat(/** @scrutinizer ignore-type */ $img, $x, $y);
Loading history...
1051
                $red   = (($rgb >> 16) & 0xFF);
1052
                $green = (($rgb >> 8) & 0xFF);
1053
                $blue  = ($rgb & 0xFF);
1054
                $luminance += $this->getLuminance($red, $green, $blue);
1055
            }
1056
        }
1057
        
1058
        return $luminance / $numPixels;
1059
    }
1060
1061
1062
1063
    /**
1064
     * Calculate luminance value with different strategies.
1065
     *
1066
     * @param integer $red   The color red.
1067
     * @param integer $green The color green.
1068
     * @param integer $blue  The color blue.
1069
     *
1070
     * @return float $luminance with a value between 0 and 1.
1071
     */
1072
    public function getLuminance($red, $green, $blue)
1073
    {
1074
        switch ($this->luminanceStrategy) {
1075
            case 1:
1076
                $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255;
1077
                break;
1078
            case 2:
1079
                $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255;
1080
                break;
1081
            case 3:
1082
                $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255;
1083
                break;
1084
            case 0:
1085
            default:
1086
                $luminance = ($red + $green + $blue) / (255 * 3);
1087
        }
1088
1089
        return $luminance;
1090
    }
1091
1092
1093
1094
    /**
1095
     * Translate the luminance value to a character.
1096
     *
1097
     * @param string $position a value between 0-100 representing the
1098
     *                         luminance.
1099
     *
1100
     * @return string with the ascii character.
1101
     */
1102
    public function luminance2character($luminance)
1103
    {
1104
        $position = (int) round($luminance * ($this->charCount - 1));
1105
        $char = $this->characters[$position];
1106
        return $char;
1107
    }
1108
}
1109
1110
1111
1112
/**
1113
 * Resize and crop images on the fly, store generated images in a cache.
1114
 *
1115
 * @author  Mikael Roos [email protected]
1116
 * @example http://dbwebb.se/opensource/cimage
1117
 * @link    https://github.com/mosbth/cimage
1118
 */
1119
class CImage
1120
{
1121
1122
    /**
1123
     * Constants type of PNG image
1124
     */
1125
    const PNG_GREYSCALE         = 0;
1126
    const PNG_RGB               = 2;
1127
    const PNG_RGB_PALETTE       = 3;
1128
    const PNG_GREYSCALE_ALPHA   = 4;
1129
    const PNG_RGB_ALPHA         = 6;
1130
1131
1132
1133
    /**
1134
     * Constant for default image quality when not set
1135
     */
1136
    const JPEG_QUALITY_DEFAULT = 60;
1137
1138
1139
1140
    /**
1141
     * Quality level for JPEG images.
1142
     */
1143
    private $quality;
1144
1145
1146
1147
    /**
1148
     * Is the quality level set from external use (true) or is it default (false)?
1149
     */
1150
    private $useQuality = false;
1151
1152
1153
1154
    /**
1155
     * Constant for default image quality when not set
1156
     */
1157
    const PNG_COMPRESSION_DEFAULT = -1;
1158
1159
1160
1161
    /**
1162
     * Compression level for PNG images.
1163
     */
1164
    private $compress;
1165
1166
1167
1168
    /**
1169
     * Is the compress level set from external use (true) or is it default (false)?
1170
     */
1171
    private $useCompress = false;
1172
1173
1174
1175
1176
    /**
1177
     * Add HTTP headers for outputing image.
1178
     */
1179
    private $HTTPHeader = array();
1180
1181
1182
1183
    /**
1184
     * Default background color, red, green, blue, alpha.
1185
     *
1186
     * @todo remake when upgrading to PHP 5.5
1187
     */
1188
    /*
1189
    const BACKGROUND_COLOR = array(
1190
        'red'   => 0,
1191
        'green' => 0,
1192
        'blue'  => 0,
1193
        'alpha' => null,
1194
    );*/
1195
1196
1197
1198
    /**
1199
     * Default background color to use.
1200
     *
1201
     * @todo remake when upgrading to PHP 5.5
1202
     */
1203
    //private $bgColorDefault = self::BACKGROUND_COLOR;
1204
    private $bgColorDefault = array(
1205
        'red'   => 0,
1206
        'green' => 0,
1207
        'blue'  => 0,
1208
        'alpha' => null,
1209
    );
1210
1211
1212
    /**
1213
     * Background color to use, specified as part of options.
1214
     */
1215
    private $bgColor;
1216
1217
1218
1219
    /**
1220
     * Where to save the target file.
1221
     */
1222
    private $saveFolder;
1223
1224
1225
1226
    /**
1227
     * The working image object.
1228
     */
1229
    private $image;
1230
1231
1232
1233
    /**
1234
     * Image filename, may include subdirectory, relative from $imageFolder
1235
     */
1236
    private $imageSrc;
1237
1238
1239
1240
    /**
1241
     * Actual path to the image, $imageFolder . '/' . $imageSrc
1242
     */
1243
    private $pathToImage;
1244
1245
1246
1247
    /**
1248
     * File type for source image, as provided by getimagesize()
1249
     */
1250
    private $fileType;
1251
1252
1253
1254
    /**
1255
     * File extension to use when saving image.
1256
     */
1257
    private $extension;
1258
1259
1260
1261
    /**
1262
     * Output format, supports null (image) or json.
1263
     */
1264
    private $outputFormat = null;
1265
1266
1267
1268
    /**
1269
     * Do lossy output using external postprocessing tools.
1270
     */
1271
    private $lossy = null;
1272
1273
1274
1275
    /**
1276
     * Verbose mode to print out a trace and display the created image
1277
     */
1278
    private $verbose = false;
1279
1280
1281
1282
    /**
1283
     * Keep a log/trace on what happens
1284
     */
1285
    private $log = array();
1286
1287
1288
1289
    /**
1290
     * Handle image as palette image
1291
     */
1292
    private $palette;
1293
1294
1295
1296
    /**
1297
     * Target filename, with path, to save resulting image in.
1298
     */
1299
    private $cacheFileName;
1300
1301
1302
1303
    /**
1304
     * Set a format to save image as, or null to use original format.
1305
     */
1306
    private $saveAs;
1307
1308
1309
    /**
1310
     * Path to command for lossy optimize, for example pngquant.
1311
     */
1312
    private $pngLossy;
1313
    private $pngLossyCmd;
1314
1315
1316
1317
    /**
1318
     * Path to command for filter optimize, for example optipng.
1319
     */
1320
    private $pngFilter;
1321
    private $pngFilterCmd;
1322
1323
1324
1325
    /**
1326
     * Path to command for deflate optimize, for example pngout.
1327
     */
1328
    private $pngDeflate;
1329
    private $pngDeflateCmd;
1330
1331
1332
1333
    /**
1334
     * Path to command to optimize jpeg images, for example jpegtran or null.
1335
     */
1336
     private $jpegOptimize;
1337
     private $jpegOptimizeCmd;
1338
1339
1340
1341
    /**
1342
     * Image dimensions, calculated from loaded image.
1343
     */
1344
    private $width;  // Calculated from source image
1345
    private $height; // Calculated from source image
1346
1347
1348
    /**
1349
     * New image dimensions, incoming as argument or calculated.
1350
     */
1351
    private $newWidth;
1352
    private $newWidthOrig;  // Save original value
1353
    private $newHeight;
1354
    private $newHeightOrig; // Save original value
1355
1356
1357
    /**
1358
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
1359
     */
1360
    private $dpr = 1;
1361
1362
1363
    /**
1364
     * Always upscale images, even if they are smaller than target image.
1365
     */
1366
    const UPSCALE_DEFAULT = true;
1367
    private $upscale = self::UPSCALE_DEFAULT;
1368
1369
1370
1371
    /**
1372
     * Array with details on how to crop, incoming as argument and calculated.
1373
     */
1374
    public $crop;
1375
    public $cropOrig; // Save original value
1376
1377
1378
    /**
1379
     * String with details on how to do image convolution. String
1380
     * should map a key in the $convolvs array or be a string of
1381
     * 11 float values separated by comma. The first nine builds
1382
     * up the matrix, then divisor and last offset.
1383
     */
1384
    private $convolve;
1385
1386
1387
    /**
1388
     * Custom convolution expressions, matrix 3x3, divisor and offset.
1389
     */
1390
    private $convolves = array(
1391
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
1392
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
1393
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
1394
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
1395
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
1396
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
1397
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
1398
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
1399
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
1400
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
1401
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
1402
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
1403
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
1404
    );
1405
1406
1407
    /**
1408
     * Resize strategy to fill extra area with background color.
1409
     * True or false.
1410
     */
1411
    private $fillToFit;
1412
1413
1414
1415
    /**
1416
     * To store value for option scale.
1417
     */
1418
    private $scale;
1419
1420
1421
1422
    /**
1423
     * To store value for option.
1424
     */
1425
    private $rotateBefore;
1426
1427
1428
1429
    /**
1430
     * To store value for option.
1431
     */
1432
    private $rotateAfter;
1433
1434
1435
1436
    /**
1437
     * To store value for option.
1438
     */
1439
    private $autoRotate;
1440
1441
1442
1443
    /**
1444
     * To store value for option.
1445
     */
1446
    private $sharpen;
1447
1448
1449
1450
    /**
1451
     * To store value for option.
1452
     */
1453
    private $emboss;
1454
1455
1456
1457
    /**
1458
     * To store value for option.
1459
     */
1460
    private $blur;
1461
1462
1463
1464
    /**
1465
     * Used with option area to set which parts of the image to use.
1466
     */
1467
    private $offset;
1468
1469
1470
1471
    /**
1472
     * Calculate target dimension for image when using fill-to-fit resize strategy.
1473
     */
1474
    private $fillWidth;
1475
    private $fillHeight;
1476
1477
1478
1479
    /**
1480
     * Allow remote file download, default is to disallow remote file download.
1481
     */
1482
    private $allowRemote = false;
1483
1484
1485
1486
    /**
1487
     * Path to cache for remote download.
1488
     */
1489
    private $remoteCache;
1490
1491
1492
1493
    /**
1494
     * Pattern to recognize a remote file.
1495
     */
1496
    //private $remotePattern = '#^[http|https]://#';
1497
    private $remotePattern = '#^https?://#';
1498
1499
1500
1501
    /**
1502
     * Use the cache if true, set to false to ignore the cached file.
1503
     */
1504
    private $useCache = true;
1505
1506
1507
    /**
1508
    * Disable the fasttrackCacke to start with, inject an object to enable it.
1509
    */
1510
    private $fastTrackCache = null;
1511
1512
1513
1514
    /*
1515
     * Set whitelist for valid hostnames from where remote source can be
1516
     * downloaded.
1517
     */
1518
    private $remoteHostWhitelist = null;
1519
1520
1521
1522
    /*
1523
     * Do verbose logging to file by setting this to a filename.
1524
     */
1525
    private $verboseFileName = null;
1526
1527
1528
1529
    /*
1530
     * Output to ascii can take som options as an array.
1531
     */
1532
    private $asciiOptions = array();
1533
1534
1535
1536
    /*
1537
     * Image copy strategy, defaults to RESAMPLE.
1538
     */
1539
     const RESIZE = 1;
1540
     const RESAMPLE = 2;
1541
     private $copyStrategy = NULL;
1542
1543
1544
1545
    /**
1546
     * Properties, the class is mutable and the method setOptions()
1547
     * decides (partly) what properties are created.
1548
     *
1549
     * @todo Clean up these and check if and how they are used
1550
     */
1551
1552
    public $keepRatio;
1553
    public $cropToFit;
1554
    private $cropWidth;
1555
    private $cropHeight;
1556
    public $crop_x;
1557
    public $crop_y;
1558
    public $filters;
1559
    private $attr; // Calculated from source image
1560
1561
1562
1563
1564
    /**
1565
     * Constructor, can take arguments to init the object.
1566
     *
1567
     * @param string $imageSrc    filename which may contain subdirectory.
1568
     * @param string $imageFolder path to root folder for images.
1569
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
1570
     * @param string $saveName    name of target file when saveing.
1571
     */
1572
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
1573
    {
1574
        $this->setSource($imageSrc, $imageFolder);
1575
        $this->setTarget($saveFolder, $saveName);
1576
    }
1577
1578
1579
1580
    /**
1581
     * Inject object and use it, must be available as member.
1582
     *
1583
     * @param string $property to set as object.
1584
     * @param object $object   to set to property.
1585
     *
1586
     * @return $this
1587
     */
1588
    public function injectDependency($property, $object)
1589
    {
1590
        if (!property_exists($this, $property)) {
1591
            $this->raiseError("Injecting unknown property.");
1592
        }
1593
        $this->$property = $object;
1594
        return $this;
1595
    }
1596
1597
1598
1599
    /**
1600
     * Set verbose mode.
1601
     *
1602
     * @param boolean $mode true or false to enable and disable verbose mode,
1603
     *                      default is true.
1604
     *
1605
     * @return $this
1606
     */
1607
    public function setVerbose($mode = true)
1608
    {
1609
        $this->verbose = $mode;
1610
        return $this;
1611
    }
1612
1613
1614
1615
    /**
1616
     * Set save folder, base folder for saving cache files.
1617
     *
1618
     * @todo clean up how $this->saveFolder is used in other methods.
1619
     *
1620
     * @param string $path where to store cached files.
1621
     *
1622
     * @return $this
1623
     */
1624
    public function setSaveFolder($path)
1625
    {
1626
        $this->saveFolder = $path;
1627
        return $this;
1628
    }
1629
1630
1631
1632
    /**
1633
     * Use cache or not.
1634
     *
1635
     * @param boolean $use true or false to use cache.
1636
     *
1637
     * @return $this
1638
     */
1639
    public function useCache($use = true)
1640
    {
1641
        $this->useCache = $use;
1642
        return $this;
1643
    }
1644
1645
1646
1647
    /**
1648
     * Create and save a dummy image. Use dimensions as stated in
1649
     * $this->newWidth, or $width or default to 100 (same for height.
1650
     *
1651
     * @param integer $width  use specified width for image dimension.
1652
     * @param integer $height use specified width for image dimension.
1653
     *
1654
     * @return $this
1655
     */
1656
    public function createDummyImage($width = null, $height = null)
1657
    {
1658
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
1659
        $this->newHeight = $this->newHeight ?: $height ?: 100;
1660
1661
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1662
1663
        return $this;
1664
    }
1665
1666
1667
1668
    /**
1669
     * Allow or disallow remote image download.
1670
     *
1671
     * @param boolean $allow   true or false to enable and disable.
1672
     * @param string  $cache   path to cache dir.
1673
     * @param string  $pattern to use to detect if its a remote file.
1674
     *
1675
     * @return $this
1676
     */
1677
    public function setRemoteDownload($allow, $cache, $pattern = null)
1678
    {
1679
        $this->allowRemote = $allow;
1680
        $this->remoteCache = $cache;
1681
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
1682
1683
        $this->log(
1684
            "Set remote download to: "
1685
            . ($this->allowRemote ? "true" : "false")
1686
            . " using pattern "
1687
            . $this->remotePattern
1688
        );
1689
1690
        return $this;
1691
    }
1692
1693
1694
1695
    /**
1696
     * Check if the image resource is a remote file or not.
1697
     *
1698
     * @param string $src check if src is remote.
1699
     *
1700
     * @return boolean true if $src is a remote file, else false.
1701
     */
1702
    public function isRemoteSource($src)
1703
    {
1704
        $remote = preg_match($this->remotePattern, $src);
1705
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
1706
        return !!$remote;
1707
    }
1708
1709
1710
1711
    /**
1712
     * Set whitelist for valid hostnames from where remote source can be
1713
     * downloaded.
1714
     *
1715
     * @param array $whitelist with regexp hostnames to allow download from.
1716
     *
1717
     * @return $this
1718
     */
1719
    public function setRemoteHostWhitelist($whitelist = null)
1720
    {
1721
        $this->remoteHostWhitelist = $whitelist;
1722
        $this->log(
1723
            "Setting remote host whitelist to: "
1724
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
0 ignored issues
show
Are you sure is_null($whitelist) ? 'n... print_r($whitelist, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1724
            . (/** @scrutinizer ignore-type */ is_null($whitelist) ? "null" : print_r($whitelist, 1))
Loading history...
1725
        );
1726
        return $this;
1727
    }
1728
1729
1730
1731
    /**
1732
     * Check if the hostname for the remote image, is on a whitelist,
1733
     * if the whitelist is defined.
1734
     *
1735
     * @param string $src the remote source.
1736
     *
1737
     * @return boolean true if hostname on $src is in the whitelist, else false.
1738
     */
1739
    public function isRemoteSourceOnWhitelist($src)
1740
    {
1741
        if (is_null($this->remoteHostWhitelist)) {
1742
            $this->log("Remote host on whitelist not configured - allowing.");
1743
            return true;
1744
        }
1745
1746
        $whitelist = new CWhitelist();
1747
        $hostname = parse_url($src, PHP_URL_HOST);
1748
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
1749
1750
        $this->log(
1751
            "Remote host is on whitelist: "
1752
            . ($allow ? "true" : "false")
1753
        );
1754
        return $allow;
1755
    }
1756
1757
1758
1759
    /**
1760
     * Check if file extension is valid as a file extension.
1761
     *
1762
     * @param string $extension of image file.
1763
     *
1764
     * @return $this
1765
     */
1766
    private function checkFileExtension($extension)
1767
    {
1768
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
1769
1770
        in_array(strtolower($extension), $valid)
1771
            or $this->raiseError('Not a valid file extension.');
1772
1773
        return $this;
1774
    }
1775
1776
1777
1778
    /**
1779
     * Normalize the file extension.
1780
     *
1781
     * @param string $extension of image file or skip to use internal.
1782
     *
1783
     * @return string $extension as a normalized file extension.
1784
     */
1785
    private function normalizeFileExtension($extension = null)
1786
    {
1787
        $extension = strtolower($extension ? $extension : $this->extension);
1788
1789
        if ($extension == 'jpeg') {
1790
                $extension = 'jpg';
1791
        }
1792
1793
        return $extension;
1794
    }
1795
1796
1797
1798
    /**
1799
     * Download a remote image and return path to its local copy.
1800
     *
1801
     * @param string $src remote path to image.
1802
     *
1803
     * @return string as path to downloaded remote source.
1804
     */
1805
    public function downloadRemoteSource($src)
1806
    {
1807
        if (!$this->isRemoteSourceOnWhitelist($src)) {
1808
            throw new Exception("Hostname is not on whitelist for remote sources.");
1809
        }
1810
1811
        $remote = new CRemoteImage();
1812
1813
        if (!is_writable($this->remoteCache)) {
1814
            $this->log("The remote cache is not writable.");
1815
        }
1816
1817
        $remote->setCache($this->remoteCache);
1818
        $remote->useCache($this->useCache);
1819
        $src = $remote->download($src);
1820
1821
        $this->log("Remote HTTP status: " . $remote->getStatus());
1822
        $this->log("Remote item is in local cache: $src");
1823
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
0 ignored issues
show
Are you sure print_r($remote->getDetails(), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1823
        $this->log("Remote details on cache:" . /** @scrutinizer ignore-type */ print_r($remote->getDetails(), true));
Loading history...
1824
1825
        return $src;
1826
    }
1827
1828
1829
1830
    /**
1831
     * Set source file to use as image source.
1832
     *
1833
     * @param string $src of image.
1834
     * @param string $dir as optional base directory where images are.
1835
     *
1836
     * @return $this
1837
     */
1838
    public function setSource($src, $dir = null)
1839
    {
1840
        if (!isset($src)) {
1841
            $this->imageSrc = null;
1842
            $this->pathToImage = null;
1843
            return $this;
1844
        }
1845
1846
        if ($this->allowRemote && $this->isRemoteSource($src)) {
1847
            $src = $this->downloadRemoteSource($src);
1848
            $dir = null;
1849
        }
1850
1851
        if (!isset($dir)) {
1852
            $dir = dirname($src);
1853
            $src = basename($src);
1854
        }
1855
1856
        $this->imageSrc     = ltrim($src, '/');
1857
        $imageFolder        = rtrim($dir, '/');
1858
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
1859
1860
        return $this;
1861
    }
1862
1863
1864
1865
    /**
1866
     * Set target file.
1867
     *
1868
     * @param string $src of target image.
1869
     * @param string $dir as optional base directory where images are stored.
1870
     *                    Uses $this->saveFolder if null.
1871
     *
1872
     * @return $this
1873
     */
1874
    public function setTarget($src = null, $dir = null)
1875
    {
1876
        if (!isset($src)) {
1877
            $this->cacheFileName = null;
1878
            return $this;
1879
        }
1880
1881
        if (isset($dir)) {
1882
            $this->saveFolder = rtrim($dir, '/');
1883
        }
1884
1885
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
1886
1887
        // Sanitize filename
1888
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
1889
        $this->log("The cache file name is: " . $this->cacheFileName);
1890
1891
        return $this;
1892
    }
1893
1894
1895
1896
    /**
1897
     * Get filename of target file.
1898
     *
1899
     * @return Boolean|String as filename of target or false if not set.
1900
     */
1901
    public function getTarget()
1902
    {
1903
        return $this->cacheFileName;
1904
    }
1905
1906
1907
1908
    /**
1909
     * Set options to use when processing image.
1910
     *
1911
     * @param array $args used when processing image.
1912
     *
1913
     * @return $this
1914
     */
1915
    public function setOptions($args)
1916
    {
1917
        $this->log("Set new options for processing image.");
1918
1919
        $defaults = array(
1920
            // Options for calculate dimensions
1921
            'newWidth'    => null,
1922
            'newHeight'   => null,
1923
            'aspectRatio' => null,
1924
            'keepRatio'   => true,
1925
            'cropToFit'   => false,
1926
            'fillToFit'   => null,
1927
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
1928
            'area'        => null, //'0,0,0,0',
1929
            'upscale'     => self::UPSCALE_DEFAULT,
1930
1931
            // Options for caching or using original
1932
            'useCache'    => true,
1933
            'useOriginal' => true,
1934
1935
            // Pre-processing, before resizing is done
1936
            'scale'        => null,
1937
            'rotateBefore' => null,
1938
            'autoRotate'  => false,
1939
1940
            // General options
1941
            'bgColor'     => null,
1942
1943
            // Post-processing, after resizing is done
1944
            'palette'     => null,
1945
            'filters'     => null,
1946
            'sharpen'     => null,
1947
            'emboss'      => null,
1948
            'blur'        => null,
1949
            'convolve'       => null,
1950
            'rotateAfter' => null,
1951
1952
            // Output format
1953
            'outputFormat' => null,
1954
            'dpr'          => 1,
1955
1956
            // Postprocessing using external tools
1957
            'lossy' => null,
1958
        );
1959
1960
        // Convert crop settings from string to array
1961
        if (isset($args['crop']) && !is_array($args['crop'])) {
1962
            $pices = explode(',', $args['crop']);
1963
            $args['crop'] = array(
1964
                'width'   => $pices[0],
1965
                'height'  => $pices[1],
1966
                'start_x' => $pices[2],
1967
                'start_y' => $pices[3],
1968
            );
1969
        }
1970
1971
        // Convert area settings from string to array
1972
        if (isset($args['area']) && !is_array($args['area'])) {
1973
                $pices = explode(',', $args['area']);
1974
                $args['area'] = array(
1975
                    'top'    => $pices[0],
1976
                    'right'  => $pices[1],
1977
                    'bottom' => $pices[2],
1978
                    'left'   => $pices[3],
1979
                );
1980
        }
1981
1982
        // Convert filter settings from array of string to array of array
1983
        if (isset($args['filters']) && is_array($args['filters'])) {
1984
            foreach ($args['filters'] as $key => $filterStr) {
1985
                $parts = explode(',', $filterStr);
1986
                $filter = $this->mapFilter($parts[0]);
1987
                $filter['str'] = $filterStr;
1988
                for ($i=1; $i<=$filter['argc']; $i++) {
1989
                    if (isset($parts[$i])) {
1990
                        $filter["arg{$i}"] = $parts[$i];
1991
                    } else {
1992
                        throw new Exception(
1993
                            'Missing arg to filter, review how many arguments are needed at
1994
                            http://php.net/manual/en/function.imagefilter.php'
1995
                        );
1996
                    }
1997
                }
1998
                $args['filters'][$key] = $filter;
1999
            }
2000
        }
2001
2002
        // Merge default arguments with incoming and set properties.
2003
        //$args = array_merge_recursive($defaults, $args);
2004
        $args = array_merge($defaults, $args);
2005
        foreach ($defaults as $key => $val) {
2006
            $this->{$key} = $args[$key];
2007
        }
2008
2009
        if ($this->bgColor) {
2010
            $this->setDefaultBackgroundColor($this->bgColor);
2011
        }
2012
2013
        // Save original values to enable re-calculating
2014
        $this->newWidthOrig  = $this->newWidth;
2015
        $this->newHeightOrig = $this->newHeight;
2016
        $this->cropOrig      = $this->crop;
2017
2018
        return $this;
2019
    }
2020
2021
2022
2023
    /**
2024
     * Map filter name to PHP filter and id.
2025
     *
2026
     * @param string $name the name of the filter.
2027
     *
2028
     * @return array with filter settings
2029
     * @throws Exception
2030
     */
2031
    private function mapFilter($name)
2032
    {
2033
        $map = array(
2034
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
2035
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
2036
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
2037
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
2038
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
2039
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
2040
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
2041
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
2042
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
2043
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
2044
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
2045
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
2046
        );
2047
2048
        if (isset($map[$name])) {
2049
            return $map[$name];
2050
        } else {
2051
            throw new Exception('No such filter.');
2052
        }
2053
    }
2054
2055
2056
2057
    /**
2058
     * Load image details from original image file.
2059
     *
2060
     * @param string $file the file to load or null to use $this->pathToImage.
2061
     *
2062
     * @return $this
2063
     * @throws Exception
2064
     */
2065
    public function loadImageDetails($file = null)
2066
    {
2067
        $file = $file ? $file : $this->pathToImage;
2068
2069
        is_readable($file)
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of is_readable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2069
        is_readable(/** @scrutinizer ignore-type */ $file)
Loading history...
2070
            or $this->raiseError('Image file does not exist.');
2071
2072
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of getimagesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2072
        $info = list($this->width, $this->height, $this->fileType) = getimagesize(/** @scrutinizer ignore-type */ $file);
Loading history...
2073
        if (empty($info)) {
2074
            // To support webp
2075
            $this->fileType = false;
2076
            if (function_exists("exif_imagetype")) {
2077
                $this->fileType = exif_imagetype($file);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of exif_imagetype() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2077
                $this->fileType = exif_imagetype(/** @scrutinizer ignore-type */ $file);
Loading history...
2078
                if ($this->fileType === false) {
2079
                    if (function_exists("imagecreatefromwebp")) {
2080
                        $webp = imagecreatefromwebp($file);
2081
                        if ($webp !== false) {
2082
                            $this->width  = imagesx($webp);
2083
                            $this->height = imagesy($webp);
2084
                            $this->fileType = IMG_WEBP;
2085
                        }
2086
                    }
2087
                }
2088
            }
2089
        }
2090
2091
        if (!$this->fileType) {
2092
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
2093
        }
2094
2095
        if ($this->verbose) {
2096
            $this->log("Loading image details for: {$file}");
2097
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
2098
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2098
            $this->log(" Image filesize: " . filesize(/** @scrutinizer ignore-type */ $file) . " bytes.");
Loading history...
2099
            $this->log(" Image mimetype: " . $this->getMimeType());
0 ignored issues
show
Are you sure $this->getMimeType() of type CImage can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2099
            $this->log(" Image mimetype: " . /** @scrutinizer ignore-type */ $this->getMimeType());
Loading history...
2100
        }
2101
2102
        return $this;
2103
    }
2104
2105
2106
2107
    /**
2108
     * Get mime type for image type.
2109
     *
2110
     * @return $this
2111
     * @throws Exception
2112
     */
2113
    protected function getMimeType()
2114
    {
2115
        if ($this->fileType === IMG_WEBP) {
2116
            return "image/webp";
0 ignored issues
show
Bug Best Practice introduced by
The expression return 'image/webp' returns the type string which is incompatible with the documented return type CImage.
Loading history...
2117
        }
2118
2119
        return image_type_to_mime_type($this->fileType);
0 ignored issues
show
Bug Best Practice introduced by
The expression return image_type_to_mime_type($this->fileType) returns the type string which is incompatible with the documented return type CImage.
Loading history...
2120
    }
2121
2122
2123
2124
    /**
2125
     * Init new width and height and do some sanity checks on constraints, before any
2126
     * processing can be done.
2127
     *
2128
     * @return $this
2129
     * @throws Exception
2130
     */
2131
    public function initDimensions()
2132
    {
2133
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
2134
2135
        // width as %
2136
        if ($this->newWidth
2137
            && $this->newWidth[strlen($this->newWidth)-1] == '%') {
2138
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
2139
            $this->log("Setting new width based on % to {$this->newWidth}");
2140
        }
2141
2142
        // height as %
2143
        if ($this->newHeight
2144
            && $this->newHeight[strlen($this->newHeight)-1] == '%') {
2145
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
2146
            $this->log("Setting new height based on % to {$this->newHeight}");
2147
        }
2148
2149
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
0 ignored issues
show
Bug Best Practice introduced by
The property aspectRatio does not exist on CImage. Did you maybe forget to declare it?
Loading history...
2150
2151
        // width & height from aspect ratio
2152
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
2153
            if ($this->aspectRatio >= 1) {
2154
                $this->newWidth   = $this->width;
2155
                $this->newHeight  = $this->width / $this->aspectRatio;
2156
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
2157
2158
            } else {
2159
                $this->newHeight  = $this->height;
2160
                $this->newWidth   = $this->height * $this->aspectRatio;
2161
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
2162
            }
2163
2164
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
2165
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
2166
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
2167
2168
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
2169
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
2170
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
2171
        }
2172
2173
        // Change width & height based on dpr
2174
        if ($this->dpr != 1) {
2175
            if (!is_null($this->newWidth)) {
2176
                $this->newWidth  = round($this->newWidth * $this->dpr);
2177
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
2178
            }
2179
            if (!is_null($this->newHeight)) {
2180
                $this->newHeight = round($this->newHeight * $this->dpr);
2181
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
2182
            }
2183
        }
2184
2185
        // Check values to be within domain
2186
        is_null($this->newWidth)
2187
            or is_numeric($this->newWidth)
2188
            or $this->raiseError('Width not numeric');
2189
2190
        is_null($this->newHeight)
2191
            or is_numeric($this->newHeight)
2192
            or $this->raiseError('Height not numeric');
2193
2194
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
2195
2196
        return $this;
2197
    }
2198
2199
2200
2201
    /**
2202
     * Calculate new width and height of image, based on settings.
2203
     *
2204
     * @return $this
2205
     */
2206
    public function calculateNewWidthAndHeight()
2207
    {
2208
        // Crop, use cropped width and height as base for calulations
2209
        $this->log("Calculate new width and height.");
2210
        $this->log("Original width x height is {$this->width} x {$this->height}.");
2211
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
2212
2213
        // Check if there is an area to crop off
2214
        if (isset($this->area)) {
2215
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
2216
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
2217
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
2218
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
2219
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
2220
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
2221
            $this->width  = $this->offset['width'];
2222
            $this->height = $this->offset['height'];
2223
            $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']}%.");
2224
            $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.");
2225
        }
2226
2227
        $width  = $this->width;
2228
        $height = $this->height;
2229
2230
        // Check if crop is set
2231
        if ($this->crop) {
2232
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
2233
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
2234
2235
            if ($this->crop['start_x'] == 'left') {
2236
                $this->crop['start_x'] = 0;
2237
            } elseif ($this->crop['start_x'] == 'right') {
2238
                $this->crop['start_x'] = $this->width - $width;
2239
            } elseif ($this->crop['start_x'] == 'center') {
2240
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
2241
            }
2242
2243
            if ($this->crop['start_y'] == 'top') {
2244
                $this->crop['start_y'] = 0;
2245
            } elseif ($this->crop['start_y'] == 'bottom') {
2246
                $this->crop['start_y'] = $this->height - $height;
2247
            } elseif ($this->crop['start_y'] == 'center') {
2248
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
2249
            }
2250
2251
            $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.");
2252
        }
2253
2254
        // Calculate new width and height if keeping aspect-ratio.
2255
        if ($this->keepRatio) {
2256
2257
            $this->log("Keep aspect ratio.");
2258
2259
            // Crop-to-fit and both new width and height are set.
2260
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
2261
2262
                // Use newWidth and newHeigh as width/height, image should fit in box.
2263
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
2264
2265
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
2266
2267
                // Both new width and height are set.
2268
                // Use newWidth and newHeigh as max width/height, image should not be larger.
2269
                $ratioWidth  = $width  / $this->newWidth;
2270
                $ratioHeight = $height / $this->newHeight;
2271
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
2272
                $this->newWidth  = round($width  / $ratio);
2273
                $this->newHeight = round($height / $ratio);
2274
                $this->log("New width and height was set.");
2275
2276
            } elseif (isset($this->newWidth)) {
2277
2278
                // Use new width as max-width
2279
                $factor = (float)$this->newWidth / (float)$width;
2280
                $this->newHeight = round($factor * $height);
2281
                $this->log("New width was set.");
2282
2283
            } elseif (isset($this->newHeight)) {
2284
2285
                // Use new height as max-hight
2286
                $factor = (float)$this->newHeight / (float)$height;
2287
                $this->newWidth = round($factor * $width);
2288
                $this->log("New height was set.");
2289
2290
            } else {
2291
2292
                // Use existing width and height as new width and height.
2293
                $this->newWidth = $width;
2294
                $this->newHeight = $height;
2295
            }
2296
            
2297
2298
            // Get image dimensions for pre-resize image.
2299
            if ($this->cropToFit || $this->fillToFit) {
2300
2301
                // Get relations of original & target image
2302
                $ratioWidth  = $width  / $this->newWidth;
2303
                $ratioHeight = $height / $this->newHeight;
2304
2305
                if ($this->cropToFit) {
2306
2307
                    // Use newWidth and newHeigh as defined width/height,
2308
                    // image should fit the area.
2309
                    $this->log("Crop to fit.");
2310
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
2311
                    $this->cropWidth  = round($width  / $ratio);
2312
                    $this->cropHeight = round($height / $ratio);
2313
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
2314
2315
                } elseif ($this->fillToFit) {
2316
2317
                    // Use newWidth and newHeigh as defined width/height,
2318
                    // image should fit the area.
2319
                    $this->log("Fill to fit.");
2320
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
2321
                    $this->fillWidth  = round($width  / $ratio);
2322
                    $this->fillHeight = round($height / $ratio);
2323
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
2324
                }
2325
            }
2326
        }
2327
2328
        // Crop, ensure to set new width and height
2329
        if ($this->crop) {
2330
            $this->log("Crop.");
2331
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
2332
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
2333
        }
2334
2335
        // Fill to fit, ensure to set new width and height
2336
        /*if ($this->fillToFit) {
2337
            $this->log("FillToFit.");
2338
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
2339
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
2340
        }*/
2341
2342
        // No new height or width is set, use existing measures.
2343
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
2344
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
2345
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
2346
2347
        return $this;
2348
    }
2349
2350
2351
2352
    /**
2353
     * Re-calculate image dimensions when original image dimension has changed.
2354
     *
2355
     * @return $this
2356
     */
2357
    public function reCalculateDimensions()
2358
    {
2359
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
2360
2361
        $this->newWidth  = $this->newWidthOrig;
2362
        $this->newHeight = $this->newHeightOrig;
2363
        $this->crop      = $this->cropOrig;
2364
2365
        $this->initDimensions()
2366
             ->calculateNewWidthAndHeight();
2367
2368
        return $this;
2369
    }
2370
2371
2372
2373
    /**
2374
     * Set extension for filename to save as.
2375
     *
2376
     * @param string $saveas extension to save image as
2377
     *
2378
     * @return $this
2379
     */
2380
    public function setSaveAsExtension($saveAs = null)
2381
    {
2382
        if (isset($saveAs)) {
2383
            $saveAs = strtolower($saveAs);
2384
            $this->checkFileExtension($saveAs);
2385
            $this->saveAs = $saveAs;
2386
            $this->extension = $saveAs;
2387
        }
2388
2389
        $this->log("Prepare to save image as: " . $this->extension);
2390
2391
        return $this;
2392
    }
2393
2394
2395
2396
    /**
2397
     * Set JPEG quality to use when saving image
2398
     *
2399
     * @param int $quality as the quality to set.
2400
     *
2401
     * @return $this
2402
     */
2403
    public function setJpegQuality($quality = null)
2404
    {
2405
        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 0. 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...
2406
            $this->useQuality = true;
2407
        }
2408
2409
        $this->quality = isset($quality)
2410
            ? $quality
2411
            : self::JPEG_QUALITY_DEFAULT;
2412
2413
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
2414
            or $this->raiseError('Quality not in range.');
2415
2416
        $this->log("Setting JPEG quality to {$this->quality}.");
2417
2418
        return $this;
2419
    }
2420
2421
2422
2423
    /**
2424
     * Set PNG compressen algorithm to use when saving image
2425
     *
2426
     * @param int $compress as the algorithm to use.
2427
     *
2428
     * @return $this
2429
     */
2430
    public function setPngCompression($compress = null)
2431
    {
2432
        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 0. 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...
2433
            $this->useCompress = true;
2434
        }
2435
2436
        $this->compress = isset($compress)
2437
            ? $compress
2438
            : self::PNG_COMPRESSION_DEFAULT;
2439
2440
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
2441
            or $this->raiseError('Quality not in range.');
2442
2443
        $this->log("Setting PNG compression level to {$this->compress}.");
2444
2445
        return $this;
2446
    }
2447
2448
2449
2450
    /**
2451
     * Use original image if possible, check options which affects image processing.
2452
     *
2453
     * @param boolean $useOrig default is to use original if possible, else set to false.
2454
     *
2455
     * @return $this
2456
     */
2457
    public function useOriginalIfPossible($useOrig = true)
2458
    {
2459
        if ($useOrig
2460
            && ($this->newWidth == $this->width)
2461
            && ($this->newHeight == $this->height)
2462
            && !$this->area
0 ignored issues
show
Bug Best Practice introduced by
The property area does not exist on CImage. Did you maybe forget to declare it?
Loading history...
2463
            && !$this->crop
2464
            && !$this->cropToFit
2465
            && !$this->fillToFit
2466
            && !$this->filters
2467
            && !$this->sharpen
2468
            && !$this->emboss
2469
            && !$this->blur
2470
            && !$this->convolve
2471
            && !$this->palette
2472
            && !$this->useQuality
2473
            && !$this->useCompress
2474
            && !$this->saveAs
2475
            && !$this->rotateBefore
2476
            && !$this->rotateAfter
2477
            && !$this->autoRotate
2478
            && !$this->bgColor
2479
            && ($this->upscale === self::UPSCALE_DEFAULT)
2480
            && !$this->lossy
2481
        ) {
2482
            $this->log("Using original image.");
2483
            $this->output($this->pathToImage);
2484
        }
2485
2486
        return $this;
2487
    }
2488
2489
2490
2491
    /**
2492
     * Generate filename to save file in cache.
2493
     *
2494
     * @param string  $base      as optional basepath for storing file.
2495
     * @param boolean $useSubdir use or skip the subdir part when creating the
2496
     *                           filename.
2497
     * @param string  $prefix    to add as part of filename
2498
     *
2499
     * @return $this
2500
     */
2501
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
2502
    {
2503
        $filename     = basename($this->pathToImage);
0 ignored issues
show
It seems like $this->pathToImage can also be of type null; however, parameter $path of basename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2503
        $filename     = basename(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
2504
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
2505
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
2506
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
2507
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
2508
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
2509
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
2510
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
2511
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
2512
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
2513
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
2514
        $lossy        = $this->lossy        ? "_l"                       : null;
2515
2516
        $saveAs = $this->normalizeFileExtension();
2517
        $saveAs = $saveAs ? "_$saveAs" : null;
2518
2519
        $copyStrat = null;
2520
        if ($this->copyStrategy === self::RESIZE) {
2521
            $copyStrat = "_rs";
2522
        }
2523
2524
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
2525
        $height = $this->newHeight ? '_' . $this->newHeight : null;
2526
2527
        $offset = isset($this->offset)
2528
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
2529
            : null;
2530
2531
        $crop = $this->crop
2532
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
2533
            : null;
2534
2535
        $filters = null;
2536
        if (isset($this->filters)) {
2537
            foreach ($this->filters as $filter) {
2538
                if (is_array($filter)) {
2539
                    $filters .= "_f{$filter['id']}";
2540
                    for ($i=1; $i<=$filter['argc']; $i++) {
2541
                        $filters .= "-".$filter["arg{$i}"];
2542
                    }
2543
                }
2544
            }
2545
        }
2546
2547
        $sharpen = $this->sharpen ? 's' : null;
2548
        $emboss  = $this->emboss  ? 'e' : null;
2549
        $blur    = $this->blur    ? 'b' : null;
2550
        $palette = $this->palette ? 'p' : null;
2551
2552
        $autoRotate = $this->autoRotate ? 'ar' : null;
2553
2554
        $optimize  = $this->jpegOptimize ? 'o' : null;
2555
        $optimize .= $this->pngFilter    ? 'f' : null;
2556
        $optimize .= $this->pngDeflate   ? 'd' : null;
2557
2558
        $convolve = null;
2559
        if ($this->convolve) {
2560
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
2561
        }
2562
2563
        $upscale = null;
2564
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
2565
            $upscale = '_nu';
2566
        }
2567
2568
        $subdir = null;
2569
        if ($useSubdir === true) {
2570
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
0 ignored issues
show
It seems like $this->imageSrc can also be of type null; however, parameter $path of dirname() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2570
            $subdir = str_replace('/', '-', dirname(/** @scrutinizer ignore-type */ $this->imageSrc));
Loading history...
2571
            $subdir = ($subdir == '.') ? '_.' : $subdir;
2572
            $subdir .= '_';
2573
        }
2574
2575
        $file = $prefix . $subdir . $filename . $width . $height
2576
            . $offset . $crop . $cropToFit . $fillToFit
2577
            . $crop_x . $crop_y . $upscale
2578
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
2579
            . $optimize . $compress
2580
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
2581
            . $convolve . $copyStrat . $lossy . $saveAs;
2582
2583
        return $this->setTarget($file, $base);
2584
    }
2585
2586
2587
2588
    /**
2589
     * Use cached version of image, if possible.
2590
     *
2591
     * @param boolean $useCache is default true, set to false to avoid using cached object.
2592
     *
2593
     * @return $this
2594
     */
2595
    public function useCacheIfPossible($useCache = true)
2596
    {
2597
        if ($useCache && is_readable($this->cacheFileName)) {
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of is_readable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2597
        if ($useCache && is_readable(/** @scrutinizer ignore-type */ $this->cacheFileName)) {
Loading history...
2598
            $fileTime   = filemtime($this->pathToImage);
0 ignored issues
show
It seems like $this->pathToImage can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2598
            $fileTime   = filemtime(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
2599
            $cacheTime  = filemtime($this->cacheFileName);
2600
2601
            if ($fileTime <= $cacheTime) {
2602
                if ($this->useCache) {
2603
                    if ($this->verbose) {
2604
                        $this->log("Use cached file.");
2605
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2605
                        $this->log("Cached image filesize: " . filesize(/** @scrutinizer ignore-type */ $this->cacheFileName) . " bytes.");
Loading history...
2606
                    }
2607
                    $this->output($this->cacheFileName, $this->outputFormat);
2608
                } else {
2609
                    $this->log("Cache is valid but ignoring it by intention.");
2610
                }
2611
            } else {
2612
                $this->log("Original file is modified, ignoring cache.");
2613
            }
2614
        } else {
2615
            $this->log("Cachefile does not exists or ignoring it.");
2616
        }
2617
2618
        return $this;
2619
    }
2620
2621
2622
2623
    /**
2624
     * Load image from disk. Try to load image without verbose error message,
2625
     * if fail, load again and display error messages.
2626
     *
2627
     * @param string $src of image.
2628
     * @param string $dir as base directory where images are.
2629
     *
2630
     * @return $this
2631
     *
2632
     */
2633
    public function load($src = null, $dir = null)
2634
    {
2635
        if (isset($src)) {
2636
            $this->setSource($src, $dir);
2637
        }
2638
2639
        $this->loadImageDetails();
2640
2641
        if ($this->fileType === IMG_WEBP) {
2642
            $this->image = imagecreatefromwebp($this->pathToImage);
2643
        } else {
2644
            $imageAsString = file_get_contents($this->pathToImage);
0 ignored issues
show
It seems like $this->pathToImage can also be of type null; however, parameter $filename of file_get_contents() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2644
            $imageAsString = file_get_contents(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
2645
            $this->image = imagecreatefromstring($imageAsString);
2646
        }
2647
        if ($this->image === false) {
2648
            throw new Exception("Could not load image.");
2649
        }
2650
2651
        /* Removed v0.7.7
2652
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
2653
            $type = $this->getPngType();
2654
            $hasFewColors = imagecolorstotal($this->image);
2655
2656
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
2657
                if ($this->verbose) {
2658
                    $this->log("Handle this image as a palette image.");
2659
                }
2660
                $this->palette = true;
2661
            }
2662
        }
2663
        */
2664
2665
        if ($this->verbose) {
2666
            $this->log("### Image successfully loaded from file.");
2667
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2668
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2669
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
0 ignored issues
show
It seems like $this->image can also be of type GdImage; however, parameter $im of CImage::colorsTotal() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2669
            $this->log(" Number of colors in image = " . $this->colorsTotal(/** @scrutinizer ignore-type */ $this->image));
Loading history...
2670
            $index = imagecolortransparent($this->image);
2671
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2672
        }
2673
2674
        return $this;
2675
    }
2676
2677
2678
2679
    /**
2680
     * Get the type of PNG image.
2681
     *
2682
     * @param string $filename to use instead of default.
2683
     *
2684
     * @return int as the type of the png-image
2685
     *
2686
     */
2687
    public function getPngType($filename = null)
2688
    {
2689
        $filename = $filename ? $filename : $this->pathToImage;
2690
2691
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
0 ignored issues
show
It seems like $filename can also be of type null; however, parameter $filename of file_get_contents() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2691
        $pngType = ord(file_get_contents(/** @scrutinizer ignore-type */ $filename, false, null, 25, 1));
Loading history...
2692
2693
        if ($this->verbose) {
2694
            $this->log("Checking png type of: " . $filename);
2695
            $this->log($this->getPngTypeAsString($pngType));
2696
        }
2697
2698
        return $pngType;
2699
    }
2700
2701
2702
2703
    /**
2704
     * Get the type of PNG image as a verbose string.
2705
     *
2706
     * @param integer $type     to use, default is to check the type.
2707
     * @param string  $filename to use instead of default.
2708
     *
2709
     * @return int as the type of the png-image
2710
     *
2711
     */
2712
    private function getPngTypeAsString($pngType = null, $filename = null)
2713
    {
2714
        if ($filename || !$pngType) {
2715
            $pngType = $this->getPngType($filename);
2716
        }
2717
2718
        $index = imagecolortransparent($this->image);
2719
        $transparent = null;
2720
        if ($index != -1) {
2721
            $transparent = " (transparent)";
2722
        }
2723
2724
        switch ($pngType) {
2725
2726
            case self::PNG_GREYSCALE:
2727
                $text = "PNG is type 0, Greyscale$transparent";
2728
                break;
2729
2730
            case self::PNG_RGB:
2731
                $text = "PNG is type 2, RGB$transparent";
2732
                break;
2733
2734
            case self::PNG_RGB_PALETTE:
2735
                $text = "PNG is type 3, RGB with palette$transparent";
2736
                break;
2737
2738
            case self::PNG_GREYSCALE_ALPHA:
2739
                $text = "PNG is type 4, Greyscale with alpha channel";
2740
                break;
2741
2742
            case self::PNG_RGB_ALPHA:
2743
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
2744
                break;
2745
2746
            default:
2747
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
2748
        }
2749
2750
        return $text;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $text returns the type string which is incompatible with the documented return type integer.
Loading history...
2751
    }
2752
2753
2754
2755
2756
    /**
2757
     * Calculate number of colors in an image.
2758
     *
2759
     * @param resource $im the image.
2760
     *
2761
     * @return int
2762
     */
2763
    private function colorsTotal($im)
2764
    {
2765
        if (imageistruecolor($im)) {
2766
            $this->log("Colors as true color.");
2767
            $h = imagesy($im);
2768
            $w = imagesx($im);
2769
            $c = array();
2770
            for ($x=0; $x < $w; $x++) {
2771
                for ($y=0; $y < $h; $y++) {
2772
                    @$c['c'.imagecolorat($im, $x, $y)]++;
2773
                }
2774
            }
2775
            return count($c);
2776
        } else {
2777
            $this->log("Colors as palette.");
2778
            return imagecolorstotal($im);
2779
        }
2780
    }
2781
2782
2783
2784
    /**
2785
     * Preprocess image before rezising it.
2786
     *
2787
     * @return $this
2788
     */
2789
    public function preResize()
2790
    {
2791
        $this->log("### Pre-process before resizing");
2792
2793
        // Rotate image
2794
        if ($this->rotateBefore) {
2795
            $this->log("Rotating image.");
2796
            $this->rotate($this->rotateBefore, $this->bgColor)
2797
                 ->reCalculateDimensions();
2798
        }
2799
2800
        // Auto-rotate image
2801
        if ($this->autoRotate) {
2802
            $this->log("Auto rotating image.");
2803
            $this->rotateExif()
2804
                 ->reCalculateDimensions();
2805
        }
2806
2807
        // Scale the original image before starting
2808
        if (isset($this->scale)) {
2809
            $this->log("Scale by {$this->scale}%");
2810
            $newWidth  = $this->width * $this->scale / 100;
2811
            $newHeight = $this->height * $this->scale / 100;
2812
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
2813
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
2814
            $this->image = $img;
2815
            $this->width = $newWidth;
2816
            $this->height = $newHeight;
2817
        }
2818
2819
        return $this;
2820
    }
2821
2822
2823
2824
    /**
2825
     * Resize or resample the image while resizing.
2826
     *
2827
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
2828
     *
2829
     * @return $this
2830
     */
2831
     public function setCopyResizeStrategy($strategy)
2832
     {
2833
         $this->copyStrategy = $strategy;
2834
         return $this;
2835
     }
2836
2837
2838
2839
    /**
2840
     * Resize and or crop the image.
2841
     *
2842
     * @return void
2843
     */
2844
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
2845
    {
2846
        if($this->copyStrategy == self::RESIZE) {
2847
            $this->log("Copy by resize");
2848
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
2849
        } else {
2850
            $this->log("Copy by resample");
2851
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
2852
        }
2853
    }
2854
2855
2856
2857
    /**
2858
     * Resize and or crop the image.
2859
     *
2860
     * @return $this
2861
     */
2862
    public function resize()
2863
    {
2864
2865
        $this->log("### Starting to Resize()");
2866
        $this->log("Upscale = '$this->upscale'");
2867
2868
        // Only use a specified area of the image, $this->offset is defining the area to use
2869
        if (isset($this->offset)) {
2870
2871
            $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']}");
2872
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
2873
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
2874
            $this->image = $img;
2875
            $this->width = $this->offset['width'];
2876
            $this->height = $this->offset['height'];
2877
        }
2878
2879
        if ($this->crop) {
2880
2881
            // Do as crop, take only part of image
2882
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
2883
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
2884
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
2885
            $this->image = $img;
2886
            $this->width = $this->crop['width'];
2887
            $this->height = $this->crop['height'];
2888
        }
2889
2890
        if (!$this->upscale) {
2891
            // Consider rewriting the no-upscale code to fit within this if-statement,
2892
            // likely to be more readable code.
2893
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
2894
        }
2895
2896
        if ($this->cropToFit) {
2897
2898
            // Resize by crop to fit
2899
            $this->log("Resizing using strategy - Crop to fit");
2900
2901
            if (!$this->upscale 
2902
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
2903
                $this->log("Resizing - smaller image, do not upscale.");
2904
2905
                $posX = 0;
2906
                $posY = 0;
2907
                $cropX = 0;
2908
                $cropY = 0;
2909
2910
                if ($this->newWidth > $this->width) {
2911
                    $posX = round(($this->newWidth - $this->width) / 2);
2912
                }
2913
                if ($this->newWidth < $this->width) {
2914
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
2915
                }
2916
2917
                if ($this->newHeight > $this->height) {
2918
                    $posY = round(($this->newHeight - $this->height) / 2);
2919
                }
2920
                if ($this->newHeight < $this->height) {
2921
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
2922
                }
2923
                $this->log(" cwidth: $this->cropWidth");
2924
                $this->log(" cheight: $this->cropHeight");
2925
                $this->log(" nwidth: $this->newWidth");
2926
                $this->log(" nheight: $this->newHeight");
2927
                $this->log(" width: $this->width");
2928
                $this->log(" height: $this->height");
2929
                $this->log(" posX: $posX");
2930
                $this->log(" posY: $posY");
2931
                $this->log(" cropX: $cropX");
2932
                $this->log(" cropY: $cropY");
2933
2934
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
2935
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
0 ignored issues
show
It seems like $cropY can also be of type double; however, parameter $src_y of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2935
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, /** @scrutinizer ignore-type */ $cropY, $this->width, $this->height);
Loading history...
It seems like $posX can also be of type double; however, parameter $dst_x of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2935
                imagecopy($imageResized, $this->image, /** @scrutinizer ignore-type */ $posX, $posY, $cropX, $cropY, $this->width, $this->height);
Loading history...
It seems like $posY can also be of type double; however, parameter $dst_y of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2935
                imagecopy($imageResized, $this->image, $posX, /** @scrutinizer ignore-type */ $posY, $cropX, $cropY, $this->width, $this->height);
Loading history...
It seems like $cropX can also be of type double; however, parameter $src_x of imagecopy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2935
                imagecopy($imageResized, $this->image, $posX, $posY, /** @scrutinizer ignore-type */ $cropX, $cropY, $this->width, $this->height);
Loading history...
2936
            } else {
2937
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
2938
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
2939
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
2940
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
2941
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
2942
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
2943
            }
2944
2945
            $this->image = $imageResized;
2946
            $this->width = $this->newWidth;
2947
            $this->height = $this->newHeight;
2948
2949
        } elseif ($this->fillToFit) {
2950
2951
            // Resize by fill to fit
2952
            $this->log("Resizing using strategy - Fill to fit");
2953
2954
            $posX = 0;
2955
            $posY = 0;
2956
2957
            $ratioOrig = $this->width / $this->height;
2958
            $ratioNew  = $this->newWidth / $this->newHeight;
2959
2960
            // Check ratio for landscape or portrait
2961
            if ($ratioOrig < $ratioNew) {
2962
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
2963
            } else {
2964
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
2965
            }
2966
2967
            if (!$this->upscale
2968
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
2969
            ) {
2970
2971
                $this->log("Resizing - smaller image, do not upscale.");
2972
                $posX = round(($this->newWidth - $this->width) / 2);
2973
                $posY = round(($this->newHeight - $this->height) / 2);
2974
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
2975
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
2976
2977
            } else {
2978
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
2979
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
2980
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
2981
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
2982
            }
2983
2984
            $this->image = $imageResized;
2985
            $this->width = $this->newWidth;
2986
            $this->height = $this->newHeight;
2987
2988
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
2989
2990
            // Resize it
2991
            $this->log("Resizing, new height and/or width");
2992
2993
            if (!$this->upscale
2994
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
2995
            ) {
2996
                $this->log("Resizing - smaller image, do not upscale.");
2997
2998
                if (!$this->keepRatio) {
2999
                    $this->log("Resizing - stretch to fit selected.");
3000
3001
                    $posX = 0;
3002
                    $posY = 0;
3003
                    $cropX = 0;
3004
                    $cropY = 0;
3005
3006
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
3007
                        $posX = round(($this->newWidth - $this->width) / 2);
3008
                        $posY = round(($this->newHeight - $this->height) / 2);
3009
                    } elseif ($this->newWidth > $this->width) {
3010
                        $posX = round(($this->newWidth - $this->width) / 2);
3011
                        $cropY = round(($this->height - $this->newHeight) / 2);
3012
                    } elseif ($this->newHeight > $this->height) {
3013
                        $posY = round(($this->newHeight - $this->height) / 2);
3014
                        $cropX = round(($this->width - $this->newWidth) / 2);
3015
                    }
3016
3017
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
3018
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
3019
                    $this->image = $imageResized;
3020
                    $this->width = $this->newWidth;
3021
                    $this->height = $this->newHeight;
3022
                }
3023
            } else {
3024
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
3025
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
3026
                $this->image = $imageResized;
3027
                $this->width = $this->newWidth;
3028
                $this->height = $this->newHeight;
3029
            }
3030
        }
3031
3032
        return $this;
3033
    }
3034
3035
3036
3037
    /**
3038
     * Postprocess image after rezising image.
3039
     *
3040
     * @return $this
3041
     */
3042
    public function postResize()
3043
    {
3044
        $this->log("### Post-process after resizing");
3045
3046
        // Rotate image
3047
        if ($this->rotateAfter) {
3048
            $this->log("Rotating image.");
3049
            $this->rotate($this->rotateAfter, $this->bgColor);
3050
        }
3051
3052
        // Apply filters
3053
        if (isset($this->filters) && is_array($this->filters)) {
3054
3055
            foreach ($this->filters as $filter) {
3056
                $this->log("Applying filter {$filter['type']}.");
3057
3058
                switch ($filter['argc']) {
3059
3060
                    case 0:
3061
                        imagefilter($this->image, $filter['type']);
3062
                        break;
3063
3064
                    case 1:
3065
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
3066
                        break;
3067
3068
                    case 2:
3069
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
3070
                        break;
3071
3072
                    case 3:
3073
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
3074
                        break;
3075
3076
                    case 4:
3077
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
3078
                        break;
3079
                }
3080
            }
3081
        }
3082
3083
        // Convert to palette image
3084
        if ($this->palette) {
3085
            $this->log("Converting to palette image.");
3086
            $this->trueColorToPalette();
3087
        }
3088
3089
        // Blur the image
3090
        if ($this->blur) {
3091
            $this->log("Blur.");
3092
            $this->blurImage();
3093
        }
3094
3095
        // Emboss the image
3096
        if ($this->emboss) {
3097
            $this->log("Emboss.");
3098
            $this->embossImage();
3099
        }
3100
3101
        // Sharpen the image
3102
        if ($this->sharpen) {
3103
            $this->log("Sharpen.");
3104
            $this->sharpenImage();
3105
        }
3106
3107
        // Custom convolution
3108
        if ($this->convolve) {
3109
            //$this->log("Convolve: " . $this->convolve);
3110
            $this->imageConvolution();
3111
        }
3112
3113
        return $this;
3114
    }
3115
3116
3117
3118
    /**
3119
     * Rotate image using angle.
3120
     *
3121
     * @param float $angle        to rotate image.
3122
     * @param int   $anglebgColor to fill image with if needed.
3123
     *
3124
     * @return $this
3125
     */
3126
    public function rotate($angle, $bgColor)
0 ignored issues
show
The parameter $bgColor is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

3126
    public function rotate($angle, /** @scrutinizer ignore-unused */ $bgColor)

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

Loading history...
3127
    {
3128
        $this->log("Rotate image " . $angle . " degrees with filler color.");
3129
3130
        $color = $this->getBackgroundColor();
3131
        $this->image = imagerotate($this->image, $angle, $color);
0 ignored issues
show
$color of type color is incompatible with the type integer expected by parameter $bgd_color of imagerotate(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3131
        $this->image = imagerotate($this->image, $angle, /** @scrutinizer ignore-type */ $color);
Loading history...
3132
3133
        $this->width  = imagesx($this->image);
3134
        $this->height = imagesy($this->image);
3135
3136
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
3137
3138
        return $this;
3139
    }
3140
3141
3142
3143
    /**
3144
     * Rotate image using information in EXIF.
3145
     *
3146
     * @return $this
3147
     */
3148
    public function rotateExif()
3149
    {
3150
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
3151
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
3152
            return $this;
3153
        }
3154
3155
        $exif = exif_read_data($this->pathToImage);
3156
3157
        if (!empty($exif['Orientation'])) {
3158
            switch ($exif['Orientation']) {
3159
                case 3:
3160
                    $this->log("Autorotate 180.");
3161
                    $this->rotate(180, $this->bgColor);
3162
                    break;
3163
3164
                case 6:
3165
                    $this->log("Autorotate -90.");
3166
                    $this->rotate(-90, $this->bgColor);
3167
                    break;
3168
3169
                case 8:
3170
                    $this->log("Autorotate 90.");
3171
                    $this->rotate(90, $this->bgColor);
3172
                    break;
3173
3174
                default:
3175
                    $this->log("Autorotate ignored, unknown value as orientation.");
3176
            }
3177
        } else {
3178
            $this->log("Autorotate ignored, no orientation in EXIF.");
3179
        }
3180
3181
        return $this;
3182
    }
3183
3184
3185
3186
    /**
3187
     * Convert true color image to palette image, keeping alpha.
3188
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
3189
     *
3190
     * @return void
3191
     */
3192
    public function trueColorToPalette()
3193
    {
3194
        $img = imagecreatetruecolor($this->width, $this->height);
3195
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
3196
        imagecolortransparent($img, $bga);
3197
        imagefill($img, 0, 0, $bga);
3198
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
3199
        imagetruecolortopalette($img, false, 255);
3200
        imagesavealpha($img, true);
3201
3202
        if (imageistruecolor($this->image)) {
3203
            $this->log("Matching colors with true color image.");
3204
            imagecolormatch($this->image, $img);
3205
        }
3206
3207
        $this->image = $img;
3208
    }
3209
3210
3211
3212
    /**
3213
     * Sharpen image using image convolution.
3214
     *
3215
     * @return $this
3216
     */
3217
    public function sharpenImage()
3218
    {
3219
        $this->imageConvolution('sharpen');
3220
        return $this;
3221
    }
3222
3223
3224
3225
    /**
3226
     * Emboss image using image convolution.
3227
     *
3228
     * @return $this
3229
     */
3230
    public function embossImage()
3231
    {
3232
        $this->imageConvolution('emboss');
3233
        return $this;
3234
    }
3235
3236
3237
3238
    /**
3239
     * Blur image using image convolution.
3240
     *
3241
     * @return $this
3242
     */
3243
    public function blurImage()
3244
    {
3245
        $this->imageConvolution('blur');
3246
        return $this;
3247
    }
3248
3249
3250
3251
    /**
3252
     * Create convolve expression and return arguments for image convolution.
3253
     *
3254
     * @param string $expression constant string which evaluates to a list of
3255
     *                           11 numbers separated by komma or such a list.
3256
     *
3257
     * @return array as $matrix (3x3), $divisor and $offset
3258
     */
3259
    public function createConvolveArguments($expression)
3260
    {
3261
        // Check of matching constant
3262
        if (isset($this->convolves[$expression])) {
3263
            $expression = $this->convolves[$expression];
3264
        }
3265
3266
        $part = explode(',', $expression);
3267
        $this->log("Creating convolution expressen: $expression");
3268
3269
        // Expect list of 11 numbers, split by , and build up arguments
3270
        if (count($part) != 11) {
3271
            throw new Exception(
3272
                "Missmatch in argument convolve. Expected comma-separated string with
3273
                11 float values. Got $expression."
3274
            );
3275
        }
3276
3277
        array_walk($part, function ($item, $key) {
0 ignored issues
show
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

3277
        array_walk($part, function ($item, /** @scrutinizer ignore-unused */ $key) {

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

Loading history...
3278
            if (!is_numeric($item)) {
3279
                throw new Exception("Argument to convolve expression should be float but is not.");
3280
            }
3281
        });
3282
3283
        return array(
3284
            array(
3285
                array($part[0], $part[1], $part[2]),
3286
                array($part[3], $part[4], $part[5]),
3287
                array($part[6], $part[7], $part[8]),
3288
            ),
3289
            $part[9],
3290
            $part[10],
3291
        );
3292
    }
3293
3294
3295
3296
    /**
3297
     * Add custom expressions (or overwrite existing) for image convolution.
3298
     *
3299
     * @param array $options Key value array with strings to be converted
3300
     *                       to convolution expressions.
3301
     *
3302
     * @return $this
3303
     */
3304
    public function addConvolveExpressions($options)
3305
    {
3306
        $this->convolves = array_merge($this->convolves, $options);
3307
        return $this;
3308
    }
3309
3310
3311
3312
    /**
3313
     * Image convolution.
3314
     *
3315
     * @param string $options A string with 11 float separated by comma.
3316
     *
3317
     * @return $this
3318
     */
3319
    public function imageConvolution($options = null)
3320
    {
3321
        // Use incoming options or use $this.
3322
        $options = $options ? $options : $this->convolve;
3323
3324
        // Treat incoming as string, split by +
3325
        $this->log("Convolution with '$options'");
3326
        $options = explode(":", $options);
3327
3328
        // Check each option if it matches constant value
3329
        foreach ($options as $option) {
3330
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
3331
            imageconvolution($this->image, $matrix, $divisor, $offset);
3332
        }
3333
3334
        return $this;
3335
    }
3336
3337
3338
3339
    /**
3340
     * Set default background color between 000000-FFFFFF or if using
3341
     * alpha 00000000-FFFFFF7F.
3342
     *
3343
     * @param string $color as hex value.
3344
     *
3345
     * @return $this
3346
    */
3347
    public function setDefaultBackgroundColor($color)
3348
    {
3349
        $this->log("Setting default background color to '$color'.");
3350
3351
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
3352
            throw new Exception(
3353
                "Background color needs a hex value of 6 or 8
3354
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
3355
                Current value was: '$color'."
3356
            );
3357
        }
3358
3359
        $red    = hexdec(substr($color, 0, 2));
3360
        $green  = hexdec(substr($color, 2, 2));
3361
        $blue   = hexdec(substr($color, 4, 2));
3362
3363
        $alpha = (strlen($color) == 8)
3364
            ? hexdec(substr($color, 6, 2))
3365
            : null;
3366
3367
        if (($red < 0 || $red > 255)
3368
            || ($green < 0 || $green > 255)
3369
            || ($blue < 0 || $blue > 255)
3370
            || ($alpha < 0 || $alpha > 127)
3371
        ) {
3372
            throw new Exception(
3373
                "Background color out of range. Red, green blue
3374
                should be 00-FF and alpha should be 00-7F.
3375
                Current value was: '$color'."
3376
            );
3377
        }
3378
3379
        $this->bgColor = strtolower($color);
3380
        $this->bgColorDefault = array(
3381
            'red'   => $red,
3382
            'green' => $green,
3383
            'blue'  => $blue,
3384
            'alpha' => $alpha
3385
        );
3386
3387
        return $this;
3388
    }
3389
3390
3391
3392
    /**
3393
     * Get the background color.
3394
     *
3395
     * @param resource $img the image to work with or null if using $this->image.
3396
     *
3397
     * @return color value or null if no background color is set.
3398
    */
3399
    private function getBackgroundColor($img = null)
3400
    {
3401
        $img = isset($img) ? $img : $this->image;
3402
3403
        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...
3404
3405
            $red   = $this->bgColorDefault['red'];
3406
            $green = $this->bgColorDefault['green'];
3407
            $blue  = $this->bgColorDefault['blue'];
3408
            $alpha = $this->bgColorDefault['alpha'];
3409
3410
            if ($alpha) {
3411
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
3412
            } else {
3413
                $color = imagecolorallocate($img, $red, $green, $blue);
3414
            }
3415
3416
            return $color;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $color returns the type integer which is incompatible with the documented return type color.
Loading history...
3417
3418
        } else {
3419
            return 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return 0 returns the type integer which is incompatible with the documented return type color.
Loading history...
3420
        }
3421
    }
3422
3423
3424
3425
    /**
3426
     * Create a image and keep transparency for png and gifs.
3427
     *
3428
     * @param int $width of the new image.
3429
     * @param int $height of the new image.
3430
     *
3431
     * @return image resource.
3432
    */
3433
    private function createImageKeepTransparency($width, $height)
3434
    {
3435
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
3436
        $img = imagecreatetruecolor($width, $height);
3437
        imagealphablending($img, false);
3438
        imagesavealpha($img, true);
3439
3440
        $index = $this->image
3441
            ? imagecolortransparent($this->image)
3442
            : -1;
3443
3444
        if ($index != -1) {
3445
3446
            imagealphablending($img, true);
3447
            $transparent = imagecolorsforindex($this->image, $index);
3448
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
3449
            imagefill($img, 0, 0, $color);
3450
            $index = imagecolortransparent($img, $color);
3451
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
3452
3453
        } 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...
3454
3455
            $color = $this->getBackgroundColor($img);
0 ignored issues
show
It seems like $img can also be of type GdImage; however, parameter $img of CImage::getBackgroundColor() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3455
            $color = $this->getBackgroundColor(/** @scrutinizer ignore-type */ $img);
Loading history...
3456
            imagefill($img, 0, 0, $color);
0 ignored issues
show
$color of type color is incompatible with the type integer expected by parameter $color of imagefill(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3456
            imagefill($img, 0, 0, /** @scrutinizer ignore-type */ $color);
Loading history...
3457
            $this->Log("Filling image with background color.");
3458
        }
3459
3460
        return $img;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $img returns the type GdImage|resource which is incompatible with the documented return type image.
Loading history...
3461
    }
3462
3463
3464
3465
    /**
3466
     * Set optimizing  and post-processing options.
3467
     *
3468
     * @param array $options with config for postprocessing with external tools.
3469
     *
3470
     * @return $this
3471
     */
3472
    public function setPostProcessingOptions($options)
3473
    {
3474
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
3475
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
3476
        } else {
3477
            $this->jpegOptimizeCmd = null;
3478
        }
3479
3480
        if (array_key_exists("png_lossy", $options) 
3481
            && $options['png_lossy'] !== false) {
3482
            $this->pngLossy = $options['png_lossy'];
3483
            $this->pngLossyCmd = $options['png_lossy_cmd'];
3484
        } else {
3485
            $this->pngLossyCmd = null;
3486
        }
3487
3488
        if (isset($options['png_filter']) && $options['png_filter']) {
3489
            $this->pngFilterCmd = $options['png_filter_cmd'];
3490
        } else {
3491
            $this->pngFilterCmd = null;
3492
        }
3493
3494
        if (isset($options['png_deflate']) && $options['png_deflate']) {
3495
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
3496
        } else {
3497
            $this->pngDeflateCmd = null;
3498
        }
3499
3500
        return $this;
3501
    }
3502
3503
3504
3505
    /**
3506
     * Find out the type (file extension) for the image to be saved.
3507
     *
3508
     * @return string as image extension.
3509
     */
3510
    protected function getTargetImageExtension()
3511
    {
3512
        // switch on mimetype
3513
        if (isset($this->extension)) {
3514
            return strtolower($this->extension);
3515
        } elseif ($this->fileType === IMG_WEBP) {
3516
            return "webp";
3517
        }
3518
3519
        return substr(image_type_to_extension($this->fileType), 1);
3520
    }
3521
3522
3523
3524
    /**
3525
     * Save image.
3526
     *
3527
     * @param string  $src       as target filename.
3528
     * @param string  $base      as base directory where to store images.
3529
     * @param boolean $overwrite or not, default to always overwrite file.
3530
     *
3531
     * @return $this or false if no folder is set.
3532
     */
3533
    public function save($src = null, $base = null, $overwrite = true)
3534
    {
3535
        if (isset($src)) {
3536
            $this->setTarget($src, $base);
3537
        }
3538
3539
        if ($overwrite === false && is_file($this->cacheFileName)) {
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3539
        if ($overwrite === false && is_file(/** @scrutinizer ignore-type */ $this->cacheFileName)) {
Loading history...
3540
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
3541
            return;
3542
        }
3543
3544
        is_writable($this->saveFolder)
3545
            or $this->raiseError('Target directory is not writable.');
3546
3547
        $type = $this->getTargetImageExtension();
3548
        $this->Log("Saving image as " . $type);
3549
        switch($type) {
3550
3551
            case 'jpeg':
3552
            case 'jpg':
3553
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
3554
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
3555
3556
                // Use JPEG optimize if defined
3557
                if ($this->jpegOptimizeCmd) {
3558
                    if ($this->verbose) {
3559
                        clearstatcache();
3560
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3560
                        $this->log("Filesize before optimize: " . filesize(/** @scrutinizer ignore-type */ $this->cacheFileName) . " bytes.");
Loading history...
3561
                    }
3562
                    $res = array();
3563
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
3564
                    exec($cmd, $res);
3565
                    $this->log($cmd);
3566
                    $this->log($res);
3567
                }
3568
                break;
3569
3570
            case 'gif':
3571
                $this->Log("Saving image as GIF to cache.");
3572
                imagegif($this->image, $this->cacheFileName);
3573
                break;
3574
3575
            case 'webp':
3576
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
3577
                imagewebp($this->image, $this->cacheFileName, $this->quality);
3578
                break;
3579
3580
            case 'png':
3581
            default:
3582
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
3583
3584
                // Turn off alpha blending and set alpha flag
3585
                imagealphablending($this->image, false);
3586
                imagesavealpha($this->image, true);
3587
                imagepng($this->image, $this->cacheFileName, $this->compress);
3588
3589
                // Use external program to process lossy PNG, if defined
3590
                $lossyEnabled = $this->pngLossy === true;
3591
                $lossySoftEnabled = $this->pngLossy === null;
3592
                $lossyActiveEnabled = $this->lossy === true;
3593
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
3594
                    if ($this->verbose) {
3595
                        clearstatcache();
3596
                        $this->log("Lossy enabled: $lossyEnabled");
3597
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
3598
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
3599
                    }
3600
                    $res = array();
3601
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
3602
                    exec($cmd, $res);
3603
                    $this->Log($cmd);
3604
                    $this->Log($res);
3605
                }
3606
3607
                // Use external program to filter PNG, if defined
3608
                if ($this->pngFilterCmd) {
3609
                    if ($this->verbose) {
3610
                        clearstatcache();
3611
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
3612
                    }
3613
                    $res = array();
3614
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
3615
                    exec($cmd, $res);
3616
                    $this->Log($cmd);
3617
                    $this->Log($res);
3618
                }
3619
3620
                // Use external program to deflate PNG, if defined
3621
                if ($this->pngDeflateCmd) {
3622
                    if ($this->verbose) {
3623
                        clearstatcache();
3624
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
3625
                    }
3626
                    $res = array();
3627
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
3628
                    exec($cmd, $res);
3629
                    $this->Log($cmd);
3630
                    $this->Log($res);
3631
                }
3632
                break;
3633
        }
3634
3635
        if ($this->verbose) {
3636
            clearstatcache();
3637
            $this->log("Saved image to cache.");
3638
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
3639
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
3640
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
3641
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
3642
            $index = imagecolortransparent($this->image);
3643
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
3644
        }
3645
3646
        return $this;
3647
    }
3648
3649
3650
3651
    /**
3652
     * Convert image from one colorpsace/color profile to sRGB without
3653
     * color profile.
3654
     *
3655
     * @param string  $src      of image.
3656
     * @param string  $dir      as base directory where images are.
3657
     * @param string  $cache    as base directory where to store images.
3658
     * @param string  $iccFile  filename of colorprofile.
3659
     * @param boolean $useCache or not, default to always use cache.
3660
     *
3661
     * @return string | boolean false if no conversion else the converted
3662
     *                          filename.
3663
     */
3664
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
3665
    {
3666
        if ($this->verbose) {
3667
            $this->log("# Converting image to sRGB colorspace.");
3668
        }
3669
3670
        if (!class_exists("Imagick")) {
3671
            $this->log(" Ignoring since Imagemagick is not installed.");
3672
            return false;
3673
        }
3674
3675
        // Prepare
3676
        $this->setSaveFolder($cache)
3677
             ->setSource($src, $dir)
3678
             ->generateFilename(null, false, 'srgb_');
3679
3680
        // Check if the cached version is accurate.
3681
        if ($useCache && is_readable($this->cacheFileName)) {
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $filename of is_readable() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3681
        if ($useCache && is_readable(/** @scrutinizer ignore-type */ $this->cacheFileName)) {
Loading history...
3682
            $fileTime  = filemtime($this->pathToImage);
0 ignored issues
show
It seems like $this->pathToImage can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3682
            $fileTime  = filemtime(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
3683
            $cacheTime = filemtime($this->cacheFileName);
3684
3685
            if ($fileTime <= $cacheTime) {
3686
                $this->log(" Using cached version: " . $this->cacheFileName);
3687
                return $this->cacheFileName;
3688
            }
3689
        }
3690
3691
        // Only covert if cachedir is writable
3692
        if (is_writable($this->saveFolder)) {
3693
            // Load file and check if conversion is needed
3694
            $image      = new Imagick($this->pathToImage);
3695
            $colorspace = $image->getImageColorspace();
3696
            $this->log(" Current colorspace: " . $colorspace);
3697
3698
            $profiles      = $image->getImageProfiles('*', false);
3699
            $hasICCProfile = (array_search('icc', $profiles) !== false);
3700
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
3701
3702
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
3703
                $this->log(" Converting to sRGB.");
3704
3705
                $sRGBicc = file_get_contents($iccFile);
3706
                $image->profileImage('icc', $sRGBicc);
3707
3708
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
3709
                $image->writeImage($this->cacheFileName);
3710
                return $this->cacheFileName;
3711
            }
3712
        }
3713
3714
        return false;
3715
    }
3716
3717
3718
3719
    /**
3720
     * Create a hard link, as an alias, to the cached file.
3721
     *
3722
     * @param string $alias where to store the link,
3723
     *                      filename without extension.
3724
     *
3725
     * @return $this
3726
     */
3727
    public function linkToCacheFile($alias)
3728
    {
3729
        if ($alias === null) {
0 ignored issues
show
The condition $alias === null is always false.
Loading history...
3730
            $this->log("Ignore creating alias.");
3731
            return $this;
3732
        }
3733
3734
        if (is_readable($alias)) {
3735
            unlink($alias);
3736
        }
3737
3738
        $res = link($this->cacheFileName, $alias);
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $target of link() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3738
        $res = link(/** @scrutinizer ignore-type */ $this->cacheFileName, $alias);
Loading history...
3739
3740
        if ($res) {
3741
            $this->log("Created an alias as: $alias");
3742
        } else {
3743
            $this->log("Failed to create the alias: $alias");
3744
        }
3745
3746
        return $this;
3747
    }
3748
3749
3750
3751
    /**
3752
     * Add HTTP header for output together with image.
3753
     *
3754
     * @param string $type  the header type such as "Cache-Control"
3755
     * @param string $value the value to use
3756
     *
3757
     * @return void
3758
     */
3759
    public function addHTTPHeader($type, $value)
3760
    {
3761
        $this->HTTPHeader[$type] = $value;
3762
    }
3763
3764
3765
3766
    /**
3767
     * Output image to browser using caching.
3768
     *
3769
     * @param string $file   to read and output, default is to
3770
     *                       use $this->cacheFileName
3771
     * @param string $format set to json to output file as json
3772
     *                       object with details
3773
     *
3774
     * @return void
3775
     */
3776
    public function output($file = null, $format = null)
3777
    {
3778
        if (is_null($file)) {
3779
            $file = $this->cacheFileName;
3780
        }
3781
3782
        if (is_null($format)) {
3783
            $format = $this->outputFormat;
3784
        }
3785
3786
        $this->log("### Output");
3787
        $this->log("Output format is: $format");
3788
3789
        if (!$this->verbose && $format == 'json') {
3790
            header('Content-type: application/json');
3791
            echo $this->json($file);
3792
            exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3793
        } elseif ($format == 'ascii') {
3794
            header('Content-type: text/plain');
3795
            echo $this->ascii($file);
3796
            exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3797
        }
3798
3799
        $this->log("Outputting image: $file");
3800
3801
        // Get image modification time
3802
        clearstatcache();
3803
        $lastModified = filemtime($file);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3803
        $lastModified = filemtime(/** @scrutinizer ignore-type */ $file);
Loading history...
3804
        $lastModifiedFormat = "D, d M Y H:i:s";
3805
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
3806
3807
        if (!$this->verbose) {
3808
            $header = "Last-Modified: $gmdate GMT";
3809
            header($header);
3810
            $this->fastTrackCache->addHeader($header);
3811
            $this->fastTrackCache->setLastModified($lastModified);
3812
        }
3813
3814
        foreach ($this->HTTPHeader as $key => $val) {
3815
            $header = "$key: $val";
3816
            header($header);
3817
            $this->fastTrackCache->addHeader($header);
3818
        }
3819
3820
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
3821
            && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
3822
3823
            if ($this->verbose) {
3824
                $this->log("304 not modified");
3825
                $this->verboseOutput();
3826
                exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3827
            }
3828
3829
            header("HTTP/1.0 304 Not Modified");
3830
            if (CIMAGE_DEBUG) {
3831
                trace(__CLASS__ . " 304");
3832
            }
3833
3834
        } else {
3835
3836
            $this->loadImageDetails($file);
3837
            $mime = $this->getMimeType();
3838
            $size = filesize($file);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3838
            $size = filesize(/** @scrutinizer ignore-type */ $file);
Loading history...
3839
3840
            if ($this->verbose) {
3841
                $this->log("Last-Modified: " . $gmdate . " GMT");
3842
                $this->log("Content-type: " . $mime);
0 ignored issues
show
Are you sure $mime of type CImage can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3842
                $this->log("Content-type: " . /** @scrutinizer ignore-type */ $mime);
Loading history...
3843
                $this->log("Content-length: " . $size);
3844
                $this->verboseOutput();
3845
3846
                if (is_null($this->verboseFileName)) {
3847
                    exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3848
                }
3849
            }
3850
3851
            $header = "Content-type: $mime";
3852
            header($header);
3853
            $this->fastTrackCache->addHeaderOnOutput($header);
3854
3855
            $header = "Content-length: $size";
3856
            header($header);
3857
            $this->fastTrackCache->addHeaderOnOutput($header);
3858
3859
            $this->fastTrackCache->setSource($file);
3860
            $this->fastTrackCache->writeToCache();
3861
            if (CIMAGE_DEBUG) {
3862
                trace(__CLASS__ . " 200");
3863
            }
3864
            readfile($file);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of readfile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3864
            readfile(/** @scrutinizer ignore-type */ $file);
Loading history...
3865
        }
3866
3867
        exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3868
    }
3869
3870
3871
3872
    /**
3873
     * Create a JSON object from the image details.
3874
     *
3875
     * @param string $file the file to output.
3876
     *
3877
     * @return string json-encoded representation of the image.
3878
     */
3879
    public function json($file = null)
3880
    {
3881
        $file = $file ? $file : $this->cacheFileName;
3882
3883
        $details = array();
3884
3885
        clearstatcache();
3886
3887
        $details['src']       = $this->imageSrc;
3888
        $lastModified         = filemtime($this->pathToImage);
0 ignored issues
show
It seems like $this->pathToImage can also be of type null; however, parameter $filename of filemtime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3888
        $lastModified         = filemtime(/** @scrutinizer ignore-type */ $this->pathToImage);
Loading history...
3889
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
3890
3891
        $details['cache']       = basename($this->cacheFileName);
0 ignored issues
show
It seems like $this->cacheFileName can also be of type null; however, parameter $path of basename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3891
        $details['cache']       = basename(/** @scrutinizer ignore-type */ $this->cacheFileName);
Loading history...
3892
        $lastModified           = filemtime($this->cacheFileName);
3893
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
3894
3895
        $this->load($file);
3896
3897
        $details['filename']    = basename($file);
3898
        $details['mimeType']    = $this->getMimeType($this->fileType);
0 ignored issues
show
The call to CImage::getMimeType() has too many arguments starting with $this->fileType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3898
        /** @scrutinizer ignore-call */ 
3899
        $details['mimeType']    = $this->getMimeType($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. Please note the @ignore annotation hint above.

Loading history...
3899
        $details['width']       = $this->width;
3900
        $details['height']      = $this->height;
3901
        $details['aspectRatio'] = round($this->width / $this->height, 3);
3902
        $details['size']        = filesize($file);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3902
        $details['size']        = filesize(/** @scrutinizer ignore-type */ $file);
Loading history...
3903
        $details['colors'] = $this->colorsTotal($this->image);
0 ignored issues
show
It seems like $this->image can also be of type GdImage; however, parameter $im of CImage::colorsTotal() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3903
        $details['colors'] = $this->colorsTotal(/** @scrutinizer ignore-type */ $this->image);
Loading history...
3904
        $details['includedFiles'] = count(get_included_files());
3905
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
3906
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
3907
        $details['memoryLimit'] = ini_get('memory_limit');
3908
3909
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
3910
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
3911
        }
3912
3913
        if ($details['mimeType'] == 'image/png') {
3914
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
3915
        }
3916
3917
        $options = null;
3918
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
3919
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
3920
        }
3921
3922
        return json_encode($details, $options);
0 ignored issues
show
It seems like $options can also be of type null; however, parameter $flags of json_encode() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3922
        return json_encode($details, /** @scrutinizer ignore-type */ $options);
Loading history...
3923
    }
3924
3925
3926
3927
    /**
3928
     * Set options for creating ascii version of image.
3929
     *
3930
     * @param array $options empty to use default or set options to change.
3931
     *
3932
     * @return void.
0 ignored issues
show
Documentation Bug introduced by
The doc comment void. at position 0 could not be parsed: Unknown type name 'void.' at position 0 in void..
Loading history...
3933
     */
3934
    public function setAsciiOptions($options = array())
3935
    {
3936
        $this->asciiOptions = $options;
3937
    }
3938
3939
3940
3941
    /**
3942
     * Create an ASCII version from the image details.
3943
     *
3944
     * @param string $file the file to output.
3945
     *
3946
     * @return string ASCII representation of the image.
3947
     */
3948
    public function ascii($file = null)
3949
    {
3950
        $file = $file ? $file : $this->cacheFileName;
3951
3952
        $asciiArt = new CAsciiArt();
3953
        $asciiArt->setOptions($this->asciiOptions);
3954
        return $asciiArt->createFromFile($file);
3955
    }
3956
3957
3958
3959
    /**
3960
     * Log an event if verbose mode.
3961
     *
3962
     * @param string $message to log.
3963
     *
3964
     * @return this
3965
     */
3966
    public function log($message)
3967
    {
3968
        if ($this->verbose) {
3969
            $this->log[] = $message;
3970
        }
3971
3972
        return $this;
3973
    }
3974
3975
3976
3977
    /**
3978
     * Do verbose output to a file.
3979
     *
3980
     * @param string $fileName where to write the verbose output.
3981
     *
3982
     * @return void
3983
     */
3984
    public function setVerboseToFile($fileName)
3985
    {
3986
        $this->log("Setting verbose output to file.");
3987
        $this->verboseFileName = $fileName;
3988
    }
3989
3990
3991
3992
    /**
3993
     * Do verbose output and print out the log and the actual images.
3994
     *
3995
     * @return void
3996
     */
3997
    private function verboseOutput()
3998
    {
3999
        $log = null;
4000
        $this->log("### Summary of verbose log");
4001
        $this->log("As JSON: \n" . $this->json());
4002
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
4003
        $this->log("Memory limit: " . ini_get('memory_limit'));
4004
4005
        $included = get_included_files();
4006
        $this->log("Included files: " . count($included));
4007
4008
        foreach ($this->log as $val) {
4009
            if (is_array($val)) {
4010
                foreach ($val as $val1) {
4011
                    $log .= htmlentities($val1) . '<br/>';
4012
                }
4013
            } else {
4014
                $log .= htmlentities($val) . '<br/>';
4015
            }
4016
        }
4017
4018
        if (!is_null($this->verboseFileName)) {
4019
            file_put_contents(
4020
                $this->verboseFileName,
4021
                str_replace("<br/>", "\n", $log)
4022
            );
4023
        } else {
4024
            echo <<<EOD
4025
<h1>CImage Verbose Output</h1>
4026
<pre>{$log}</pre>
4027
EOD;
4028
        }
4029
    }
4030
4031
4032
4033
    /**
4034
     * Raise error, enables to implement a selection of error methods.
4035
     *
4036
     * @param string $message the error message to display.
4037
     *
4038
     * @return void
4039
     * @throws Exception
4040
     */
4041
    private function raiseError($message)
4042
    {
4043
        throw new Exception($message);
4044
    }
4045
}
4046
4047
4048
4049
/**
4050
 * Deal with the cache directory and cached items.
4051
 *
4052
 */
4053
class CCache
4054
{
4055
    /**
4056
     * Path to the cache directory.
4057
     */
4058
    private $path;
4059
4060
4061
4062
    /**
4063
     * Set the path to the cache dir which must exist.
4064
     *
4065
     * @param string path to the cache dir.
0 ignored issues
show
The type path was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
4066
     *
4067
     * @throws Exception when $path is not a directory.
4068
     *
4069
     * @return $this
4070
     */
4071
    public function setDir($path)
4072
    {
4073
        if (!is_dir($path)) {
4074
            throw new Exception("Cachedir is not a directory.");
4075
        }
4076
4077
        $this->path = $path;
4078
4079
        return $this;
4080
    }
4081
4082
4083
4084
    /**
4085
     * Get the path to the cache subdir and try to create it if its not there.
4086
     *
4087
     * @param string $subdir name of subdir
4088
     * @param array  $create default is to try to create the subdir
4089
     *
4090
     * @return string | boolean as real path to the subdir or
4091
     *                          false if it does not exists
4092
     */
4093
    public function getPathToSubdir($subdir, $create = true)
4094
    {
4095
        $path = realpath($this->path . "/" . $subdir);
4096
4097
        if (is_dir($path)) {
4098
            return $path;
4099
        }
4100
4101
        if ($create && is_writable($this->path)) {
4102
            $path = $this->path . "/" . $subdir;
4103
4104
            if (mkdir($path)) {
4105
                return realpath($path);
4106
            }
4107
        }
4108
4109
        return false;
4110
    }
4111
4112
4113
4114
    /**
4115
     * Get status of the cache subdir.
4116
     *
4117
     * @param string $subdir name of subdir
4118
     *
4119
     * @return string with status
4120
     */
4121
    public function getStatusOfSubdir($subdir)
4122
    {
4123
        $path = realpath($this->path . "/" . $subdir);
4124
4125
        $exists = is_dir($path);
4126
        $res  = $exists ? "exists" : "does not exist";
4127
        
4128
        if ($exists) {
4129
            $res .= is_writable($path) ? ", writable" : ", not writable";
4130
        }
4131
4132
        return $res;
4133
    }
4134
4135
4136
4137
    /**
4138
     * Remove the cache subdir.
4139
     *
4140
     * @param string $subdir name of subdir
4141
     *
4142
     * @return null | boolean true if success else false, null if no operation
4143
     */
4144
    public function removeSubdir($subdir)
4145
    {
4146
        $path = realpath($this->path . "/" . $subdir);
4147
4148
        if (is_dir($path)) {
4149
            return rmdir($path);
4150
        }
4151
4152
        return null;
4153
    }
4154
}
4155
4156
4157
4158
/**
4159
 * Enable a fast track cache with a json representation of the image delivery.
4160
 *
4161
 */
4162
class CFastTrackCache
4163
{
4164
    /**
4165
     * Cache is disabled to start with.
4166
     */
4167
    private $enabled = false;
4168
4169
4170
4171
    /**
4172
     * Path to the cache directory.
4173
     */
4174
    private $path;
4175
4176
4177
4178
    /**
4179
     * Filename of current cache item.
4180
     */
4181
    private $filename;
4182
4183
4184
4185
    /**
4186
     * Container with items to store as cached item.
4187
     */
4188
    private $container;
4189
4190
4191
4192
    /**
4193
     * Enable or disable cache.
4194
     *
4195
     * @param boolean $enable set to true to enable, false to disable
4196
     *
4197
     * @return $this
4198
     */
4199
    public function enable($enabled)
4200
    {
4201
        $this->enabled = $enabled;
4202
        return $this;
4203
    }
4204
4205
4206
4207
    /**
4208
     * Set the path to the cache dir which must exist.
4209
     *
4210
     * @param string $path to the cache dir.
4211
     *
4212
     * @throws Exception when $path is not a directory.
4213
     *
4214
     * @return $this
4215
     */
4216
    public function setCacheDir($path)
4217
    {
4218
        if (!is_dir($path)) {
4219
            throw new Exception("Cachedir is not a directory.");
4220
        }
4221
4222
        $this->path = rtrim($path, "/");
4223
4224
        return $this;
4225
    }
4226
4227
4228
4229
    /**
4230
     * Set the filename to store in cache, use the querystring to create that
4231
     * filename.
4232
     *
4233
     * @param array $clear items to clear in $_GET when creating the filename.
4234
     *
4235
     * @return string as filename created.
4236
     */
4237
    public function setFilename($clear)
4238
    {
4239
        $query = $_GET;
4240
4241
        // Remove parts from querystring that should not be part of filename
4242
        foreach ($clear as $value) {
4243
            unset($query[$value]);
4244
        }
4245
4246
        arsort($query);
4247
        $queryAsString = http_build_query($query);
4248
4249
        $this->filename = md5($queryAsString);
4250
4251
        if (CIMAGE_DEBUG) {
4252
            $this->container["query-string"] = $queryAsString;
4253
        }
4254
4255
        return $this->filename;
4256
    }
4257
4258
4259
4260
    /**
4261
     * Add header items.
4262
     *
4263
     * @param string $header add this as header.
4264
     *
4265
     * @return $this
4266
     */
4267
    public function addHeader($header)
4268
    {
4269
        $this->container["header"][] = $header;
4270
        return $this;
4271
    }
4272
4273
4274
4275
    /**
4276
     * Add header items on output, these are not output when 304.
4277
     *
4278
     * @param string $header add this as header.
4279
     *
4280
     * @return $this
4281
     */
4282
    public function addHeaderOnOutput($header)
4283
    {
4284
        $this->container["header-output"][] = $header;
4285
        return $this;
4286
    }
4287
4288
4289
4290
    /**
4291
     * Set path to source image to.
4292
     *
4293
     * @param string $source path to source image file.
4294
     *
4295
     * @return $this
4296
     */
4297
    public function setSource($source)
4298
    {
4299
        $this->container["source"] = $source;
4300
        return $this;
4301
    }
4302
4303
4304
4305
    /**
4306
     * Set last modified of source image, use to check for 304.
4307
     *
4308
     * @param string $lastModified
4309
     *
4310
     * @return $this
4311
     */
4312
    public function setLastModified($lastModified)
4313
    {
4314
        $this->container["last-modified"] = $lastModified;
4315
        return $this;
4316
    }
4317
4318
4319
4320
    /**
4321
     * Get filename of cached item.
4322
     *
4323
     * @return string as filename.
4324
     */
4325
    public function getFilename()
4326
    {
4327
        return $this->path . "/" . $this->filename;
4328
    }
4329
4330
4331
4332
    /**
4333
     * Write current item to cache.
4334
     *
4335
     * @return boolean if cache file was written.
4336
     */
4337
    public function writeToCache()
4338
    {
4339
        if (!$this->enabled) {
4340
            return false;
4341
        }
4342
4343
        if (is_dir($this->path) && is_writable($this->path)) {
4344
            $filename = $this->getFilename();
4345
            return file_put_contents($filename, json_encode($this->container)) !== false;
4346
        }
4347
4348
        return false;
4349
    }
4350
4351
4352
4353
    /**
4354
     * Output current item from cache, if available.
4355
     *
4356
     * @return void
4357
     */
4358
    public function output()
4359
    {
4360
        $filename = $this->getFilename();
4361
        if (!is_readable($filename)) {
4362
            return;
4363
        }
4364
4365
        $item = json_decode(file_get_contents($filename), true);
4366
4367
        if (!is_readable($item["source"])) {
4368
            return;
4369
        }
4370
4371
        foreach ($item["header"] as $value) {
4372
            header($value);
4373
        }
4374
4375
        if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])
4376
            && strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]) == $item["last-modified"]) {
4377
            header("HTTP/1.0 304 Not Modified");
4378
            if (CIMAGE_DEBUG) {
4379
                trace(__CLASS__ . " 304");
4380
            }
4381
            exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
4382
        }
4383
4384
        foreach ($item["header-output"] as $value) {
4385
            header($value);
4386
        }
4387
4388
        if (CIMAGE_DEBUG) {
4389
            trace(__CLASS__ . " 200");
4390
        }
4391
        readfile($item["source"]);
4392
        exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
4393
    }
4394
}
4395
4396
4397
4398
/**
4399
 * Resize and crop images on the fly, store generated images in a cache.
4400
 *
4401
 * @author  Mikael Roos [email protected]
4402
 * @example http://dbwebb.se/opensource/cimage
4403
 * @link    https://github.com/mosbth/cimage
4404
 *
4405
 */
4406
4407
/**
4408
 * Custom exception handler.
4409
 */
4410
set_exception_handler(function ($exception) {
4411
    errorPage(
4412
        "<p><b>img.php: Uncaught exception:</b> <p>"
4413
        . $exception->getMessage()
4414
        . "</p><pre>"
4415
        . $exception->getTraceAsString()
4416
        . "</pre>",
4417
        500
4418
    );
4419
});
4420
4421
4422
4423
/**
4424
 * Get configuration options from file, if the file exists, else use $config
4425
 * if its defined or create an empty $config.
4426
 */
4427
$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php';
4428
4429
if (is_file($configFile)) {
4430
    $config = require $configFile;
4431
} elseif (!isset($config)) {
4432
    $config = array();
4433
}
4434
4435
// Make CIMAGE_DEBUG false by default, if not already defined
4436
if (!defined("CIMAGE_DEBUG")) {
4437
    define("CIMAGE_DEBUG", false);
4438
}
4439
4440
4441
4442
/**
4443
 * Setup the autoloader, but not when using a bundle.
4444
 */
4445
if (!defined("CIMAGE_BUNDLE")) {
4446
    if (!isset($config["autoloader"])) {
4447
        die("CImage: Missing autoloader.");
4448
    }
4449
4450
    require $config["autoloader"];
4451
}
4452
4453
4454
4455
/**
4456
* verbose, v - do a verbose dump of what happens
4457
* vf - do verbose dump to file
4458
*/
4459
$verbose = getDefined(array('verbose', 'v'), true, false);
4460
$verboseFile = getDefined('vf', true, false);
4461
verbose("img.php version = " . CIMAGE_VERSION);
4462
4463
4464
4465
/**
4466
* status - do a verbose dump of the configuration
4467
*/
4468
$status = getDefined('status', true, false);
4469
4470
4471
4472
/**
4473
 * Set mode as strict, production or development.
4474
 * Default is production environment.
4475
 */
4476
$mode = getConfig('mode', 'production');
4477
4478
// Settings for any mode
4479
set_time_limit(20);
4480
ini_set('gd.jpeg_ignore_warning', 1);
4481
4482
if (!extension_loaded('gd')) {
4483
    errorPage("Extension gd is not loaded.", 500);
4484
}
4485
4486
// Specific settings for each mode
4487
if ($mode == 'strict') {
4488
4489
    error_reporting(0);
4490
    ini_set('display_errors', 0);
4491
    ini_set('log_errors', 1);
4492
    $verbose = false;
4493
    $status = false;
4494
    $verboseFile = false;
4495
4496
} elseif ($mode == 'production') {
4497
4498
    error_reporting(-1);
4499
    ini_set('display_errors', 0);
4500
    ini_set('log_errors', 1);
4501
    $verbose = false;
4502
    $status = false;
4503
    $verboseFile = false;
4504
4505
} elseif ($mode == 'development') {
4506
4507
    error_reporting(-1);
4508
    ini_set('display_errors', 1);
4509
    ini_set('log_errors', 0);
4510
    $verboseFile = false;
4511
4512
} elseif ($mode == 'test') {
4513
4514
    error_reporting(-1);
4515
    ini_set('display_errors', 1);
4516
    ini_set('log_errors', 0);
4517
4518
} else {
4519
    errorPage("Unknown mode: $mode", 500);
4520
}
4521
4522
verbose("mode = $mode");
4523
verbose("error log = " . ini_get('error_log'));
4524
4525
4526
4527
/**
4528
 * Set default timezone if not set or if its set in the config-file.
4529
 */
4530
$defaultTimezone = getConfig('default_timezone', null);
4531
4532
if ($defaultTimezone) {
4533
    date_default_timezone_set($defaultTimezone);
4534
} elseif (!ini_get('default_timezone')) {
4535
    date_default_timezone_set('UTC');
4536
}
4537
4538
4539
4540
/**
4541
 * Check if passwords are configured, used and match.
4542
 * Options decide themself if they require passwords to be used.
4543
 */
4544
$pwdConfig   = getConfig('password', false);
4545
$pwdAlways   = getConfig('password_always', false);
4546
$pwdType     = getConfig('password_type', 'text');
4547
$pwd         = get(array('password', 'pwd'), null);
4548
4549
// Check if passwords match, if configured to use passwords
4550
$passwordMatch = null;
4551
if ($pwd) {
4552
    switch ($pwdType) {
4553
        case 'md5':
4554
            $passwordMatch = ($pwdConfig === md5($pwd));
4555
            break;
4556
        case 'hash':
4557
            $passwordMatch = password_verify($pwd, $pwdConfig);
0 ignored issues
show
It seems like $pwdConfig can also be of type false; however, parameter $hash of password_verify() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4557
            $passwordMatch = password_verify($pwd, /** @scrutinizer ignore-type */ $pwdConfig);
Loading history...
4558
            break;
4559
        case 'text':
4560
            $passwordMatch = ($pwdConfig === $pwd);
4561
            break;
4562
        default:
4563
            $passwordMatch = false;
4564
    }
4565
}
4566
4567
if ($pwdAlways && $passwordMatch !== true) {
4568
    errorPage("Password required and does not match or exists.", 403);
4569
}
4570
4571
verbose("password match = $passwordMatch");
4572
4573
4574
4575
/**
4576
 * Prevent hotlinking, leeching, of images by controlling who access them
4577
 * from where.
4578
 *
4579
 */
4580
$allowHotlinking = getConfig('allow_hotlinking', true);
4581
$hotlinkingWhitelist = getConfig('hotlinking_whitelist', array());
4582
4583
$serverName  = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
4584
$referer     = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
4585
$refererHost = parse_url($referer, PHP_URL_HOST);
4586
4587
if (!$allowHotlinking) {
4588
    if ($passwordMatch) {
4589
        ; // Always allow when password match
4590
        verbose("Hotlinking since passwordmatch");
4591
    } elseif ($passwordMatch === false) {
4592
        errorPage("Hotlinking/leeching not allowed when password missmatch.", 403);
4593
    } elseif (!$referer) {
4594
        errorPage("Hotlinking/leeching not allowed and referer is missing.", 403);
4595
    } elseif (strcmp($serverName, $refererHost) == 0) {
0 ignored issues
show
It seems like $serverName can also be of type null; however, parameter $string1 of strcmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4595
    } elseif (strcmp(/** @scrutinizer ignore-type */ $serverName, $refererHost) == 0) {
Loading history...
4596
        ; // Allow when serverName matches refererHost
4597
        verbose("Hotlinking disallowed but serverName matches refererHost.");
4598
    } elseif (!empty($hotlinkingWhitelist)) {
4599
        $whitelist = new CWhitelist();
4600
        $allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist);
4601
4602
        if ($allowedByWhitelist) {
4603
            verbose("Hotlinking/leeching allowed by whitelist.");
4604
        } else {
4605
            errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.", 403);
4606
        }
4607
4608
    } else {
4609
        errorPage("Hotlinking/leeching not allowed.", 403);
4610
    }
4611
}
4612
4613
verbose("allow_hotlinking = $allowHotlinking");
4614
verbose("referer = $referer");
4615
verbose("referer host = $refererHost");
4616
4617
4618
4619
/**
4620
 * Create the class for the image.
4621
 */
4622
$CImage = getConfig('CImage', 'CImage');
4623
$img = new $CImage();
4624
$img->setVerbose($verbose || $verboseFile);
4625
4626
4627
4628
/**
4629
 * Get the cachepath from config.
4630
 */
4631
$CCache = getConfig('CCache', 'CCache');
4632
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');
4633
$cache = new $CCache();
4634
$cache->setDir($cachePath);
4635
4636
4637
4638
/**
4639
 * no-cache, nc - skip the cached version and process and create a new version in cache.
4640
 */
4641
$useCache = getDefined(array('no-cache', 'nc'), false, true);
4642
4643
verbose("use cache = $useCache");
4644
4645
4646
4647
/**
4648
 * Prepare fast track cache for swriting cache items.
4649
 */
4650
$fastTrackCache = "fasttrack";
4651
$allowFastTrackCache = getConfig('fast_track_allow', false);
4652
4653
$CFastTrackCache = getConfig('CFastTrackCache', 'CFastTrackCache');
4654
$ftc = new $CFastTrackCache();
4655
$ftc->setCacheDir($cache->getPathToSubdir($fastTrackCache))
4656
    ->enable($allowFastTrackCache)
4657
    ->setFilename(array('no-cache', 'nc'));
4658
$img->injectDependency("fastTrackCache", $ftc);
4659
4660
4661
4662
/**
4663
 *  Load and output images from fast track cache, if items are available
4664
 * in cache.
4665
 */
4666
if ($useCache && $allowFastTrackCache) {
4667
    if (CIMAGE_DEBUG) {
4668
        trace("img.php fast track cache enabled and used");
4669
    }
4670
    $ftc->output();
4671
}
4672
4673
4674
4675
/**
4676
 * Allow or disallow remote download of images from other servers.
4677
 * Passwords apply if used.
4678
 *
4679
 */
4680
$allowRemote = getConfig('remote_allow', false);
4681
4682
if ($allowRemote && $passwordMatch !== false) {
4683
    $cacheRemote = $cache->getPathToSubdir("remote");
4684
    
4685
    $pattern = getConfig('remote_pattern', null);
4686
    $img->setRemoteDownload($allowRemote, $cacheRemote, $pattern);
4687
4688
    $whitelist = getConfig('remote_whitelist', null);
4689
    $img->setRemoteHostWhitelist($whitelist);
4690
}
4691
4692
4693
4694
/**
4695
 * shortcut, sc - extend arguments with a constant value, defined
4696
 * in config-file.
4697
 */
4698
$shortcut       = get(array('shortcut', 'sc'), null);
4699
$shortcutConfig = getConfig('shortcut', array(
4700
    'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
4701
));
4702
4703
verbose("shortcut = $shortcut");
4704
4705
if (isset($shortcut)
4706
    && isset($shortcutConfig[$shortcut])) {
4707
4708
    parse_str($shortcutConfig[$shortcut], $get);
4709
    verbose("shortcut-constant = {$shortcutConfig[$shortcut]}");
4710
    $_GET = array_merge($_GET, $get);
4711
}
4712
4713
4714
4715
/**
4716
 * src - the source image file.
4717
 */
4718
$srcImage = urldecode(get('src'))
0 ignored issues
show
It seems like get('src') can also be of type null; however, parameter $string of urldecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4718
$srcImage = urldecode(/** @scrutinizer ignore-type */ get('src'))
Loading history...
4719
    or errorPage('Must set src-attribute.', 404);
4720
4721
// Get settings for src-alt as backup image
4722
$srcAltImage = urldecode(get('src-alt', null));
4723
$srcAltConfig = getConfig('src_alt', null);
4724
if (empty($srcAltImage)) {
4725
    $srcAltImage = $srcAltConfig;
4726
}
4727
4728
// Check for valid/invalid characters
4729
$imagePath           = getConfig('image_path', __DIR__ . '/img/');
4730
$imagePathConstraint = getConfig('image_path_constraint', true);
4731
$validFilename       = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#');
4732
4733
// Source is remote
4734
$remoteSource = false;
4735
4736
// Dummy image feature
4737
$dummyEnabled  = getConfig('dummy_enabled', true);
4738
$dummyFilename = getConfig('dummy_filename', 'dummy');
4739
$dummyImage = false;
4740
4741
preg_match($validFilename, $srcImage)
4742
    or errorPage('Source filename contains invalid characters.', 404);
4743
4744
if ($dummyEnabled && $srcImage === $dummyFilename) {
4745
4746
    // Prepare to create a dummy image and use it as the source image.
4747
    $dummyImage = true;
4748
4749
} elseif ($allowRemote && $img->isRemoteSource($srcImage)) {
4750
4751
    // If source is a remote file, ignore local file checks.
4752
    $remoteSource = true;
4753
4754
} else {
4755
4756
    // Check if file exists on disk or try using src-alt
4757
    $pathToImage = realpath($imagePath . $srcImage);
4758
4759
    if (!is_file($pathToImage) && !empty($srcAltImage)) {
4760
        // Try using the src-alt instead
4761
        $srcImage = $srcAltImage;
4762
        $pathToImage = realpath($imagePath . $srcImage);
4763
4764
        preg_match($validFilename, $srcImage)
4765
            or errorPage('Source (alt) filename contains invalid characters.', 404);
4766
4767
        if ($dummyEnabled && $srcImage === $dummyFilename) {
4768
            // Check if src-alt is the dummy image
4769
            $dummyImage = true;
4770
        }
4771
    }
4772
4773
    if (!$dummyImage) {
4774
        is_file($pathToImage)
4775
            or errorPage(
4776
                'Source image is not a valid file, check the filename and that a
4777
                matching file exists on the filesystem.',
4778
                404
4779
            );
4780
    } 
4781
}
4782
4783
if ($imagePathConstraint && !$dummyImage && !$remoteSource) {
4784
    // Check that the image is a file below the directory 'image_path'.
4785
    $imageDir = realpath($imagePath);
4786
4787
    substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0
4788
        or errorPage(
4789
            'Security constraint: Source image is not below the directory "image_path"
4790
            as specified in the config file img_config.php.',
4791
            404
4792
        );
4793
}
4794
4795
verbose("src = $srcImage");
4796
4797
4798
4799
/**
4800
 * Manage size constants from config file, use constants to replace values
4801
 * for width and height.
4802
 */
4803
$sizeConstant = getConfig('size_constant', function () {
4804
4805
    // Set sizes to map constant to value, easier to use with width or height
4806
    $sizes = array(
4807
        'w1' => 613,
4808
        'w2' => 630,
4809
    );
4810
4811
    // Add grid column width, useful for use as predefined size for width (or height).
4812
    $gridColumnWidth = 30;
4813
    $gridGutterWidth = 10;
4814
    $gridColumns     = 24;
4815
4816
    for ($i = 1; $i <= $gridColumns; $i++) {
4817
        $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth;
4818
    }
4819
4820
    return $sizes;
4821
});
4822
4823
$sizes = call_user_func($sizeConstant);
4824
4825
4826
4827
/**
4828
 * width, w - set target width, affecting the resulting image width, height and resize options
4829
 */
4830
$newWidth     = get(array('width', 'w'));
4831
$maxWidth     = getConfig('max_width', 2000);
4832
4833
// Check to replace predefined size
4834
if (isset($sizes[$newWidth])) {
4835
    $newWidth = $sizes[$newWidth];
4836
}
4837
4838
// Support width as % of original width
4839
if ($newWidth && $newWidth[strlen($newWidth)-1] == '%') {
4840
    is_numeric(substr($newWidth, 0, -1))
4841
        or errorPage('Width % not numeric.', 404);
4842
} else {
4843
    is_null($newWidth)
4844
        or ($newWidth > 10 && $newWidth <= $maxWidth)
4845
        or errorPage('Width out of range.', 404);
4846
}
4847
4848
verbose("new width = $newWidth");
4849
4850
4851
4852
/**
4853
 * height, h - set target height, affecting the resulting image width, height and resize options
4854
 */
4855
$newHeight = get(array('height', 'h'));
4856
$maxHeight = getConfig('max_height', 2000);
4857
4858
// Check to replace predefined size
4859
if (isset($sizes[$newHeight])) {
4860
    $newHeight = $sizes[$newHeight];
4861
}
4862
4863
// height
4864
if ($newHeight && $newHeight[strlen($newHeight)-1] == '%') {
4865
    is_numeric(substr($newHeight, 0, -1))
4866
        or errorPage('Height % out of range.', 404);
4867
} else {
4868
    is_null($newHeight)
4869
        or ($newHeight > 10 && $newHeight <= $maxHeight)
4870
        or errorPage('Height out of range.', 404);
4871
}
4872
4873
verbose("new height = $newHeight");
4874
4875
4876
4877
/**
4878
 * aspect-ratio, ar - affecting the resulting image width, height and resize options
4879
 */
4880
$aspectRatio         = get(array('aspect-ratio', 'ar'));
4881
$aspectRatioConstant = getConfig('aspect_ratio_constant', function () {
4882
    return array(
4883
        '3:1'    => 3/1,
4884
        '3:2'    => 3/2,
4885
        '4:3'    => 4/3,
4886
        '8:5'    => 8/5,
4887
        '16:10'  => 16/10,
4888
        '16:9'   => 16/9,
4889
        'golden' => 1.618,
4890
    );
4891
});
4892
4893
// Check to replace predefined aspect ratio
4894
$aspectRatios = call_user_func($aspectRatioConstant);
4895
$negateAspectRatio = ($aspectRatio && $aspectRatio[0] == '!') ? true : false;
4896
$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio;
4897
4898
if (isset($aspectRatios[$aspectRatio])) {
4899
    $aspectRatio = $aspectRatios[$aspectRatio];
4900
}
4901
4902
if ($negateAspectRatio) {
4903
    $aspectRatio = 1 / $aspectRatio;
4904
}
4905
4906
is_null($aspectRatio)
4907
    or is_numeric($aspectRatio)
4908
    or errorPage('Aspect ratio out of range', 404);
4909
4910
verbose("aspect ratio = $aspectRatio");
4911
4912
4913
4914
/**
4915
 * crop-to-fit, cf - affecting the resulting image width, height and resize options
4916
 */
4917
$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false);
4918
4919
verbose("crop to fit = $cropToFit");
4920
4921
4922
4923
/**
4924
 * Set default background color from config file.
4925
 */
4926
$backgroundColor = getConfig('background_color', null);
4927
4928
if ($backgroundColor) {
4929
    $img->setDefaultBackgroundColor($backgroundColor);
4930
    verbose("Using default background_color = $backgroundColor");
4931
}
4932
4933
4934
4935
/**
4936
 * bgColor - Default background color to use
4937
 */
4938
$bgColor = get(array('bgColor', 'bg-color', 'bgc'), null);
4939
4940
verbose("bgColor = $bgColor");
4941
4942
4943
4944
/**
4945
 * Do or do not resample image when resizing.
4946
 */
4947
$resizeStrategy = getDefined(array('no-resample'), true, false);
4948
4949
if ($resizeStrategy) {
4950
    $img->setCopyResizeStrategy($img::RESIZE);
4951
    verbose("Setting = Resize instead of resample");
4952
}
4953
4954
4955
4956
4957
/**
4958
 * fill-to-fit, ff - affecting the resulting image width, height and resize options
4959
 */
4960
$fillToFit = get(array('fill-to-fit', 'ff'), null);
4961
4962
verbose("fill-to-fit = $fillToFit");
4963
4964
if ($fillToFit !== null) {
4965
4966
    if (!empty($fillToFit)) {
4967
        $bgColor   = $fillToFit;
4968
        verbose("fillToFit changed bgColor to = $bgColor");
4969
    }
4970
4971
    $fillToFit = true;
4972
    verbose("fill-to-fit (fixed) = $fillToFit");
4973
}
4974
4975
4976
4977
/**
4978
 * no-ratio, nr, stretch - affecting the resulting image width, height and resize options
4979
 */
4980
$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true);
4981
4982
verbose("keep ratio = $keepRatio");
4983
4984
4985
4986
/**
4987
 * crop, c - affecting the resulting image width, height and resize options
4988
 */
4989
$crop = get(array('crop', 'c'));
4990
4991
verbose("crop = $crop");
4992
4993
4994
4995
/**
4996
 * area, a - affecting the resulting image width, height and resize options
4997
 */
4998
$area = get(array('area', 'a'));
4999
5000
verbose("area = $area");
5001
5002
5003
5004
/**
5005
 * skip-original, so - skip the original image and always process a new image
5006
 */
5007
$useOriginal = getDefined(array('skip-original', 'so'), false, true);
5008
$useOriginalDefault = getConfig('skip_original', false);
5009
5010
if ($useOriginalDefault === true) {
5011
    verbose("skip original is default ON");
5012
    $useOriginal = false;
5013
}
5014
5015
verbose("use original = $useOriginal");
5016
5017
5018
5019
/**
5020
 * quality, q - set level of quality for jpeg images
5021
 */
5022
$quality = get(array('quality', 'q'));
5023
$qualityDefault = getConfig('jpg_quality', null);
5024
5025
is_null($quality)
5026
    or ($quality > 0 and $quality <= 100)
5027
    or errorPage('Quality out of range', 404);
5028
5029
if (is_null($quality) && !is_null($qualityDefault)) {
5030
    $quality = $qualityDefault;
5031
}
5032
5033
verbose("quality = $quality");
5034
5035
5036
5037
/**
5038
 * compress, co - what strategy to use when compressing png images
5039
 */
5040
$compress = get(array('compress', 'co'));
5041
$compressDefault = getConfig('png_compression', null);
5042
5043
is_null($compress)
5044
    or ($compress > 0 and $compress <= 9)
5045
    or errorPage('Compress out of range', 404);
5046
5047
if (is_null($compress) && !is_null($compressDefault)) {
5048
    $compress = $compressDefault;
5049
}
5050
5051
verbose("compress = $compress");
5052
5053
5054
5055
/**
5056
 * save-as, sa - what type of image to save
5057
 */
5058
$saveAs = get(array('save-as', 'sa'));
5059
5060
verbose("save as = $saveAs");
5061
5062
5063
5064
/**
5065
 * scale, s - Processing option, scale up or down the image prior actual resize
5066
 */
5067
$scale = get(array('scale', 's'));
5068
5069
is_null($scale)
5070
    or ($scale >= 0 and $scale <= 400)
5071
    or errorPage('Scale out of range', 404);
5072
5073
verbose("scale = $scale");
5074
5075
5076
5077
/**
5078
 * palette, p - Processing option, create a palette version of the image
5079
 */
5080
$palette = getDefined(array('palette', 'p'), true, false);
5081
5082
verbose("palette = $palette");
5083
5084
5085
5086
/**
5087
 * sharpen - Processing option, post filter for sharpen effect
5088
 */
5089
$sharpen = getDefined('sharpen', true, null);
5090
5091
verbose("sharpen = $sharpen");
5092
5093
5094
5095
/**
5096
 * emboss - Processing option, post filter for emboss effect
5097
 */
5098
$emboss = getDefined('emboss', true, null);
5099
5100
verbose("emboss = $emboss");
5101
5102
5103
5104
/**
5105
 * blur - Processing option, post filter for blur effect
5106
 */
5107
$blur = getDefined('blur', true, null);
5108
5109
verbose("blur = $blur");
5110
5111
5112
5113
/**
5114
 * rotateBefore - Rotate the image with an angle, before processing
5115
 */
5116
$rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb'));
5117
5118
is_null($rotateBefore)
5119
    or ($rotateBefore >= -360 and $rotateBefore <= 360)
5120
    or errorPage('RotateBefore out of range', 404);
5121
5122
verbose("rotateBefore = $rotateBefore");
5123
5124
5125
5126
/**
5127
 * rotateAfter - Rotate the image with an angle, before processing
5128
 */
5129
$rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r'));
5130
5131
is_null($rotateAfter)
5132
    or ($rotateAfter >= -360 and $rotateAfter <= 360)
5133
    or errorPage('RotateBefore out of range', 404);
5134
5135
verbose("rotateAfter = $rotateAfter");
5136
5137
5138
5139
/**
5140
 * autoRotate - Auto rotate based on EXIF information
5141
 */
5142
$autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false);
5143
5144
verbose("autoRotate = $autoRotate");
5145
5146
5147
5148
/**
5149
 * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter()
5150
 */
5151
$filters = array();
5152
$filter = get(array('filter', 'f'));
5153
if ($filter) {
5154
    $filters[] = $filter;
5155
}
5156
5157
for ($i = 0; $i < 10; $i++) {
5158
    $filter = get(array("filter{$i}", "f{$i}"));
5159
    if ($filter) {
5160
        $filters[] = $filter;
5161
    }
5162
}
5163
5164
verbose("filters = " . print_r($filters, 1));
0 ignored issues
show
Are you sure print_r($filters, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5164
verbose("filters = " . /** @scrutinizer ignore-type */ print_r($filters, 1));
Loading history...
5165
5166
5167
5168
/**
5169
* json -  output the image as a JSON object with details on the image.
5170
* ascii - output the image as ASCII art.
5171
 */
5172
$outputFormat = getDefined('json', 'json', null);
5173
$outputFormat = getDefined('ascii', 'ascii', $outputFormat);
5174
5175
verbose("outputformat = $outputFormat");
5176
5177
if ($outputFormat == 'ascii') {
5178
    $defaultOptions = getConfig(
5179
        'ascii-options',
5180
        array(
5181
            "characterSet" => 'two',
5182
            "scale" => 14,
5183
            "luminanceStrategy" => 3,
5184
            "customCharacterSet" => null,
5185
        )
5186
    );
5187
    $options = get('ascii');
5188
    $options = explode(',', $options);
0 ignored issues
show
It seems like $options can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5188
    $options = explode(',', /** @scrutinizer ignore-type */ $options);
Loading history...
5189
5190
    if (isset($options[0]) && !empty($options[0])) {
5191
        $defaultOptions['characterSet'] = $options[0];
5192
    }
5193
5194
    if (isset($options[1]) && !empty($options[1])) {
5195
        $defaultOptions['scale'] = $options[1];
5196
    }
5197
5198
    if (isset($options[2]) && !empty($options[2])) {
5199
        $defaultOptions['luminanceStrategy'] = $options[2];
5200
    }
5201
5202
    if (count($options) > 3) {
5203
        // Last option is custom character string
5204
        unset($options[0]);
5205
        unset($options[1]);
5206
        unset($options[2]);
5207
        $characterString = implode($options);
5208
        $defaultOptions['customCharacterSet'] = $characterString;
5209
    }
5210
5211
    $img->setAsciiOptions($defaultOptions);
5212
}
5213
5214
5215
5216
5217
/**
5218
 * dpr - change to get larger image to easier support larger dpr, such as retina.
5219
 */
5220
$dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1);
5221
5222
verbose("dpr = $dpr");
5223
5224
5225
5226
/**
5227
 * convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php
5228
 */
5229
$convolve = get('convolve', null);
5230
$convolutionConstant = getConfig('convolution_constant', array());
5231
5232
// Check if the convolve is matching an existing constant
5233
if ($convolve && isset($convolutionConstant)) {
5234
    $img->addConvolveExpressions($convolutionConstant);
5235
    verbose("convolve constant = " . print_r($convolutionConstant, 1));
0 ignored issues
show
Are you sure print_r($convolutionConstant, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5235
    verbose("convolve constant = " . /** @scrutinizer ignore-type */ print_r($convolutionConstant, 1));
Loading history...
5236
}
5237
5238
verbose("convolve = " . print_r($convolve, 1));
0 ignored issues
show
Are you sure print_r($convolve, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5238
verbose("convolve = " . /** @scrutinizer ignore-type */ print_r($convolve, 1));
Loading history...
5239
5240
5241
5242
/**
5243
 * no-upscale, nu - Do not upscale smaller image to larger dimension.
5244
 */
5245
$upscale = getDefined(array('no-upscale', 'nu'), false, true);
5246
5247
verbose("upscale = $upscale");
5248
5249
5250
5251
/**
5252
 * Get details for post processing
5253
 */
5254
$postProcessing = getConfig('postprocessing', array(
5255
    'png_lossy'        => false,
5256
    'png_lossy_cmd'    => '/usr/local/bin/pngquant --force --output',
5257
5258
    'png_filter'        => false,
5259
    'png_filter_cmd'    => '/usr/local/bin/optipng -q',
5260
5261
    'png_deflate'       => false,
5262
    'png_deflate_cmd'   => '/usr/local/bin/pngout -q',
5263
5264
    'jpeg_optimize'     => false,
5265
    'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize',
5266
));
5267
5268
5269
5270
/**
5271
 * lossy - Do lossy postprocessing, if available.
5272
 */
5273
$lossy = getDefined(array('lossy'), true, null);
5274
5275
verbose("lossy = $lossy");
5276
5277
5278
5279
/**
5280
 * alias - Save resulting image to another alias name.
5281
 * Password always apply, must be defined.
5282
 */
5283
$alias          = get('alias', null);
5284
$aliasPath      = getConfig('alias_path', null);
5285
$validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#');
5286
$aliasTarget    = null;
5287
5288
if ($alias && $aliasPath && $passwordMatch) {
5289
5290
    $aliasTarget = $aliasPath . $alias;
5291
    $useCache    = false;
5292
5293
    is_writable($aliasPath)
5294
        or errorPage("Directory for alias is not writable.", 403);
5295
5296
    preg_match($validAliasname, $alias)
5297
        or errorPage('Filename for alias contains invalid characters. Do not add extension.', 404);
5298
5299
} elseif ($alias) {
5300
    errorPage('Alias is not enabled in the config file or password not matching.', 403);
5301
}
5302
5303
verbose("alias = $alias");
5304
5305
5306
5307
/**
5308
 * Add cache control HTTP header.
5309
 */
5310
$cacheControl = getConfig('cache_control', null);
5311
5312
if ($cacheControl) {
5313
    verbose("cacheControl = $cacheControl");
5314
    $img->addHTTPHeader("Cache-Control", $cacheControl);
5315
}
5316
5317
5318
5319
/**
5320
 * Prepare a dummy image and use it as source image.
5321
 */
5322
if ($dummyImage === true) {
5323
    $dummyDir = $cache->getPathToSubdir("dummy");
5324
5325
    $img->setSaveFolder($dummyDir)
5326
        ->setSource($dummyFilename, $dummyDir)
5327
        ->setOptions(
5328
            array(
5329
                'newWidth'  => $newWidth,
5330
                'newHeight' => $newHeight,
5331
                'bgColor'   => $bgColor,
5332
            )
5333
        )
5334
        ->setJpegQuality($quality)
5335
        ->setPngCompression($compress)
5336
        ->createDummyImage()
5337
        ->generateFilename(null, false)
5338
        ->save(null, null, false);
5339
5340
    $srcImage = $img->getTarget();
5341
    $imagePath = null;
5342
5343
    verbose("src (updated) = $srcImage");
5344
}
5345
5346
5347
5348
/**
5349
 * Prepare a sRGB version of the image and use it as source image.
5350
 */
5351
$srgbDefault = getConfig('srgb_default', false);
5352
$srgbColorProfile = getConfig('srgb_colorprofile', __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc');
5353
$srgb = getDefined('srgb', true, null);
5354
5355
if ($srgb || $srgbDefault) {
5356
5357
    $filename = $img->convert2sRGBColorSpace(
5358
        $srcImage,
5359
        $imagePath,
5360
        $cache->getPathToSubdir("srgb"),
5361
        $srgbColorProfile,
5362
        $useCache
5363
    );
5364
5365
    if ($filename) {
5366
        $srcImage = $img->getTarget();
5367
        $imagePath = null;
5368
        verbose("srgb conversion and saved to cache = $srcImage");
5369
    } else {
5370
        verbose("srgb not op");
5371
    }
5372
}
5373
5374
5375
5376
/**
5377
 * Display status
5378
 */
5379
if ($status) {
5380
    $text  = "img.php version = " . CIMAGE_VERSION . "\n";
5381
    $text .= "PHP version = " . PHP_VERSION . "\n";
5382
    $text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n";
5383
    $text .= "Allow remote images = $allowRemote\n";
5384
5385
    $res = $cache->getStatusOfSubdir("");
5386
    $text .= "Cache $res\n";
5387
5388
    $res = $cache->getStatusOfSubdir("remote");
5389
    $text .= "Cache remote $res\n";
5390
5391
    $res = $cache->getStatusOfSubdir("dummy");
5392
    $text .= "Cache dummy $res\n";
5393
5394
    $res = $cache->getStatusOfSubdir("srgb");
5395
    $text .= "Cache srgb $res\n";
5396
5397
    $res = $cache->getStatusOfSubdir($fastTrackCache);
5398
    $text .= "Cache fasttrack $res\n";
5399
5400
    $text .= "Alias path writable = " . is_writable($aliasPath) . "\n";
5401
5402
    $no = extension_loaded('exif') ? null : 'NOT';
5403
    $text .= "Extension exif is $no loaded.<br>";
5404
5405
    $no = extension_loaded('curl') ? null : 'NOT';
5406
    $text .= "Extension curl is $no loaded.<br>";
5407
5408
    $no = extension_loaded('imagick') ? null : 'NOT';
5409
    $text .= "Extension imagick is $no loaded.<br>";
5410
5411
    $no = extension_loaded('gd') ? null : 'NOT';
5412
    $text .= "Extension gd is $no loaded.<br>";
5413
5414
    $text .= checkExternalCommand("PNG LOSSY", $postProcessing["png_lossy"], $postProcessing["png_lossy_cmd"]);
0 ignored issues
show
Are you sure the usage of checkExternalCommand('PN...ssing['png_lossy_cmd']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
5415
    $text .= checkExternalCommand("PNG FILTER", $postProcessing["png_filter"], $postProcessing["png_filter_cmd"]);
0 ignored issues
show
Are you sure the usage of checkExternalCommand('PN...sing['png_filter_cmd']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
5416
    $text .= checkExternalCommand("PNG DEFLATE", $postProcessing["png_deflate"], $postProcessing["png_deflate_cmd"]);
0 ignored issues
show
Are you sure the usage of checkExternalCommand('PN...ing['png_deflate_cmd']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
5417
    $text .= checkExternalCommand("JPEG OPTIMIZE", $postProcessing["jpeg_optimize"], $postProcessing["jpeg_optimize_cmd"]);
0 ignored issues
show
Are you sure the usage of checkExternalCommand('JP...g['jpeg_optimize_cmd']) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
5418
5419
    if (!$no) {
5420
        $text .= print_r(gd_info(), 1);
5421
    }
5422
5423
    echo <<<EOD
5424
<!doctype html>
5425
<html lang=en>
5426
<meta charset=utf-8>
5427
<title>CImage status</title>
5428
<pre>$text</pre>
5429
EOD;
5430
    exit;
5431
}
5432
5433
5434
5435
/**
5436
 * Log verbose details to file
5437
 */
5438
if ($verboseFile) {
5439
    $img->setVerboseToFile("$cachePath/log.txt");
5440
}
5441
5442
5443
5444
/**
5445
 * Hook after img.php configuration and before processing with CImage
5446
 */
5447
$hookBeforeCImage = getConfig('hook_before_CImage', null);
5448
5449
if (is_callable($hookBeforeCImage)) {
5450
    verbose("hookBeforeCImage activated");
5451
5452
    $allConfig = $hookBeforeCImage($img, array(
5453
            // Options for calculate dimensions
5454
            'newWidth'  => $newWidth,
5455
            'newHeight' => $newHeight,
5456
            'aspectRatio' => $aspectRatio,
5457
            'keepRatio' => $keepRatio,
5458
            'cropToFit' => $cropToFit,
5459
            'fillToFit' => $fillToFit,
5460
            'crop'      => $crop,
5461
            'area'      => $area,
5462
            'upscale'   => $upscale,
5463
5464
            // Pre-processing, before resizing is done
5465
            'scale'        => $scale,
5466
            'rotateBefore' => $rotateBefore,
5467
            'autoRotate'   => $autoRotate,
5468
5469
            // General processing options
5470
            'bgColor'    => $bgColor,
5471
5472
            // Post-processing, after resizing is done
5473
            'palette'   => $palette,
5474
            'filters'   => $filters,
5475
            'sharpen'   => $sharpen,
5476
            'emboss'    => $emboss,
5477
            'blur'      => $blur,
5478
            'convolve'  => $convolve,
5479
            'rotateAfter' => $rotateAfter,
5480
5481
            // Output format
5482
            'outputFormat' => $outputFormat,
5483
            'dpr'          => $dpr,
5484
5485
            // Other
5486
            'postProcessing' => $postProcessing,
5487
            'lossy' => $lossy,
5488
    ));
5489
    verbose(print_r($allConfig, 1));
0 ignored issues
show
It seems like print_r($allConfig, 1) can also be of type true; however, parameter $msg of verbose() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5489
    verbose(/** @scrutinizer ignore-type */ print_r($allConfig, 1));
Loading history...
5490
    extract($allConfig);
5491
}
5492
5493
5494
5495
/**
5496
 * Display image if verbose mode
5497
 */
5498
if ($verbose) {
5499
    $query = array();
5500
    parse_str($_SERVER['QUERY_STRING'], $query);
5501
    unset($query['verbose']);
5502
    unset($query['v']);
5503
    unset($query['nocache']);
5504
    unset($query['nc']);
5505
    unset($query['json']);
5506
    $url1 = '?' . htmlentities(urldecode(http_build_query($query)));
5507
    $url2 = '?' . urldecode(http_build_query($query));
5508
    echo <<<EOD
5509
<!doctype html>
5510
<html lang=en>
5511
<meta charset=utf-8>
5512
<title>CImage verbose output</title>
5513
<style>body{background-color: #ddd}</style>
5514
<a href=$url1><code>$url1</code></a><br>
5515
<img src='{$url1}' />
5516
<pre id="json"></pre>
5517
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
5518
<script type="text/javascript">
5519
window.getDetails = function (url, id) {
5520
  $.getJSON(url, function(data) {
5521
    element = document.getElementById(id);
5522
    element.innerHTML = "filename: " + data.filename + "\\nmime type: " + data.mimeType + "\\ncolors: " + data.colors + "\\nsize: " + data.size + "\\nwidth: " + data.width + "\\nheigh: " + data.height + "\\naspect-ratio: " + data.aspectRatio + ( data.pngType ? "\\npng-type: " + data.pngType : '');
5523
  });
5524
}
5525
</script>
5526
<script type="text/javascript">window.getDetails("{$url2}&json", "json")</script>
5527
EOD;
5528
}
5529
5530
5531
5532
/**
5533
 * Load, process and output the image
5534
 */
5535
$img->log("Incoming arguments: " . print_r(verbose(), 1))
0 ignored issues
show
Are you sure print_r(verbose(), 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5535
$img->log("Incoming arguments: " . /** @scrutinizer ignore-type */ print_r(verbose(), 1))
Loading history...
Are you sure the usage of verbose() is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
5536
    ->setSaveFolder($cachePath)
5537
    ->useCache($useCache)
5538
    ->setSource($srcImage, $imagePath)
5539
    ->setOptions(
5540
        array(
5541
            // Options for calculate dimensions
5542
            'newWidth'  => $newWidth,
5543
            'newHeight' => $newHeight,
5544
            'aspectRatio' => $aspectRatio,
5545
            'keepRatio' => $keepRatio,
5546
            'cropToFit' => $cropToFit,
5547
            'fillToFit' => $fillToFit,
5548
            'crop'      => $crop,
5549
            'area'      => $area,
5550
            'upscale'   => $upscale,
5551
5552
            // Pre-processing, before resizing is done
5553
            'scale'        => $scale,
5554
            'rotateBefore' => $rotateBefore,
5555
            'autoRotate'   => $autoRotate,
5556
5557
            // General processing options
5558
            'bgColor'    => $bgColor,
5559
5560
            // Post-processing, after resizing is done
5561
            'palette'   => $palette,
5562
            'filters'   => $filters,
5563
            'sharpen'   => $sharpen,
5564
            'emboss'    => $emboss,
5565
            'blur'      => $blur,
5566
            'convolve'  => $convolve,
5567
            'rotateAfter' => $rotateAfter,
5568
5569
            // Output format
5570
            'outputFormat' => $outputFormat,
5571
            'dpr'          => $dpr,
5572
5573
            // Postprocessing using external tools
5574
            'lossy' => $lossy,
5575
        )
5576
    )
5577
    ->loadImageDetails()
5578
    ->initDimensions()
5579
    ->calculateNewWidthAndHeight()
5580
    ->setSaveAsExtension($saveAs)
5581
    ->setJpegQuality($quality)
5582
    ->setPngCompression($compress)
5583
    ->useOriginalIfPossible($useOriginal)
5584
    ->generateFilename($cachePath)
5585
    ->useCacheIfPossible($useCache)
5586
    ->load()
5587
    ->preResize()
5588
    ->resize()
5589
    ->postResize()
5590
    ->setPostProcessingOptions($postProcessing)
5591
    ->save()
5592
    ->linkToCacheFile($aliasTarget)
5593
    ->output();
5594
5595
5596
5597