Completed
Push — 0713 ( 6118f2 )
by Mikael
02:49
created

CImage.php (6 issues)

Severity

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 * Resize and crop images on the fly, store generated images in a cache.
4
 *
5
 * @author  Mikael Roos [email protected]
6
 * @example http://dbwebb.se/opensource/cimage
7
 * @link    https://github.com/mosbth/cimage
8
 */
9
class CImage
10
{
11
12
    /**
13
     * Constants type of PNG image
14
     */
15
    const PNG_GREYSCALE         = 0;
16
    const PNG_RGB               = 2;
17
    const PNG_RGB_PALETTE       = 3;
18
    const PNG_GREYSCALE_ALPHA   = 4;
19
    const PNG_RGB_ALPHA         = 6;
20
21
22
23
    /**
24
     * Constant for default image quality when not set
25
     */
26
    const JPEG_QUALITY_DEFAULT = 60;
27
28
29
30
    /**
31
     * Quality level for JPEG images.
32
     */
33
    private $quality;
34
35
36
37
    /**
38
     * Is the quality level set from external use (true) or is it default (false)?
39
     */
40
    private $useQuality = false;
41
42
43
44
    /**
45
     * Constant for default image quality when not set
46
     */
47
    const PNG_COMPRESSION_DEFAULT = -1;
48
49
50
51
    /**
52
     * Compression level for PNG images.
53
     */
54
    private $compress;
55
56
57
58
    /**
59
     * Is the compress level set from external use (true) or is it default (false)?
60
     */
61
    private $useCompress = false;
62
63
64
65
66
    /**
67
     * Add HTTP headers for outputing image.
68
     */
69
    private $HTTPHeader = array();
70
71
72
73
    /**
74
     * Default background color, red, green, blue, alpha.
75
     *
76
     * @todo remake when upgrading to PHP 5.5
77
     */
78
    /*
79
    const BACKGROUND_COLOR = array(
80
        'red'   => 0,
81
        'green' => 0,
82
        'blue'  => 0,
83
        'alpha' => null,
84
    );*/
85
86
87
88
    /**
89
     * Default background color to use.
90
     *
91
     * @todo remake when upgrading to PHP 5.5
92
     */
93
    //private $bgColorDefault = self::BACKGROUND_COLOR;
94
    private $bgColorDefault = array(
95
        'red'   => 0,
96
        'green' => 0,
97
        'blue'  => 0,
98
        'alpha' => null,
99
    );
100
101
102
    /**
103
     * Background color to use, specified as part of options.
104
     */
105
    private $bgColor;
106
107
108
109
    /**
110
     * Where to save the target file.
111
     */
112
    private $saveFolder;
113
114
115
116
    /**
117
     * The working image object.
118
     */
119
    private $image;
120
121
122
123
    /**
124
     * Image filename, may include subdirectory, relative from $imageFolder
125
     */
126
    private $imageSrc;
127
128
129
130
    /**
131
     * Actual path to the image, $imageFolder . '/' . $imageSrc
132
     */
133
    private $pathToImage;
134
135
136
137
    /**
138
     * File type for source image, as provided by getimagesize()
139
     */
140
    private $fileType;
141
142
143
144
    /**
145
     * File extension to use when saving image.
146
     */
147
    private $extension;
148
149
150
151
    /**
152
     * Output format, supports null (image) or json.
153
     */
154
    private $outputFormat = null;
155
156
157
158
    /**
159
     * Verbose mode to print out a trace and display the created image
160
     */
161
    private $verbose = false;
162
163
164
165
    /**
166
     * Keep a log/trace on what happens
167
     */
168
    private $log = array();
169
170
171
172
    /**
173
     * Handle image as palette image
174
     */
175
    private $palette;
176
177
178
179
    /**
180
     * Target filename, with path, to save resulting image in.
181
     */
182
    private $cacheFileName;
183
184
185
186
    /**
187
     * Set a format to save image as, or null to use original format.
188
     */
189
    private $saveAs;
190
191
192
    /**
193
     * Path to command for filter optimize, for example optipng or null.
194
     */
195
    private $pngFilter;
196
    private $pngFilterCmd;
197
198
199
200
    /**
201
     * Path to command for deflate optimize, for example pngout or null.
202
     */
203
    private $pngDeflate;
204
    private $pngDeflateCmd;
205
206
207
208
    /**
209
     * Path to command to optimize jpeg images, for example jpegtran or null.
210
     */
211
     private $jpegOptimize;
212
     private $jpegOptimizeCmd;
213
214
215
216
    /**
217
     * Image dimensions, calculated from loaded image.
218
     */
219
    private $width;  // Calculated from source image
220
    private $height; // Calculated from source image
221
222
223
    /**
224
     * New image dimensions, incoming as argument or calculated.
225
     */
226
    private $newWidth;
227
    private $newWidthOrig;  // Save original value
228
    private $newHeight;
229
    private $newHeightOrig; // Save original value
230
231
232
    /**
233
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
234
     */
235
    private $dpr = 1;
236
237
238
    /**
239
     * Always upscale images, even if they are smaller than target image.
240
     */
241
    const UPSCALE_DEFAULT = true;
242
    private $upscale = self::UPSCALE_DEFAULT;
243
244
245
246
    /**
247
     * Array with details on how to crop, incoming as argument and calculated.
248
     */
249
    public $crop;
250
    public $cropOrig; // Save original value
251
252
253
    /**
254
     * String with details on how to do image convolution. String
255
     * should map a key in the $convolvs array or be a string of
256
     * 11 float values separated by comma. The first nine builds
257
     * up the matrix, then divisor and last offset.
258
     */
259
    private $convolve;
260
261
262
    /**
263
     * Custom convolution expressions, matrix 3x3, divisor and offset.
264
     */
265
    private $convolves = array(
266
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
267
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
268
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
269
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
270
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
271
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
272
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
273
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
274
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
275
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
276
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
277
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
278
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
279
    );
280
281
282
    /**
283
     * Resize strategy to fill extra area with background color.
284
     * True or false.
285
     */
286
    private $fillToFit;
287
288
289
290
    /**
291
     * To store value for option scale.
292
     */
293
    private $scale;
294
295
296
297
    /**
298
     * To store value for option.
299
     */
300
    private $rotateBefore;
301
302
303
304
    /**
305
     * To store value for option.
306
     */
307
    private $rotateAfter;
308
309
310
311
    /**
312
     * To store value for option.
313
     */
314
    private $autoRotate;
315
316
317
318
    /**
319
     * To store value for option.
320
     */
321
    private $sharpen;
322
323
324
325
    /**
326
     * To store value for option.
327
     */
328
    private $emboss;
329
330
331
332
    /**
333
     * To store value for option.
334
     */
335
    private $blur;
336
337
338
339
    /**
340
     * Used with option area to set which parts of the image to use.
341
     */
342
    private $offset;
343
344
345
346
    /**
347
     * Calculate target dimension for image when using fill-to-fit resize strategy.
348
     */
349
    private $fillWidth;
350
    private $fillHeight;
351
352
353
354
    /**
355
     * Allow remote file download, default is to disallow remote file download.
356
     */
357
    private $allowRemote = false;
358
359
360
361
    /**
362
     * Path to cache for remote download.
363
     */
364
    private $remoteCache;
365
366
367
368
    /**
369
     * Pattern to recognize a remote file.
370
     */
371
    //private $remotePattern = '#^[http|https]://#';
372
    private $remotePattern = '#^https?://#';
373
374
375
376
    /**
377
     * Use the cache if true, set to false to ignore the cached file.
378
     */
379
    private $useCache = true;
380
381
382
383
    /*
384
     * Set whitelist for valid hostnames from where remote source can be
385
     * downloaded.
386
     */
387
    private $remoteHostWhitelist = null;
388
389
390
391
    /*
392
     * Do verbose logging to file by setting this to a filename.
393
     */
394
    private $verboseFileName = null;
395
396
397
398
    /*
399
     * Output to ascii can take som options as an array.
400
     */
401
    private $asciiOptions = array();
402
403
404
405
    /*
406
     * Image copy strategy, defaults to RESAMPLE.
407
     */
408
     const RESIZE = 1;
409
     const RESAMPLE = 2;
410
     private $copyStrategy = NULL;
411
412
413
414
    /**
415
     * Properties, the class is mutable and the method setOptions()
416
     * decides (partly) what properties are created.
417
     *
418
     * @todo Clean up these and check if and how they are used
419
     */
420
421
    public $keepRatio;
422
    public $cropToFit;
423
    private $cropWidth;
424
    private $cropHeight;
425
    public $crop_x;
426
    public $crop_y;
427
    public $filters;
428
    private $attr; // Calculated from source image
429
430
431
432
433
    /**
434 7
     * Constructor, can take arguments to init the object.
435
     *
436 7
     * @param string $imageSrc    filename which may contain subdirectory.
437 7
     * @param string $imageFolder path to root folder for images.
438 7
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
439
     * @param string $saveName    name of target file when saveing.
440
     */
441
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
442
    {
443
        $this->setSource($imageSrc, $imageFolder);
444
        $this->setTarget($saveFolder, $saveName);
445
    }
446
447
448
449
    /**
450
     * Set verbose mode.
451
     *
452
     * @param boolean $mode true or false to enable and disable verbose mode,
453
     *                      default is true.
454
     *
455
     * @return $this
456
     */
457
    public function setVerbose($mode = true)
458
    {
459
        $this->verbose = $mode;
460
        return $this;
461
    }
462
463
464
465
    /**
466
     * Set save folder, base folder for saving cache files.
467 2
     *
468
     * @todo clean up how $this->saveFolder is used in other methods.
469 2
     *
470 2
     * @param string $path where to store cached files.
471
     *
472
     * @return $this
473
     */
474
    public function setSaveFolder($path)
475
    {
476
        $this->saveFolder = $path;
477
        return $this;
478
    }
479
480
481
482
    /**
483
     * Use cache or not.
484
     *
485
     * @param boolean $use true or false to use cache.
486
     *
487
     * @return $this
488
     */
489
    public function useCache($use = true)
490
    {
491
        $this->useCache = $use;
492
        return $this;
493
    }
494
495
496
497
    /**
498
     * Create and save a dummy image. Use dimensions as stated in
499 2
     * $this->newWidth, or $width or default to 100 (same for height.
500
     *
501 2
     * @param integer $width  use specified width for image dimension.
502 2
     * @param integer $height use specified width for image dimension.
503
     *
504 2
     * @return $this
505
     */
506 2
    public function createDummyImage($width = null, $height = null)
507
    {
508
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
509
        $this->newHeight = $this->newHeight ?: $height ?: 100;
510
511
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
512
513
        return $this;
514
    }
515
516
517
518
    /**
519 2
     * Allow or disallow remote image download.
520
     *
521 2
     * @param boolean $allow   true or false to enable and disable.
522 2
     * @param string  $cache   path to cache dir.
523
     * @param string  $pattern to use to detect if its a remote file.
524 2
     *
525
     * @return $this
526 2
     */
527 2
    public function setRemoteDownload($allow, $cache, $pattern = null)
528 2
    {
529 2
        $this->allowRemote = $allow;
530
        $this->remoteCache = $cache;
531 2
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
532
533
        $this->log(
534
            "Set remote download to: "
535
            . ($this->allowRemote ? "true" : "false")
536
            . " using pattern "
537
            . $this->remotePattern
538
        );
539
540
        return $this;
541
    }
542
543 2
544
545 2
    /**
546 2
     * Check if the image resource is a remote file or not.
547 2
     *
548
     * @param string $src check if src is remote.
549
     *
550
     * @return boolean true if $src is a remote file, else false.
551
     */
552
    public function isRemoteSource($src)
553
    {
554
        $remote = preg_match($this->remotePattern, $src);
555
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
556
        return !!$remote;
557
    }
558
559
560 2
561
    /**
562 2
     * Set whitelist for valid hostnames from where remote source can be
563 2
     * downloaded.
564
     *
565 2
     * @param array $whitelist with regexp hostnames to allow download from.
566 2
     *
567 2
     * @return $this
568
     */
569
    public function setRemoteHostWhitelist($whitelist = null)
570
    {
571
        $this->remoteHostWhitelist = $whitelist;
572
        $this->log(
573
            "Setting remote host whitelist to: "
574
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
575
        );
576
        return $this;
577
    }
578
579
580 3
581
    /**
582 3
     * Check if the hostname for the remote image, is on a whitelist,
583 1
     * if the whitelist is defined.
584 1
     *
585
     * @param string $src the remote source.
586
     *
587 2
     * @return boolean true if hostname on $src is in the whitelist, else false.
588 2
     */
589 2
    public function isRemoteSourceOnWhitelist($src)
590
    {
591 2
        if (is_null($this->remoteHostWhitelist)) {
592
            $this->log("Remote host on whitelist not configured - allowing.");
593 2
            return true;
594 2
        }
595 2
596
        $whitelist = new CWhitelist();
597
        $hostname = parse_url($src, PHP_URL_HOST);
598
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
599
600
        $this->log(
601
            "Remote host is on whitelist: "
602
            . ($allow ? "true" : "false")
603
        );
604
        return $allow;
605
    }
606
607
608
609
    /**
610
     * Check if file extension is valid as a file extension.
611
     *
612
     * @param string $extension of image file.
613
     *
614
     * @return $this
615
     */
616
    private function checkFileExtension($extension)
617
    {
618
        $valid = array('jpg', 'jpeg', 'png', 'gif');
619
620
        in_array(strtolower($extension), $valid)
621
            or $this->raiseError('Not a valid file extension.');
622
623
        return $this;
624
    }
625
626 2
627
628 2
    /**
629
     * Normalize the file extension.
630 2
     *
631
     * @param string $extension of image file or skip to use internal.
632
     *
633
     * @return string $extension as a normalized file extension.
634 2
     */
635
    private function normalizeFileExtension($extension = null)
636
    {
637
        $extension = strtolower($extension ? $extension : $this->extension);
638
639
        if ($extension == 'jpeg') {
640
                $extension = 'jpg';
641
            }
642
643
        return $extension;
644
    }
645
646
647
648
    /**
649
     * Download a remote image and return path to its local copy.
650
     *
651
     * @param string $src remote path to image.
652
     *
653
     * @return string as path to downloaded remote source.
654
     */
655
    public function downloadRemoteSource($src)
656
    {
657
        if (!$this->isRemoteSourceOnWhitelist($src)) {
658
            throw new Exception("Hostname is not on whitelist for remote sources.");
659
        }
660
661
        $remote = new CRemoteImage();
662
663
        if (!is_writable($this->remoteCache)) {
664
            $this->log("The remote cache is not writable.");
665
        }
666
667
        $remote->setCache($this->remoteCache);
668
        $remote->useCache($this->useCache);
669
        $src = $remote->download($src);
670
671
        $this->log("Remote HTTP status: " . $remote->getStatus());
672
        $this->log("Remote item is in local cache: $src");
673
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
674
675
        return $src;
676
    }
677
678
679
680
    /**
681
     * Set source file to use as image source.
682
     *
683
     * @param string $src of image.
684
     * @param string $dir as optional base directory where images are.
685
     *
686
     * @return $this
687
     */
688 7
    public function setSource($src, $dir = null)
689
    {
690 7
        if (!isset($src)) {
691 7
            $this->imageSrc = null;
692 7
            $this->pathToImage = null;
693 7
            return $this;
694
        }
695
696 2
        if ($this->allowRemote && $this->isRemoteSource($src)) {
697
            $src = $this->downloadRemoteSource($src);
698
            $dir = null;
699
        }
700
701 2
        if (!isset($dir)) {
702
            $dir = dirname($src);
703
            $src = basename($src);
704
        }
705
706 2
        $this->imageSrc     = ltrim($src, '/');
707 2
        $imageFolder        = rtrim($dir, '/');
708 2
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
709
710 2
        return $this;
711
    }
712
713
714
715
    /**
716
     * Set target file.
717
     *
718
     * @param string $src of target image.
719
     * @param string $dir as optional base directory where images are stored.
720
     *                    Uses $this->saveFolder if null.
721
     *
722
     * @return $this
723
     */
724 7
    public function setTarget($src = null, $dir = null)
725
    {
726 7
        if (!isset($src)) {
727 7
            $this->cacheFileName = null;
728 7
            return $this;
729
        }
730
731 2
        if (isset($dir)) {
732
            $this->saveFolder = rtrim($dir, '/');
733
        }
734
735 2
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
736
737
        // Sanitize filename
738 2
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
739 2
        $this->log("The cache file name is: " . $this->cacheFileName);
740
741 2
        return $this;
742
    }
743
744
745
746
    /**
747
     * Get filename of target file.
748
     *
749
     * @return Boolean|String as filename of target or false if not set.
750
     */
751 2
    public function getTarget()
752
    {
753 2
        return $this->cacheFileName;
754
    }
755
756
757
758
    /**
759
     * Set options to use when processing image.
760
     *
761
     * @param array $args used when processing image.
762
     *
763
     * @return $this
764
     */
765
    public function setOptions($args)
766
    {
767
        $this->log("Set new options for processing image.");
768
769
        $defaults = array(
770
            // Options for calculate dimensions
771
            'newWidth'    => null,
772
            'newHeight'   => null,
773
            'aspectRatio' => null,
774
            'keepRatio'   => true,
775
            'cropToFit'   => false,
776
            'fillToFit'   => null,
777
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
778
            'area'        => null, //'0,0,0,0',
779
            'upscale'     => self::UPSCALE_DEFAULT,
780
781
            // Options for caching or using original
782
            'useCache'    => true,
783
            'useOriginal' => true,
784
785
            // Pre-processing, before resizing is done
786
            'scale'        => null,
787
            'rotateBefore' => null,
788
            'autoRotate'  => false,
789
790
            // General options
791
            'bgColor'     => null,
792
793
            // Post-processing, after resizing is done
794
            'palette'     => null,
795
            'filters'     => null,
796
            'sharpen'     => null,
797
            'emboss'      => null,
798
            'blur'        => null,
799
            'convolve'       => null,
800
            'rotateAfter' => null,
801
802
            // Output format
803
            'outputFormat' => null,
804
            'dpr'          => 1,
805
        );
806
807
        // Convert crop settings from string to array
808
        if (isset($args['crop']) && !is_array($args['crop'])) {
809
            $pices = explode(',', $args['crop']);
810
            $args['crop'] = array(
811
                'width'   => $pices[0],
812
                'height'  => $pices[1],
813
                'start_x' => $pices[2],
814
                'start_y' => $pices[3],
815
            );
816
        }
817
818
        // Convert area settings from string to array
819
        if (isset($args['area']) && !is_array($args['area'])) {
820
                $pices = explode(',', $args['area']);
821
                $args['area'] = array(
822
                    'top'    => $pices[0],
823
                    'right'  => $pices[1],
824
                    'bottom' => $pices[2],
825
                    'left'   => $pices[3],
826
                );
827
        }
828
829
        // Convert filter settings from array of string to array of array
830
        if (isset($args['filters']) && is_array($args['filters'])) {
831
            foreach ($args['filters'] as $key => $filterStr) {
832
                $parts = explode(',', $filterStr);
833
                $filter = $this->mapFilter($parts[0]);
834
                $filter['str'] = $filterStr;
835
                for ($i=1; $i<=$filter['argc']; $i++) {
836
                    if (isset($parts[$i])) {
837
                        $filter["arg{$i}"] = $parts[$i];
838
                    } else {
839
                        throw new Exception(
840
                            'Missing arg to filter, review how many arguments are needed at
841
                            http://php.net/manual/en/function.imagefilter.php'
842
                        );
843
                    }
844
                }
845
                $args['filters'][$key] = $filter;
846
            }
847
        }
848
849
        // Merge default arguments with incoming and set properties.
850
        //$args = array_merge_recursive($defaults, $args);
851
        $args = array_merge($defaults, $args);
852
        foreach ($defaults as $key => $val) {
853
            $this->{$key} = $args[$key];
854
        }
855
856
        if ($this->bgColor) {
857
            $this->setDefaultBackgroundColor($this->bgColor);
858
        }
859
860
        // Save original values to enable re-calculating
861
        $this->newWidthOrig  = $this->newWidth;
862
        $this->newHeightOrig = $this->newHeight;
863
        $this->cropOrig      = $this->crop;
864
865
        return $this;
866
    }
867
868
869
870
    /**
871
     * Map filter name to PHP filter and id.
872
     *
873
     * @param string $name the name of the filter.
874
     *
875
     * @return array with filter settings
876
     * @throws Exception
877
     */
878
    private function mapFilter($name)
879
    {
880
        $map = array(
881
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
882
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
883
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
884
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
885
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
886
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
887
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
888
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
889
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
890
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
891
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
892
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
893
        );
894
895
        if (isset($map[$name])) {
896
            return $map[$name];
897
        } else {
898
            throw new Exception('No such filter.');
899
        }
900
    }
901
902
903
904
    /**
905
     * Load image details from original image file.
906
     *
907
     * @param string $file the file to load or null to use $this->pathToImage.
908
     *
909
     * @return $this
910
     * @throws Exception
911
     */
912
    public function loadImageDetails($file = null)
913
    {
914
        $file = $file ? $file : $this->pathToImage;
915
916
        is_readable($file)
917
            or $this->raiseError('Image file does not exist.');
918
919
        // Get details on image
920
        $info = list($this->width, $this->height, $this->fileType, $this->attr) = getimagesize($file);
921
        if (empty($info)) {
922
            throw new Exception("The file doesn't seem to be a valid image.");
923
        }
924
925
        if ($this->verbose) {
926
            $this->log("Loading image details for: {$file}");
927
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
928
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
929
            $this->log(" Image mimetype: " . image_type_to_mime_type($this->fileType));
930
        }
931
932
        return $this;
933
    }
934
935
936
937
    /**
938
     * Init new width and height and do some sanity checks on constraints, before any
939
     * processing can be done.
940
     *
941
     * @return $this
942
     * @throws Exception
943
     */
944
    public function initDimensions()
945
    {
946
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
947
948
        // width as %
949
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
950
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
951
            $this->log("Setting new width based on % to {$this->newWidth}");
952
        }
953
954
        // height as %
955
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
956
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
957
            $this->log("Setting new height based on % to {$this->newHeight}");
958
        }
959
960
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
961
962
        // width & height from aspect ratio
963
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
964
            if ($this->aspectRatio >= 1) {
965
                $this->newWidth   = $this->width;
966
                $this->newHeight  = $this->width / $this->aspectRatio;
967
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
968
969
            } else {
970
                $this->newHeight  = $this->height;
971
                $this->newWidth   = $this->height * $this->aspectRatio;
972
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
973
            }
974
975
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
976
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
977
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
978
979
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
980
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
981
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
982
        }
983
984
        // Change width & height based on dpr
985
        if ($this->dpr != 1) {
986
            if (!is_null($this->newWidth)) {
987
                $this->newWidth  = round($this->newWidth * $this->dpr);
988
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
989
            }
990
            if (!is_null($this->newHeight)) {
991
                $this->newHeight = round($this->newHeight * $this->dpr);
992
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
993
            }
994
        }
995
996
        // Check values to be within domain
997
        is_null($this->newWidth)
998
            or is_numeric($this->newWidth)
999
            or $this->raiseError('Width not numeric');
1000
1001
        is_null($this->newHeight)
1002
            or is_numeric($this->newHeight)
1003
            or $this->raiseError('Height not numeric');
1004
1005
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1006
1007
        return $this;
1008
    }
1009
1010
1011
1012
    /**
1013
     * Calculate new width and height of image, based on settings.
1014
     *
1015
     * @return $this
1016
     */
1017
    public function calculateNewWidthAndHeight()
1018
    {
1019
        // Crop, use cropped width and height as base for calulations
1020
        $this->log("Calculate new width and height.");
1021
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1022
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1023
1024
        // Check if there is an area to crop off
1025
        if (isset($this->area)) {
1026
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1027
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1028
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1029
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1030
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1031
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1032
            $this->width  = $this->offset['width'];
1033
            $this->height = $this->offset['height'];
1034
            $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']}%.");
1035
            $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.");
1036
        }
1037
1038
        $width  = $this->width;
1039
        $height = $this->height;
1040
1041
        // Check if crop is set
1042
        if ($this->crop) {
1043
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1044
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1045
1046
            if ($this->crop['start_x'] == 'left') {
1047
                $this->crop['start_x'] = 0;
1048
            } elseif ($this->crop['start_x'] == 'right') {
1049
                $this->crop['start_x'] = $this->width - $width;
1050
            } elseif ($this->crop['start_x'] == 'center') {
1051
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1052
            }
1053
1054
            if ($this->crop['start_y'] == 'top') {
1055
                $this->crop['start_y'] = 0;
1056
            } elseif ($this->crop['start_y'] == 'bottom') {
1057
                $this->crop['start_y'] = $this->height - $height;
1058
            } elseif ($this->crop['start_y'] == 'center') {
1059
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1060
            }
1061
1062
            $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.");
1063
        }
1064
1065
        // Calculate new width and height if keeping aspect-ratio.
1066
        if ($this->keepRatio) {
1067
1068
            $this->log("Keep aspect ratio.");
1069
1070
            // Crop-to-fit and both new width and height are set.
1071
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1072
1073
                // Use newWidth and newHeigh as width/height, image should fit in box.
1074
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1075
1076
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1077
1078
                // Both new width and height are set.
1079
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1080
                $ratioWidth  = $width  / $this->newWidth;
1081
                $ratioHeight = $height / $this->newHeight;
1082
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1083
                $this->newWidth  = round($width  / $ratio);
1084
                $this->newHeight = round($height / $ratio);
1085
                $this->log("New width and height was set.");
1086
1087
            } elseif (isset($this->newWidth)) {
1088
1089
                // Use new width as max-width
1090
                $factor = (float)$this->newWidth / (float)$width;
1091
                $this->newHeight = round($factor * $height);
1092
                $this->log("New width was set.");
1093
1094
            } elseif (isset($this->newHeight)) {
1095
1096
                // Use new height as max-hight
1097
                $factor = (float)$this->newHeight / (float)$height;
1098
                $this->newWidth = round($factor * $width);
1099
                $this->log("New height was set.");
1100
1101
            } else {
1102
1103
                // Use existing width and height as new width and height.
1104
                $this->newWidth = $width;
1105
                $this->newHeight = $height;
1106
            }
1107
            
1108
1109
            // Get image dimensions for pre-resize image.
1110
            if ($this->cropToFit || $this->fillToFit) {
1111
1112
                // Get relations of original & target image
1113
                $ratioWidth  = $width  / $this->newWidth;
1114
                $ratioHeight = $height / $this->newHeight;
1115
1116
                if ($this->cropToFit) {
1117
1118
                    // Use newWidth and newHeigh as defined width/height,
1119
                    // image should fit the area.
1120
                    $this->log("Crop to fit.");
1121
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1122
                    $this->cropWidth  = round($width  / $ratio);
1123
                    $this->cropHeight = round($height / $ratio);
1124
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1125
1126
                } elseif ($this->fillToFit) {
1127
1128
                    // Use newWidth and newHeigh as defined width/height,
1129
                    // image should fit the area.
1130
                    $this->log("Fill to fit.");
1131
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1132
                    $this->fillWidth  = round($width  / $ratio);
1133
                    $this->fillHeight = round($height / $ratio);
1134
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1135
                }
1136
            }
1137
        }
1138
1139
        // Crop, ensure to set new width and height
1140
        if ($this->crop) {
1141
            $this->log("Crop.");
1142
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1143
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1144
        }
1145
1146
        // Fill to fit, ensure to set new width and height
1147
        /*if ($this->fillToFit) {
1148
            $this->log("FillToFit.");
1149
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1150
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1151
        }*/
1152
1153
        // No new height or width is set, use existing measures.
1154
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1155
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1156
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1157
1158
        return $this;
1159
    }
1160
1161
1162
1163
    /**
1164
     * Re-calculate image dimensions when original image dimension has changed.
1165
     *
1166
     * @return $this
1167
     */
1168
    public function reCalculateDimensions()
1169
    {
1170
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1171
1172
        $this->newWidth  = $this->newWidthOrig;
1173
        $this->newHeight = $this->newHeightOrig;
1174
        $this->crop      = $this->cropOrig;
1175
1176
        $this->initDimensions()
1177
             ->calculateNewWidthAndHeight();
1178
1179
        return $this;
1180
    }
1181
1182
1183
1184
    /**
1185
     * Set extension for filename to save as.
1186
     *
1187
     * @param string $saveas extension to save image as
1188
     *
1189
     * @return $this
1190
     */
1191
    public function setSaveAsExtension($saveAs = null)
1192
    {
1193
        if (isset($saveAs)) {
1194
            $saveAs = strtolower($saveAs);
1195
            $this->checkFileExtension($saveAs);
1196
            $this->saveAs = $saveAs;
1197
            $this->extension = $saveAs;
1198
        }
1199
1200
        $this->log("Prepare to save image as: " . $this->extension);
1201
1202
        return $this;
1203
    }
1204
1205
1206
1207
    /**
1208
     * Set JPEG quality to use when saving image
1209
     *
1210
     * @param int $quality as the quality to set.
1211
     *
1212
     * @return $this
1213
     */
1214
    public function setJpegQuality($quality = null)
1215
    {
1216
        if ($quality) {
1217
            $this->useQuality = true;
1218
        }
1219
1220
        $this->quality = isset($quality)
1221
            ? $quality
1222
            : self::JPEG_QUALITY_DEFAULT;
1223
1224
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1225
            or $this->raiseError('Quality not in range.');
1226
1227
        $this->log("Setting JPEG quality to {$this->quality}.");
1228
1229
        return $this;
1230
    }
1231
1232
1233
1234
    /**
1235
     * Set PNG compressen algorithm to use when saving image
1236
     *
1237
     * @param int $compress as the algorithm to use.
1238
     *
1239
     * @return $this
1240
     */
1241
    public function setPngCompression($compress = null)
1242
    {
1243
        if ($compress) {
1244
            $this->useCompress = true;
1245
        }
1246
1247
        $this->compress = isset($compress)
1248
            ? $compress
1249
            : self::PNG_COMPRESSION_DEFAULT;
1250
1251
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1252
            or $this->raiseError('Quality not in range.');
1253
1254
        $this->log("Setting PNG compression level to {$this->compress}.");
1255
1256
        return $this;
1257
    }
1258
1259
1260
1261
    /**
1262
     * Use original image if possible, check options which affects image processing.
1263
     *
1264
     * @param boolean $useOrig default is to use original if possible, else set to false.
1265
     *
1266
     * @return $this
1267
     */
1268
    public function useOriginalIfPossible($useOrig = true)
1269
    {
1270
        if ($useOrig
1271
            && ($this->newWidth == $this->width)
1272
            && ($this->newHeight == $this->height)
1273
            && !$this->area
1274
            && !$this->crop
1275
            && !$this->cropToFit
1276
            && !$this->fillToFit
1277
            && !$this->filters
1278
            && !$this->sharpen
1279
            && !$this->emboss
1280
            && !$this->blur
1281
            && !$this->convolve
1282
            && !$this->palette
1283
            && !$this->useQuality
1284
            && !$this->useCompress
1285
            && !$this->saveAs
1286
            && !$this->rotateBefore
1287
            && !$this->rotateAfter
1288
            && !$this->autoRotate
1289
            && !$this->bgColor
1290
            && ($this->upscale === self::UPSCALE_DEFAULT)
1291
        ) {
1292
            $this->log("Using original image.");
1293
            $this->output($this->pathToImage);
1294
        }
1295
1296
        return $this;
1297
    }
1298
1299
1300
1301
    /**
1302
     * Generate filename to save file in cache.
1303
     *
1304 2
     * @param string  $base      as optional basepath for storing file.
1305
     * @param boolean $useSubdir use or skip the subdir part when creating the
1306 2
     *                           filename.
1307 2
     * @param string  $prefix    to add as part of filename
1308 2
     *
1309 2
     * @return $this
1310 2
     */
1311 2
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1312 2
    {
1313 2
        $filename     = basename($this->pathToImage);
1314 2
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1315 2
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1316 2
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1317
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1318 2
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1319 2
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1320
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1321 2
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1322 2
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1323
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1324
1325
        $saveAs = $this->normalizeFileExtension();
1326 2
        $saveAs = $saveAs ? "_$saveAs" : null;
1327 2
1328
        $copyStrat = null;
1329 2
        if ($this->copyStrategy === self::RESIZE) {
1330 2
            $copyStrat = "_rs";
1331 2
        }
1332
1333 2
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1334 2
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1335 2
1336
        $offset = isset($this->offset)
1337 2
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1338 2
            : null;
1339
1340
        $crop = $this->crop
1341
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1342
            : null;
1343
1344
        $filters = null;
1345
        if (isset($this->filters)) {
1346
            foreach ($this->filters as $filter) {
1347
                if (is_array($filter)) {
1348
                    $filters .= "_f{$filter['id']}";
1349 2
                    for ($i=1; $i<=$filter['argc']; $i++) {
1350 2
                        $filters .= "-".$filter["arg{$i}"];
1351 2
                    }
1352 2
                }
1353
            }
1354 2
        }
1355
1356 2
        $sharpen = $this->sharpen ? 's' : null;
1357 2
        $emboss  = $this->emboss  ? 'e' : null;
1358 2
        $blur    = $this->blur    ? 'b' : null;
1359
        $palette = $this->palette ? 'p' : null;
1360 2
1361 2
        $autoRotate = $this->autoRotate ? 'ar' : null;
1362
1363
        $optimize  = $this->jpegOptimize ? 'o' : null;
1364
        $optimize .= $this->pngFilter    ? 'f' : null;
1365 2
        $optimize .= $this->pngDeflate   ? 'd' : null;
1366 2
1367
        $convolve = null;
1368
        if ($this->convolve) {
1369
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1370 2
        }
1371 2
1372
        $upscale = null;
1373
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1374
            $upscale = '_nu';
1375
        }
1376
1377 2
        $subdir = null;
1378 2
        if ($useSubdir === true) {
1379 2
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1380 2
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1381 2
            $subdir .= '_';
1382 2
        }
1383 2
1384
        $file = $prefix . $subdir . $filename . $width . $height
1385 2
            . $offset . $crop . $cropToFit . $fillToFit
1386
            . $crop_x . $crop_y . $upscale
1387
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1388
            . $optimize . $compress
1389
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1390
            . $convolve . $copyStrat . $saveAs;
1391
1392
        return $this->setTarget($file, $base);
1393
    }
1394
1395
1396
1397
    /**
1398
     * Use cached version of image, if possible.
1399
     *
1400
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1401
     *
1402
     * @return $this
1403
     */
1404
    public function useCacheIfPossible($useCache = true)
1405
    {
1406
        if ($useCache && is_readable($this->cacheFileName)) {
1407
            $fileTime   = filemtime($this->pathToImage);
1408
            $cacheTime  = filemtime($this->cacheFileName);
1409
1410
            if ($fileTime <= $cacheTime) {
1411
                if ($this->useCache) {
1412
                    if ($this->verbose) {
1413
                        $this->log("Use cached file.");
1414
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1415
                    }
1416
                    $this->output($this->cacheFileName, $this->outputFormat);
1417
                } else {
1418
                    $this->log("Cache is valid but ignoring it by intention.");
1419
                }
1420
            } else {
1421
                $this->log("Original file is modified, ignoring cache.");
1422
            }
1423
        } else {
1424
            $this->log("Cachefile does not exists or ignoring it.");
1425
        }
1426
1427
        return $this;
1428
    }
1429
1430
1431
1432
    /**
1433
     * Load image from disk. Try to load image without verbose error message,
1434
     * if fail, load again and display error messages.
1435
     *
1436
     * @param string $src of image.
1437
     * @param string $dir as base directory where images are.
1438
     *
1439
     * @return $this
1440
     *
1441
     */
1442
    public function load($src = null, $dir = null)
1443
    {
1444
        if (isset($src)) {
1445
            $this->setSource($src, $dir);
1446
        }
1447
1448
        $this->loadImageDetails($this->pathToImage);
1449
1450
        $this->image = imagecreatefromstring(file_get_contents($this->pathToImage));
0 ignored issues
show
Security File Exposure introduced by
$this->pathToImage can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
1451
        if ($this->image === false) {
1452
            throw new Exception("Could not load image.");
1453
        }
1454
1455
        /* Removed v0.7.7
1456
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1457
            $type = $this->getPngType();
1458
            $hasFewColors = imagecolorstotal($this->image);
1459
1460
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1461
                if ($this->verbose) {
1462
                    $this->log("Handle this image as a palette image.");
1463
                }
1464
                $this->palette = true;
1465
            }
1466
        }
1467
        */
1468
1469
        if ($this->verbose) {
1470
            $this->log("### Image successfully loaded from file.");
1471
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1472
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1473
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1474
            $index = imagecolortransparent($this->image);
1475
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1476
        }
1477
1478
        return $this;
1479
    }
1480
1481
1482
1483
    /**
1484
     * Get the type of PNG image.
1485
     *
1486
     * @param string $filename to use instead of default.
1487
     *
1488
     * @return int as the type of the png-image
1489
     *
1490
     */
1491
    public function getPngType($filename = null)
1492
    {
1493
        $filename = $filename ? $filename : $this->pathToImage;
1494
1495
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
0 ignored issues
show
Security File Exposure introduced by
$filename can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
1496
1497
        if ($this->verbose) {
1498
            $this->log("Checking png type of: " . $filename);
1499
            $this->log($this->getPngTypeAsString($pngType));
1500
        }
1501
1502
        return $pngType;
1503
    }
1504
1505
1506
1507
    /**
1508
     * Get the type of PNG image as a verbose string.
1509
     *
1510
     * @param integer $type     to use, default is to check the type.
1511
     * @param string  $filename to use instead of default.
1512
     *
1513
     * @return int as the type of the png-image
1514
     *
1515
     */
1516
    private function getPngTypeAsString($pngType = null, $filename = null)
1517
    {
1518
        if ($filename || !$pngType) {
1519
            $pngType = $this->getPngType($filename);
1520
        }
1521
1522
        $index = imagecolortransparent($this->image);
1523
        $transparent = null;
1524
        if ($index != -1) {
1525
            $transparent = " (transparent)";
1526
        }
1527
1528
        switch ($pngType) {
1529
1530
            case self::PNG_GREYSCALE:
1531
                $text = "PNG is type 0, Greyscale$transparent";
1532
                break;
1533
1534
            case self::PNG_RGB:
1535
                $text = "PNG is type 2, RGB$transparent";
1536
                break;
1537
1538
            case self::PNG_RGB_PALETTE:
1539
                $text = "PNG is type 3, RGB with palette$transparent";
1540
                break;
1541
1542
            case self::PNG_GREYSCALE_ALPHA:
1543
                $text = "PNG is type 4, Greyscale with alpha channel";
1544
                break;
1545
1546
            case self::PNG_RGB_ALPHA:
1547
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1548
                break;
1549
1550
            default:
1551
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1552
        }
1553
1554
        return $text;
1555
    }
1556
1557
1558
1559
1560
    /**
1561
     * Calculate number of colors in an image.
1562
     *
1563
     * @param resource $im the image.
1564
     *
1565
     * @return int
1566
     */
1567
    private function colorsTotal($im)
1568
    {
1569
        if (imageistruecolor($im)) {
1570
            $this->log("Colors as true color.");
1571
            $h = imagesy($im);
1572
            $w = imagesx($im);
1573
            $c = array();
1574
            for ($x=0; $x < $w; $x++) {
1575
                for ($y=0; $y < $h; $y++) {
1576
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1577
                }
1578
            }
1579
            return count($c);
1580
        } else {
1581
            $this->log("Colors as palette.");
1582
            return imagecolorstotal($im);
1583
        }
1584
    }
1585
1586
1587
1588
    /**
1589
     * Preprocess image before rezising it.
1590
     *
1591
     * @return $this
1592
     */
1593
    public function preResize()
1594
    {
1595
        $this->log("### Pre-process before resizing");
1596
1597
        // Rotate image
1598
        if ($this->rotateBefore) {
1599
            $this->log("Rotating image.");
1600
            $this->rotate($this->rotateBefore, $this->bgColor)
1601
                 ->reCalculateDimensions();
1602
        }
1603
1604
        // Auto-rotate image
1605
        if ($this->autoRotate) {
1606
            $this->log("Auto rotating image.");
1607
            $this->rotateExif()
1608
                 ->reCalculateDimensions();
1609
        }
1610
1611
        // Scale the original image before starting
1612
        if (isset($this->scale)) {
1613
            $this->log("Scale by {$this->scale}%");
1614
            $newWidth  = $this->width * $this->scale / 100;
1615
            $newHeight = $this->height * $this->scale / 100;
1616
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1617
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1618
            $this->image = $img;
1619
            $this->width = $newWidth;
1620
            $this->height = $newHeight;
1621
        }
1622
1623
        return $this;
1624
    }
1625
1626
1627
1628
    /**
1629
     * Resize or resample the image while resizing.
1630
     *
1631
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1632
     *
1633
     * @return $this
1634
     */
1635
     public function setCopyResizeStrategy($strategy)
1636
     {
1637
         $this->copyStrategy = $strategy;
1638
         return $this;
1639
     }
1640
1641
1642
1643
    /**
1644
     * Resize and or crop the image.
1645
     *
1646
     * @return void
1647
     */
1648
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1649
    {
1650
        if($this->copyStrategy == self::RESIZE) {
1651
            $this->log("Copy by resize");
1652
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1653
        } else {
1654
            $this->log("Copy by resample");
1655
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1656
        }
1657
    }
1658
1659
1660
1661
    /**
1662
     * Resize and or crop the image.
1663
     *
1664
     * @return $this
1665
     */
1666
    public function resize()
1667
    {
1668
1669
        $this->log("### Starting to Resize()");
1670
        $this->log("Upscale = '$this->upscale'");
1671
1672
        // Only use a specified area of the image, $this->offset is defining the area to use
1673
        if (isset($this->offset)) {
1674
1675
            $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']}");
1676
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1677
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1678
            $this->image = $img;
1679
            $this->width = $this->offset['width'];
1680
            $this->height = $this->offset['height'];
1681
        }
1682
1683
        if ($this->crop) {
1684
1685
            // Do as crop, take only part of image
1686
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1687
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1688
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1689
            $this->image = $img;
1690
            $this->width = $this->crop['width'];
1691
            $this->height = $this->crop['height'];
1692
        }
1693
1694
        if (!$this->upscale) {
1695
            // Consider rewriting the no-upscale code to fit within this if-statement,
1696
            // likely to be more readable code.
1697
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1698
        }
1699
1700
        if ($this->cropToFit) {
1701
1702
            // Resize by crop to fit
1703
            $this->log("Resizing using strategy - Crop to fit");
1704
1705
            if (!$this->upscale 
1706
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1707
                $this->log("Resizing - smaller image, do not upscale.");
1708
1709
                $posX = 0;
1710
                $posY = 0;
1711
                $cropX = 0;
1712
                $cropY = 0;
1713
1714
                if ($this->newWidth > $this->width) {
1715
                    $posX = round(($this->newWidth - $this->width) / 2);
1716
                }
1717
                if ($this->newWidth < $this->width) {
1718
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1719
                }
1720
1721
                if ($this->newHeight > $this->height) {
1722
                    $posY = round(($this->newHeight - $this->height) / 2);
1723
                }
1724
                if ($this->newHeight < $this->height) {
1725
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1726
                }
1727
                $this->log(" cwidth: $this->cropWidth");
1728
                $this->log(" cheight: $this->cropHeight");
1729
                $this->log(" nwidth: $this->newWidth");
1730
                $this->log(" nheight: $this->newHeight");
1731
                $this->log(" width: $this->width");
1732
                $this->log(" height: $this->height");
1733
                $this->log(" posX: $posX");
1734
                $this->log(" posY: $posY");
1735
                $this->log(" cropX: $cropX");
1736
                $this->log(" cropY: $cropY");
1737
1738
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1739
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1740
            } else {
1741
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1742
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1743
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1744
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1745
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1746
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1747
            }
1748
1749
            $this->image = $imageResized;
1750
            $this->width = $this->newWidth;
1751
            $this->height = $this->newHeight;
1752
1753
        } elseif ($this->fillToFit) {
1754
1755
            // Resize by fill to fit
1756
            $this->log("Resizing using strategy - Fill to fit");
1757
1758
            $posX = 0;
1759
            $posY = 0;
1760
1761
            $ratioOrig = $this->width / $this->height;
1762
            $ratioNew  = $this->newWidth / $this->newHeight;
1763
1764
            // Check ratio for landscape or portrait
1765
            if ($ratioOrig < $ratioNew) {
1766
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1767
            } else {
1768
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1769
            }
1770
1771
            if (!$this->upscale
1772
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1773
            ) {
1774
1775
                $this->log("Resizing - smaller image, do not upscale.");
1776
                $posX = round(($this->newWidth - $this->width) / 2);
1777
                $posY = round(($this->newHeight - $this->height) / 2);
1778
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1779
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1780
1781
            } else {
1782
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1783
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1784
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1785
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1786
            }
1787
1788
            $this->image = $imageResized;
1789
            $this->width = $this->newWidth;
1790
            $this->height = $this->newHeight;
1791
1792
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1793
1794
            // Resize it
1795
            $this->log("Resizing, new height and/or width");
1796
1797
            if (!$this->upscale
1798
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1799
            ) {
1800
                $this->log("Resizing - smaller image, do not upscale.");
1801
1802
                if (!$this->keepRatio) {
1803
                    $this->log("Resizing - stretch to fit selected.");
1804
1805
                    $posX = 0;
1806
                    $posY = 0;
1807
                    $cropX = 0;
1808
                    $cropY = 0;
1809
1810
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1811
                        $posX = round(($this->newWidth - $this->width) / 2);
1812
                        $posY = round(($this->newHeight - $this->height) / 2);
1813
                    } elseif ($this->newWidth > $this->width) {
1814
                        $posX = round(($this->newWidth - $this->width) / 2);
1815
                        $cropY = round(($this->height - $this->newHeight) / 2);
1816
                    } elseif ($this->newHeight > $this->height) {
1817
                        $posY = round(($this->newHeight - $this->height) / 2);
1818
                        $cropX = round(($this->width - $this->newWidth) / 2);
1819
                    }
1820
1821
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1822
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1823
                    $this->image = $imageResized;
1824
                    $this->width = $this->newWidth;
1825
                    $this->height = $this->newHeight;
1826
                }
1827
            } else {
1828
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1829
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1830
                $this->image = $imageResized;
1831
                $this->width = $this->newWidth;
1832
                $this->height = $this->newHeight;
1833
            }
1834
        }
1835
1836
        return $this;
1837
    }
1838
1839
1840
1841
    /**
1842
     * Postprocess image after rezising image.
1843
     *
1844
     * @return $this
1845
     */
1846
    public function postResize()
1847
    {
1848
        $this->log("### Post-process after resizing");
1849
1850
        // Rotate image
1851
        if ($this->rotateAfter) {
1852
            $this->log("Rotating image.");
1853
            $this->rotate($this->rotateAfter, $this->bgColor);
1854
        }
1855
1856
        // Apply filters
1857
        if (isset($this->filters) && is_array($this->filters)) {
1858
1859
            foreach ($this->filters as $filter) {
1860
                $this->log("Applying filter {$filter['type']}.");
1861
1862
                switch ($filter['argc']) {
1863
1864
                    case 0:
1865
                        imagefilter($this->image, $filter['type']);
1866
                        break;
1867
1868
                    case 1:
1869
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1870
                        break;
1871
1872
                    case 2:
1873
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1874
                        break;
1875
1876
                    case 3:
1877
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1878
                        break;
1879
1880
                    case 4:
1881
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1882
                        break;
1883
                }
1884
            }
1885
        }
1886
1887
        // Convert to palette image
1888
        if ($this->palette) {
1889
            $this->log("Converting to palette image.");
1890
            $this->trueColorToPalette();
1891
        }
1892
1893
        // Blur the image
1894
        if ($this->blur) {
1895
            $this->log("Blur.");
1896
            $this->blurImage();
1897
        }
1898
1899
        // Emboss the image
1900
        if ($this->emboss) {
1901
            $this->log("Emboss.");
1902
            $this->embossImage();
1903
        }
1904
1905
        // Sharpen the image
1906
        if ($this->sharpen) {
1907
            $this->log("Sharpen.");
1908
            $this->sharpenImage();
1909
        }
1910
1911
        // Custom convolution
1912
        if ($this->convolve) {
1913
            //$this->log("Convolve: " . $this->convolve);
1914
            $this->imageConvolution();
1915
        }
1916
1917
        return $this;
1918
    }
1919
1920
1921
1922
    /**
1923
     * Rotate image using angle.
1924
     *
1925
     * @param float $angle        to rotate image.
1926
     * @param int   $anglebgColor to fill image with if needed.
1927
     *
1928
     * @return $this
1929
     */
1930
    public function rotate($angle, $bgColor)
1931
    {
1932
        $this->log("Rotate image " . $angle . " degrees with filler color.");
1933
1934
        $color = $this->getBackgroundColor();
1935
        $this->image = imagerotate($this->image, $angle, $color);
1936
1937
        $this->width  = imagesx($this->image);
1938
        $this->height = imagesy($this->image);
1939
1940
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
1941
1942
        return $this;
1943
    }
1944
1945
1946
1947
    /**
1948
     * Rotate image using information in EXIF.
1949
     *
1950
     * @return $this
1951
     */
1952
    public function rotateExif()
1953
    {
1954
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
1955
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
1956
            return $this;
1957
        }
1958
1959
        $exif = exif_read_data($this->pathToImage);
1960
1961
        if (!empty($exif['Orientation'])) {
1962
            switch ($exif['Orientation']) {
1963
                case 3:
1964
                    $this->log("Autorotate 180.");
1965
                    $this->rotate(180, $this->bgColor);
1966
                    break;
1967
1968
                case 6:
1969
                    $this->log("Autorotate -90.");
1970
                    $this->rotate(-90, $this->bgColor);
1971
                    break;
1972
1973
                case 8:
1974
                    $this->log("Autorotate 90.");
1975
                    $this->rotate(90, $this->bgColor);
1976
                    break;
1977
1978
                default:
1979
                    $this->log("Autorotate ignored, unknown value as orientation.");
1980
            }
1981
        } else {
1982
            $this->log("Autorotate ignored, no orientation in EXIF.");
1983
        }
1984
1985
        return $this;
1986
    }
1987
1988
1989
1990
    /**
1991
     * Convert true color image to palette image, keeping alpha.
1992
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
1993
     *
1994
     * @return void
1995
     */
1996
    public function trueColorToPalette()
1997
    {
1998
        $img = imagecreatetruecolor($this->width, $this->height);
1999
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2000
        imagecolortransparent($img, $bga);
2001
        imagefill($img, 0, 0, $bga);
2002
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2003
        imagetruecolortopalette($img, false, 255);
2004
        imagesavealpha($img, true);
2005
2006
        if (imageistruecolor($this->image)) {
2007
            $this->log("Matching colors with true color image.");
2008
            imagecolormatch($this->image, $img);
2009
        }
2010
2011
        $this->image = $img;
2012
    }
2013
2014
2015
2016
    /**
2017
     * Sharpen image using image convolution.
2018
     *
2019
     * @return $this
2020
     */
2021
    public function sharpenImage()
2022
    {
2023
        $this->imageConvolution('sharpen');
2024
        return $this;
2025
    }
2026
2027
2028
2029
    /**
2030
     * Emboss image using image convolution.
2031
     *
2032
     * @return $this
2033
     */
2034
    public function embossImage()
2035
    {
2036
        $this->imageConvolution('emboss');
2037
        return $this;
2038
    }
2039
2040
2041
2042
    /**
2043
     * Blur image using image convolution.
2044
     *
2045
     * @return $this
2046
     */
2047
    public function blurImage()
2048
    {
2049
        $this->imageConvolution('blur');
2050
        return $this;
2051
    }
2052
2053
2054
2055
    /**
2056
     * Create convolve expression and return arguments for image convolution.
2057
     *
2058
     * @param string $expression constant string which evaluates to a list of
2059
     *                           11 numbers separated by komma or such a list.
2060
     *
2061
     * @return array as $matrix (3x3), $divisor and $offset
2062
     */
2063
    public function createConvolveArguments($expression)
2064
    {
2065
        // Check of matching constant
2066
        if (isset($this->convolves[$expression])) {
2067
            $expression = $this->convolves[$expression];
2068
        }
2069
2070
        $part = explode(',', $expression);
2071
        $this->log("Creating convolution expressen: $expression");
2072
2073
        // Expect list of 11 numbers, split by , and build up arguments
2074
        if (count($part) != 11) {
2075
            throw new Exception(
2076
                "Missmatch in argument convolve. Expected comma-separated string with
2077
                11 float values. Got $expression."
2078
            );
2079
        }
2080
2081
        array_walk($part, function ($item, $key) {
2082
            if (!is_numeric($item)) {
2083
                throw new Exception("Argument to convolve expression should be float but is not.");
2084
            }
2085
        });
2086
2087
        return array(
2088
            array(
2089
                array($part[0], $part[1], $part[2]),
2090
                array($part[3], $part[4], $part[5]),
2091
                array($part[6], $part[7], $part[8]),
2092
            ),
2093
            $part[9],
2094
            $part[10],
2095
        );
2096
    }
2097
2098
2099
2100
    /**
2101
     * Add custom expressions (or overwrite existing) for image convolution.
2102
     *
2103
     * @param array $options Key value array with strings to be converted
2104
     *                       to convolution expressions.
2105
     *
2106
     * @return $this
2107
     */
2108
    public function addConvolveExpressions($options)
2109
    {
2110
        $this->convolves = array_merge($this->convolves, $options);
2111
        return $this;
2112
    }
2113
2114
2115
2116
    /**
2117
     * Image convolution.
2118
     *
2119
     * @param string $options A string with 11 float separated by comma.
2120
     *
2121
     * @return $this
2122
     */
2123
    public function imageConvolution($options = null)
2124
    {
2125
        // Use incoming options or use $this.
2126
        $options = $options ? $options : $this->convolve;
2127
2128
        // Treat incoming as string, split by +
2129
        $this->log("Convolution with '$options'");
2130
        $options = explode(":", $options);
2131
2132
        // Check each option if it matches constant value
2133
        foreach ($options as $option) {
2134
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2135
            imageconvolution($this->image, $matrix, $divisor, $offset);
2136
        }
2137
2138
        return $this;
2139
    }
2140
2141
2142
2143
    /**
2144
     * Set default background color between 000000-FFFFFF or if using
2145
     * alpha 00000000-FFFFFF7F.
2146
     *
2147
     * @param string $color as hex value.
2148
     *
2149
     * @return $this
2150
    */
2151
    public function setDefaultBackgroundColor($color)
2152
    {
2153
        $this->log("Setting default background color to '$color'.");
2154
2155
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2156
            throw new Exception(
2157
                "Background color needs a hex value of 6 or 8
2158
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2159
                Current value was: '$color'."
2160
            );
2161
        }
2162
2163
        $red    = hexdec(substr($color, 0, 2));
2164
        $green  = hexdec(substr($color, 2, 2));
2165
        $blue   = hexdec(substr($color, 4, 2));
2166
2167
        $alpha = (strlen($color) == 8)
2168
            ? hexdec(substr($color, 6, 2))
2169
            : null;
2170
2171
        if (($red < 0 || $red > 255)
2172
            || ($green < 0 || $green > 255)
2173
            || ($blue < 0 || $blue > 255)
2174
            || ($alpha < 0 || $alpha > 127)
2175
        ) {
2176
            throw new Exception(
2177
                "Background color out of range. Red, green blue
2178
                should be 00-FF and alpha should be 00-7F.
2179
                Current value was: '$color'."
2180
            );
2181 2
        }
2182
2183 2
        $this->bgColor = strtolower($color);
2184
        $this->bgColorDefault = array(
2185 2
            'red'   => $red,
2186
            'green' => $green,
2187 2
            'blue'  => $blue,
2188 2
            'alpha' => $alpha
2189 2
        );
2190 2
2191
        return $this;
2192 2
    }
2193
2194
2195 2
2196
    /**
2197
     * Get the background color.
2198 2
     *
2199
     * @param resource $img the image to work with or null if using $this->image.
2200
     *
2201
     * @return color value or null if no background color is set.
2202
    */
2203
    private function getBackgroundColor($img = null)
2204
    {
2205
        $img = isset($img) ? $img : $this->image;
2206
2207
        if ($this->bgColorDefault) {
2208
2209
            $red   = $this->bgColorDefault['red'];
2210
            $green = $this->bgColorDefault['green'];
2211
            $blue  = $this->bgColorDefault['blue'];
2212
            $alpha = $this->bgColorDefault['alpha'];
2213
2214
            if ($alpha) {
2215 2
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2216
            } else {
2217 2
                $color = imagecolorallocate($img, $red, $green, $blue);
2218 2
            }
2219 2
2220 2
            return $color;
2221
2222 2
        } else {
2223 2
            return 0;
2224 2
        }
2225
    }
2226 2
2227
2228
2229
    /**
2230
     * Create a image and keep transparency for png and gifs.
2231
     *
2232
     * @param int $width of the new image.
2233
     * @param int $height of the new image.
2234
     *
2235 2
     * @return image resource.
2236
    */
2237 2
    private function createImageKeepTransparency($width, $height)
2238 2
    {
2239 2
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2240 2
        $img = imagecreatetruecolor($width, $height);
2241
        imagealphablending($img, false);
2242 2
        imagesavealpha($img, true);
2243
2244
        $index = $this->image
2245
            ? imagecolortransparent($this->image)
2246
            : -1;
2247
2248
        if ($index != -1) {
2249
2250
            imagealphablending($img, true);
2251
            $transparent = imagecolorsforindex($this->image, $index);
2252
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2253
            imagefill($img, 0, 0, $color);
2254
            $index = imagecolortransparent($img, $color);
2255
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2256
2257
        } elseif ($this->bgColorDefault) {
2258
2259
            $color = $this->getBackgroundColor($img);
2260
            imagefill($img, 0, 0, $color);
2261
            $this->Log("Filling image with background color.");
2262
        }
2263
2264
        return $img;
2265
    }
2266
2267
2268
2269
    /**
2270
     * Set optimizing  and post-processing options.
2271
     *
2272
     * @param array $options with config for postprocessing with external tools.
2273
     *
2274
     * @return $this
2275
     */
2276
    public function setPostProcessingOptions($options)
2277
    {
2278
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2279
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2280
        } else {
2281
            $this->jpegOptimizeCmd = null;
2282
        }
2283
2284 2
        if (isset($options['png_filter']) && $options['png_filter']) {
2285
            $this->pngFilterCmd = $options['png_filter_cmd'];
2286
        } else {
2287 2
            $this->pngFilterCmd = null;
2288
        }
2289
2290 2
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2291
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2292
        } else {
2293
            $this->pngDeflateCmd = null;
2294
        }
2295
2296
        return $this;
2297
    }
2298
2299
2300
2301
    /**
2302
     * Find out the type (file extension) for the image to be saved.
2303
     *
2304
     * @return string as image extension.
2305 2
     */
2306
    protected function getTargetImageExtension()
2307 2
    {
2308
        // switch on mimetype
2309
        if (isset($this->extension)) {
2310
            return strtolower($this->extension);
2311 2
        } else {
2312
            return substr(image_type_to_extension($this->fileType), 1);
2313
        }
2314
    }
2315
2316 2
2317
2318
    /**
2319 2
     * Save image.
2320 2
     *
2321
     * @param string  $src       as target filename.
2322
     * @param string  $base      as base directory where to store images.
2323 2
     * @param boolean $overwrite or not, default to always overwrite file.
2324 2
     *
2325
     * @return $this or false if no folder is set.
2326
     */
2327
    public function save($src = null, $base = null, $overwrite = true)
2328
    {
2329
        if (isset($src)) {
2330
            $this->setTarget($src, $base);
2331
        }
2332
2333
        if ($overwrite === false && is_file($this->cacheFileName)) {
2334
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2335
            return;
2336
        }
2337
2338
        is_writable($this->saveFolder)
2339
            or $this->raiseError('Target directory is not writable.');
2340
2341
        $type = $this->getTargetImageExtension();
2342 2
        $this->Log("Saving image as " . $type);
2343
        switch($type) {
2344
2345
            case 'jpeg':
2346
            case 'jpg':
2347 2
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2348 2
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2349 2
2350
                // Use JPEG optimize if defined
2351
                if ($this->jpegOptimizeCmd) {
2352 2
                    if ($this->verbose) {
2353 2
                        clearstatcache();
2354 2
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2355
                    }
2356
                    $res = array();
2357 2
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2358
                    exec($cmd, $res);
2359
                    $this->log($cmd);
2360
                    $this->log($res);
2361
                }
2362
                break;
2363
2364
            case 'gif':
2365
                $this->Log("Saving image as GIF to cache.");
2366
                imagegif($this->image, $this->cacheFileName);
2367
                break;
2368
2369
            case 'png':
2370 2
            default:
2371
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2372
2373
                // Turn off alpha blending and set alpha flag
2374
                imagealphablending($this->image, false);
2375
                imagesavealpha($this->image, true);
2376
                imagepng($this->image, $this->cacheFileName, $this->compress);
2377
2378
                // Use external program to filter PNG, if defined
2379
                if ($this->pngFilterCmd) {
2380
                    if ($this->verbose) {
2381 2
                        clearstatcache();
2382 2
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2383
                    }
2384 2
                    $res = array();
2385
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2386
                    exec($cmd, $res);
2387
                    $this->Log($cmd);
2388
                    $this->Log($res);
2389
                }
2390
2391
                // Use external program to deflate PNG, if defined
2392
                if ($this->pngDeflateCmd) {
2393
                    if ($this->verbose) {
2394
                        clearstatcache();
2395 2
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2396
                    }
2397
                    $res = array();
2398
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2399
                    exec($cmd, $res);
2400
                    $this->Log($cmd);
2401
                    $this->Log($res);
2402
                }
2403
                break;
2404
        }
2405
2406
        if ($this->verbose) {
2407
            clearstatcache();
2408
            $this->log("Saved image to cache.");
2409
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2410
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2411
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2412
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2413
            $index = imagecolortransparent($this->image);
2414
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2415
        }
2416
2417
        return $this;
2418
    }
2419
2420
2421
2422
    /**
2423
     * Convert image from one colorpsace/color profile to sRGB without
2424
     * color profile.
2425
     *
2426
     * @param string  $src      of image.
2427
     * @param string  $dir      as base directory where images are.
2428
     * @param string  $cache    as base directory where to store images.
2429
     * @param string  $iccFile  filename of colorprofile.
2430
     * @param boolean $useCache or not, default to always use cache.
2431
     *
2432
     * @return string | boolean false if no conversion else the converted
2433
     *                          filename.
2434
     */
2435
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2436
    {
2437
        if ($this->verbose) {
2438
            $this->log("# Converting image to sRGB colorspace.");
2439
        }
2440
2441
        if (!class_exists("Imagick")) {
2442
            $this->log(" Ignoring since Imagemagick is not installed.");
2443
            return false;
2444
        }
2445
2446
        // Prepare
2447
        $this->setSaveFolder($cache)
2448
             ->setSource($src, $dir)
2449
             ->generateFilename(null, false, 'srgb_');
2450
2451
        // Check if the cached version is accurate.
2452
        if ($useCache && is_readable($this->cacheFileName)) {
2453
            $fileTime  = filemtime($this->pathToImage);
2454
            $cacheTime = filemtime($this->cacheFileName);
2455
2456
            if ($fileTime <= $cacheTime) {
2457
                $this->log(" Using cached version: " . $this->cacheFileName);
2458
                return $this->cacheFileName;
2459
            }
2460
        }
2461
2462
        // Only covert if cachedir is writable
2463
        if (is_writable($this->saveFolder)) {
2464
            // Load file and check if conversion is needed
2465
            $image      = new Imagick($this->pathToImage);
2466
            $colorspace = $image->getImageColorspace();
2467
            $this->log(" Current colorspace: " . $colorspace);
2468
2469
            $profiles      = $image->getImageProfiles('*', false);
2470
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2471
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2472
2473
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2474
                $this->log(" Converting to sRGB.");
2475
2476
                $sRGBicc = file_get_contents($iccFile);
2477
                $image->profileImage('icc', $sRGBicc);
2478
2479
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2480
                $image->writeImage($this->cacheFileName);
2481
                return $this->cacheFileName;
2482
            }
2483
        }
2484
2485
        return false;
2486
    }
2487
2488
2489
2490
    /**
2491
     * Create a hard link, as an alias, to the cached file.
2492
     *
2493
     * @param string $alias where to store the link,
2494
     *                      filename without extension.
2495
     *
2496
     * @return $this
2497
     */
2498
    public function linkToCacheFile($alias)
2499
    {
2500
        if ($alias === null) {
2501
            $this->log("Ignore creating alias.");
2502
            return $this;
2503
        }
2504
2505
        if (is_readable($alias)) {
2506
            unlink($alias);
0 ignored issues
show
Security File Manipulation introduced by
$alias can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2507
        }
2508
2509
        $res = link($this->cacheFileName, $alias);
2510
2511
        if ($res) {
2512
            $this->log("Created an alias as: $alias");
2513
        } else {
2514
            $this->log("Failed to create the alias: $alias");
2515
        }
2516
2517
        return $this;
2518
    }
2519
2520
2521
2522
    /**
2523
     * Add HTTP header for putputting together with image.
2524
     *
2525
     * @param string $type  the header type such as "Cache-Control"
2526
     * @param string $value the value to use
2527
     *
2528
     * @return void
2529
     */
2530
    public function addHTTPHeader($type, $value)
2531
    {
2532
        $this->HTTPHeader[$type] = $value;
2533
    }
2534
2535
2536
2537
    /**
2538
     * Output image to browser using caching.
2539
     *
2540
     * @param string $file   to read and output, default is to
2541
     *                       use $this->cacheFileName
2542
     * @param string $format set to json to output file as json
2543
     *                       object with details
2544
     *
2545
     * @return void
2546
     */
2547
    public function output($file = null, $format = null)
2548
    {
2549
        if (is_null($file)) {
2550
            $file = $this->cacheFileName;
2551
        }
2552
2553
        if (is_null($format)) {
2554
            $format = $this->outputFormat;
2555
        }
2556
2557
        $this->log("Output format is: $format");
2558
2559
        if (!$this->verbose && $format == 'json') {
2560
            header('Content-type: application/json');
2561
            echo $this->json($file);
2562
            exit;
2563
        } elseif ($format == 'ascii') {
2564
            header('Content-type: text/plain');
2565
            echo $this->ascii($file);
0 ignored issues
show
Security Cross-Site Scripting introduced by
$this->ascii($file) can contain request data and is used in output context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2566
            exit;
2567
        }
2568
2569
        $this->log("Outputting image: $file");
2570
2571
        // Get image modification time
2572
        clearstatcache();
2573
        $lastModified = filemtime($file);
2574
        $gmdate = gmdate("D, d M Y H:i:s", $lastModified);
2575
2576
        if (!$this->verbose) {
2577
            header('Last-Modified: ' . $gmdate . " GMT");
2578
        }
2579
2580
        foreach($this->HTTPHeader as $key => $val) {
2581
            header("$key: $val");
2582
        }
2583
2584
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2585
2586
            if ($this->verbose) {
2587
                $this->log("304 not modified");
2588
                $this->verboseOutput();
2589
                exit;
2590
            }
2591
2592
            header("HTTP/1.0 304 Not Modified");
2593
2594
        } else {
2595
2596
            // Get details on image
2597
            $info = getimagesize($file);
2598
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
2599
            $mime = $info['mime'];
2600
            $size = filesize($file);
2601
2602
            if ($this->verbose) {
2603
                $this->log("Last-Modified: " . $gmdate . " GMT");
2604
                $this->log("Content-type: " . $mime);
2605
                $this->log("Content-length: " . $size);
2606
                $this->verboseOutput();
2607
2608
                if (is_null($this->verboseFileName)) {
2609
                    exit;
2610
                }
2611
            }
2612
2613
            header("Content-type: $mime");
2614
            header("Content-length: $size");
2615
            readfile($file);
0 ignored issues
show
Security File Exposure introduced by
$file can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2616
        }
2617
2618
        exit;
2619
    }
2620
2621
2622
2623
    /**
2624
     * Create a JSON object from the image details.
2625
     *
2626
     * @param string $file the file to output.
2627 7
     *
2628
     * @return string json-encoded representation of the image.
2629 7
     */
2630
    public function json($file = null)
2631
    {
2632
        $file = $file ? $file : $this->cacheFileName;
2633 7
2634
        $details = array();
2635
2636
        clearstatcache();
2637
2638
        $details['src']       = $this->imageSrc;
2639
        $lastModified         = filemtime($this->pathToImage);
2640
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2641
2642
        $details['cache']       = basename($this->cacheFileName);
2643
        $lastModified           = filemtime($this->cacheFileName);
2644
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2645
2646
        $this->load($file);
2647
2648
        $details['filename']    = basename($file);
2649
        $details['mimeType']    = image_type_to_mime_type($this->fileType);
2650
        $details['width']       = $this->width;
2651
        $details['height']      = $this->height;
2652
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2653
        $details['size']        = filesize($file);
2654
        $details['colors'] = $this->colorsTotal($this->image);
2655
        $details['includedFiles'] = count(get_included_files());
2656
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2657
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2658
        $details['memoryLimit'] = ini_get('memory_limit');
2659
2660
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2661
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2662
        }
2663
2664
        if ($details['mimeType'] == 'image/png') {
2665
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2666
        }
2667
2668
        $options = null;
2669
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2670
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2671
        }
2672
2673
        return json_encode($details, $options);
2674
    }
2675
2676
2677
2678
    /**
2679
     * Set options for creating ascii version of image.
2680
     *
2681
     * @param array $options empty to use default or set options to change.
2682
     *
2683
     * @return void.
2684
     */
2685
    public function setAsciiOptions($options = array())
2686
    {
2687
        $this->asciiOptions = $options;
2688
    }
2689
2690
2691
2692
    /**
2693
     * Create an ASCII version from the image details.
2694
     *
2695
     * @param string $file the file to output.
2696
     *
2697
     * @return string ASCII representation of the image.
2698
     */
2699
    public function ascii($file = null)
2700
    {
2701
        $file = $file ? $file : $this->cacheFileName;
2702
2703
        $asciiArt = new CAsciiArt();
2704
        $asciiArt->setOptions($this->asciiOptions);
2705
        return $asciiArt->createFromFile($file);
2706
    }
2707
2708
2709
2710
    /**
2711
     * Log an event if verbose mode.
2712
     *
2713
     * @param string $message to log.
2714
     *
2715
     * @return this
2716
     */
2717
    public function log($message)
2718
    {
2719
        if ($this->verbose) {
2720
            $this->log[] = $message;
2721
        }
2722
2723
        return $this;
2724
    }
2725
2726
2727
2728
    /**
2729
     * Do verbose output to a file.
2730
     *
2731
     * @param string $fileName where to write the verbose output.
2732
     *
2733
     * @return void
2734
     */
2735
    public function setVerboseToFile($fileName)
2736
    {
2737
        $this->log("Setting verbose output to file.");
2738
        $this->verboseFileName = $fileName;
2739
    }
2740
2741
2742
2743
    /**
2744
     * Do verbose output and print out the log and the actual images.
2745
     *
2746
     * @return void
2747
     */
2748
    private function verboseOutput()
2749
    {
2750
        $log = null;
2751
        $this->log("As JSON: \n" . $this->json());
2752
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2753
        $this->log("Memory limit: " . ini_get('memory_limit'));
2754
2755
        $included = get_included_files();
2756
        $this->log("Included files: " . count($included));
2757
2758
        foreach ($this->log as $val) {
2759
            if (is_array($val)) {
2760
                foreach ($val as $val1) {
2761
                    $log .= htmlentities($val1) . '<br/>';
2762
                }
2763
            } else {
2764
                $log .= htmlentities($val) . '<br/>';
2765
            }
2766
        }
2767
2768
        if (!is_null($this->verboseFileName)) {
2769
            file_put_contents(
2770
                $this->verboseFileName,
2771
                str_replace("<br/>", "\n", $log)
0 ignored issues
show
Security File Manipulation introduced by
str_replace('<br/>', ' ', $log) can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
2772
            );
2773
        } else {
2774
            echo <<<EOD
2775
<h1>CImage Verbose Output</h1>
2776
<pre>{$log}</pre>
2777
EOD;
2778
        }
2779
    }
2780
2781
2782
2783
    /**
2784
     * Raise error, enables to implement a selection of error methods.
2785
     *
2786
     * @param string $message the error message to display.
2787
     *
2788
     * @return void
2789
     * @throws Exception
2790
     */
2791
    private function raiseError($message)
2792
    {
2793
        throw new Exception($message);
2794
    }
2795
}
2796