Completed
Push — master ( b871dd...c32ae6 )
by Mikael
07:45 queued 05:18
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
     * Pattern to recognize a remote file.
363
     */
364
    //private $remotePattern = '#^[http|https]://#';
365
    private $remotePattern = '#^https?://#';
366
367
368
369
    /**
370
     * Use the cache if true, set to false to ignore the cached file.
371
     */
372
    private $useCache = true;
373
374
375
376
    /*
377
     * Set whitelist for valid hostnames from where remote source can be
378
     * downloaded.
379
     */
380
    private $remoteHostWhitelist = null;
381
382
383
384
    /*
385
     * Do verbose logging to file by setting this to a filename.
386
     */
387
    private $verboseFileName = null;
388
389
390
391
    /*
392
     * Output to ascii can take som options as an array.
393
     */
394
    private $asciiOptions = array();
395
396
397
398
    /*
399
     * Image copy strategy, defaults to RESAMPLE.
400
     */
401
     const RESIZE = 1;
402
     const RESAMPLE = 2;
403
     private $copyStrategy = NULL;
404
405
406
407
    /**
408
     * Properties, the class is mutable and the method setOptions()
409
     * decides (partly) what properties are created.
410
     *
411
     * @todo Clean up these and check if and how they are used
412
     */
413
414
    public $keepRatio;
415
    public $cropToFit;
416
    private $cropWidth;
417
    private $cropHeight;
418
    public $crop_x;
419
    public $crop_y;
420
    public $filters;
421
    private $attr; // Calculated from source image
422
423
424
425
426
    /**
427
     * Constructor, can take arguments to init the object.
428
     *
429
     * @param string $imageSrc    filename which may contain subdirectory.
430
     * @param string $imageFolder path to root folder for images.
431
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
432
     * @param string $saveName    name of target file when saveing.
433
     */
434 7
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
435
    {
436 7
        $this->setSource($imageSrc, $imageFolder);
437 7
        $this->setTarget($saveFolder, $saveName);
438 7
    }
439
440
441
442
    /**
443
     * Set verbose mode.
444
     *
445
     * @param boolean $mode true or false to enable and disable verbose mode,
446
     *                      default is true.
447
     *
448
     * @return $this
449
     */
450
    public function setVerbose($mode = true)
451
    {
452
        $this->verbose = $mode;
453
        return $this;
454
    }
455
456
457
458
    /**
459
     * Set save folder, base folder for saving cache files.
460
     *
461
     * @todo clean up how $this->saveFolder is used in other methods.
462
     *
463
     * @param string $path where to store cached files.
464
     *
465
     * @return $this
466
     */
467 2
    public function setSaveFolder($path)
468
    {
469 2
        $this->saveFolder = $path;
470 2
        return $this;
471
    }
472
473
474
475
    /**
476
     * Use cache or not.
477
     *
478
     * @param boolean $use true or false to use cache.
479
     *
480
     * @return $this
481
     */
482
    public function useCache($use = true)
483
    {
484
        $this->useCache = $use;
485
        return $this;
486
    }
487
488
489
490
    /**
491
     * Create and save a dummy image. Use dimensions as stated in
492
     * $this->newWidth, or $width or default to 100 (same for height.
493
     *
494
     * @param integer $width  use specified width for image dimension.
495
     * @param integer $height use specified width for image dimension.
496
     *
497
     * @return $this
498
     */
499 2
    public function createDummyImage($width = null, $height = null)
500
    {
501 2
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
502 2
        $this->newHeight = $this->newHeight ?: $height ?: 100;
503
504 2
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
505
506 2
        return $this;
507
    }
508
509
510
511
    /**
512
     * Allow or disallow remote image download.
513
     *
514
     * @param boolean $allow   true or false to enable and disable.
515
     * @param string  $pattern to use to detect if its a remote file.
516
     *
517
     * @return $this
518
     */
519 2
    public function setRemoteDownload($allow, $pattern = null)
520
    {
521 2
        $this->allowRemote = $allow;
522 2
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
523
524 2
        $this->log(
525
            "Set remote download to: "
526 2
            . ($this->allowRemote ? "true" : "false")
527 2
            . " using pattern "
528 2
            . $this->remotePattern
529 2
        );
530
531 2
        return $this;
532
    }
533
534
535
536
    /**
537
     * Check if the image resource is a remote file or not.
538
     *
539
     * @param string $src check if src is remote.
540
     *
541
     * @return boolean true if $src is a remote file, else false.
542
     */
543 2
    public function isRemoteSource($src)
544
    {
545 2
        $remote = preg_match($this->remotePattern, $src);
546 2
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
547 2
        return !!$remote;
548
    }
549
550
551
552
    /**
553
     * Set whitelist for valid hostnames from where remote source can be
554
     * downloaded.
555
     *
556
     * @param array $whitelist with regexp hostnames to allow download from.
557
     *
558
     * @return $this
559
     */
560 2
    public function setRemoteHostWhitelist($whitelist = null)
561
    {
562 2
        $this->remoteHostWhitelist = $whitelist;
563 2
        $this->log(
564
            "Setting remote host whitelist to: "
565 2
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
566 2
        );
567 2
        return $this;
568
    }
569
570
571
572
    /**
573
     * Check if the hostname for the remote image, is on a whitelist,
574
     * if the whitelist is defined.
575
     *
576
     * @param string $src the remote source.
577
     *
578
     * @return boolean true if hostname on $src is in the whitelist, else false.
579
     */
580 3
    public function isRemoteSourceOnWhitelist($src)
581
    {
582 3
        if (is_null($this->remoteHostWhitelist)) {
583 1
            $this->log("Remote host on whitelist not configured - allowing.");
584 1
            return true;
585
        }
586
587 2
        $whitelist = new CWhitelist();
588 2
        $hostname = parse_url($src, PHP_URL_HOST);
589 2
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
590
591 2
        $this->log(
592
            "Remote host is on whitelist: "
593 2
            . ($allow ? "true" : "false")
594 2
        );
595 2
        return $allow;
596
    }
597
598
599
600
    /**
601
     * Check if file extension is valid as a file extension.
602
     *
603
     * @param string $extension of image file.
604
     *
605
     * @return $this
606
     */
607
    private function checkFileExtension($extension)
608
    {
609
        $valid = array('jpg', 'jpeg', 'png', 'gif');
610
611
        in_array(strtolower($extension), $valid)
612
            or $this->raiseError('Not a valid file extension.');
613
614
        return $this;
615
    }
616
617
618
619
    /**
620
     * Normalize the file extension.
621
     *
622
     * @param string $extension of image file or skip to use internal.
623
     *
624
     * @return string $extension as a normalized file extension.
625
     */
626 2
    private function normalizeFileExtension($extension = null)
627
    {
628 2
        $extension = strtolower($extension ? $extension : $this->extension);
629
        
630 2
        if ($extension == 'jpeg') {
631
                $extension = 'jpg';
632
            }
633
634 2
        return $extension;
635
    }
636
637
638
639
    /**
640
     * Download a remote image and return path to its local copy.
641
     *
642
     * @param string $src remote path to image.
643
     *
644
     * @return string as path to downloaded remote source.
645
     */
646
    public function downloadRemoteSource($src)
647
    {
648
        if (!$this->isRemoteSourceOnWhitelist($src)) {
649
            throw new Exception("Hostname is not on whitelist for remote sources.");
650
        }
651
        
652
        $remote = new CRemoteImage();
653
        $cache  = $this->saveFolder . "/remote/";
654
655
        if (!is_dir($cache)) {
656
            if (!is_writable($this->saveFolder)) {
657
                throw new Exception("Can not create remote cache, cachefolder not writable.");
658
            }
659
            mkdir($cache);
660
            $this->log("The remote cache does not exists, creating it.");
661
        }
662
663
        if (!is_writable($cache)) {
664
            $this->log("The remote cache is not writable.");
665
        }
666
667
        $remote->setCache($cache);
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
            }
1102
1103
            // Get image dimensions for pre-resize image.
1104
            if ($this->cropToFit || $this->fillToFit) {
1105
1106
                // Get relations of original & target image
1107
                $ratioWidth  = $width  / $this->newWidth;
1108
                $ratioHeight = $height / $this->newHeight;
1109
1110
                if ($this->cropToFit) {
1111
1112
                    // Use newWidth and newHeigh as defined width/height,
1113
                    // image should fit the area.
1114
                    $this->log("Crop to fit.");
1115
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1116
                    $this->cropWidth  = round($width  / $ratio);
1117
                    $this->cropHeight = round($height / $ratio);
1118
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1119
1120
                } elseif ($this->fillToFit) {
1121
1122
                    // Use newWidth and newHeigh as defined width/height,
1123
                    // image should fit the area.
1124
                    $this->log("Fill to fit.");
1125
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1126
                    $this->fillWidth  = round($width  / $ratio);
1127
                    $this->fillHeight = round($height / $ratio);
1128
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1129
                }
1130
            }
1131
        }
1132
1133
        // Crop, ensure to set new width and height
1134
        if ($this->crop) {
1135
            $this->log("Crop.");
1136
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1137
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1138
        }
1139
1140
        // Fill to fit, ensure to set new width and height
1141
        /*if ($this->fillToFit) {
1142
            $this->log("FillToFit.");
1143
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1144
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1145
        }*/
1146
1147
        // No new height or width is set, use existing measures.
1148
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1149
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1150
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1151
1152
        return $this;
1153
    }
1154
1155
1156
1157
    /**
1158
     * Re-calculate image dimensions when original image dimension has changed.
1159
     *
1160
     * @return $this
1161
     */
1162
    public function reCalculateDimensions()
1163
    {
1164
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1165
1166
        $this->newWidth  = $this->newWidthOrig;
1167
        $this->newHeight = $this->newHeightOrig;
1168
        $this->crop      = $this->cropOrig;
1169
1170
        $this->initDimensions()
1171
             ->calculateNewWidthAndHeight();
1172
1173
        return $this;
1174
    }
1175
1176
1177
1178
    /**
1179
     * Set extension for filename to save as.
1180
     *
1181
     * @param string $saveas extension to save image as
1182
     *
1183
     * @return $this
1184
     */
1185
    public function setSaveAsExtension($saveAs = null)
1186
    {
1187
        if (isset($saveAs)) {
1188
            $saveAs = strtolower($saveAs);
1189
            $this->checkFileExtension($saveAs);
1190
            $this->saveAs = $saveAs;
1191
            $this->extension = $saveAs;
1192
        }
1193
1194
        $this->log("Prepare to save image as: " . $this->extension);
1195
1196
        return $this;
1197
    }
1198
1199
1200
1201
    /**
1202
     * Set JPEG quality to use when saving image
1203
     *
1204
     * @param int $quality as the quality to set.
1205
     *
1206
     * @return $this
1207
     */
1208
    public function setJpegQuality($quality = null)
1209
    {
1210
        if ($quality) {
1211
            $this->useQuality = true;
1212
        }
1213
1214
        $this->quality = isset($quality)
1215
            ? $quality
1216
            : self::JPEG_QUALITY_DEFAULT;
1217
1218
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1219
            or $this->raiseError('Quality not in range.');
1220
1221
        $this->log("Setting JPEG quality to {$this->quality}.");
1222
1223
        return $this;
1224
    }
1225
1226
1227
1228
    /**
1229
     * Set PNG compressen algorithm to use when saving image
1230
     *
1231
     * @param int $compress as the algorithm to use.
1232
     *
1233
     * @return $this
1234
     */
1235
    public function setPngCompression($compress = null)
1236
    {
1237
        if ($compress) {
1238
            $this->useCompress = true;
1239
        }
1240
1241
        $this->compress = isset($compress)
1242
            ? $compress
1243
            : self::PNG_COMPRESSION_DEFAULT;
1244
1245
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1246
            or $this->raiseError('Quality not in range.');
1247
1248
        $this->log("Setting PNG compression level to {$this->compress}.");
1249
1250
        return $this;
1251
    }
1252
1253
1254
1255
    /**
1256
     * Use original image if possible, check options which affects image processing.
1257
     *
1258
     * @param boolean $useOrig default is to use original if possible, else set to false.
1259
     *
1260
     * @return $this
1261
     */
1262
    public function useOriginalIfPossible($useOrig = true)
1263
    {
1264
        if ($useOrig
1265
            && ($this->newWidth == $this->width)
1266
            && ($this->newHeight == $this->height)
1267
            && !$this->area
1268
            && !$this->crop
1269
            && !$this->cropToFit
1270
            && !$this->fillToFit
1271
            && !$this->filters
1272
            && !$this->sharpen
1273
            && !$this->emboss
1274
            && !$this->blur
1275
            && !$this->convolve
1276
            && !$this->palette
1277
            && !$this->useQuality
1278
            && !$this->useCompress
1279
            && !$this->saveAs
1280
            && !$this->rotateBefore
1281
            && !$this->rotateAfter
1282
            && !$this->autoRotate
1283
            && !$this->bgColor
1284
            && ($this->upscale === self::UPSCALE_DEFAULT)
1285
        ) {
1286
            $this->log("Using original image.");
1287
            $this->output($this->pathToImage);
1288
        }
1289
1290
        return $this;
1291
    }
1292
1293
1294
1295
    /**
1296
     * Generate filename to save file in cache.
1297
     *
1298
     * @param string  $base      as optional basepath for storing file.
1299
     * @param boolean $useSubdir use or skip the subdir part when creating the
1300
     *                           filename.
1301
     *
1302
     * @return $this
1303
     */
1304 2
    public function generateFilename($base = null, $useSubdir = true)
1305
    {
1306 2
        $filename     = basename($this->pathToImage);
1307 2
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1308 2
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1309 2
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1310 2
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1311 2
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1312 2
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1313 2
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1314 2
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1315 2
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1316 2
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1317
1318 2
        $saveAs = $this->normalizeFileExtension();
1319 2
        $saveAs = $saveAs ? "_$saveAs" : null;
1320
1321 2
        $copyStrat = null;
1322 2
        if ($this->copyStrategy === self::RESIZE) {
1323
            $copyStrat = "_rs";
1324
        }
1325
        
1326 2
        $width  = $this->newWidth;
1327 2
        $height = $this->newHeight;
1328
1329 2
        $offset = isset($this->offset)
1330 2
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1331 2
            : null;
1332
1333 2
        $crop = $this->crop
1334 2
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1335 2
            : null;
1336
1337 2
        $filters = null;
1338 2
        if (isset($this->filters)) {
1339
            foreach ($this->filters as $filter) {
1340
                if (is_array($filter)) {
1341
                    $filters .= "_f{$filter['id']}";
1342
                    for ($i=1; $i<=$filter['argc']; $i++) {
1343
                        $filters .= "-".$filter["arg{$i}"];
1344
                    }
1345
                }
1346
            }
1347
        }
1348
1349 2
        $sharpen = $this->sharpen ? 's' : null;
1350 2
        $emboss  = $this->emboss  ? 'e' : null;
1351 2
        $blur    = $this->blur    ? 'b' : null;
1352 2
        $palette = $this->palette ? 'p' : null;
1353
1354 2
        $autoRotate = $this->autoRotate ? 'ar' : null;
1355
1356 2
        $optimize  = $this->jpegOptimize ? 'o' : null;
1357 2
        $optimize .= $this->pngFilter    ? 'f' : null;
1358 2
        $optimize .= $this->pngDeflate   ? 'd' : null;
1359
1360 2
        $convolve = null;
1361 2
        if ($this->convolve) {
1362
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1363
        }
1364
1365 2
        $upscale = null;
1366 2
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1367
            $upscale = '_nu';
1368
        }
1369
1370 2
        $subdir = null;
1371 2
        if ($useSubdir === true) {
1372
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1373
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1374
            $subdir .= '_';
1375
        }
1376
        
1377 2
        $file = $subdir . $filename . '_' . $width . '_'
1378 2
            . $height . $offset . $crop . $cropToFit . $fillToFit
1379 2
            . $crop_x . $crop_y . $upscale
1380 2
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1381 2
            . $optimize . $compress
1382 2
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1383 2
            . $convolve . $copyStrat . $saveAs;
1384
1385 2
        return $this->setTarget($file, $base);
1386
    }
1387
1388
1389
1390
    /**
1391
     * Use cached version of image, if possible.
1392
     *
1393
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1394
     *
1395
     * @return $this
1396
     */
1397
    public function useCacheIfPossible($useCache = true)
1398
    {
1399
        if ($useCache && is_readable($this->cacheFileName)) {
1400
            $fileTime   = filemtime($this->pathToImage);
1401
            $cacheTime  = filemtime($this->cacheFileName);
1402
1403
            if ($fileTime <= $cacheTime) {
1404
                if ($this->useCache) {
1405
                    if ($this->verbose) {
1406
                        $this->log("Use cached file.");
1407
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1408
                    }
1409
                    $this->output($this->cacheFileName, $this->outputFormat);
1410
                } else {
1411
                    $this->log("Cache is valid but ignoring it by intention.");
1412
                }
1413
            } else {
1414
                $this->log("Original file is modified, ignoring cache.");
1415
            }
1416
        } else {
1417
            $this->log("Cachefile does not exists or ignoring it.");
1418
        }
1419
1420
        return $this;
1421
    }
1422
1423
1424
1425
    /**
1426
     * Load image from disk. Try to load image without verbose error message,
1427
     * if fail, load again and display error messages.
1428
     *
1429
     * @param string $src of image.
1430
     * @param string $dir as base directory where images are.
1431
     *
1432
     * @return $this
1433
     *
1434
     */
1435
    public function load($src = null, $dir = null)
1436
    {
1437
        if (isset($src)) {
1438
            $this->setSource($src, $dir);
1439
        }
1440
1441
        $this->loadImageDetails($this->pathToImage);
1442
1443
        $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...
1444
        if ($this->image === false) {
1445
            throw new Exception("Could not load image.");
1446
        }
1447
        
1448
        /* Removed v0.7.7
1449
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1450
            $type = $this->getPngType();
1451
            $hasFewColors = imagecolorstotal($this->image);
1452
1453
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1454
                if ($this->verbose) {
1455
                    $this->log("Handle this image as a palette image.");
1456
                }
1457
                $this->palette = true;
1458
            }
1459
        }
1460
        */
1461
1462
        if ($this->verbose) {
1463
            $this->log("### Image successfully loaded from file.");
1464
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1465
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1466
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1467
            $index = imagecolortransparent($this->image);
1468
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1469
        }
1470
1471
        return $this;
1472
    }
1473
1474
1475
1476
    /**
1477
     * Get the type of PNG image.
1478
     *
1479
     * @param string $filename to use instead of default.
1480
     *
1481
     * @return int as the type of the png-image
1482
     *
1483
     */
1484
    public function getPngType($filename = null)
1485
    {
1486
        $filename = $filename ? $filename : $this->pathToImage;
1487
        
1488
        $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...
1489
1490
        if ($this->verbose) {
1491
            $this->log("Checking png type of: " . $filename);
1492
            $this->log($this->getPngTypeAsString($pngType));
1493
        }
1494
        
1495
        return $pngType;
1496
    }
1497
1498
1499
1500
    /**
1501
     * Get the type of PNG image as a verbose string.
1502
     *
1503
     * @param integer $type     to use, default is to check the type.
1504
     * @param string  $filename to use instead of default.
1505
     *
1506
     * @return int as the type of the png-image
1507
     *
1508
     */
1509
    private function getPngTypeAsString($pngType = null, $filename = null)
1510
    {
1511
        if ($filename || !$pngType) {
1512
            $pngType = $this->getPngType($filename);
1513
        }
1514
1515
        $index = imagecolortransparent($this->image);
1516
        $transparent = null;
1517
        if ($index != -1) {
1518
            $transparent = " (transparent)";            
1519
        }
1520
1521
        switch ($pngType) {
1522
1523
            case self::PNG_GREYSCALE:
1524
                $text = "PNG is type 0, Greyscale$transparent";
1525
                break;
1526
1527
            case self::PNG_RGB:
1528
                $text = "PNG is type 2, RGB$transparent";
1529
                break;
1530
1531
            case self::PNG_RGB_PALETTE:
1532
                $text = "PNG is type 3, RGB with palette$transparent";
1533
                break;
1534
1535
            case self::PNG_GREYSCALE_ALPHA:
1536
                $text = "PNG is type 4, Greyscale with alpha channel";
1537
                break;
1538
1539
            case self::PNG_RGB_ALPHA:
1540
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1541
                break;
1542
1543
            default:
1544
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1545
        }
1546
1547
        return $text;
1548
    }
1549
1550
1551
1552
1553
    /**
1554
     * Calculate number of colors in an image.
1555
     *
1556
     * @param resource $im the image.
1557
     *
1558
     * @return int
1559
     */
1560
    private function colorsTotal($im)
1561
    {
1562
        if (imageistruecolor($im)) {
1563
            $this->log("Colors as true color.");
1564
            $h = imagesy($im);
1565
            $w = imagesx($im);
1566
            $c = array();
1567
            for ($x=0; $x < $w; $x++) {
1568
                for ($y=0; $y < $h; $y++) {
1569
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1570
                }
1571
            }
1572
            return count($c);
1573
        } else {
1574
            $this->log("Colors as palette.");
1575
            return imagecolorstotal($im);
1576
        }
1577
    }
1578
1579
1580
1581
    /**
1582
     * Preprocess image before rezising it.
1583
     *
1584
     * @return $this
1585
     */
1586
    public function preResize()
1587
    {
1588
        $this->log("### Pre-process before resizing");
1589
1590
        // Rotate image
1591
        if ($this->rotateBefore) {
1592
            $this->log("Rotating image.");
1593
            $this->rotate($this->rotateBefore, $this->bgColor)
1594
                 ->reCalculateDimensions();
1595
        }
1596
1597
        // Auto-rotate image
1598
        if ($this->autoRotate) {
1599
            $this->log("Auto rotating image.");
1600
            $this->rotateExif()
1601
                 ->reCalculateDimensions();
1602
        }
1603
1604
        // Scale the original image before starting
1605
        if (isset($this->scale)) {
1606
            $this->log("Scale by {$this->scale}%");
1607
            $newWidth  = $this->width * $this->scale / 100;
1608
            $newHeight = $this->height * $this->scale / 100;
1609
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1610
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1611
            $this->image = $img;
1612
            $this->width = $newWidth;
1613
            $this->height = $newHeight;
1614
        }
1615
1616
        return $this;
1617
    }
1618
1619
1620
1621
    /**
1622
     * Resize or resample the image while resizing.
1623
     *
1624
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1625
     *
1626
     * @return $this
1627
     */
1628
     public function setCopyResizeStrategy($strategy)
1629
     {
1630
         $this->copyStrategy = $strategy;
1631
         return $this;
1632
     }
1633
1634
1635
1636
    /**
1637
     * Resize and or crop the image.
1638
     *
1639
     * @return void
1640
     */
1641
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1642
    {
1643
        if($this->copyStrategy == self::RESIZE) {
1644
            $this->log("Copy by resize");
1645
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1646
        } else {
1647
            $this->log("Copy by resample");
1648
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1649
        }
1650
    }
1651
1652
1653
1654
    /**
1655
     * Resize and or crop the image.
1656
     *
1657
     * @return $this
1658
     */
1659
    public function resize()
1660
    {
1661
1662
        $this->log("### Starting to Resize()");
1663
        $this->log("Upscale = '$this->upscale'");
1664
1665
        // Only use a specified area of the image, $this->offset is defining the area to use
1666
        if (isset($this->offset)) {
1667
1668
            $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']}");
1669
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1670
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1671
            $this->image = $img;
1672
            $this->width = $this->offset['width'];
1673
            $this->height = $this->offset['height'];
1674
        }
1675
1676
        if ($this->crop) {
1677
1678
            // Do as crop, take only part of image
1679
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1680
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1681
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1682
            $this->image = $img;
1683
            $this->width = $this->crop['width'];
1684
            $this->height = $this->crop['height'];
1685
        }
1686
1687
        if (!$this->upscale) {
1688
            // Consider rewriting the no-upscale code to fit within this if-statement,
1689
            // likely to be more readable code.
1690
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1691
        }
1692
1693
        if ($this->cropToFit) {
1694
1695
            // Resize by crop to fit
1696
            $this->log("Resizing using strategy - Crop to fit");
1697
1698
            if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1699
                $this->log("Resizing - smaller image, do not upscale.");
1700
1701
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1702
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1703
1704
                $posX = 0;
1705
                $posY = 0;
1706
1707
                if ($this->newWidth > $this->width) {
1708
                    $posX = round(($this->newWidth - $this->width) / 2);
1709
                }
1710
1711
                if ($this->newHeight > $this->height) {
1712
                    $posY = round(($this->newHeight - $this->height) / 2);
1713
                }
1714
1715
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1716
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight);
1717
            } else {
1718
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1719
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1720
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1721
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1722
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1723
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1724
            }
1725
1726
            $this->image = $imageResized;
1727
            $this->width = $this->newWidth;
1728
            $this->height = $this->newHeight;
1729
1730
        } elseif ($this->fillToFit) {
1731
1732
            // Resize by fill to fit
1733
            $this->log("Resizing using strategy - Fill to fit");
1734
1735
            $posX = 0;
1736
            $posY = 0;
1737
1738
            $ratioOrig = $this->width / $this->height;
1739
            $ratioNew  = $this->newWidth / $this->newHeight;
1740
1741
            // Check ratio for landscape or portrait
1742
            if ($ratioOrig < $ratioNew) {
1743
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1744
            } else {
1745
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1746
            }
1747
1748
            if (!$this->upscale
1749
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1750
            ) {
1751
1752
                $this->log("Resizing - smaller image, do not upscale.");
1753
                $posX = round(($this->fillWidth - $this->width) / 2);
1754
                $posY = round(($this->fillHeight - $this->height) / 2);
1755
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1756
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1757
1758
            } else {
1759
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1760
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1761
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1762
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1763
            }
1764
1765
            $this->image = $imageResized;
1766
            $this->width = $this->newWidth;
1767
            $this->height = $this->newHeight;
1768
1769
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1770
1771
            // Resize it
1772
            $this->log("Resizing, new height and/or width");
1773
1774
            if (!$this->upscale
1775
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1776
            ) {
1777
                $this->log("Resizing - smaller image, do not upscale.");
1778
1779
                if (!$this->keepRatio) {
1780
                    $this->log("Resizing - stretch to fit selected.");
1781
1782
                    $posX = 0;
1783
                    $posY = 0;
1784
                    $cropX = 0;
1785
                    $cropY = 0;
1786
1787
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1788
                        $posX = round(($this->newWidth - $this->width) / 2);
1789
                        $posY = round(($this->newHeight - $this->height) / 2);
1790
                    } elseif ($this->newWidth > $this->width) {
1791
                        $posX = round(($this->newWidth - $this->width) / 2);
1792
                        $cropY = round(($this->height - $this->newHeight) / 2);
1793
                    } elseif ($this->newHeight > $this->height) {
1794
                        $posY = round(($this->newHeight - $this->height) / 2);
1795
                        $cropX = round(($this->width - $this->newWidth) / 2);
1796
                    }
1797
1798
                    //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY.");
1799
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1800
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight);
1801
                    $this->image = $imageResized;
1802
                    $this->width = $this->newWidth;
1803
                    $this->height = $this->newHeight;
1804
                }
1805
            } else {
1806
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1807
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1808
                $this->image = $imageResized;
1809
                $this->width = $this->newWidth;
1810
                $this->height = $this->newHeight;
1811
            }
1812
        }
1813
1814
        return $this;
1815
    }
1816
1817
1818
1819
    /**
1820
     * Postprocess image after rezising image.
1821
     *
1822
     * @return $this
1823
     */
1824
    public function postResize()
1825
    {
1826
        $this->log("### Post-process after resizing");
1827
1828
        // Rotate image
1829
        if ($this->rotateAfter) {
1830
            $this->log("Rotating image.");
1831
            $this->rotate($this->rotateAfter, $this->bgColor);
1832
        }
1833
1834
        // Apply filters
1835
        if (isset($this->filters) && is_array($this->filters)) {
1836
1837
            foreach ($this->filters as $filter) {
1838
                $this->log("Applying filter {$filter['type']}.");
1839
1840
                switch ($filter['argc']) {
1841
1842
                    case 0:
1843
                        imagefilter($this->image, $filter['type']);
1844
                        break;
1845
1846
                    case 1:
1847
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1848
                        break;
1849
1850
                    case 2:
1851
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1852
                        break;
1853
1854
                    case 3:
1855
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1856
                        break;
1857
1858
                    case 4:
1859
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1860
                        break;
1861
                }
1862
            }
1863
        }
1864
1865
        // Convert to palette image
1866
        if ($this->palette) {
1867
            $this->log("Converting to palette image.");
1868
            $this->trueColorToPalette();
1869
        }
1870
1871
        // Blur the image
1872
        if ($this->blur) {
1873
            $this->log("Blur.");
1874
            $this->blurImage();
1875
        }
1876
1877
        // Emboss the image
1878
        if ($this->emboss) {
1879
            $this->log("Emboss.");
1880
            $this->embossImage();
1881
        }
1882
1883
        // Sharpen the image
1884
        if ($this->sharpen) {
1885
            $this->log("Sharpen.");
1886
            $this->sharpenImage();
1887
        }
1888
1889
        // Custom convolution
1890
        if ($this->convolve) {
1891
            //$this->log("Convolve: " . $this->convolve);
1892
            $this->imageConvolution();
1893
        }
1894
1895
        return $this;
1896
    }
1897
1898
1899
1900
    /**
1901
     * Rotate image using angle.
1902
     *
1903
     * @param float $angle        to rotate image.
1904
     * @param int   $anglebgColor to fill image with if needed.
1905
     *
1906
     * @return $this
1907
     */
1908
    public function rotate($angle, $bgColor)
1909
    {
1910
        $this->log("Rotate image " . $angle . " degrees with filler color.");
1911
1912
        $color = $this->getBackgroundColor();
1913
        $this->image = imagerotate($this->image, $angle, $color);
1914
1915
        $this->width  = imagesx($this->image);
1916
        $this->height = imagesy($this->image);
1917
1918
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
1919
1920
        return $this;
1921
    }
1922
1923
1924
1925
    /**
1926
     * Rotate image using information in EXIF.
1927
     *
1928
     * @return $this
1929
     */
1930
    public function rotateExif()
1931
    {
1932
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
1933
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
1934
            return $this;
1935
        }
1936
1937
        $exif = exif_read_data($this->pathToImage);
1938
1939
        if (!empty($exif['Orientation'])) {
1940
            switch ($exif['Orientation']) {
1941
                case 3:
1942
                    $this->log("Autorotate 180.");
1943
                    $this->rotate(180, $this->bgColor);
1944
                    break;
1945
1946
                case 6:
1947
                    $this->log("Autorotate -90.");
1948
                    $this->rotate(-90, $this->bgColor);
1949
                    break;
1950
1951
                case 8:
1952
                    $this->log("Autorotate 90.");
1953
                    $this->rotate(90, $this->bgColor);
1954
                    break;
1955
1956
                default:
1957
                    $this->log("Autorotate ignored, unknown value as orientation.");
1958
            }
1959
        } else {
1960
            $this->log("Autorotate ignored, no orientation in EXIF.");
1961
        }
1962
1963
        return $this;
1964
    }
1965
1966
1967
1968
    /**
1969
     * Convert true color image to palette image, keeping alpha.
1970
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
1971
     *
1972
     * @return void
1973
     */
1974
    public function trueColorToPalette()
1975
    {
1976
        $img = imagecreatetruecolor($this->width, $this->height);
1977
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
1978
        imagecolortransparent($img, $bga);
1979
        imagefill($img, 0, 0, $bga);
1980
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
1981
        imagetruecolortopalette($img, false, 255);
1982
        imagesavealpha($img, true);
1983
1984
        if (imageistruecolor($this->image)) {
1985
            $this->log("Matching colors with true color image.");
1986
            imagecolormatch($this->image, $img);
1987
        }
1988
1989
        $this->image = $img;
1990
    }
1991
1992
1993
1994
    /**
1995
     * Sharpen image using image convolution.
1996
     *
1997
     * @return $this
1998
     */
1999
    public function sharpenImage()
2000
    {
2001
        $this->imageConvolution('sharpen');
2002
        return $this;
2003
    }
2004
2005
2006
2007
    /**
2008
     * Emboss image using image convolution.
2009
     *
2010
     * @return $this
2011
     */
2012
    public function embossImage()
2013
    {
2014
        $this->imageConvolution('emboss');
2015
        return $this;
2016
    }
2017
2018
2019
2020
    /**
2021
     * Blur image using image convolution.
2022
     *
2023
     * @return $this
2024
     */
2025
    public function blurImage()
2026
    {
2027
        $this->imageConvolution('blur');
2028
        return $this;
2029
    }
2030
2031
2032
2033
    /**
2034
     * Create convolve expression and return arguments for image convolution.
2035
     *
2036
     * @param string $expression constant string which evaluates to a list of
2037
     *                           11 numbers separated by komma or such a list.
2038
     *
2039
     * @return array as $matrix (3x3), $divisor and $offset
2040
     */
2041
    public function createConvolveArguments($expression)
2042
    {
2043
        // Check of matching constant
2044
        if (isset($this->convolves[$expression])) {
2045
            $expression = $this->convolves[$expression];
2046
        }
2047
2048
        $part = explode(',', $expression);
2049
        $this->log("Creating convolution expressen: $expression");
2050
2051
        // Expect list of 11 numbers, split by , and build up arguments
2052
        if (count($part) != 11) {
2053
            throw new Exception(
2054
                "Missmatch in argument convolve. Expected comma-separated string with
2055
                11 float values. Got $expression."
2056
            );
2057
        }
2058
2059
        array_walk($part, function ($item, $key) {
2060
            if (!is_numeric($item)) {
2061
                throw new Exception("Argument to convolve expression should be float but is not.");
2062
            }
2063
        });
2064
2065
        return array(
2066
            array(
2067
                array($part[0], $part[1], $part[2]),
2068
                array($part[3], $part[4], $part[5]),
2069
                array($part[6], $part[7], $part[8]),
2070
            ),
2071
            $part[9],
2072
            $part[10],
2073
        );
2074
    }
2075
2076
2077
2078
    /**
2079
     * Add custom expressions (or overwrite existing) for image convolution.
2080
     *
2081
     * @param array $options Key value array with strings to be converted
2082
     *                       to convolution expressions.
2083
     *
2084
     * @return $this
2085
     */
2086
    public function addConvolveExpressions($options)
2087
    {
2088
        $this->convolves = array_merge($this->convolves, $options);
2089
        return $this;
2090
    }
2091
2092
2093
2094
    /**
2095
     * Image convolution.
2096
     *
2097
     * @param string $options A string with 11 float separated by comma.
2098
     *
2099
     * @return $this
2100
     */
2101
    public function imageConvolution($options = null)
2102
    {
2103
        // Use incoming options or use $this.
2104
        $options = $options ? $options : $this->convolve;
2105
2106
        // Treat incoming as string, split by +
2107
        $this->log("Convolution with '$options'");
2108
        $options = explode(":", $options);
2109
2110
        // Check each option if it matches constant value
2111
        foreach ($options as $option) {
2112
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2113
            imageconvolution($this->image, $matrix, $divisor, $offset);
2114
        }
2115
2116
        return $this;
2117
    }
2118
2119
2120
2121
    /**
2122
     * Set default background color between 000000-FFFFFF or if using
2123
     * alpha 00000000-FFFFFF7F.
2124
     *
2125
     * @param string $color as hex value.
2126
     *
2127
     * @return $this
2128
    */
2129
    public function setDefaultBackgroundColor($color)
2130
    {
2131
        $this->log("Setting default background color to '$color'.");
2132
2133
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2134
            throw new Exception(
2135
                "Background color needs a hex value of 6 or 8
2136
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2137
                Current value was: '$color'."
2138
            );
2139
        }
2140
2141
        $red    = hexdec(substr($color, 0, 2));
2142
        $green  = hexdec(substr($color, 2, 2));
2143
        $blue   = hexdec(substr($color, 4, 2));
2144
2145
        $alpha = (strlen($color) == 8)
2146
            ? hexdec(substr($color, 6, 2))
2147
            : null;
2148
2149
        if (($red < 0 || $red > 255)
2150
            || ($green < 0 || $green > 255)
2151
            || ($blue < 0 || $blue > 255)
2152
            || ($alpha < 0 || $alpha > 127)
2153
        ) {
2154
            throw new Exception(
2155
                "Background color out of range. Red, green blue
2156
                should be 00-FF and alpha should be 00-7F.
2157
                Current value was: '$color'."
2158
            );
2159
        }
2160
2161
        $this->bgColor = strtolower($color);
2162
        $this->bgColorDefault = array(
2163
            'red'   => $red,
2164
            'green' => $green,
2165
            'blue'  => $blue,
2166
            'alpha' => $alpha
2167
        );
2168
2169
        return $this;
2170
    }
2171
2172
2173
2174
    /**
2175
     * Get the background color.
2176
     *
2177
     * @param resource $img the image to work with or null if using $this->image.
2178
     *
2179
     * @return color value or null if no background color is set.
2180
    */
2181 2
    private function getBackgroundColor($img = null)
2182
    {
2183 2
        $img = isset($img) ? $img : $this->image;
2184
2185 2
        if ($this->bgColorDefault) {
2186
2187 2
            $red   = $this->bgColorDefault['red'];
2188 2
            $green = $this->bgColorDefault['green'];
2189 2
            $blue  = $this->bgColorDefault['blue'];
2190 2
            $alpha = $this->bgColorDefault['alpha'];
2191
2192 2
            if ($alpha) {
2193
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2194
            } else {
2195 2
                $color = imagecolorallocate($img, $red, $green, $blue);
2196
            }
2197
2198 2
            return $color;
2199
2200
        } else {
2201
            return 0;
2202
        }
2203
    }
2204
2205
2206
2207
    /**
2208
     * Create a image and keep transparency for png and gifs.
2209
     *
2210
     * @param int $width of the new image.
2211
     * @param int $height of the new image.
2212
     *
2213
     * @return image resource.
2214
    */
2215 2
    private function createImageKeepTransparency($width, $height)
2216
    {
2217 2
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2218 2
        $img = imagecreatetruecolor($width, $height);
2219 2
        imagealphablending($img, false);
2220 2
        imagesavealpha($img, true);
2221
2222 2
        $index = $this->image
2223 2
            ? imagecolortransparent($this->image)
2224 2
            : -1;
2225
            
2226 2
        if ($index != -1) {
2227
2228
            imagealphablending($img, true);
2229
            $transparent = imagecolorsforindex($this->image, $index);
2230
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2231
            imagefill($img, 0, 0, $color);
2232
            $index = imagecolortransparent($img, $color);
2233
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2234
2235 2
        } elseif ($this->bgColorDefault) {
2236
2237 2
            $color = $this->getBackgroundColor($img);
2238 2
            imagefill($img, 0, 0, $color);
2239 2
            $this->Log("Filling image with background color.");
2240 2
        }
2241
2242 2
        return $img;
2243
    }
2244
2245
2246
2247
    /**
2248
     * Set optimizing  and post-processing options.
2249
     *
2250
     * @param array $options with config for postprocessing with external tools.
2251
     *
2252
     * @return $this
2253
     */
2254
    public function setPostProcessingOptions($options)
2255
    {
2256
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2257
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2258
        } else {
2259
            $this->jpegOptimizeCmd = null;
2260
        }
2261
2262
        if (isset($options['png_filter']) && $options['png_filter']) {
2263
            $this->pngFilterCmd = $options['png_filter_cmd'];
2264
        } else {
2265
            $this->pngFilterCmd = null;
2266
        }
2267
2268
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2269
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2270
        } else {
2271
            $this->pngDeflateCmd = null;
2272
        }
2273
2274
        return $this;
2275
    }
2276
2277
2278
2279
    /**
2280
     * Find out the type (file extension) for the image to be saved.
2281
     *
2282
     * @return string as image extension.
2283
     */
2284 2
    protected function getTargetImageExtension()
2285
    {
2286
        // switch on mimetype
2287 2
        if (isset($this->extension)) {
2288
            return strtolower($this->extension);
2289
        } else {
2290 2
            return substr(image_type_to_extension($this->fileType), 1);
2291
        }
2292
    }
2293
    
2294
    
2295
2296
    /**
2297
     * Save image.
2298
     *
2299
     * @param string  $src       as target filename.
2300
     * @param string  $base      as base directory where to store images.
2301
     * @param boolean $overwrite or not, default to always overwrite file.
2302
     *
2303
     * @return $this or false if no folder is set.
2304
     */
2305 2
    public function save($src = null, $base = null, $overwrite = true)
2306
    {
2307 2
        if (isset($src)) {
2308
            $this->setTarget($src, $base);
2309
        }
2310
2311 2
        if ($overwrite === false && is_file($this->cacheFileName)) {
2312
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2313
            return;
2314
        }
2315
2316 2
        is_writable($this->saveFolder)
2317
            or $this->raiseError('Target directory is not writable.');
2318
2319 2
        $type = $this->getTargetImageExtension();
2320 2
        $this->Log("Saving image as " . $type);
2321
        switch($type) {
2322
2323 2
            case 'jpeg':
2324 2
            case 'jpg':
2325
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2326
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2327
2328
                // Use JPEG optimize if defined
2329
                if ($this->jpegOptimizeCmd) {
2330
                    if ($this->verbose) {
2331
                        clearstatcache();
2332
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2333
                    }
2334
                    $res = array();
2335
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2336
                    exec($cmd, $res);
2337
                    $this->log($cmd);
2338
                    $this->log($res);
2339
                }
2340
                break;
2341
2342 2
            case 'gif':
2343
                $this->Log("Saving image as GIF to cache.");
2344
                imagegif($this->image, $this->cacheFileName);
2345
                break;
2346
2347 2
            case 'png':
2348 2
            default:
2349 2
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2350
2351
                // Turn off alpha blending and set alpha flag
2352 2
                imagealphablending($this->image, false);
2353 2
                imagesavealpha($this->image, true);
2354 2
                imagepng($this->image, $this->cacheFileName, $this->compress);
2355
2356
                // Use external program to filter PNG, if defined
2357 2
                if ($this->pngFilterCmd) {
2358
                    if ($this->verbose) {
2359
                        clearstatcache();
2360
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2361
                    }
2362
                    $res = array();
2363
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2364
                    exec($cmd, $res);
2365
                    $this->Log($cmd);
2366
                    $this->Log($res);
2367
                }
2368
2369
                // Use external program to deflate PNG, if defined
2370 2
                if ($this->pngDeflateCmd) {
2371
                    if ($this->verbose) {
2372
                        clearstatcache();
2373
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2374
                    }
2375
                    $res = array();
2376
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2377
                    exec($cmd, $res);
2378
                    $this->Log($cmd);
2379
                    $this->Log($res);
2380
                }
2381 2
                break;
2382 2
        }
2383
2384 2
        if ($this->verbose) {
2385
            clearstatcache();
2386
            $this->log("Saved image to cache.");
2387
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2388
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2389
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2390
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2391
            $index = imagecolortransparent($this->image);
2392
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2393
        }
2394
2395 2
        return $this;
2396
    }
2397
2398
2399
2400
    /**
2401
     * Create a hard link, as an alias, to the cached file.
2402
     *
2403
     * @param string $alias where to store the link,
2404
     *                      filename without extension.
2405
     *
2406
     * @return $this
2407
     */
2408
    public function linkToCacheFile($alias)
2409
    {
2410
        if ($alias === null) {
2411
            $this->log("Ignore creating alias.");
2412
            return $this;
2413
        }
2414
2415
        if (is_readable($alias)) {
2416
            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...
2417
        }
2418
2419
        $res = link($this->cacheFileName, $alias);
2420
2421
        if ($res) {
2422
            $this->log("Created an alias as: $alias");
2423
        } else {
2424
            $this->log("Failed to create the alias: $alias");
2425
        }
2426
2427
        return $this;
2428
    }
2429
2430
2431
2432
    /**
2433
     * Add HTTP header for putputting together with image.
2434
     *
2435
     * @param string $type  the header type such as "Cache-Control"
2436
     * @param string $value the value to use
2437
     *
2438
     * @return void
2439
     */
2440
    public function addHTTPHeader($type, $value)
2441
    {
2442
        $this->HTTPHeader[$type] = $value;
2443
    }
2444
2445
2446
2447
    /**
2448
     * Output image to browser using caching.
2449
     *
2450
     * @param string $file   to read and output, default is to
2451
     *                       use $this->cacheFileName
2452
     * @param string $format set to json to output file as json
2453
     *                       object with details
2454
     *
2455
     * @return void
2456
     */
2457
    public function output($file = null, $format = null)
2458
    {
2459
        if (is_null($file)) {
2460
            $file = $this->cacheFileName;
2461
        }
2462
2463
        if (is_null($format)) {
2464
            $format = $this->outputFormat;
2465
        }
2466
2467
        $this->log("Output format is: $format");
2468
2469
        if (!$this->verbose && $format == 'json') {
2470
            header('Content-type: application/json');
2471
            echo $this->json($file);
2472
            exit;
2473
        } elseif ($format == 'ascii') {
2474
            header('Content-type: text/plain');
2475
            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...
2476
            exit;
2477
        }
2478
2479
        $this->log("Outputting image: $file");
2480
2481
        // Get image modification time
2482
        clearstatcache();
2483
        $lastModified = filemtime($file);
2484
        $gmdate = gmdate("D, d M Y H:i:s", $lastModified);
2485
2486
        if (!$this->verbose) {
2487
            header('Last-Modified: ' . $gmdate . " GMT");
2488
        }
2489
2490
        foreach($this->HTTPHeader as $key => $val) {
2491
            header("$key: $val");
2492
        }
2493
2494
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2495
2496
            if ($this->verbose) {
2497
                $this->log("304 not modified");
2498
                $this->verboseOutput();
2499
                exit;
2500
            }
2501
2502
            header("HTTP/1.0 304 Not Modified");
2503
2504
        } else {
2505
2506
            // Get details on image
2507
            $info = getimagesize($file);
2508
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
2509
            $mime = $info['mime'];
2510
            $size = filesize($file);
2511
2512
            if ($this->verbose) {
2513
                $this->log("Last-Modified: " . $gmdate . " GMT");
2514
                $this->log("Content-type: " . $mime);
2515
                $this->log("Content-length: " . $size);
2516
                $this->verboseOutput();
2517
                
2518
                if (is_null($this->verboseFileName)) {
2519
                    exit;
2520
                }
2521
            }
2522
2523
            header("Content-type: $mime");
2524
            header("Content-length: $size");
2525
            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...
2526
        }
2527
2528
        exit;
2529
    }
2530
2531
2532
2533
    /**
2534
     * Create a JSON object from the image details.
2535
     *
2536
     * @param string $file the file to output.
2537
     *
2538
     * @return string json-encoded representation of the image.
2539
     */
2540
    public function json($file = null)
2541
    {
2542
        $file = $file ? $file : $this->cacheFileName;
2543
2544
        $details = array();
2545
2546
        clearstatcache();
2547
2548
        $details['src']       = $this->imageSrc;
2549
        $lastModified         = filemtime($this->pathToImage);
2550
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2551
2552
        $details['cache']       = basename($this->cacheFileName);
2553
        $lastModified           = filemtime($this->cacheFileName);
2554
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2555
2556
        $this->load($file);
2557
2558
        $details['filename']    = basename($file);
2559
        $details['mimeType']    = image_type_to_mime_type($this->fileType);
2560
        $details['width']       = $this->width;
2561
        $details['height']      = $this->height;
2562
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2563
        $details['size']        = filesize($file);
2564
        $details['colors'] = $this->colorsTotal($this->image);
2565
        $details['includedFiles'] = count(get_included_files());
2566
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2567
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2568
        $details['memoryLimit'] = ini_get('memory_limit');
2569
        
2570
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2571
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2572
        }
2573
2574
        if ($details['mimeType'] == 'image/png') {
2575
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2576
        }
2577
2578
        $options = null;
2579
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2580
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2581
        }
2582
2583
        return json_encode($details, $options);
2584
    }
2585
2586
2587
2588
    /**
2589
     * Set options for creating ascii version of image.
2590
     *
2591
     * @param array $options empty to use default or set options to change.
2592
     *
2593
     * @return void.
2594
     */
2595
    public function setAsciiOptions($options = array())
2596
    {
2597
        $this->asciiOptions = $options;
2598
    }
2599
2600
2601
2602
    /**
2603
     * Create an ASCII version from the image details.
2604
     *
2605
     * @param string $file the file to output.
2606
     *
2607
     * @return string ASCII representation of the image.
2608
     */
2609
    public function ascii($file = null)
2610
    {
2611
        $file = $file ? $file : $this->cacheFileName;
2612
2613
        $asciiArt = new CAsciiArt();
2614
        $asciiArt->setOptions($this->asciiOptions);
2615
        return $asciiArt->createFromFile($file);
2616
    }
2617
2618
2619
2620
    /**
2621
     * Log an event if verbose mode.
2622
     *
2623
     * @param string $message to log.
2624
     *
2625
     * @return this
2626
     */
2627 7
    public function log($message)
2628
    {
2629 7
        if ($this->verbose) {
2630
            $this->log[] = $message;
2631
        }
2632
2633 7
        return $this;
2634
    }
2635
2636
2637
2638
    /**
2639
     * Do verbose output to a file.
2640
     *
2641
     * @param string $fileName where to write the verbose output.
2642
     *
2643
     * @return void
2644
     */
2645
    public function setVerboseToFile($fileName)
2646
    {
2647
        $this->log("Setting verbose output to file.");
2648
        $this->verboseFileName = $fileName;
2649
    }
2650
2651
2652
2653
    /**
2654
     * Do verbose output and print out the log and the actual images.
2655
     *
2656
     * @return void
2657
     */
2658
    private function verboseOutput()
2659
    {
2660
        $log = null;
2661
        $this->log("As JSON: \n" . $this->json());
2662
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2663
        $this->log("Memory limit: " . ini_get('memory_limit'));
2664
2665
        $included = get_included_files();
2666
        $this->log("Included files: " . count($included));
2667
2668
        foreach ($this->log as $val) {
2669
            if (is_array($val)) {
2670
                foreach ($val as $val1) {
2671
                    $log .= htmlentities($val1) . '<br/>';
2672
                }
2673
            } else {
2674
                $log .= htmlentities($val) . '<br/>';
2675
            }
2676
        }
2677
2678
        if (!is_null($this->verboseFileName)) {
2679
            file_put_contents(
2680
                $this->verboseFileName,
2681
                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...
2682
            );
2683
        } else {
2684
            echo <<<EOD
2685
<h1>CImage Verbose Output</h1>
2686
<pre>{$log}</pre>
2687
EOD;
2688
        }
2689
    }
2690
2691
2692
2693
    /**
2694
     * Raise error, enables to implement a selection of error methods.
2695
     *
2696
     * @param string $message the error message to display.
2697
     *
2698
     * @return void
2699
     * @throws Exception
2700
     */
2701
    private function raiseError($message)
2702
    {
2703
        throw new Exception($message);
2704
    }
2705
}
2706