Completed
Push — 0715 ( 7677fc...b5de49 )
by Mikael
03:18
created

CImage.php (5 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 * Resize and crop images on the fly, store generated images in a cache.
4
 *
5
 * @author  Mikael Roos [email protected]
6
 * @example http://dbwebb.se/opensource/cimage
7
 * @link    https://github.com/mosbth/cimage
8
 */
9
class CImage
10
{
11
12
    /**
13
     * Constants type of PNG image
14
     */
15
    const PNG_GREYSCALE         = 0;
16
    const PNG_RGB               = 2;
17
    const PNG_RGB_PALETTE       = 3;
18
    const PNG_GREYSCALE_ALPHA   = 4;
19
    const PNG_RGB_ALPHA         = 6;
20
21
22
23
    /**
24
     * Constant for default image quality when not set
25
     */
26
    const JPEG_QUALITY_DEFAULT = 60;
27
28
29
30
    /**
31
     * Quality level for JPEG images.
32
     */
33
    private $quality;
34
35
36
37
    /**
38
     * Is the quality level set from external use (true) or is it default (false)?
39
     */
40
    private $useQuality = false;
41
42
43
44
    /**
45
     * Constant for default image quality when not set
46
     */
47
    const PNG_COMPRESSION_DEFAULT = -1;
48
49
50
51
    /**
52
     * Compression level for PNG images.
53
     */
54
    private $compress;
55
56
57
58
    /**
59
     * Is the compress level set from external use (true) or is it default (false)?
60
     */
61
    private $useCompress = false;
62
63
64
65
66
    /**
67
     * Add HTTP headers for outputing image.
68
     */
69
    private $HTTPHeader = array();
70
71
72
73
    /**
74
     * Default background color, red, green, blue, alpha.
75
     *
76
     * @todo remake when upgrading to PHP 5.5
77
     */
78
    /*
79
    const BACKGROUND_COLOR = array(
80
        'red'   => 0,
81
        'green' => 0,
82
        'blue'  => 0,
83
        'alpha' => null,
84
    );*/
85
86
87
88
    /**
89
     * Default background color to use.
90
     *
91
     * @todo remake when upgrading to PHP 5.5
92
     */
93
    //private $bgColorDefault = self::BACKGROUND_COLOR;
94
    private $bgColorDefault = array(
95
        'red'   => 0,
96
        'green' => 0,
97
        'blue'  => 0,
98
        'alpha' => null,
99
    );
100
101
102
    /**
103
     * Background color to use, specified as part of options.
104
     */
105
    private $bgColor;
106
107
108
109
    /**
110
     * Where to save the target file.
111
     */
112
    private $saveFolder;
113
114
115
116
    /**
117
     * The working image object.
118
     */
119
    private $image;
120
121
122
123
    /**
124
     * Image filename, may include subdirectory, relative from $imageFolder
125
     */
126
    private $imageSrc;
127
128
129
130
    /**
131
     * Actual path to the image, $imageFolder . '/' . $imageSrc
132
     */
133
    private $pathToImage;
134
135
136
137
    /**
138
     * File type for source image, as provided by getimagesize()
139
     */
140
    private $fileType;
141
142
143
144
    /**
145
     * File extension to use when saving image.
146
     */
147
    private $extension;
148
149
150
151
    /**
152
     * Output format, supports null (image) or json.
153
     */
154
    private $outputFormat = null;
155
156
157
158
    /**
159
     * Verbose mode to print out a trace and display the created image
160
     */
161
    private $verbose = false;
162
163
164
165
    /**
166
     * Keep a log/trace on what happens
167
     */
168
    private $log = array();
169
170
171
172
    /**
173
     * Handle image as palette image
174
     */
175
    private $palette;
176
177
178
179
    /**
180
     * Target filename, with path, to save resulting image in.
181
     */
182
    private $cacheFileName;
183
184
185
186
    /**
187
     * Set a format to save image as, or null to use original format.
188
     */
189
    private $saveAs;
190
191
192
    /**
193
     * Path to command for filter optimize, for example optipng or null.
194
     */
195
    private $pngFilter;
196
    private $pngFilterCmd;
197
198
199
200
    /**
201
     * Path to command for deflate optimize, for example pngout or null.
202
     */
203
    private $pngDeflate;
204
    private $pngDeflateCmd;
205
206
207
208
    /**
209
     * Path to command to optimize jpeg images, for example jpegtran or null.
210
     */
211
     private $jpegOptimize;
212
     private $jpegOptimizeCmd;
213
214
215
216
    /**
217
     * Image dimensions, calculated from loaded image.
218
     */
219
    private $width;  // Calculated from source image
220
    private $height; // Calculated from source image
221
222
223
    /**
224
     * New image dimensions, incoming as argument or calculated.
225
     */
226
    private $newWidth;
227
    private $newWidthOrig;  // Save original value
228
    private $newHeight;
229
    private $newHeightOrig; // Save original value
230
231
232
    /**
233
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
234
     */
235
    private $dpr = 1;
236
237
238
    /**
239
     * Always upscale images, even if they are smaller than target image.
240
     */
241
    const UPSCALE_DEFAULT = true;
242
    private $upscale = self::UPSCALE_DEFAULT;
243
244
245
246
    /**
247
     * Array with details on how to crop, incoming as argument and calculated.
248
     */
249
    public $crop;
250
    public $cropOrig; // Save original value
251
252
253
    /**
254
     * String with details on how to do image convolution. String
255
     * should map a key in the $convolvs array or be a string of
256
     * 11 float values separated by comma. The first nine builds
257
     * up the matrix, then divisor and last offset.
258
     */
259
    private $convolve;
260
261
262
    /**
263
     * Custom convolution expressions, matrix 3x3, divisor and offset.
264
     */
265
    private $convolves = array(
266
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
267
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
268
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
269
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
270
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
271
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
272
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
273
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
274
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
275
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
276
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
277
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
278
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
279
    );
280
281
282
    /**
283
     * Resize strategy to fill extra area with background color.
284
     * True or false.
285
     */
286
    private $fillToFit;
287
288
289
290
    /**
291
     * To store value for option scale.
292
     */
293
    private $scale;
294
295
296
297
    /**
298
     * To store value for option.
299
     */
300
    private $rotateBefore;
301
302
303
304
    /**
305
     * To store value for option.
306
     */
307
    private $rotateAfter;
308
309
310
311
    /**
312
     * To store value for option.
313
     */
314
    private $autoRotate;
315
316
317
318
    /**
319
     * To store value for option.
320
     */
321
    private $sharpen;
322
323
324
325
    /**
326
     * To store value for option.
327
     */
328
    private $emboss;
329
330
331
332
    /**
333
     * To store value for option.
334
     */
335
    private $blur;
336
337
338
339
    /**
340
     * Used with option area to set which parts of the image to use.
341
     */
342
    private $offset;
343
344
345
346
    /**
347
     * Calculate target dimension for image when using fill-to-fit resize strategy.
348
     */
349
    private $fillWidth;
350
    private $fillHeight;
351
352
353
354
    /**
355
     * Allow remote file download, default is to disallow remote file download.
356
     */
357
    private $allowRemote = false;
358
359
360
361
    /**
362
     * Path to cache for remote download.
363
     */
364
    private $remoteCache;
365
366
367
368
    /**
369
     * Pattern to recognize a remote file.
370
     */
371
    //private $remotePattern = '#^[http|https]://#';
372
    private $remotePattern = '#^https?://#';
373
374
375
376
    /**
377
     * Use the cache if true, set to false to ignore the cached file.
378
     */
379
    private $useCache = true;
380
381
382
    /**
383
    * Disable the fasttrackCacke to start with, inject an object to enable it.
384
    */
385
    private $fastTrackCache = null;
386
387
388
389
    /*
390
     * Set whitelist for valid hostnames from where remote source can be
391
     * downloaded.
392
     */
393
    private $remoteHostWhitelist = null;
394
395
396
397
    /*
398
     * Do verbose logging to file by setting this to a filename.
399
     */
400
    private $verboseFileName = null;
401
402
403
404
    /*
405
     * Output to ascii can take som options as an array.
406
     */
407
    private $asciiOptions = array();
408
409
410
411
    /*
412
     * Image copy strategy, defaults to RESAMPLE.
413
     */
414
     const RESIZE = 1;
415
     const RESAMPLE = 2;
416
     private $copyStrategy = NULL;
417
418
419
420
    /**
421
     * Properties, the class is mutable and the method setOptions()
422
     * decides (partly) what properties are created.
423
     *
424
     * @todo Clean up these and check if and how they are used
425
     */
426
427
    public $keepRatio;
428
    public $cropToFit;
429
    private $cropWidth;
430
    private $cropHeight;
431
    public $crop_x;
432
    public $crop_y;
433
    public $filters;
434
    private $attr; // Calculated from source image
435
436
437
438
439
    /**
440
     * Constructor, can take arguments to init the object.
441
     *
442
     * @param string $imageSrc    filename which may contain subdirectory.
443
     * @param string $imageFolder path to root folder for images.
444
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
445
     * @param string $saveName    name of target file when saveing.
446
     */
447
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
448
    {
449
        $this->setSource($imageSrc, $imageFolder);
450
        $this->setTarget($saveFolder, $saveName);
451
    }
452
453
454
455
    /**
456
     * Inject object and use it, must be available as member.
457
     *
458
     * @param string $property to set as object.
459
     * @param object $object   to set to property.
460
     *
461
     * @return $this
462
     */
463
    public function injectDependency($property, $object)
464
    {
465
        if (!property_exists($this, $property)) {
466
            $this->raiseError("Injecting unknown property.");
467
        }
468
        $this->$property = $object;
469
        return $this;
470
    }
471
472
473
474
    /**
475
     * Set verbose mode.
476
     *
477
     * @param boolean $mode true or false to enable and disable verbose mode,
478
     *                      default is true.
479
     *
480
     * @return $this
481
     */
482
    public function setVerbose($mode = true)
483
    {
484
        $this->verbose = $mode;
485
        return $this;
486
    }
487
488
489
490
    /**
491
     * Set save folder, base folder for saving cache files.
492
     *
493
     * @todo clean up how $this->saveFolder is used in other methods.
494
     *
495
     * @param string $path where to store cached files.
496
     *
497
     * @return $this
498
     */
499
    public function setSaveFolder($path)
500
    {
501
        $this->saveFolder = $path;
502
        return $this;
503
    }
504
505
506
507
    /**
508
     * Use cache or not.
509
     *
510
     * @param boolean $use true or false to use cache.
511
     *
512
     * @return $this
513
     */
514
    public function useCache($use = true)
515
    {
516
        $this->useCache = $use;
517
        return $this;
518
    }
519
520
521
522
    /**
523
     * Create and save a dummy image. Use dimensions as stated in
524
     * $this->newWidth, or $width or default to 100 (same for height.
525
     *
526
     * @param integer $width  use specified width for image dimension.
527
     * @param integer $height use specified width for image dimension.
528
     *
529
     * @return $this
530
     */
531
    public function createDummyImage($width = null, $height = null)
532
    {
533
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
534
        $this->newHeight = $this->newHeight ?: $height ?: 100;
535
536
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
537
538
        return $this;
539
    }
540
541
542
543
    /**
544
     * Allow or disallow remote image download.
545
     *
546
     * @param boolean $allow   true or false to enable and disable.
547
     * @param string  $cache   path to cache dir.
548
     * @param string  $pattern to use to detect if its a remote file.
549
     *
550
     * @return $this
551
     */
552
    public function setRemoteDownload($allow, $cache, $pattern = null)
553
    {
554
        $this->allowRemote = $allow;
555
        $this->remoteCache = $cache;
556
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
557
558
        $this->log(
559
            "Set remote download to: "
560
            . ($this->allowRemote ? "true" : "false")
561
            . " using pattern "
562
            . $this->remotePattern
563
        );
564
565
        return $this;
566
    }
567
568
569
570
    /**
571
     * Check if the image resource is a remote file or not.
572
     *
573
     * @param string $src check if src is remote.
574
     *
575
     * @return boolean true if $src is a remote file, else false.
576
     */
577
    public function isRemoteSource($src)
578
    {
579
        $remote = preg_match($this->remotePattern, $src);
580
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
581
        return !!$remote;
582
    }
583
584
585
586
    /**
587
     * Set whitelist for valid hostnames from where remote source can be
588
     * downloaded.
589
     *
590
     * @param array $whitelist with regexp hostnames to allow download from.
591
     *
592
     * @return $this
593
     */
594
    public function setRemoteHostWhitelist($whitelist = null)
595
    {
596
        $this->remoteHostWhitelist = $whitelist;
597
        $this->log(
598
            "Setting remote host whitelist to: "
599
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
600
        );
601
        return $this;
602
    }
603
604
605
606
    /**
607
     * Check if the hostname for the remote image, is on a whitelist,
608
     * if the whitelist is defined.
609
     *
610
     * @param string $src the remote source.
611
     *
612
     * @return boolean true if hostname on $src is in the whitelist, else false.
613
     */
614
    public function isRemoteSourceOnWhitelist($src)
615
    {
616
        if (is_null($this->remoteHostWhitelist)) {
617
            $this->log("Remote host on whitelist not configured - allowing.");
618
            return true;
619
        }
620
621
        $whitelist = new CWhitelist();
622
        $hostname = parse_url($src, PHP_URL_HOST);
623
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
624
625
        $this->log(
626
            "Remote host is on whitelist: "
627
            . ($allow ? "true" : "false")
628
        );
629
        return $allow;
630
    }
631
632
633
634
    /**
635
     * Check if file extension is valid as a file extension.
636
     *
637
     * @param string $extension of image file.
638
     *
639
     * @return $this
640
     */
641
    private function checkFileExtension($extension)
642
    {
643
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
644
645
        in_array(strtolower($extension), $valid)
646
            or $this->raiseError('Not a valid file extension.');
647
648
        return $this;
649
    }
650
651
652
653
    /**
654
     * Normalize the file extension.
655
     *
656
     * @param string $extension of image file or skip to use internal.
657
     *
658
     * @return string $extension as a normalized file extension.
659
     */
660
    private function normalizeFileExtension($extension = null)
661
    {
662
        $extension = strtolower($extension ? $extension : $this->extension);
663
664
        if ($extension == 'jpeg') {
665
                $extension = 'jpg';
666
        }
667
668
        return $extension;
669
    }
670
671
672
673
    /**
674
     * Download a remote image and return path to its local copy.
675
     *
676
     * @param string $src remote path to image.
677
     *
678
     * @return string as path to downloaded remote source.
679
     */
680
    public function downloadRemoteSource($src)
681
    {
682
        if (!$this->isRemoteSourceOnWhitelist($src)) {
683
            throw new Exception("Hostname is not on whitelist for remote sources.");
684
        }
685
686
        $remote = new CRemoteImage();
687
688
        if (!is_writable($this->remoteCache)) {
689
            $this->log("The remote cache is not writable.");
690
        }
691
692
        $remote->setCache($this->remoteCache);
693
        $remote->useCache($this->useCache);
694
        $src = $remote->download($src);
695
696
        $this->log("Remote HTTP status: " . $remote->getStatus());
697
        $this->log("Remote item is in local cache: $src");
698
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
699
700
        return $src;
701
    }
702
703
704
705
    /**
706
     * Set source file to use as image source.
707
     *
708
     * @param string $src of image.
709
     * @param string $dir as optional base directory where images are.
710
     *
711
     * @return $this
712
     */
713
    public function setSource($src, $dir = null)
714
    {
715
        if (!isset($src)) {
716
            $this->imageSrc = null;
717
            $this->pathToImage = null;
718
            return $this;
719
        }
720
721
        if ($this->allowRemote && $this->isRemoteSource($src)) {
722
            $src = $this->downloadRemoteSource($src);
723
            $dir = null;
724
        }
725
726
        if (!isset($dir)) {
727
            $dir = dirname($src);
728
            $src = basename($src);
729
        }
730
731
        $this->imageSrc     = ltrim($src, '/');
732
        $imageFolder        = rtrim($dir, '/');
733
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
734
735
        return $this;
736
    }
737
738
739
740
    /**
741
     * Set target file.
742
     *
743
     * @param string $src of target image.
744
     * @param string $dir as optional base directory where images are stored.
745
     *                    Uses $this->saveFolder if null.
746
     *
747
     * @return $this
748
     */
749
    public function setTarget($src = null, $dir = null)
750
    {
751
        if (!isset($src)) {
752
            $this->cacheFileName = null;
753
            return $this;
754
        }
755
756
        if (isset($dir)) {
757
            $this->saveFolder = rtrim($dir, '/');
758
        }
759
760
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
761
762
        // Sanitize filename
763
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
764
        $this->log("The cache file name is: " . $this->cacheFileName);
765
766
        return $this;
767
    }
768
769
770
771
    /**
772
     * Get filename of target file.
773
     *
774
     * @return Boolean|String as filename of target or false if not set.
775
     */
776
    public function getTarget()
777
    {
778
        return $this->cacheFileName;
779
    }
780
781
782
783
    /**
784
     * Set options to use when processing image.
785
     *
786
     * @param array $args used when processing image.
787
     *
788
     * @return $this
789
     */
790
    public function setOptions($args)
791
    {
792
        $this->log("Set new options for processing image.");
793
794
        $defaults = array(
795
            // Options for calculate dimensions
796
            'newWidth'    => null,
797
            'newHeight'   => null,
798
            'aspectRatio' => null,
799
            'keepRatio'   => true,
800
            'cropToFit'   => false,
801
            'fillToFit'   => null,
802
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
803
            'area'        => null, //'0,0,0,0',
804
            'upscale'     => self::UPSCALE_DEFAULT,
805
806
            // Options for caching or using original
807
            'useCache'    => true,
808
            'useOriginal' => true,
809
810
            // Pre-processing, before resizing is done
811
            'scale'        => null,
812
            'rotateBefore' => null,
813
            'autoRotate'  => false,
814
815
            // General options
816
            'bgColor'     => null,
817
818
            // Post-processing, after resizing is done
819
            'palette'     => null,
820
            'filters'     => null,
821
            'sharpen'     => null,
822
            'emboss'      => null,
823
            'blur'        => null,
824
            'convolve'       => null,
825
            'rotateAfter' => null,
826
827
            // Output format
828
            'outputFormat' => null,
829
            'dpr'          => 1,
830
        );
831
832
        // Convert crop settings from string to array
833
        if (isset($args['crop']) && !is_array($args['crop'])) {
834
            $pices = explode(',', $args['crop']);
835
            $args['crop'] = array(
836
                'width'   => $pices[0],
837
                'height'  => $pices[1],
838
                'start_x' => $pices[2],
839
                'start_y' => $pices[3],
840
            );
841
        }
842
843
        // Convert area settings from string to array
844
        if (isset($args['area']) && !is_array($args['area'])) {
845
                $pices = explode(',', $args['area']);
846
                $args['area'] = array(
847
                    'top'    => $pices[0],
848
                    'right'  => $pices[1],
849
                    'bottom' => $pices[2],
850
                    'left'   => $pices[3],
851
                );
852
        }
853
854
        // Convert filter settings from array of string to array of array
855
        if (isset($args['filters']) && is_array($args['filters'])) {
856
            foreach ($args['filters'] as $key => $filterStr) {
857
                $parts = explode(',', $filterStr);
858
                $filter = $this->mapFilter($parts[0]);
859
                $filter['str'] = $filterStr;
860
                for ($i=1; $i<=$filter['argc']; $i++) {
861
                    if (isset($parts[$i])) {
862
                        $filter["arg{$i}"] = $parts[$i];
863
                    } else {
864
                        throw new Exception(
865
                            'Missing arg to filter, review how many arguments are needed at
866
                            http://php.net/manual/en/function.imagefilter.php'
867
                        );
868
                    }
869
                }
870
                $args['filters'][$key] = $filter;
871
            }
872
        }
873
874
        // Merge default arguments with incoming and set properties.
875
        //$args = array_merge_recursive($defaults, $args);
876
        $args = array_merge($defaults, $args);
877
        foreach ($defaults as $key => $val) {
878
            $this->{$key} = $args[$key];
879
        }
880
881
        if ($this->bgColor) {
882
            $this->setDefaultBackgroundColor($this->bgColor);
883
        }
884
885
        // Save original values to enable re-calculating
886
        $this->newWidthOrig  = $this->newWidth;
887
        $this->newHeightOrig = $this->newHeight;
888
        $this->cropOrig      = $this->crop;
889
890
        return $this;
891
    }
892
893
894
895
    /**
896
     * Map filter name to PHP filter and id.
897
     *
898
     * @param string $name the name of the filter.
899
     *
900
     * @return array with filter settings
901
     * @throws Exception
902
     */
903
    private function mapFilter($name)
904
    {
905
        $map = array(
906
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
907
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
908
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
909
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
910
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
911
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
912
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
913
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
914
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
915
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
916
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
917
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
918
        );
919
920
        if (isset($map[$name])) {
921
            return $map[$name];
922
        } else {
923
            throw new Exception('No such filter.');
924
        }
925
    }
926
927
928
929
    /**
930
     * Load image details from original image file.
931
     *
932
     * @param string $file the file to load or null to use $this->pathToImage.
933
     *
934
     * @return $this
935
     * @throws Exception
936
     */
937
    public function loadImageDetails($file = null)
938
    {
939
        $file = $file ? $file : $this->pathToImage;
940
941
        is_readable($file)
942
            or $this->raiseError('Image file does not exist.');
943
944
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($this->pathToImage);
945
        if (empty($info)) {
946
            // To support webp
947
            $this->fileType = false;
948
            if (function_exists("exif_imagetype")) {
949
                $this->fileType = exif_imagetype($file);
950
                if ($this->fileType === false) {
951
                    if (function_exists("imagecreatefromwebp")) {
952
                        die("before create webp " . $file);
0 ignored issues
show
Coding Style Compatibility introduced by
The method loadImageDetails() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
953
                        $webp = imagecreatefromwebp($file);
0 ignored issues
show
$webp = imagecreatefromwebp($file); does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
$webp is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
954
                        die("after create webp " . $file);
0 ignored issues
show
Coding Style Compatibility introduced by
The method loadImageDetails() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
955
                        if ($webp !== false) {
0 ignored issues
show
if ($webp !== false) { ...>fileType = IMG_WEBP; } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
956
                            $this->width  = imagesx($webp);
957
                            $this->height = imagesy($webp);
958
                            $this->fileType = IMG_WEBP;
959
                        }
960
                    }
961
                }
962
            }
963
        }
964
965
        if (!$this->fileType) {
966
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
967
        }
968
969
        if ($this->verbose) {
970
            $this->log("Loading image details for: {$file}");
971
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
972
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
973
            $this->log(" Image mimetype: " . $this->getMimeType());
974
        }
975
976
        return $this;
977
    }
978
979
980
981
    /**
982
     * Get mime type for image type.
983
     *
984
     * @return $this
985
     * @throws Exception
986
     */
987
    protected function getMimeType()
988
    {
989
        if ($this->fileType === IMG_WEBP) {
990
            return "image/webp";
991
        }
992
993
        return image_type_to_mime_type($this->fileType);
994
    }
995
996
997
998
    /**
999
     * Init new width and height and do some sanity checks on constraints, before any
1000
     * processing can be done.
1001
     *
1002
     * @return $this
1003
     * @throws Exception
1004
     */
1005
    public function initDimensions()
1006
    {
1007
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1008
1009
        // width as %
1010
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
1011
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1012
            $this->log("Setting new width based on % to {$this->newWidth}");
1013
        }
1014
1015
        // height as %
1016
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
1017
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1018
            $this->log("Setting new height based on % to {$this->newHeight}");
1019
        }
1020
1021
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
1022
1023
        // width & height from aspect ratio
1024
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1025
            if ($this->aspectRatio >= 1) {
1026
                $this->newWidth   = $this->width;
1027
                $this->newHeight  = $this->width / $this->aspectRatio;
1028
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1029
1030
            } else {
1031
                $this->newHeight  = $this->height;
1032
                $this->newWidth   = $this->height * $this->aspectRatio;
1033
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1034
            }
1035
1036
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1037
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1038
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1039
1040
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1041
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1042
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1043
        }
1044
1045
        // Change width & height based on dpr
1046
        if ($this->dpr != 1) {
1047
            if (!is_null($this->newWidth)) {
1048
                $this->newWidth  = round($this->newWidth * $this->dpr);
1049
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1050
            }
1051
            if (!is_null($this->newHeight)) {
1052
                $this->newHeight = round($this->newHeight * $this->dpr);
1053
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1054
            }
1055
        }
1056
1057
        // Check values to be within domain
1058
        is_null($this->newWidth)
1059
            or is_numeric($this->newWidth)
1060
            or $this->raiseError('Width not numeric');
1061
1062
        is_null($this->newHeight)
1063
            or is_numeric($this->newHeight)
1064
            or $this->raiseError('Height not numeric');
1065
1066
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1067
1068
        return $this;
1069
    }
1070
1071
1072
1073
    /**
1074
     * Calculate new width and height of image, based on settings.
1075
     *
1076
     * @return $this
1077
     */
1078
    public function calculateNewWidthAndHeight()
1079
    {
1080
        // Crop, use cropped width and height as base for calulations
1081
        $this->log("Calculate new width and height.");
1082
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1083
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1084
1085
        // Check if there is an area to crop off
1086
        if (isset($this->area)) {
1087
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1088
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1089
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1090
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1091
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1092
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1093
            $this->width  = $this->offset['width'];
1094
            $this->height = $this->offset['height'];
1095
            $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']}%.");
1096
            $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.");
1097
        }
1098
1099
        $width  = $this->width;
1100
        $height = $this->height;
1101
1102
        // Check if crop is set
1103
        if ($this->crop) {
1104
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1105
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1106
1107
            if ($this->crop['start_x'] == 'left') {
1108
                $this->crop['start_x'] = 0;
1109
            } elseif ($this->crop['start_x'] == 'right') {
1110
                $this->crop['start_x'] = $this->width - $width;
1111
            } elseif ($this->crop['start_x'] == 'center') {
1112
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1113
            }
1114
1115
            if ($this->crop['start_y'] == 'top') {
1116
                $this->crop['start_y'] = 0;
1117
            } elseif ($this->crop['start_y'] == 'bottom') {
1118
                $this->crop['start_y'] = $this->height - $height;
1119
            } elseif ($this->crop['start_y'] == 'center') {
1120
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1121
            }
1122
1123
            $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.");
1124
        }
1125
1126
        // Calculate new width and height if keeping aspect-ratio.
1127
        if ($this->keepRatio) {
1128
1129
            $this->log("Keep aspect ratio.");
1130
1131
            // Crop-to-fit and both new width and height are set.
1132
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1133
1134
                // Use newWidth and newHeigh as width/height, image should fit in box.
1135
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1136
1137
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1138
1139
                // Both new width and height are set.
1140
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1141
                $ratioWidth  = $width  / $this->newWidth;
1142
                $ratioHeight = $height / $this->newHeight;
1143
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1144
                $this->newWidth  = round($width  / $ratio);
1145
                $this->newHeight = round($height / $ratio);
1146
                $this->log("New width and height was set.");
1147
1148
            } elseif (isset($this->newWidth)) {
1149
1150
                // Use new width as max-width
1151
                $factor = (float)$this->newWidth / (float)$width;
1152
                $this->newHeight = round($factor * $height);
1153
                $this->log("New width was set.");
1154
1155
            } elseif (isset($this->newHeight)) {
1156
1157
                // Use new height as max-hight
1158
                $factor = (float)$this->newHeight / (float)$height;
1159
                $this->newWidth = round($factor * $width);
1160
                $this->log("New height was set.");
1161
1162
            } else {
1163
1164
                // Use existing width and height as new width and height.
1165
                $this->newWidth = $width;
1166
                $this->newHeight = $height;
1167
            }
1168
            
1169
1170
            // Get image dimensions for pre-resize image.
1171
            if ($this->cropToFit || $this->fillToFit) {
1172
1173
                // Get relations of original & target image
1174
                $ratioWidth  = $width  / $this->newWidth;
1175
                $ratioHeight = $height / $this->newHeight;
1176
1177
                if ($this->cropToFit) {
1178
1179
                    // Use newWidth and newHeigh as defined width/height,
1180
                    // image should fit the area.
1181
                    $this->log("Crop to fit.");
1182
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1183
                    $this->cropWidth  = round($width  / $ratio);
1184
                    $this->cropHeight = round($height / $ratio);
1185
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1186
1187
                } elseif ($this->fillToFit) {
1188
1189
                    // Use newWidth and newHeigh as defined width/height,
1190
                    // image should fit the area.
1191
                    $this->log("Fill to fit.");
1192
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1193
                    $this->fillWidth  = round($width  / $ratio);
1194
                    $this->fillHeight = round($height / $ratio);
1195
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1196
                }
1197
            }
1198
        }
1199
1200
        // Crop, ensure to set new width and height
1201
        if ($this->crop) {
1202
            $this->log("Crop.");
1203
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1204
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1205
        }
1206
1207
        // Fill to fit, ensure to set new width and height
1208
        /*if ($this->fillToFit) {
1209
            $this->log("FillToFit.");
1210
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1211
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1212
        }*/
1213
1214
        // No new height or width is set, use existing measures.
1215
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1216
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1217
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1218
1219
        return $this;
1220
    }
1221
1222
1223
1224
    /**
1225
     * Re-calculate image dimensions when original image dimension has changed.
1226
     *
1227
     * @return $this
1228
     */
1229
    public function reCalculateDimensions()
1230
    {
1231
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1232
1233
        $this->newWidth  = $this->newWidthOrig;
1234
        $this->newHeight = $this->newHeightOrig;
1235
        $this->crop      = $this->cropOrig;
1236
1237
        $this->initDimensions()
1238
             ->calculateNewWidthAndHeight();
1239
1240
        return $this;
1241
    }
1242
1243
1244
1245
    /**
1246
     * Set extension for filename to save as.
1247
     *
1248
     * @param string $saveas extension to save image as
1249
     *
1250
     * @return $this
1251
     */
1252
    public function setSaveAsExtension($saveAs = null)
1253
    {
1254
        if (isset($saveAs)) {
1255
            $saveAs = strtolower($saveAs);
1256
            $this->checkFileExtension($saveAs);
1257
            $this->saveAs = $saveAs;
1258
            $this->extension = $saveAs;
1259
        }
1260
1261
        $this->log("Prepare to save image as: " . $this->extension);
1262
1263
        return $this;
1264
    }
1265
1266
1267
1268
    /**
1269
     * Set JPEG quality to use when saving image
1270
     *
1271
     * @param int $quality as the quality to set.
1272
     *
1273
     * @return $this
1274
     */
1275
    public function setJpegQuality($quality = null)
1276
    {
1277
        if ($quality) {
1278
            $this->useQuality = true;
1279
        }
1280
1281
        $this->quality = isset($quality)
1282
            ? $quality
1283
            : self::JPEG_QUALITY_DEFAULT;
1284
1285
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1286
            or $this->raiseError('Quality not in range.');
1287
1288
        $this->log("Setting JPEG quality to {$this->quality}.");
1289
1290
        return $this;
1291
    }
1292
1293
1294
1295
    /**
1296
     * Set PNG compressen algorithm to use when saving image
1297
     *
1298
     * @param int $compress as the algorithm to use.
1299
     *
1300
     * @return $this
1301
     */
1302
    public function setPngCompression($compress = null)
1303
    {
1304
        if ($compress) {
1305
            $this->useCompress = true;
1306
        }
1307
1308
        $this->compress = isset($compress)
1309
            ? $compress
1310
            : self::PNG_COMPRESSION_DEFAULT;
1311
1312
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1313
            or $this->raiseError('Quality not in range.');
1314
1315
        $this->log("Setting PNG compression level to {$this->compress}.");
1316
1317
        return $this;
1318
    }
1319
1320
1321
1322
    /**
1323
     * Use original image if possible, check options which affects image processing.
1324
     *
1325
     * @param boolean $useOrig default is to use original if possible, else set to false.
1326
     *
1327
     * @return $this
1328
     */
1329
    public function useOriginalIfPossible($useOrig = true)
1330
    {
1331
        if ($useOrig
1332
            && ($this->newWidth == $this->width)
1333
            && ($this->newHeight == $this->height)
1334
            && !$this->area
1335
            && !$this->crop
1336
            && !$this->cropToFit
1337
            && !$this->fillToFit
1338
            && !$this->filters
1339
            && !$this->sharpen
1340
            && !$this->emboss
1341
            && !$this->blur
1342
            && !$this->convolve
1343
            && !$this->palette
1344
            && !$this->useQuality
1345
            && !$this->useCompress
1346
            && !$this->saveAs
1347
            && !$this->rotateBefore
1348
            && !$this->rotateAfter
1349
            && !$this->autoRotate
1350
            && !$this->bgColor
1351
            && ($this->upscale === self::UPSCALE_DEFAULT)
1352
        ) {
1353
            $this->log("Using original image.");
1354
            $this->output($this->pathToImage);
1355
        }
1356
1357
        return $this;
1358
    }
1359
1360
1361
1362
    /**
1363
     * Generate filename to save file in cache.
1364
     *
1365
     * @param string  $base      as optional basepath for storing file.
1366
     * @param boolean $useSubdir use or skip the subdir part when creating the
1367
     *                           filename.
1368
     * @param string  $prefix    to add as part of filename
1369
     *
1370
     * @return $this
1371
     */
1372
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1373
    {
1374
        $filename     = basename($this->pathToImage);
1375
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1376
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1377
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1378
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1379
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1380
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1381
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1382
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1383
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1384
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1385
1386
        $saveAs = $this->normalizeFileExtension();
1387
        $saveAs = $saveAs ? "_$saveAs" : null;
1388
1389
        $copyStrat = null;
1390
        if ($this->copyStrategy === self::RESIZE) {
1391
            $copyStrat = "_rs";
1392
        }
1393
1394
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1395
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1396
1397
        $offset = isset($this->offset)
1398
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1399
            : null;
1400
1401
        $crop = $this->crop
1402
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1403
            : null;
1404
1405
        $filters = null;
1406
        if (isset($this->filters)) {
1407
            foreach ($this->filters as $filter) {
1408
                if (is_array($filter)) {
1409
                    $filters .= "_f{$filter['id']}";
1410
                    for ($i=1; $i<=$filter['argc']; $i++) {
1411
                        $filters .= "-".$filter["arg{$i}"];
1412
                    }
1413
                }
1414
            }
1415
        }
1416
1417
        $sharpen = $this->sharpen ? 's' : null;
1418
        $emboss  = $this->emboss  ? 'e' : null;
1419
        $blur    = $this->blur    ? 'b' : null;
1420
        $palette = $this->palette ? 'p' : null;
1421
1422
        $autoRotate = $this->autoRotate ? 'ar' : null;
1423
1424
        $optimize  = $this->jpegOptimize ? 'o' : null;
1425
        $optimize .= $this->pngFilter    ? 'f' : null;
1426
        $optimize .= $this->pngDeflate   ? 'd' : null;
1427
1428
        $convolve = null;
1429
        if ($this->convolve) {
1430
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1431
        }
1432
1433
        $upscale = null;
1434
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1435
            $upscale = '_nu';
1436
        }
1437
1438
        $subdir = null;
1439
        if ($useSubdir === true) {
1440
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1441
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1442
            $subdir .= '_';
1443
        }
1444
1445
        $file = $prefix . $subdir . $filename . $width . $height
1446
            . $offset . $crop . $cropToFit . $fillToFit
1447
            . $crop_x . $crop_y . $upscale
1448
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1449
            . $optimize . $compress
1450
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1451
            . $convolve . $copyStrat . $saveAs;
1452
1453
        return $this->setTarget($file, $base);
1454
    }
1455
1456
1457
1458
    /**
1459
     * Use cached version of image, if possible.
1460
     *
1461
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1462
     *
1463
     * @return $this
1464
     */
1465
    public function useCacheIfPossible($useCache = true)
1466
    {
1467
        if ($useCache && is_readable($this->cacheFileName)) {
1468
            $fileTime   = filemtime($this->pathToImage);
1469
            $cacheTime  = filemtime($this->cacheFileName);
1470
1471
            if ($fileTime <= $cacheTime) {
1472
                if ($this->useCache) {
1473
                    if ($this->verbose) {
1474
                        $this->log("Use cached file.");
1475
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1476
                    }
1477
                    $this->output($this->cacheFileName, $this->outputFormat);
1478
                } else {
1479
                    $this->log("Cache is valid but ignoring it by intention.");
1480
                }
1481
            } else {
1482
                $this->log("Original file is modified, ignoring cache.");
1483
            }
1484
        } else {
1485
            $this->log("Cachefile does not exists or ignoring it.");
1486
        }
1487
1488
        return $this;
1489
    }
1490
1491
1492
1493
    /**
1494
     * Load image from disk. Try to load image without verbose error message,
1495
     * if fail, load again and display error messages.
1496
     *
1497
     * @param string $src of image.
1498
     * @param string $dir as base directory where images are.
1499
     *
1500
     * @return $this
1501
     *
1502
     */
1503
    public function load($src = null, $dir = null)
1504
    {
1505
        if (isset($src)) {
1506
            $this->setSource($src, $dir);
1507
        }
1508
1509
        $this->loadImageDetails();
1510
1511
        $imageAsString = file_get_contents($this->pathToImage);
1512
        $this->image = imagecreatefromstring($imageAsString);
1513
        if ($this->image === false) {
1514
            throw new Exception("Could not load image.");
1515
        }
1516
1517
        /* Removed v0.7.7
1518
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1519
            $type = $this->getPngType();
1520
            $hasFewColors = imagecolorstotal($this->image);
1521
1522
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1523
                if ($this->verbose) {
1524
                    $this->log("Handle this image as a palette image.");
1525
                }
1526
                $this->palette = true;
1527
            }
1528
        }
1529
        */
1530
1531
        if ($this->verbose) {
1532
            $this->log("### Image successfully loaded from file.");
1533
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1534
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1535
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1536
            $index = imagecolortransparent($this->image);
1537
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1538
        }
1539
1540
        return $this;
1541
    }
1542
1543
1544
1545
    /**
1546
     * Get the type of PNG image.
1547
     *
1548
     * @param string $filename to use instead of default.
1549
     *
1550
     * @return int as the type of the png-image
1551
     *
1552
     */
1553
    public function getPngType($filename = null)
1554
    {
1555
        $filename = $filename ? $filename : $this->pathToImage;
1556
1557
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1558
1559
        if ($this->verbose) {
1560
            $this->log("Checking png type of: " . $filename);
1561
            $this->log($this->getPngTypeAsString($pngType));
1562
        }
1563
1564
        return $pngType;
1565
    }
1566
1567
1568
1569
    /**
1570
     * Get the type of PNG image as a verbose string.
1571
     *
1572
     * @param integer $type     to use, default is to check the type.
1573
     * @param string  $filename to use instead of default.
1574
     *
1575
     * @return int as the type of the png-image
1576
     *
1577
     */
1578
    private function getPngTypeAsString($pngType = null, $filename = null)
1579
    {
1580
        if ($filename || !$pngType) {
1581
            $pngType = $this->getPngType($filename);
1582
        }
1583
1584
        $index = imagecolortransparent($this->image);
1585
        $transparent = null;
1586
        if ($index != -1) {
1587
            $transparent = " (transparent)";
1588
        }
1589
1590
        switch ($pngType) {
1591
1592
            case self::PNG_GREYSCALE:
1593
                $text = "PNG is type 0, Greyscale$transparent";
1594
                break;
1595
1596
            case self::PNG_RGB:
1597
                $text = "PNG is type 2, RGB$transparent";
1598
                break;
1599
1600
            case self::PNG_RGB_PALETTE:
1601
                $text = "PNG is type 3, RGB with palette$transparent";
1602
                break;
1603
1604
            case self::PNG_GREYSCALE_ALPHA:
1605
                $text = "PNG is type 4, Greyscale with alpha channel";
1606
                break;
1607
1608
            case self::PNG_RGB_ALPHA:
1609
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1610
                break;
1611
1612
            default:
1613
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1614
        }
1615
1616
        return $text;
1617
    }
1618
1619
1620
1621
1622
    /**
1623
     * Calculate number of colors in an image.
1624
     *
1625
     * @param resource $im the image.
1626
     *
1627
     * @return int
1628
     */
1629
    private function colorsTotal($im)
1630
    {
1631
        if (imageistruecolor($im)) {
1632
            $this->log("Colors as true color.");
1633
            $h = imagesy($im);
1634
            $w = imagesx($im);
1635
            $c = array();
1636
            for ($x=0; $x < $w; $x++) {
1637
                for ($y=0; $y < $h; $y++) {
1638
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1639
                }
1640
            }
1641
            return count($c);
1642
        } else {
1643
            $this->log("Colors as palette.");
1644
            return imagecolorstotal($im);
1645
        }
1646
    }
1647
1648
1649
1650
    /**
1651
     * Preprocess image before rezising it.
1652
     *
1653
     * @return $this
1654
     */
1655
    public function preResize()
1656
    {
1657
        $this->log("### Pre-process before resizing");
1658
1659
        // Rotate image
1660
        if ($this->rotateBefore) {
1661
            $this->log("Rotating image.");
1662
            $this->rotate($this->rotateBefore, $this->bgColor)
1663
                 ->reCalculateDimensions();
1664
        }
1665
1666
        // Auto-rotate image
1667
        if ($this->autoRotate) {
1668
            $this->log("Auto rotating image.");
1669
            $this->rotateExif()
1670
                 ->reCalculateDimensions();
1671
        }
1672
1673
        // Scale the original image before starting
1674
        if (isset($this->scale)) {
1675
            $this->log("Scale by {$this->scale}%");
1676
            $newWidth  = $this->width * $this->scale / 100;
1677
            $newHeight = $this->height * $this->scale / 100;
1678
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1679
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1680
            $this->image = $img;
1681
            $this->width = $newWidth;
1682
            $this->height = $newHeight;
1683
        }
1684
1685
        return $this;
1686
    }
1687
1688
1689
1690
    /**
1691
     * Resize or resample the image while resizing.
1692
     *
1693
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1694
     *
1695
     * @return $this
1696
     */
1697
     public function setCopyResizeStrategy($strategy)
1698
     {
1699
         $this->copyStrategy = $strategy;
1700
         return $this;
1701
     }
1702
1703
1704
1705
    /**
1706
     * Resize and or crop the image.
1707
     *
1708
     * @return void
1709
     */
1710
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1711
    {
1712
        if($this->copyStrategy == self::RESIZE) {
1713
            $this->log("Copy by resize");
1714
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1715
        } else {
1716
            $this->log("Copy by resample");
1717
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1718
        }
1719
    }
1720
1721
1722
1723
    /**
1724
     * Resize and or crop the image.
1725
     *
1726
     * @return $this
1727
     */
1728
    public function resize()
1729
    {
1730
1731
        $this->log("### Starting to Resize()");
1732
        $this->log("Upscale = '$this->upscale'");
1733
1734
        // Only use a specified area of the image, $this->offset is defining the area to use
1735
        if (isset($this->offset)) {
1736
1737
            $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']}");
1738
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1739
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1740
            $this->image = $img;
1741
            $this->width = $this->offset['width'];
1742
            $this->height = $this->offset['height'];
1743
        }
1744
1745
        if ($this->crop) {
1746
1747
            // Do as crop, take only part of image
1748
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1749
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1750
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1751
            $this->image = $img;
1752
            $this->width = $this->crop['width'];
1753
            $this->height = $this->crop['height'];
1754
        }
1755
1756
        if (!$this->upscale) {
1757
            // Consider rewriting the no-upscale code to fit within this if-statement,
1758
            // likely to be more readable code.
1759
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1760
        }
1761
1762
        if ($this->cropToFit) {
1763
1764
            // Resize by crop to fit
1765
            $this->log("Resizing using strategy - Crop to fit");
1766
1767
            if (!$this->upscale 
1768
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1769
                $this->log("Resizing - smaller image, do not upscale.");
1770
1771
                $posX = 0;
1772
                $posY = 0;
1773
                $cropX = 0;
1774
                $cropY = 0;
1775
1776
                if ($this->newWidth > $this->width) {
1777
                    $posX = round(($this->newWidth - $this->width) / 2);
1778
                }
1779
                if ($this->newWidth < $this->width) {
1780
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1781
                }
1782
1783
                if ($this->newHeight > $this->height) {
1784
                    $posY = round(($this->newHeight - $this->height) / 2);
1785
                }
1786
                if ($this->newHeight < $this->height) {
1787
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1788
                }
1789
                $this->log(" cwidth: $this->cropWidth");
1790
                $this->log(" cheight: $this->cropHeight");
1791
                $this->log(" nwidth: $this->newWidth");
1792
                $this->log(" nheight: $this->newHeight");
1793
                $this->log(" width: $this->width");
1794
                $this->log(" height: $this->height");
1795
                $this->log(" posX: $posX");
1796
                $this->log(" posY: $posY");
1797
                $this->log(" cropX: $cropX");
1798
                $this->log(" cropY: $cropY");
1799
1800
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1801
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1802
            } else {
1803
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1804
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1805
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1806
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1807
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1808
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1809
            }
1810
1811
            $this->image = $imageResized;
1812
            $this->width = $this->newWidth;
1813
            $this->height = $this->newHeight;
1814
1815
        } elseif ($this->fillToFit) {
1816
1817
            // Resize by fill to fit
1818
            $this->log("Resizing using strategy - Fill to fit");
1819
1820
            $posX = 0;
1821
            $posY = 0;
1822
1823
            $ratioOrig = $this->width / $this->height;
1824
            $ratioNew  = $this->newWidth / $this->newHeight;
1825
1826
            // Check ratio for landscape or portrait
1827
            if ($ratioOrig < $ratioNew) {
1828
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1829
            } else {
1830
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1831
            }
1832
1833
            if (!$this->upscale
1834
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1835
            ) {
1836
1837
                $this->log("Resizing - smaller image, do not upscale.");
1838
                $posX = round(($this->newWidth - $this->width) / 2);
1839
                $posY = round(($this->newHeight - $this->height) / 2);
1840
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1841
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1842
1843
            } else {
1844
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1845
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1846
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1847
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1848
            }
1849
1850
            $this->image = $imageResized;
1851
            $this->width = $this->newWidth;
1852
            $this->height = $this->newHeight;
1853
1854
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1855
1856
            // Resize it
1857
            $this->log("Resizing, new height and/or width");
1858
1859
            if (!$this->upscale
1860
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1861
            ) {
1862
                $this->log("Resizing - smaller image, do not upscale.");
1863
1864
                if (!$this->keepRatio) {
1865
                    $this->log("Resizing - stretch to fit selected.");
1866
1867
                    $posX = 0;
1868
                    $posY = 0;
1869
                    $cropX = 0;
1870
                    $cropY = 0;
1871
1872
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1873
                        $posX = round(($this->newWidth - $this->width) / 2);
1874
                        $posY = round(($this->newHeight - $this->height) / 2);
1875
                    } elseif ($this->newWidth > $this->width) {
1876
                        $posX = round(($this->newWidth - $this->width) / 2);
1877
                        $cropY = round(($this->height - $this->newHeight) / 2);
1878
                    } elseif ($this->newHeight > $this->height) {
1879
                        $posY = round(($this->newHeight - $this->height) / 2);
1880
                        $cropX = round(($this->width - $this->newWidth) / 2);
1881
                    }
1882
1883
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1884
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1885
                    $this->image = $imageResized;
1886
                    $this->width = $this->newWidth;
1887
                    $this->height = $this->newHeight;
1888
                }
1889
            } else {
1890
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1891
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1892
                $this->image = $imageResized;
1893
                $this->width = $this->newWidth;
1894
                $this->height = $this->newHeight;
1895
            }
1896
        }
1897
1898
        return $this;
1899
    }
1900
1901
1902
1903
    /**
1904
     * Postprocess image after rezising image.
1905
     *
1906
     * @return $this
1907
     */
1908
    public function postResize()
1909
    {
1910
        $this->log("### Post-process after resizing");
1911
1912
        // Rotate image
1913
        if ($this->rotateAfter) {
1914
            $this->log("Rotating image.");
1915
            $this->rotate($this->rotateAfter, $this->bgColor);
1916
        }
1917
1918
        // Apply filters
1919
        if (isset($this->filters) && is_array($this->filters)) {
1920
1921
            foreach ($this->filters as $filter) {
1922
                $this->log("Applying filter {$filter['type']}.");
1923
1924
                switch ($filter['argc']) {
1925
1926
                    case 0:
1927
                        imagefilter($this->image, $filter['type']);
1928
                        break;
1929
1930
                    case 1:
1931
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1932
                        break;
1933
1934
                    case 2:
1935
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1936
                        break;
1937
1938
                    case 3:
1939
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1940
                        break;
1941
1942
                    case 4:
1943
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1944
                        break;
1945
                }
1946
            }
1947
        }
1948
1949
        // Convert to palette image
1950
        if ($this->palette) {
1951
            $this->log("Converting to palette image.");
1952
            $this->trueColorToPalette();
1953
        }
1954
1955
        // Blur the image
1956
        if ($this->blur) {
1957
            $this->log("Blur.");
1958
            $this->blurImage();
1959
        }
1960
1961
        // Emboss the image
1962
        if ($this->emboss) {
1963
            $this->log("Emboss.");
1964
            $this->embossImage();
1965
        }
1966
1967
        // Sharpen the image
1968
        if ($this->sharpen) {
1969
            $this->log("Sharpen.");
1970
            $this->sharpenImage();
1971
        }
1972
1973
        // Custom convolution
1974
        if ($this->convolve) {
1975
            //$this->log("Convolve: " . $this->convolve);
1976
            $this->imageConvolution();
1977
        }
1978
1979
        return $this;
1980
    }
1981
1982
1983
1984
    /**
1985
     * Rotate image using angle.
1986
     *
1987
     * @param float $angle        to rotate image.
1988
     * @param int   $anglebgColor to fill image with if needed.
1989
     *
1990
     * @return $this
1991
     */
1992
    public function rotate($angle, $bgColor)
1993
    {
1994
        $this->log("Rotate image " . $angle . " degrees with filler color.");
1995
1996
        $color = $this->getBackgroundColor();
1997
        $this->image = imagerotate($this->image, $angle, $color);
1998
1999
        $this->width  = imagesx($this->image);
2000
        $this->height = imagesy($this->image);
2001
2002
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2003
2004
        return $this;
2005
    }
2006
2007
2008
2009
    /**
2010
     * Rotate image using information in EXIF.
2011
     *
2012
     * @return $this
2013
     */
2014
    public function rotateExif()
2015
    {
2016
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2017
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2018
            return $this;
2019
        }
2020
2021
        $exif = exif_read_data($this->pathToImage);
2022
2023
        if (!empty($exif['Orientation'])) {
2024
            switch ($exif['Orientation']) {
2025
                case 3:
2026
                    $this->log("Autorotate 180.");
2027
                    $this->rotate(180, $this->bgColor);
2028
                    break;
2029
2030
                case 6:
2031
                    $this->log("Autorotate -90.");
2032
                    $this->rotate(-90, $this->bgColor);
2033
                    break;
2034
2035
                case 8:
2036
                    $this->log("Autorotate 90.");
2037
                    $this->rotate(90, $this->bgColor);
2038
                    break;
2039
2040
                default:
2041
                    $this->log("Autorotate ignored, unknown value as orientation.");
2042
            }
2043
        } else {
2044
            $this->log("Autorotate ignored, no orientation in EXIF.");
2045
        }
2046
2047
        return $this;
2048
    }
2049
2050
2051
2052
    /**
2053
     * Convert true color image to palette image, keeping alpha.
2054
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2055
     *
2056
     * @return void
2057
     */
2058
    public function trueColorToPalette()
2059
    {
2060
        $img = imagecreatetruecolor($this->width, $this->height);
2061
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2062
        imagecolortransparent($img, $bga);
2063
        imagefill($img, 0, 0, $bga);
2064
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2065
        imagetruecolortopalette($img, false, 255);
2066
        imagesavealpha($img, true);
2067
2068
        if (imageistruecolor($this->image)) {
2069
            $this->log("Matching colors with true color image.");
2070
            imagecolormatch($this->image, $img);
2071
        }
2072
2073
        $this->image = $img;
2074
    }
2075
2076
2077
2078
    /**
2079
     * Sharpen image using image convolution.
2080
     *
2081
     * @return $this
2082
     */
2083
    public function sharpenImage()
2084
    {
2085
        $this->imageConvolution('sharpen');
2086
        return $this;
2087
    }
2088
2089
2090
2091
    /**
2092
     * Emboss image using image convolution.
2093
     *
2094
     * @return $this
2095
     */
2096
    public function embossImage()
2097
    {
2098
        $this->imageConvolution('emboss');
2099
        return $this;
2100
    }
2101
2102
2103
2104
    /**
2105
     * Blur image using image convolution.
2106
     *
2107
     * @return $this
2108
     */
2109
    public function blurImage()
2110
    {
2111
        $this->imageConvolution('blur');
2112
        return $this;
2113
    }
2114
2115
2116
2117
    /**
2118
     * Create convolve expression and return arguments for image convolution.
2119
     *
2120
     * @param string $expression constant string which evaluates to a list of
2121
     *                           11 numbers separated by komma or such a list.
2122
     *
2123
     * @return array as $matrix (3x3), $divisor and $offset
2124
     */
2125
    public function createConvolveArguments($expression)
2126
    {
2127
        // Check of matching constant
2128
        if (isset($this->convolves[$expression])) {
2129
            $expression = $this->convolves[$expression];
2130
        }
2131
2132
        $part = explode(',', $expression);
2133
        $this->log("Creating convolution expressen: $expression");
2134
2135
        // Expect list of 11 numbers, split by , and build up arguments
2136
        if (count($part) != 11) {
2137
            throw new Exception(
2138
                "Missmatch in argument convolve. Expected comma-separated string with
2139
                11 float values. Got $expression."
2140
            );
2141
        }
2142
2143
        array_walk($part, function ($item, $key) {
2144
            if (!is_numeric($item)) {
2145
                throw new Exception("Argument to convolve expression should be float but is not.");
2146
            }
2147
        });
2148
2149
        return array(
2150
            array(
2151
                array($part[0], $part[1], $part[2]),
2152
                array($part[3], $part[4], $part[5]),
2153
                array($part[6], $part[7], $part[8]),
2154
            ),
2155
            $part[9],
2156
            $part[10],
2157
        );
2158
    }
2159
2160
2161
2162
    /**
2163
     * Add custom expressions (or overwrite existing) for image convolution.
2164
     *
2165
     * @param array $options Key value array with strings to be converted
2166
     *                       to convolution expressions.
2167
     *
2168
     * @return $this
2169
     */
2170
    public function addConvolveExpressions($options)
2171
    {
2172
        $this->convolves = array_merge($this->convolves, $options);
2173
        return $this;
2174
    }
2175
2176
2177
2178
    /**
2179
     * Image convolution.
2180
     *
2181
     * @param string $options A string with 11 float separated by comma.
2182
     *
2183
     * @return $this
2184
     */
2185
    public function imageConvolution($options = null)
2186
    {
2187
        // Use incoming options or use $this.
2188
        $options = $options ? $options : $this->convolve;
2189
2190
        // Treat incoming as string, split by +
2191
        $this->log("Convolution with '$options'");
2192
        $options = explode(":", $options);
2193
2194
        // Check each option if it matches constant value
2195
        foreach ($options as $option) {
2196
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2197
            imageconvolution($this->image, $matrix, $divisor, $offset);
2198
        }
2199
2200
        return $this;
2201
    }
2202
2203
2204
2205
    /**
2206
     * Set default background color between 000000-FFFFFF or if using
2207
     * alpha 00000000-FFFFFF7F.
2208
     *
2209
     * @param string $color as hex value.
2210
     *
2211
     * @return $this
2212
    */
2213
    public function setDefaultBackgroundColor($color)
2214
    {
2215
        $this->log("Setting default background color to '$color'.");
2216
2217
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2218
            throw new Exception(
2219
                "Background color needs a hex value of 6 or 8
2220
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2221
                Current value was: '$color'."
2222
            );
2223
        }
2224
2225
        $red    = hexdec(substr($color, 0, 2));
2226
        $green  = hexdec(substr($color, 2, 2));
2227
        $blue   = hexdec(substr($color, 4, 2));
2228
2229
        $alpha = (strlen($color) == 8)
2230
            ? hexdec(substr($color, 6, 2))
2231
            : null;
2232
2233
        if (($red < 0 || $red > 255)
2234
            || ($green < 0 || $green > 255)
2235
            || ($blue < 0 || $blue > 255)
2236
            || ($alpha < 0 || $alpha > 127)
2237
        ) {
2238
            throw new Exception(
2239
                "Background color out of range. Red, green blue
2240
                should be 00-FF and alpha should be 00-7F.
2241
                Current value was: '$color'."
2242
            );
2243
        }
2244
2245
        $this->bgColor = strtolower($color);
2246
        $this->bgColorDefault = array(
2247
            'red'   => $red,
2248
            'green' => $green,
2249
            'blue'  => $blue,
2250
            'alpha' => $alpha
2251
        );
2252
2253
        return $this;
2254
    }
2255
2256
2257
2258
    /**
2259
     * Get the background color.
2260
     *
2261
     * @param resource $img the image to work with or null if using $this->image.
2262
     *
2263
     * @return color value or null if no background color is set.
2264
    */
2265
    private function getBackgroundColor($img = null)
2266
    {
2267
        $img = isset($img) ? $img : $this->image;
2268
2269
        if ($this->bgColorDefault) {
2270
2271
            $red   = $this->bgColorDefault['red'];
2272
            $green = $this->bgColorDefault['green'];
2273
            $blue  = $this->bgColorDefault['blue'];
2274
            $alpha = $this->bgColorDefault['alpha'];
2275
2276
            if ($alpha) {
2277
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2278
            } else {
2279
                $color = imagecolorallocate($img, $red, $green, $blue);
2280
            }
2281
2282
            return $color;
2283
2284
        } else {
2285
            return 0;
2286
        }
2287
    }
2288
2289
2290
2291
    /**
2292
     * Create a image and keep transparency for png and gifs.
2293
     *
2294
     * @param int $width of the new image.
2295
     * @param int $height of the new image.
2296
     *
2297
     * @return image resource.
2298
    */
2299
    private function createImageKeepTransparency($width, $height)
2300
    {
2301
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2302
        $img = imagecreatetruecolor($width, $height);
2303
        imagealphablending($img, false);
2304
        imagesavealpha($img, true);
2305
2306
        $index = $this->image
2307
            ? imagecolortransparent($this->image)
2308
            : -1;
2309
2310
        if ($index != -1) {
2311
2312
            imagealphablending($img, true);
2313
            $transparent = imagecolorsforindex($this->image, $index);
2314
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2315
            imagefill($img, 0, 0, $color);
2316
            $index = imagecolortransparent($img, $color);
2317
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2318
2319
        } elseif ($this->bgColorDefault) {
2320
2321
            $color = $this->getBackgroundColor($img);
2322
            imagefill($img, 0, 0, $color);
2323
            $this->Log("Filling image with background color.");
2324
        }
2325
2326
        return $img;
2327
    }
2328
2329
2330
2331
    /**
2332
     * Set optimizing  and post-processing options.
2333
     *
2334
     * @param array $options with config for postprocessing with external tools.
2335
     *
2336
     * @return $this
2337
     */
2338
    public function setPostProcessingOptions($options)
2339
    {
2340
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2341
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2342
        } else {
2343
            $this->jpegOptimizeCmd = null;
2344
        }
2345
2346
        if (isset($options['png_filter']) && $options['png_filter']) {
2347
            $this->pngFilterCmd = $options['png_filter_cmd'];
2348
        } else {
2349
            $this->pngFilterCmd = null;
2350
        }
2351
2352
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2353
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2354
        } else {
2355
            $this->pngDeflateCmd = null;
2356
        }
2357
2358
        return $this;
2359
    }
2360
2361
2362
2363
    /**
2364
     * Find out the type (file extension) for the image to be saved.
2365
     *
2366
     * @return string as image extension.
2367
     */
2368
    protected function getTargetImageExtension()
2369
    {
2370
        // switch on mimetype
2371
        if (isset($this->extension)) {
2372
            return strtolower($this->extension);
2373
        } else {
2374
            return substr(image_type_to_extension($this->fileType), 1);
2375
        }
2376
    }
2377
2378
2379
2380
    /**
2381
     * Save image.
2382
     *
2383
     * @param string  $src       as target filename.
2384
     * @param string  $base      as base directory where to store images.
2385
     * @param boolean $overwrite or not, default to always overwrite file.
2386
     *
2387
     * @return $this or false if no folder is set.
2388
     */
2389
    public function save($src = null, $base = null, $overwrite = true)
2390
    {
2391
        if (isset($src)) {
2392
            $this->setTarget($src, $base);
2393
        }
2394
2395
        if ($overwrite === false && is_file($this->cacheFileName)) {
2396
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2397
            return;
2398
        }
2399
2400
        is_writable($this->saveFolder)
2401
            or $this->raiseError('Target directory is not writable.');
2402
2403
        $type = $this->getTargetImageExtension();
2404
        $this->Log("Saving image as " . $type);
2405
        switch($type) {
2406
2407
            case 'jpeg':
2408
            case 'jpg':
2409
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2410
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2411
2412
                // Use JPEG optimize if defined
2413
                if ($this->jpegOptimizeCmd) {
2414
                    if ($this->verbose) {
2415
                        clearstatcache();
2416
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2417
                    }
2418
                    $res = array();
2419
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2420
                    exec($cmd, $res);
2421
                    $this->log($cmd);
2422
                    $this->log($res);
2423
                }
2424
                break;
2425
2426
            case 'gif':
2427
                $this->Log("Saving image as GIF to cache.");
2428
                imagegif($this->image, $this->cacheFileName);
2429
                break;
2430
2431
            case 'webp':
2432
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2433
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2434
                break;
2435
2436
            case 'png':
2437
            default:
2438
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2439
2440
                // Turn off alpha blending and set alpha flag
2441
                imagealphablending($this->image, false);
2442
                imagesavealpha($this->image, true);
2443
                imagepng($this->image, $this->cacheFileName, $this->compress);
2444
2445
                // Use external program to filter PNG, if defined
2446
                if ($this->pngFilterCmd) {
2447
                    if ($this->verbose) {
2448
                        clearstatcache();
2449
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2450
                    }
2451
                    $res = array();
2452
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2453
                    exec($cmd, $res);
2454
                    $this->Log($cmd);
2455
                    $this->Log($res);
2456
                }
2457
2458
                // Use external program to deflate PNG, if defined
2459
                if ($this->pngDeflateCmd) {
2460
                    if ($this->verbose) {
2461
                        clearstatcache();
2462
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2463
                    }
2464
                    $res = array();
2465
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2466
                    exec($cmd, $res);
2467
                    $this->Log($cmd);
2468
                    $this->Log($res);
2469
                }
2470
                break;
2471
        }
2472
2473
        if ($this->verbose) {
2474
            clearstatcache();
2475
            $this->log("Saved image to cache.");
2476
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2477
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2478
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2479
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2480
            $index = imagecolortransparent($this->image);
2481
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2482
        }
2483
2484
        return $this;
2485
    }
2486
2487
2488
2489
    /**
2490
     * Convert image from one colorpsace/color profile to sRGB without
2491
     * color profile.
2492
     *
2493
     * @param string  $src      of image.
2494
     * @param string  $dir      as base directory where images are.
2495
     * @param string  $cache    as base directory where to store images.
2496
     * @param string  $iccFile  filename of colorprofile.
2497
     * @param boolean $useCache or not, default to always use cache.
2498
     *
2499
     * @return string | boolean false if no conversion else the converted
2500
     *                          filename.
2501
     */
2502
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2503
    {
2504
        if ($this->verbose) {
2505
            $this->log("# Converting image to sRGB colorspace.");
2506
        }
2507
2508
        if (!class_exists("Imagick")) {
2509
            $this->log(" Ignoring since Imagemagick is not installed.");
2510
            return false;
2511
        }
2512
2513
        // Prepare
2514
        $this->setSaveFolder($cache)
2515
             ->setSource($src, $dir)
2516
             ->generateFilename(null, false, 'srgb_');
2517
2518
        // Check if the cached version is accurate.
2519
        if ($useCache && is_readable($this->cacheFileName)) {
2520
            $fileTime  = filemtime($this->pathToImage);
2521
            $cacheTime = filemtime($this->cacheFileName);
2522
2523
            if ($fileTime <= $cacheTime) {
2524
                $this->log(" Using cached version: " . $this->cacheFileName);
2525
                return $this->cacheFileName;
2526
            }
2527
        }
2528
2529
        // Only covert if cachedir is writable
2530
        if (is_writable($this->saveFolder)) {
2531
            // Load file and check if conversion is needed
2532
            $image      = new Imagick($this->pathToImage);
2533
            $colorspace = $image->getImageColorspace();
2534
            $this->log(" Current colorspace: " . $colorspace);
2535
2536
            $profiles      = $image->getImageProfiles('*', false);
2537
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2538
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2539
2540
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2541
                $this->log(" Converting to sRGB.");
2542
2543
                $sRGBicc = file_get_contents($iccFile);
2544
                $image->profileImage('icc', $sRGBicc);
2545
2546
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2547
                $image->writeImage($this->cacheFileName);
2548
                return $this->cacheFileName;
2549
            }
2550
        }
2551
2552
        return false;
2553
    }
2554
2555
2556
2557
    /**
2558
     * Create a hard link, as an alias, to the cached file.
2559
     *
2560
     * @param string $alias where to store the link,
2561
     *                      filename without extension.
2562
     *
2563
     * @return $this
2564
     */
2565
    public function linkToCacheFile($alias)
2566
    {
2567
        if ($alias === null) {
2568
            $this->log("Ignore creating alias.");
2569
            return $this;
2570
        }
2571
2572
        if (is_readable($alias)) {
2573
            unlink($alias);
2574
        }
2575
2576
        $res = link($this->cacheFileName, $alias);
2577
2578
        if ($res) {
2579
            $this->log("Created an alias as: $alias");
2580
        } else {
2581
            $this->log("Failed to create the alias: $alias");
2582
        }
2583
2584
        return $this;
2585
    }
2586
2587
2588
2589
    /**
2590
     * Add HTTP header for output together with image.
2591
     *
2592
     * @param string $type  the header type such as "Cache-Control"
2593
     * @param string $value the value to use
2594
     *
2595
     * @return void
2596
     */
2597
    public function addHTTPHeader($type, $value)
2598
    {
2599
        $this->HTTPHeader[$type] = $value;
2600
    }
2601
2602
2603
2604
    /**
2605
     * Output image to browser using caching.
2606
     *
2607
     * @param string $file   to read and output, default is to
2608
     *                       use $this->cacheFileName
2609
     * @param string $format set to json to output file as json
2610
     *                       object with details
2611
     *
2612
     * @return void
2613
     */
2614
    public function output($file = null, $format = null)
2615
    {
2616
        if (is_null($file)) {
2617
            $file = $this->cacheFileName;
2618
        }
2619
2620
        if (is_null($format)) {
2621
            $format = $this->outputFormat;
2622
        }
2623
2624
        $this->log("Output format is: $format");
2625
2626
        if (!$this->verbose && $format == 'json') {
2627
            header('Content-type: application/json');
2628
            echo $this->json($file);
2629
            exit;
2630
        } elseif ($format == 'ascii') {
2631
            header('Content-type: text/plain');
2632
            echo $this->ascii($file);
2633
            exit;
2634
        }
2635
2636
        $this->log("Outputting image: $file");
2637
2638
        // Get image modification time
2639
        clearstatcache();
2640
        $lastModified = filemtime($file);
2641
        $lastModifiedFormat = "D, d M Y H:i:s";
2642
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2643
2644
        if (!$this->verbose) {
2645
            $header = "Last-Modified: $gmdate GMT";
2646
            header($header);
2647
            $this->fastTrackCache->addHeader($header);
2648
            $this->fastTrackCache->setLastModified($lastModified);
2649
        }
2650
2651
        foreach ($this->HTTPHeader as $key => $val) {
2652
            $header = "$key: $val";
2653
            header($header);
2654
            $this->fastTrackCache->addHeader($header);
2655
        }
2656
2657
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2658
2659
            if ($this->verbose) {
2660
                $this->log("304 not modified");
2661
                $this->verboseOutput();
2662
                exit;
2663
            }
2664
2665
            header("HTTP/1.0 304 Not Modified");
2666
            if (CIMAGE_DEBUG) {
2667
                trace(__CLASS__ . " 304");
2668
            }
2669
2670
        } else {
2671
2672
            // Get details on image
2673
            $info = getimagesize($file);
2674
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
2675
            $mime = $info['mime'];
2676
            $size = filesize($file);
2677
2678
            if ($this->verbose) {
2679
                $this->log("Last-Modified: " . $gmdate . " GMT");
2680
                $this->log("Content-type: " . $mime);
2681
                $this->log("Content-length: " . $size);
2682
                $this->verboseOutput();
2683
2684
                if (is_null($this->verboseFileName)) {
2685
                    exit;
2686
                }
2687
            }
2688
2689
            $header = "Content-type: $mime";
2690
            header($header);
2691
            $this->fastTrackCache->addHeaderOnOutput($header);
2692
2693
            $header = "Content-length: $size";
2694
            header($header);
2695
            $this->fastTrackCache->addHeaderOnOutput($header);
2696
2697
            $this->fastTrackCache->setSource($file);
2698
            $this->fastTrackCache->writeToCache();
2699
            if (CIMAGE_DEBUG) {
2700
                trace(__CLASS__ . " 200");
2701
            }
2702
            readfile($file);
2703
        }
2704
2705
        exit;
2706
    }
2707
2708
2709
2710
    /**
2711
     * Create a JSON object from the image details.
2712
     *
2713
     * @param string $file the file to output.
2714
     *
2715
     * @return string json-encoded representation of the image.
2716
     */
2717
    public function json($file = null)
2718
    {
2719
        $file = $file ? $file : $this->cacheFileName;
2720
2721
        $details = array();
2722
2723
        clearstatcache();
2724
2725
        $details['src']       = $this->imageSrc;
2726
        $lastModified         = filemtime($this->pathToImage);
2727
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2728
2729
        $details['cache']       = basename($this->cacheFileName);
2730
        $lastModified           = filemtime($this->cacheFileName);
2731
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2732
2733
        $this->load($file);
2734
2735
        $details['filename']    = basename($file);
2736
        $details['mimeType']    = $this->getMimeType($this->fileType);
2737
        $details['width']       = $this->width;
2738
        $details['height']      = $this->height;
2739
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2740
        $details['size']        = filesize($file);
2741
        $details['colors'] = $this->colorsTotal($this->image);
2742
        $details['includedFiles'] = count(get_included_files());
2743
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2744
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2745
        $details['memoryLimit'] = ini_get('memory_limit');
2746
2747
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2748
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2749
        }
2750
2751
        if ($details['mimeType'] == 'image/png') {
2752
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2753
        }
2754
2755
        $options = null;
2756
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2757
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2758
        }
2759
2760
        return json_encode($details, $options);
2761
    }
2762
2763
2764
2765
    /**
2766
     * Set options for creating ascii version of image.
2767
     *
2768
     * @param array $options empty to use default or set options to change.
2769
     *
2770
     * @return void.
2771
     */
2772
    public function setAsciiOptions($options = array())
2773
    {
2774
        $this->asciiOptions = $options;
2775
    }
2776
2777
2778
2779
    /**
2780
     * Create an ASCII version from the image details.
2781
     *
2782
     * @param string $file the file to output.
2783
     *
2784
     * @return string ASCII representation of the image.
2785
     */
2786
    public function ascii($file = null)
2787
    {
2788
        $file = $file ? $file : $this->cacheFileName;
2789
2790
        $asciiArt = new CAsciiArt();
2791
        $asciiArt->setOptions($this->asciiOptions);
2792
        return $asciiArt->createFromFile($file);
2793
    }
2794
2795
2796
2797
    /**
2798
     * Log an event if verbose mode.
2799
     *
2800
     * @param string $message to log.
2801
     *
2802
     * @return this
2803
     */
2804
    public function log($message)
2805
    {
2806
        if ($this->verbose) {
2807
            $this->log[] = $message;
2808
        }
2809
2810
        return $this;
2811
    }
2812
2813
2814
2815
    /**
2816
     * Do verbose output to a file.
2817
     *
2818
     * @param string $fileName where to write the verbose output.
2819
     *
2820
     * @return void
2821
     */
2822
    public function setVerboseToFile($fileName)
2823
    {
2824
        $this->log("Setting verbose output to file.");
2825
        $this->verboseFileName = $fileName;
2826
    }
2827
2828
2829
2830
    /**
2831
     * Do verbose output and print out the log and the actual images.
2832
     *
2833
     * @return void
2834
     */
2835
    private function verboseOutput()
2836
    {
2837
        $log = null;
2838
        $this->log("As JSON: \n" . $this->json());
2839
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2840
        $this->log("Memory limit: " . ini_get('memory_limit'));
2841
2842
        $included = get_included_files();
2843
        $this->log("Included files: " . count($included));
2844
2845
        foreach ($this->log as $val) {
2846
            if (is_array($val)) {
2847
                foreach ($val as $val1) {
2848
                    $log .= htmlentities($val1) . '<br/>';
2849
                }
2850
            } else {
2851
                $log .= htmlentities($val) . '<br/>';
2852
            }
2853
        }
2854
2855
        if (!is_null($this->verboseFileName)) {
2856
            file_put_contents(
2857
                $this->verboseFileName,
2858
                str_replace("<br/>", "\n", $log)
2859
            );
2860
        } else {
2861
            echo <<<EOD
2862
<h1>CImage Verbose Output</h1>
2863
<pre>{$log}</pre>
2864
EOD;
2865
        }
2866
    }
2867
2868
2869
2870
    /**
2871
     * Raise error, enables to implement a selection of error methods.
2872
     *
2873
     * @param string $message the error message to display.
2874
     *
2875
     * @return void
2876
     * @throws Exception
2877
     */
2878
    private function raiseError($message)
2879
    {
2880
        throw new Exception($message);
2881
    }
2882
}
2883