Code

< 40 %
40-60 %
> 60 %
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
     * Do lossy output using external postprocessing tools.
160
     */
161
    private $lossy = null;
162
163
164
165
    /**
166
     * Verbose mode to print out a trace and display the created image
167
     */
168
    private $verbose = false;
169
170
171
172
    /**
173
     * Keep a log/trace on what happens
174
     */
175
    private $log = array();
176
177
178
179
    /**
180
     * Handle image as palette image
181
     */
182
    private $palette;
183
184
185
186
    /**
187
     * Target filename, with path, to save resulting image in.
188
     */
189
    private $cacheFileName;
190
191
192
193
    /**
194
     * Set a format to save image as, or null to use original format.
195
     */
196
    private $saveAs;
197
198
199
    /**
200
     * Path to command for lossy optimize, for example pngquant.
201
     */
202
    private $pngLossy;
203
    private $pngLossyCmd;
204
205
206
207
    /**
208
     * Path to command for filter optimize, for example optipng.
209
     */
210
    private $pngFilter;
211
    private $pngFilterCmd;
212
213
214
215
    /**
216
     * Path to command for deflate optimize, for example pngout.
217
     */
218
    private $pngDeflate;
219
    private $pngDeflateCmd;
220
221
222
223
    /**
224
     * Path to command to optimize jpeg images, for example jpegtran or null.
225
     */
226
     private $jpegOptimize;
227
     private $jpegOptimizeCmd;
228
229
230
231
    /**
232
     * Image dimensions, calculated from loaded image.
233
     */
234
    private $width;  // Calculated from source image
235
    private $height; // Calculated from source image
236
237
238
    /**
239
     * New image dimensions, incoming as argument or calculated.
240
     */
241
    private $newWidth;
242
    private $newWidthOrig;  // Save original value
243
    private $newHeight;
244
    private $newHeightOrig; // Save original value
245
246
247
    /**
248
     * Change target height & width when different dpr, dpr 2 means double image dimensions.
249
     */
250
    private $dpr = 1;
251
252
253
    /**
254
     * Always upscale images, even if they are smaller than target image.
255
     */
256
    const UPSCALE_DEFAULT = true;
257
    private $upscale = self::UPSCALE_DEFAULT;
258
259
260
261
    /**
262
     * Array with details on how to crop, incoming as argument and calculated.
263
     */
264
    public $crop;
265
    public $cropOrig; // Save original value
266
267
268
    /**
269
     * String with details on how to do image convolution. String
270
     * should map a key in the $convolvs array or be a string of
271
     * 11 float values separated by comma. The first nine builds
272
     * up the matrix, then divisor and last offset.
273
     */
274
    private $convolve;
275
276
277
    /**
278
     * Custom convolution expressions, matrix 3x3, divisor and offset.
279
     */
280
    private $convolves = array(
281
        'lighten'       => '0,0,0, 0,12,0, 0,0,0, 9, 0',
282
        'darken'        => '0,0,0, 0,6,0, 0,0,0, 9, 0',
283
        'sharpen'       => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0',
284
        'sharpen-alt'   => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0',
285
        'emboss'        => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0',
286
        'emboss-alt'    => '-2,-1,0, -1,1,1, 0,1,2, 1, 0',
287
        'blur'          => '1,1,1, 1,15,1, 1,1,1, 23, 0',
288
        'gblur'         => '1,2,1, 2,4,2, 1,2,1, 16, 0',
289
        'edge'          => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0',
290
        'edge-alt'      => '0,1,0, 1,-4,1, 0,1,0, 1, 0',
291
        'draw'          => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0',
292
        'mean'          => '1,1,1, 1,1,1, 1,1,1, 9, 0',
293
        'motion'        => '1,0,0, 0,1,0, 0,0,1, 3, 0',
294
    );
295
296
297
    /**
298
     * Resize strategy to fill extra area with background color.
299
     * True or false.
300
     */
301
    private $fillToFit;
302
303
304
305
    /**
306
     * To store value for option scale.
307
     */
308
    private $scale;
309
310
311
312
    /**
313
     * To store value for option.
314
     */
315
    private $rotateBefore;
316
317
318
319
    /**
320
     * To store value for option.
321
     */
322
    private $rotateAfter;
323
324
325
326
    /**
327
     * To store value for option.
328
     */
329
    private $autoRotate;
330
331
332
333
    /**
334
     * To store value for option.
335
     */
336
    private $sharpen;
337
338
339
340
    /**
341
     * To store value for option.
342
     */
343
    private $emboss;
344
345
346
347
    /**
348
     * To store value for option.
349
     */
350
    private $blur;
351
352
353
354
    /**
355
     * Used with option area to set which parts of the image to use.
356
     */
357
    private $offset;
358
359
360
361
    /**
362
     * Calculate target dimension for image when using fill-to-fit resize strategy.
363
     */
364
    private $fillWidth;
365
    private $fillHeight;
366
367
368
369
    /**
370
     * Allow remote file download, default is to disallow remote file download.
371
     */
372
    private $allowRemote = false;
373
374
375
376
    /**
377
     * Path to cache for remote download.
378
     */
379
    private $remoteCache;
380
381
382
383
    /**
384
     * Pattern to recognize a remote file.
385
     */
386
    //private $remotePattern = '#^[http|https]://#';
387
    private $remotePattern = '#^https?://#';
388
389
390
391
    /**
392
     * Use the cache if true, set to false to ignore the cached file.
393
     */
394
    private $useCache = true;
395
396
397
    /**
398
    * Disable the fasttrackCacke to start with, inject an object to enable it.
399
    */
400
    private $fastTrackCache = null;
401
402
403
404
    /*
405
     * Set whitelist for valid hostnames from where remote source can be
406
     * downloaded.
407
     */
408
    private $remoteHostWhitelist = null;
409
410
411
412
    /*
413
     * Do verbose logging to file by setting this to a filename.
414
     */
415
    private $verboseFileName = null;
416
417
418
419
    /*
420
     * Output to ascii can take som options as an array.
421
     */
422
    private $asciiOptions = array();
423
424
425
426
    /*
427
     * Use interlaced progressive mode for JPEG images.
428
     */
429
    private $interlace = false;
430
431
432
433
    /*
434 7
     * Image copy strategy, defaults to RESAMPLE.
435
     */
436 7
     const RESIZE = 1;
437 7
     const RESAMPLE = 2;
438 7
     private $copyStrategy = NULL;
439
440
441
442
    /**
443
     * Properties, the class is mutable and the method setOptions()
444
     * decides (partly) what properties are created.
445
     *
446
     * @todo Clean up these and check if and how they are used
447
     */
448
449
    public $keepRatio;
450
    public $cropToFit;
451
    private $cropWidth;
452
    private $cropHeight;
453
    public $crop_x;
454
    public $crop_y;
455
    public $filters;
456
    private $attr; // Calculated from source image
457
458
459
460
461
    /**
462
     * Constructor, can take arguments to init the object.
463
     *
464
     * @param string $imageSrc    filename which may contain subdirectory.
465
     * @param string $imageFolder path to root folder for images.
466
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
467 2
     * @param string $saveName    name of target file when saveing.
468
     */
469 2
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
470 2
    {
471
        $this->setSource($imageSrc, $imageFolder);
472
        $this->setTarget($saveFolder, $saveName);
473
    }
474
475
476
477
    /**
478
     * Inject object and use it, must be available as member.
479
     *
480
     * @param string $property to set as object.
481
     * @param object $object   to set to property.
482
     *
483
     * @return $this
484
     */
485
    public function injectDependency($property, $object)
486
    {
487
        if (!property_exists($this, $property)) {
488
            $this->raiseError("Injecting unknown property.");
489
        }
490
        $this->$property = $object;
491
        return $this;
492
    }
493
494
495
496
    /**
497
     * Set verbose mode.
498
     *
499 2
     * @param boolean $mode true or false to enable and disable verbose mode,
500
     *                      default is true.
501 2
     *
502 2
     * @return $this
503
     */
504 2
    public function setVerbose($mode = true)
505
    {
506 2
        $this->verbose = $mode;
507
        return $this;
508
    }
509
510
511
512
    /**
513
     * Set save folder, base folder for saving cache files.
514
     *
515
     * @todo clean up how $this->saveFolder is used in other methods.
516
     *
517
     * @param string $path where to store cached files.
518
     *
519 2
     * @return $this
520
     */
521 2
    public function setSaveFolder($path)
522 2
    {
523
        $this->saveFolder = $path;
524 2
        return $this;
525
    }
526 2
527 2
528 2
529 2
    /**
530
     * Use cache or not.
531 2
     *
532
     * @param boolean $use true or false to use cache.
533
     *
534
     * @return $this
535
     */
536
    public function useCache($use = true)
537
    {
538
        $this->useCache = $use;
539
        return $this;
540
    }
541
542
543 2
544
    /**
545 2
     * Create and save a dummy image. Use dimensions as stated in
546 2
     * $this->newWidth, or $width or default to 100 (same for height.
547 2
     *
548
     * @param integer $width  use specified width for image dimension.
549
     * @param integer $height use specified width for image dimension.
550
     *
551
     * @return $this
552
     */
553
    public function createDummyImage($width = null, $height = null)
554
    {
555
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
556
        $this->newHeight = $this->newHeight ?: $height ?: 100;
557
558
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
559
560 2
        return $this;
561
    }
562 2
563 2
564
565 2
    /**
566 2
     * Allow or disallow remote image download.
567 2
     *
568
     * @param boolean $allow   true or false to enable and disable.
569
     * @param string  $cache   path to cache dir.
570
     * @param string  $pattern to use to detect if its a remote file.
571
     *
572
     * @return $this
573
     */
574
    public function setRemoteDownload($allow, $cache, $pattern = null)
575
    {
576
        $this->allowRemote = $allow;
577
        $this->remoteCache = $cache;
578
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
579
580 3
        $this->log(
581
            "Set remote download to: "
582 3
            . ($this->allowRemote ? "true" : "false")
583 1
            . " using pattern "
584 1
            . $this->remotePattern
585
        );
586
587 2
        return $this;
588 2
    }
589 2
590
591 2
592
    /**
593 2
     * Check if the image resource is a remote file or not.
594 2
     *
595 2
     * @param string $src check if src is remote.
596
     *
597
     * @return boolean true if $src is a remote file, else false.
598
     */
599
    public function isRemoteSource($src)
600
    {
601
        $remote = preg_match($this->remotePattern, $src);
602
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
603
        return !!$remote;
604
    }
605
606
607
608
    /**
609
     * Set whitelist for valid hostnames from where remote source can be
610
     * downloaded.
611
     *
612
     * @param array $whitelist with regexp hostnames to allow download from.
613
     *
614
     * @return $this
615
     */
616
    public function setRemoteHostWhitelist($whitelist = null)
617
    {
618
        $this->remoteHostWhitelist = $whitelist;
619
        $this->log(
620
            "Setting remote host whitelist to: "
621
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
622
        );
623
        return $this;
624
    }
625
626 2
627
628 2
    /**
629
     * Check if the hostname for the remote image, is on a whitelist,
630 2
     * if the whitelist is defined.
631
     *
632
     * @param string $src the remote source.
633
     *
634 2
     * @return boolean true if hostname on $src is in the whitelist, else false.
635
     */
636
    public function isRemoteSourceOnWhitelist($src)
637
    {
638
        if (is_null($this->remoteHostWhitelist)) {
639
            $this->log("Remote host on whitelist not configured - allowing.");
640
            return true;
641
        }
642
643
        $whitelist = new CWhitelist();
644
        $hostname = parse_url($src, PHP_URL_HOST);
645
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
646
647
        $this->log(
648
            "Remote host is on whitelist: "
649
            . ($allow ? "true" : "false")
650
        );
651
        return $allow;
652
    }
653
654
655
656
    /**
657
     * Check if file extension is valid as a file extension.
658
     *
659
     * @param string $extension of image file.
660
     *
661
     * @return $this
662
     */
663
    private function checkFileExtension($extension)
664
    {
665
        $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp');
666
667
        in_array(strtolower($extension), $valid)
668
            or $this->raiseError('Not a valid file extension.');
669
670
        return $this;
671
    }
672
673
674
675
    /**
676
     * Normalize the file extension.
677
     *
678
     * @param string $extension of image file or skip to use internal.
679
     *
680
     * @return string $extension as a normalized file extension.
681
     */
682
    private function normalizeFileExtension($extension = null)
683
    {
684
        $extension = strtolower($extension ? $extension : $this->extension);
685
686
        if ($extension == 'jpeg') {
687
                $extension = 'jpg';
688 7
        }
689
690 7
        return $extension;
691 7
    }
692 7
693 7
694
695
    /**
696 2
     * Download a remote image and return path to its local copy.
697
     *
698
     * @param string $src remote path to image.
699
     *
700
     * @return string as path to downloaded remote source.
701 2
     */
702
    public function downloadRemoteSource($src)
703
    {
704
        if (!$this->isRemoteSourceOnWhitelist($src)) {
705
            throw new Exception("Hostname is not on whitelist for remote sources.");
706 2
        }
707 2
708 2
        $remote = new CRemoteImage();
709
710 2
        if (!is_writable($this->remoteCache)) {
711
            $this->log("The remote cache is not writable.");
712
        }
713
714
        $remote->setCache($this->remoteCache);
715
        $remote->useCache($this->useCache);
716
        $src = $remote->download($src);
717
718
        $this->log("Remote HTTP status: " . $remote->getStatus());
719
        $this->log("Remote item is in local cache: $src");
720
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
721
722
        return $src;
723
    }
724 7
725
726 7
727 7
    /**
728 7
     * Set source file to use as image source.
729
     *
730
     * @param string $src of image.
731 2
     * @param string $dir as optional base directory where images are.
732
     *
733
     * @return $this
734
     */
735 2
    public function setSource($src, $dir = null)
736
    {
737
        if (!isset($src)) {
738 2
            $this->imageSrc = null;
739 2
            $this->pathToImage = null;
740
            return $this;
741 2
        }
742
743
        if ($this->allowRemote && $this->isRemoteSource($src)) {
744
            $src = $this->downloadRemoteSource($src);
745
            $dir = null;
746
        }
747
748
        if (!isset($dir)) {
749
            $dir = dirname($src);
750
            $src = basename($src);
751 2
        }
752
753 2
        $this->imageSrc     = ltrim($src, '/');
754
        $imageFolder        = rtrim($dir, '/');
755
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
756
757
        return $this;
758
    }
759
760
761
762
    /**
763
     * Set target file.
764
     *
765
     * @param string $src of target image.
766
     * @param string $dir as optional base directory where images are stored.
767
     *                    Uses $this->saveFolder if null.
768
     *
769
     * @return $this
770
     */
771
    public function setTarget($src = null, $dir = null)
772
    {
773
        if (!isset($src)) {
774
            $this->cacheFileName = null;
775
            return $this;
776
        }
777
778
        if (isset($dir)) {
779
            $this->saveFolder = rtrim($dir, '/');
780
        }
781
782
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
783
784
        // Sanitize filename
785
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
786
        $this->log("The cache file name is: " . $this->cacheFileName);
787
788
        return $this;
789
    }
790
791
792
793
    /**
794
     * Get filename of target file.
795
     *
796
     * @return Boolean|String as filename of target or false if not set.
797
     */
798
    public function getTarget()
799
    {
800
        return $this->cacheFileName;
801
    }
802
803
804
805
    /**
806
     * Set options to use when processing image.
807
     *
808
     * @param array $args used when processing image.
809
     *
810
     * @return $this
811
     */
812
    public function setOptions($args)
813
    {
814
        $this->log("Set new options for processing image.");
815
816
        $defaults = array(
817
            // Options for calculate dimensions
818
            'newWidth'    => null,
819
            'newHeight'   => null,
820
            'aspectRatio' => null,
821
            'keepRatio'   => true,
822
            'cropToFit'   => false,
823
            'fillToFit'   => null,
824
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
825
            'area'        => null, //'0,0,0,0',
826
            'upscale'     => self::UPSCALE_DEFAULT,
827
828
            // Options for caching or using original
829
            'useCache'    => true,
830
            'useOriginal' => true,
831
832
            // Pre-processing, before resizing is done
833
            'scale'        => null,
834
            'rotateBefore' => null,
835
            'autoRotate'  => false,
836
837
            // General options
838
            'bgColor'     => null,
839
840
            // Post-processing, after resizing is done
841
            'palette'     => null,
842
            'filters'     => null,
843
            'sharpen'     => null,
844
            'emboss'      => null,
845
            'blur'        => null,
846
            'convolve'       => null,
847
            'rotateAfter' => null,
848
            'interlace' => null,
849
850
            // Output format
851
            'outputFormat' => null,
852
            'dpr'          => 1,
853
854
            // Postprocessing using external tools
855
            'lossy' => null,
856
        );
857
858
        // Convert crop settings from string to array
859
        if (isset($args['crop']) && !is_array($args['crop'])) {
860
            $pices = explode(',', $args['crop']);
861
            $args['crop'] = array(
862
                'width'   => $pices[0],
863
                'height'  => $pices[1],
864
                'start_x' => $pices[2],
865
                'start_y' => $pices[3],
866
            );
867
        }
868
869
        // Convert area settings from string to array
870
        if (isset($args['area']) && !is_array($args['area'])) {
871
                $pices = explode(',', $args['area']);
872
                $args['area'] = array(
873
                    'top'    => $pices[0],
874
                    'right'  => $pices[1],
875
                    'bottom' => $pices[2],
876
                    'left'   => $pices[3],
877
                );
878
        }
879
880
        // Convert filter settings from array of string to array of array
881
        if (isset($args['filters']) && is_array($args['filters'])) {
882
            foreach ($args['filters'] as $key => $filterStr) {
883
                $parts = explode(',', $filterStr);
884
                $filter = $this->mapFilter($parts[0]);
885
                $filter['str'] = $filterStr;
886
                for ($i=1; $i<=$filter['argc']; $i++) {
887
                    if (isset($parts[$i])) {
888
                        $filter["arg{$i}"] = $parts[$i];
889
                    } else {
890
                        throw new Exception(
891
                            'Missing arg to filter, review how many arguments are needed at
892
                            http://php.net/manual/en/function.imagefilter.php'
893
                        );
894
                    }
895
                }
896
                $args['filters'][$key] = $filter;
897
            }
898
        }
899
900
        // Merge default arguments with incoming and set properties.
901
        //$args = array_merge_recursive($defaults, $args);
902
        $args = array_merge($defaults, $args);
903
        foreach ($defaults as $key => $val) {
904
            $this->{$key} = $args[$key];
905
        }
906
907
        if ($this->bgColor) {
908
            $this->setDefaultBackgroundColor($this->bgColor);
909
        }
910
911
        // Save original values to enable re-calculating
912
        $this->newWidthOrig  = $this->newWidth;
913
        $this->newHeightOrig = $this->newHeight;
914
        $this->cropOrig      = $this->crop;
915
916
        return $this;
917
    }
918
919
920
921
    /**
922
     * Map filter name to PHP filter and id.
923
     *
924
     * @param string $name the name of the filter.
925
     *
926
     * @return array with filter settings
927
     * @throws Exception
928
     */
929
    private function mapFilter($name)
930
    {
931
        $map = array(
932
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
933
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
934
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
935
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
936
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
937
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
938
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
939
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
940
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
941
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
942
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
943
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
944
        );
945
946
        if (isset($map[$name])) {
947
            return $map[$name];
948
        } else {
949
            throw new Exception('No such filter.');
950
        }
951
    }
952
953
954
955
    /**
956
     * Load image details from original image file.
957
     *
958
     * @param string $file the file to load or null to use $this->pathToImage.
959
     *
960
     * @return $this
961
     * @throws Exception
962
     */
963
    public function loadImageDetails($file = null)
964
    {
965
        $file = $file ? $file : $this->pathToImage;
966
967
        is_readable($file)
968
            or $this->raiseError('Image file does not exist.');
969
970
        $info = list($this->width, $this->height, $this->fileType) = getimagesize($file);
971
        if (empty($info)) {
972
            // To support webp
973
            $this->fileType = false;
974
            if (function_exists("exif_imagetype")) {
975
                $this->fileType = exif_imagetype($file);
976
                if ($this->fileType === false) {
977
                    if (function_exists("imagecreatefromwebp")) {
978
                        $webp = imagecreatefromwebp($file);
979
                        if ($webp !== false) {
980
                            $this->width  = imagesx($webp);
981
                            $this->height = imagesy($webp);
982
                            $this->fileType = IMG_WEBP;
983
                        }
984
                    }
985
                }
986
            }
987
        }
988
989
        if (!$this->fileType) {
990
            throw new Exception("Loading image details, the file doesn't seem to be a valid image.");
991
        }
992
993
        if ($this->verbose) {
994
            $this->log("Loading image details for: {$file}");
995
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
996
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
997
            $this->log(" Image mimetype: " . $this->getMimeType());
998
        }
999
1000
        return $this;
1001
    }
1002
1003
1004
1005
    /**
1006
     * Get mime type for image type.
1007
     *
1008
     * @return $this
1009
     * @throws Exception
1010
     */
1011
    protected function getMimeType()
1012
    {
1013
        if ($this->fileType === IMG_WEBP) {
1014
            return "image/webp";
1015
        }
1016
1017
        return image_type_to_mime_type($this->fileType);
1018
    }
1019
1020
1021
1022
    /**
1023
     * Init new width and height and do some sanity checks on constraints, before any
1024
     * processing can be done.
1025
     *
1026
     * @return $this
1027
     * @throws Exception
1028
     */
1029
    public function initDimensions()
1030
    {
1031
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1032
1033
        // width as %
1034
        if ($this->newWidth
1035
            && $this->newWidth[strlen($this->newWidth)-1] == '%') {
1036
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1037
            $this->log("Setting new width based on % to {$this->newWidth}");
1038
        }
1039
1040
        // height as %
1041
        if ($this->newHeight
1042
            && $this->newHeight[strlen($this->newHeight)-1] == '%') {
1043
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1044
            $this->log("Setting new height based on % to {$this->newHeight}");
1045
        }
1046
1047
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
1048
1049
        // width & height from aspect ratio
1050
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1051
            if ($this->aspectRatio >= 1) {
1052
                $this->newWidth   = $this->width;
1053
                $this->newHeight  = $this->width / $this->aspectRatio;
1054
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1055
1056
            } else {
1057
                $this->newHeight  = $this->height;
1058
                $this->newWidth   = $this->height * $this->aspectRatio;
1059
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1060
            }
1061
1062
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1063
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1064
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1065
1066
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1067
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1068
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1069
        }
1070
1071
        // Change width & height based on dpr
1072
        if ($this->dpr != 1) {
1073
            if (!is_null($this->newWidth)) {
1074
                $this->newWidth  = round($this->newWidth * $this->dpr);
1075
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1076
            }
1077
            if (!is_null($this->newHeight)) {
1078
                $this->newHeight = round($this->newHeight * $this->dpr);
1079
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1080
            }
1081
        }
1082
1083
        // Check values to be within domain
1084
        is_null($this->newWidth)
1085
            or is_numeric($this->newWidth)
1086
            or $this->raiseError('Width not numeric');
1087
1088
        is_null($this->newHeight)
1089
            or is_numeric($this->newHeight)
1090
            or $this->raiseError('Height not numeric');
1091
1092
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1093
1094
        return $this;
1095
    }
1096
1097
1098
1099
    /**
1100
     * Calculate new width and height of image, based on settings.
1101
     *
1102
     * @return $this
1103
     */
1104
    public function calculateNewWidthAndHeight()
1105
    {
1106
        // Crop, use cropped width and height as base for calulations
1107
        $this->log("Calculate new width and height.");
1108
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1109
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1110
1111
        // Check if there is an area to crop off
1112
        if (isset($this->area)) {
1113
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1114
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1115
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1116
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1117
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1118
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1119
            $this->width  = $this->offset['width'];
1120
            $this->height = $this->offset['height'];
1121
            $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']}%.");
1122
            $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.");
1123
        }
1124
1125
        $width  = $this->width;
1126
        $height = $this->height;
1127
1128
        // Check if crop is set
1129
        if ($this->crop) {
1130
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1131
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1132
1133
            if ($this->crop['start_x'] == 'left') {
1134
                $this->crop['start_x'] = 0;
1135
            } elseif ($this->crop['start_x'] == 'right') {
1136
                $this->crop['start_x'] = $this->width - $width;
1137
            } elseif ($this->crop['start_x'] == 'center') {
1138
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1139
            }
1140
1141
            if ($this->crop['start_y'] == 'top') {
1142
                $this->crop['start_y'] = 0;
1143
            } elseif ($this->crop['start_y'] == 'bottom') {
1144
                $this->crop['start_y'] = $this->height - $height;
1145
            } elseif ($this->crop['start_y'] == 'center') {
1146
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1147
            }
1148
1149
            $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.");
1150
        }
1151
1152
        // Calculate new width and height if keeping aspect-ratio.
1153
        if ($this->keepRatio) {
1154
1155
            $this->log("Keep aspect ratio.");
1156
1157
            // Crop-to-fit and both new width and height are set.
1158
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1159
1160
                // Use newWidth and newHeigh as width/height, image should fit in box.
1161
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1162
1163
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1164
1165
                // Both new width and height are set.
1166
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1167
                $ratioWidth  = $width  / $this->newWidth;
1168
                $ratioHeight = $height / $this->newHeight;
1169
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1170
                $this->newWidth  = round($width  / $ratio);
1171
                $this->newHeight = round($height / $ratio);
1172
                $this->log("New width and height was set.");
1173
1174
            } elseif (isset($this->newWidth)) {
1175
1176
                // Use new width as max-width
1177
                $factor = (float)$this->newWidth / (float)$width;
1178
                $this->newHeight = round($factor * $height);
1179
                $this->log("New width was set.");
1180
1181
            } elseif (isset($this->newHeight)) {
1182
1183
                // Use new height as max-hight
1184
                $factor = (float)$this->newHeight / (float)$height;
1185
                $this->newWidth = round($factor * $width);
1186
                $this->log("New height was set.");
1187
1188
            } else {
1189
1190
                // Use existing width and height as new width and height.
1191
                $this->newWidth = $width;
1192
                $this->newHeight = $height;
1193
            }
1194
1195
1196
            // Get image dimensions for pre-resize image.
1197
            if ($this->cropToFit || $this->fillToFit) {
1198
1199
                // Get relations of original & target image
1200
                $ratioWidth  = $width  / $this->newWidth;
1201
                $ratioHeight = $height / $this->newHeight;
1202
1203
                if ($this->cropToFit) {
1204
1205
                    // Use newWidth and newHeigh as defined width/height,
1206
                    // image should fit the area.
1207
                    $this->log("Crop to fit.");
1208
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1209
                    $this->cropWidth  = round($width  / $ratio);
1210
                    $this->cropHeight = round($height / $ratio);
1211
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1212
1213
                } elseif ($this->fillToFit) {
1214
1215
                    // Use newWidth and newHeigh as defined width/height,
1216
                    // image should fit the area.
1217
                    $this->log("Fill to fit.");
1218
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1219
                    $this->fillWidth  = round($width  / $ratio);
1220
                    $this->fillHeight = round($height / $ratio);
1221
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1222
                }
1223
            }
1224
        }
1225
1226
        // Crop, ensure to set new width and height
1227
        if ($this->crop) {
1228
            $this->log("Crop.");
1229
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1230
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1231
        }
1232
1233
        // Fill to fit, ensure to set new width and height
1234
        /*if ($this->fillToFit) {
1235
            $this->log("FillToFit.");
1236
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1237
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1238
        }*/
1239
1240
        // No new height or width is set, use existing measures.
1241
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1242
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1243
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1244
1245
        return $this;
1246
    }
1247
1248
1249
1250
    /**
1251
     * Re-calculate image dimensions when original image dimension has changed.
1252
     *
1253
     * @return $this
1254
     */
1255
    public function reCalculateDimensions()
1256
    {
1257
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1258
1259
        $this->newWidth  = $this->newWidthOrig;
1260
        $this->newHeight = $this->newHeightOrig;
1261
        $this->crop      = $this->cropOrig;
1262
1263
        $this->initDimensions()
1264
             ->calculateNewWidthAndHeight();
1265
1266
        return $this;
1267
    }
1268
1269
1270
1271
    /**
1272
     * Set extension for filename to save as.
1273
     *
1274
     * @param string $saveas extension to save image as
1275
     *
1276
     * @return $this
1277
     */
1278
    public function setSaveAsExtension($saveAs = null)
1279
    {
1280
        if (isset($saveAs)) {
1281
            $saveAs = strtolower($saveAs);
1282
            $this->checkFileExtension($saveAs);
1283
            $this->saveAs = $saveAs;
1284
            $this->extension = $saveAs;
1285
        }
1286
1287
        $this->log("Prepare to save image as: " . $this->extension);
1288
1289
        return $this;
1290
    }
1291
1292
1293
1294
    /**
1295
     * Set JPEG quality to use when saving image
1296
     *
1297
     * @param int $quality as the quality to set.
1298
     *
1299
     * @return $this
1300
     */
1301
    public function setJpegQuality($quality = null)
1302
    {
1303
        if ($quality) {
1304 2
            $this->useQuality = true;
1305
        }
1306 2
1307 2
        $this->quality = isset($quality)
1308 2
            ? $quality
1309 2
            : self::JPEG_QUALITY_DEFAULT;
1310 2
1311 2
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1312 2
            or $this->raiseError('Quality not in range.');
1313 2
1314 2
        $this->log("Setting JPEG quality to {$this->quality}.");
1315 2
1316 2
        return $this;
1317
    }
1318 2
1319 2
1320
1321 2
    /**
1322 2
     * Set PNG compressen algorithm to use when saving image
1323
     *
1324
     * @param int $compress as the algorithm to use.
1325
     *
1326 2
     * @return $this
1327 2
     */
1328
    public function setPngCompression($compress = null)
1329 2
    {
1330 2
        if ($compress) {
1331 2
            $this->useCompress = true;
1332
        }
1333 2
1334 2
        $this->compress = isset($compress)
1335 2
            ? $compress
1336
            : self::PNG_COMPRESSION_DEFAULT;
1337 2
1338 2
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1339
            or $this->raiseError('Quality not in range.');
1340
1341
        $this->log("Setting PNG compression level to {$this->compress}.");
1342
1343
        return $this;
1344
    }
1345
1346
1347
1348
    /**
1349 2
     * Use original image if possible, check options which affects image processing.
1350 2
     *
1351 2
     * @param boolean $useOrig default is to use original if possible, else set to false.
1352 2
     *
1353
     * @return $this
1354 2
     */
1355
    public function useOriginalIfPossible($useOrig = true)
1356 2
    {
1357 2
        if ($useOrig
1358 2
            && ($this->newWidth == $this->width)
1359
            && ($this->newHeight == $this->height)
1360 2
            && !$this->area
1361 2
            && !$this->crop
1362
            && !$this->cropToFit
1363
            && !$this->fillToFit
1364
            && !$this->filters
1365 2
            && !$this->sharpen
1366 2
            && !$this->emboss
1367
            && !$this->blur
1368
            && !$this->convolve
1369
            && !$this->palette
1370 2
            && !$this->useQuality
1371 2
            && !$this->useCompress
1372
            && !$this->saveAs
1373
            && !$this->rotateBefore
1374
            && !$this->rotateAfter
1375
            && !$this->autoRotate
1376
            && !$this->bgColor
1377 2
            && ($this->upscale === self::UPSCALE_DEFAULT)
1378 2
            && !$this->lossy
1379 2
        ) {
1380 2
            $this->log("Using original image.");
1381 2
            $this->output($this->pathToImage);
1382 2
        }
1383 2
1384
        return $this;
1385 2
    }
1386
1387
1388
1389
    /**
1390
     * Generate filename to save file in cache.
1391
     *
1392
     * @param string  $base      as optional basepath for storing file.
1393
     * @param boolean $useSubdir use or skip the subdir part when creating the
1394
     *                           filename.
1395
     * @param string  $prefix    to add as part of filename
1396
     *
1397
     * @return $this
1398
     */
1399
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1400
    {
1401
        $filename     = basename($this->pathToImage);
1402
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1403
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1404
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1405
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1406
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1407
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1408
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1409
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1410
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1411
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1412
        $lossy        = $this->lossy        ? "_l"                       : null;
1413
        $interlace    = $this->interlace    ? "_i"                       : null;
1414
1415
        $saveAs = $this->normalizeFileExtension();
1416
        $saveAs = $saveAs ? "_$saveAs" : null;
1417
1418
        $copyStrat = null;
1419
        if ($this->copyStrategy === self::RESIZE) {
1420
            $copyStrat = "_rs";
1421
        }
1422
1423
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1424
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1425
1426
        $offset = isset($this->offset)
1427
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1428
            : null;
1429
1430
        $crop = $this->crop
1431
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1432
            : null;
1433
1434
        $filters = null;
1435
        if (isset($this->filters)) {
1436
            foreach ($this->filters as $filter) {
1437
                if (is_array($filter)) {
1438
                    $filters .= "_f{$filter['id']}";
1439
                    for ($i=1; $i<=$filter['argc']; $i++) {
1440
                        $filters .= "-".$filter["arg{$i}"];
1441
                    }
1442
                }
1443
            }
1444
        }
1445
1446
        $sharpen = $this->sharpen ? 's' : null;
1447
        $emboss  = $this->emboss  ? 'e' : null;
1448
        $blur    = $this->blur    ? 'b' : null;
1449
        $palette = $this->palette ? 'p' : null;
1450
1451
        $autoRotate = $this->autoRotate ? 'ar' : null;
1452
1453
        $optimize  = $this->jpegOptimize ? 'o' : null;
1454
        $optimize .= $this->pngFilter    ? 'f' : null;
1455
        $optimize .= $this->pngDeflate   ? 'd' : null;
1456
1457
        $convolve = null;
1458
        if ($this->convolve) {
1459
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1460
        }
1461
1462
        $upscale = null;
1463
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1464
            $upscale = '_nu';
1465
        }
1466
1467
        $subdir = null;
1468
        if ($useSubdir === true) {
1469
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1470
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1471
            $subdir .= '_';
1472
        }
1473
1474
        $file = $prefix . $subdir . $filename . $width . $height
1475
            . $offset . $crop . $cropToFit . $fillToFit
1476
            . $crop_x . $crop_y . $upscale
1477
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1478
            . $optimize . $compress
1479
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1480
            . $convolve . $copyStrat . $lossy . $interlace . $saveAs;
1481
1482
        return $this->setTarget($file, $base);
1483
    }
1484
1485
1486
1487
    /**
1488
     * Use cached version of image, if possible.
1489
     *
1490
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1491
     *
1492
     * @return $this
1493
     */
1494
    public function useCacheIfPossible($useCache = true)
1495
    {
1496
        if ($useCache && is_readable($this->cacheFileName)) {
1497
            $fileTime   = filemtime($this->pathToImage);
1498
            $cacheTime  = filemtime($this->cacheFileName);
1499
1500
            if ($fileTime <= $cacheTime) {
1501
                if ($this->useCache) {
1502
                    if ($this->verbose) {
1503
                        $this->log("Use cached file.");
1504
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1505
                    }
1506
                    $this->output($this->cacheFileName, $this->outputFormat);
1507
                } else {
1508
                    $this->log("Cache is valid but ignoring it by intention.");
1509
                }
1510
            } else {
1511
                $this->log("Original file is modified, ignoring cache.");
1512
            }
1513
        } else {
1514
            $this->log("Cachefile does not exists or ignoring it.");
1515
        }
1516
1517
        return $this;
1518
    }
1519
1520
1521
1522
    /**
1523
     * Load image from disk. Try to load image without verbose error message,
1524
     * if fail, load again and display error messages.
1525
     *
1526
     * @param string $src of image.
1527
     * @param string $dir as base directory where images are.
1528
     *
1529
     * @return $this
1530
     *
1531
     */
1532
    public function load($src = null, $dir = null)
1533
    {
1534
        if (isset($src)) {
1535
            $this->setSource($src, $dir);
1536
        }
1537
1538
        $this->loadImageDetails();
1539
1540
        if ($this->fileType === IMG_WEBP) {
1541
            $this->image = imagecreatefromwebp($this->pathToImage);
1542
        } else {
1543
            $imageAsString = file_get_contents($this->pathToImage);
1544
            $this->image = imagecreatefromstring($imageAsString);
1545
        }
1546
        if ($this->image === false) {
1547
            throw new Exception("Could not load image.");
1548
        }
1549
1550
        /* Removed v0.7.7
1551
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1552
            $type = $this->getPngType();
1553
            $hasFewColors = imagecolorstotal($this->image);
1554
1555
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1556
                if ($this->verbose) {
1557
                    $this->log("Handle this image as a palette image.");
1558
                }
1559
                $this->palette = true;
1560
            }
1561
        }
1562
        */
1563
1564
        if ($this->verbose) {
1565
            $this->log("### Image successfully loaded from file.");
1566
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1567
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1568
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1569
            $index = imagecolortransparent($this->image);
1570
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1571
        }
1572
1573
        return $this;
1574
    }
1575
1576
1577
1578
    /**
1579
     * Get the type of PNG image.
1580
     *
1581
     * @param string $filename to use instead of default.
1582
     *
1583
     * @return int as the type of the png-image
1584
     *
1585
     */
1586
    public function getPngType($filename = null)
1587
    {
1588
        $filename = $filename ? $filename : $this->pathToImage;
1589
1590
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1591
1592
        if ($this->verbose) {
1593
            $this->log("Checking png type of: " . $filename);
1594
            $this->log($this->getPngTypeAsString($pngType));
1595
        }
1596
1597
        return $pngType;
1598
    }
1599
1600
1601
1602
    /**
1603
     * Get the type of PNG image as a verbose string.
1604
     *
1605
     * @param integer $type     to use, default is to check the type.
1606
     * @param string  $filename to use instead of default.
1607
     *
1608
     * @return int as the type of the png-image
1609
     *
1610
     */
1611
    private function getPngTypeAsString($pngType = null, $filename = null)
1612
    {
1613
        if ($filename || !$pngType) {
1614
            $pngType = $this->getPngType($filename);
1615
        }
1616
1617
        $index = imagecolortransparent($this->image);
1618
        $transparent = null;
1619
        if ($index != -1) {
1620
            $transparent = " (transparent)";
1621
        }
1622
1623
        switch ($pngType) {
1624
1625
            case self::PNG_GREYSCALE:
1626
                $text = "PNG is type 0, Greyscale$transparent";
1627
                break;
1628
1629
            case self::PNG_RGB:
1630
                $text = "PNG is type 2, RGB$transparent";
1631
                break;
1632
1633
            case self::PNG_RGB_PALETTE:
1634
                $text = "PNG is type 3, RGB with palette$transparent";
1635
                break;
1636
1637
            case self::PNG_GREYSCALE_ALPHA:
1638
                $text = "PNG is type 4, Greyscale with alpha channel";
1639
                break;
1640
1641
            case self::PNG_RGB_ALPHA:
1642
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1643
                break;
1644
1645
            default:
1646
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1647
        }
1648
1649
        return $text;
1650
    }
1651
1652
1653
1654
1655
    /**
1656
     * Calculate number of colors in an image.
1657
     *
1658
     * @param resource $im the image.
1659
     *
1660
     * @return int
1661
     */
1662
    private function colorsTotal($im)
1663
    {
1664
        if (imageistruecolor($im)) {
1665
            $this->log("Colors as true color.");
1666
            $h = imagesy($im);
1667
            $w = imagesx($im);
1668
            $c = array();
1669
            for ($x=0; $x < $w; $x++) {
1670
                for ($y=0; $y < $h; $y++) {
1671
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1672
                }
1673
            }
1674
            return count($c);
1675
        } else {
1676
            $this->log("Colors as palette.");
1677
            return imagecolorstotal($im);
1678
        }
1679
    }
1680
1681
1682
1683
    /**
1684
     * Preprocess image before rezising it.
1685
     *
1686
     * @return $this
1687
     */
1688
    public function preResize()
1689
    {
1690
        $this->log("### Pre-process before resizing");
1691
1692
        // Rotate image
1693
        if ($this->rotateBefore) {
1694
            $this->log("Rotating image.");
1695
            $this->rotate($this->rotateBefore, $this->bgColor)
1696
                 ->reCalculateDimensions();
1697
        }
1698
1699
        // Auto-rotate image
1700
        if ($this->autoRotate) {
1701
            $this->log("Auto rotating image.");
1702
            $this->rotateExif()
1703
                 ->reCalculateDimensions();
1704
        }
1705
1706
        // Scale the original image before starting
1707
        if (isset($this->scale)) {
1708
            $this->log("Scale by {$this->scale}%");
1709
            $newWidth  = $this->width * $this->scale / 100;
1710
            $newHeight = $this->height * $this->scale / 100;
1711
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1712
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1713
            $this->image = $img;
1714
            $this->width = $newWidth;
1715
            $this->height = $newHeight;
1716
        }
1717
1718
        return $this;
1719
    }
1720
1721
1722
1723
    /**
1724
     * Resize or resample the image while resizing.
1725
     *
1726
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1727
     *
1728
     * @return $this
1729
     */
1730
     public function setCopyResizeStrategy($strategy)
1731
     {
1732
         $this->copyStrategy = $strategy;
1733
         return $this;
1734
     }
1735
1736
1737
1738
    /**
1739
     * Resize and or crop the image.
1740
     *
1741
     * @return void
1742
     */
1743
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1744
    {
1745
        if($this->copyStrategy == self::RESIZE) {
1746
            $this->log("Copy by resize");
1747
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1748
        } else {
1749
            $this->log("Copy by resample");
1750
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1751
        }
1752
    }
1753
1754
1755
1756
    /**
1757
     * Resize and or crop the image.
1758
     *
1759
     * @return $this
1760
     */
1761
    public function resize()
1762
    {
1763
1764
        $this->log("### Starting to Resize()");
1765
        $this->log("Upscale = '$this->upscale'");
1766
1767
        // Only use a specified area of the image, $this->offset is defining the area to use
1768
        if (isset($this->offset)) {
1769
1770
            $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']}");
1771
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1772
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1773
            $this->image = $img;
1774
            $this->width = $this->offset['width'];
1775
            $this->height = $this->offset['height'];
1776
        }
1777
1778
        if ($this->crop) {
1779
1780
            // Do as crop, take only part of image
1781
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1782
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1783
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1784
            $this->image = $img;
1785
            $this->width = $this->crop['width'];
1786
            $this->height = $this->crop['height'];
1787
        }
1788
1789
        if (!$this->upscale) {
1790
            // Consider rewriting the no-upscale code to fit within this if-statement,
1791
            // likely to be more readable code.
1792
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1793
        }
1794
1795
        if ($this->cropToFit) {
1796
1797
            // Resize by crop to fit
1798
            $this->log("Resizing using strategy - Crop to fit");
1799
1800
            if (!$this->upscale
1801
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1802
                $this->log("Resizing - smaller image, do not upscale.");
1803
1804
                $posX = 0;
1805
                $posY = 0;
1806
                $cropX = 0;
1807
                $cropY = 0;
1808
1809
                if ($this->newWidth > $this->width) {
1810
                    $posX = round(($this->newWidth - $this->width) / 2);
1811
                }
1812
                if ($this->newWidth < $this->width) {
1813
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1814
                }
1815
1816
                if ($this->newHeight > $this->height) {
1817
                    $posY = round(($this->newHeight - $this->height) / 2);
1818
                }
1819
                if ($this->newHeight < $this->height) {
1820
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1821
                }
1822
                $this->log(" cwidth: $this->cropWidth");
1823
                $this->log(" cheight: $this->cropHeight");
1824
                $this->log(" nwidth: $this->newWidth");
1825
                $this->log(" nheight: $this->newHeight");
1826
                $this->log(" width: $this->width");
1827
                $this->log(" height: $this->height");
1828
                $this->log(" posX: $posX");
1829
                $this->log(" posY: $posY");
1830
                $this->log(" cropX: $cropX");
1831
                $this->log(" cropY: $cropY");
1832
1833
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1834
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1835
            } else {
1836
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1837
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1838
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1839
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1840
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1841
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1842
            }
1843
1844
            $this->image = $imageResized;
1845
            $this->width = $this->newWidth;
1846
            $this->height = $this->newHeight;
1847
1848
        } elseif ($this->fillToFit) {
1849
1850
            // Resize by fill to fit
1851
            $this->log("Resizing using strategy - Fill to fit");
1852
1853
            $posX = 0;
1854
            $posY = 0;
1855
1856
            $ratioOrig = $this->width / $this->height;
1857
            $ratioNew  = $this->newWidth / $this->newHeight;
1858
1859
            // Check ratio for landscape or portrait
1860
            if ($ratioOrig < $ratioNew) {
1861
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1862
            } else {
1863
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1864
            }
1865
1866
            if (!$this->upscale
1867
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1868
            ) {
1869
1870
                $this->log("Resizing - smaller image, do not upscale.");
1871
                $posX = round(($this->newWidth - $this->width) / 2);
1872
                $posY = round(($this->newHeight - $this->height) / 2);
1873
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1874
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1875
1876
            } else {
1877
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1878
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1879
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1880
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1881
            }
1882
1883
            $this->image = $imageResized;
1884
            $this->width = $this->newWidth;
1885
            $this->height = $this->newHeight;
1886
1887
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1888
1889
            // Resize it
1890
            $this->log("Resizing, new height and/or width");
1891
1892
            if (!$this->upscale
1893
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1894
            ) {
1895
                $this->log("Resizing - smaller image, do not upscale.");
1896
1897
                if (!$this->keepRatio) {
1898
                    $this->log("Resizing - stretch to fit selected.");
1899
1900
                    $posX = 0;
1901
                    $posY = 0;
1902
                    $cropX = 0;
1903
                    $cropY = 0;
1904
1905
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1906
                        $posX = round(($this->newWidth - $this->width) / 2);
1907
                        $posY = round(($this->newHeight - $this->height) / 2);
1908
                    } elseif ($this->newWidth > $this->width) {
1909
                        $posX = round(($this->newWidth - $this->width) / 2);
1910
                        $cropY = round(($this->height - $this->newHeight) / 2);
1911
                    } elseif ($this->newHeight > $this->height) {
1912
                        $posY = round(($this->newHeight - $this->height) / 2);
1913
                        $cropX = round(($this->width - $this->newWidth) / 2);
1914
                    }
1915
1916
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1917
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1918
                    $this->image = $imageResized;
1919
                    $this->width = $this->newWidth;
1920
                    $this->height = $this->newHeight;
1921
                }
1922
            } else {
1923
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1924
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1925
                $this->image = $imageResized;
1926
                $this->width = $this->newWidth;
1927
                $this->height = $this->newHeight;
1928
            }
1929
        }
1930
1931
        return $this;
1932
    }
1933
1934
1935
1936
    /**
1937
     * Postprocess image after rezising image.
1938
     *
1939
     * @return $this
1940
     */
1941
    public function postResize()
1942
    {
1943
        $this->log("### Post-process after resizing");
1944
1945
        // Rotate image
1946
        if ($this->rotateAfter) {
1947
            $this->log("Rotating image.");
1948
            $this->rotate($this->rotateAfter, $this->bgColor);
1949
        }
1950
1951
        // Apply filters
1952
        if (isset($this->filters) && is_array($this->filters)) {
1953
1954
            foreach ($this->filters as $filter) {
1955
                $this->log("Applying filter {$filter['type']}.");
1956
1957
                switch ($filter['argc']) {
1958
1959
                    case 0:
1960
                        imagefilter($this->image, $filter['type']);
1961
                        break;
1962
1963
                    case 1:
1964
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1965
                        break;
1966
1967
                    case 2:
1968
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1969
                        break;
1970
1971
                    case 3:
1972
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1973
                        break;
1974
1975
                    case 4:
1976
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1977
                        break;
1978
                }
1979
            }
1980
        }
1981
1982
        // Convert to palette image
1983
        if ($this->palette) {
1984
            $this->log("Converting to palette image.");
1985
            $this->trueColorToPalette();
1986
        }
1987
1988
        // Blur the image
1989
        if ($this->blur) {
1990
            $this->log("Blur.");
1991
            $this->blurImage();
1992
        }
1993
1994
        // Emboss the image
1995
        if ($this->emboss) {
1996
            $this->log("Emboss.");
1997
            $this->embossImage();
1998
        }
1999
2000
        // Sharpen the image
2001
        if ($this->sharpen) {
2002
            $this->log("Sharpen.");
2003
            $this->sharpenImage();
2004
        }
2005
2006
        // Custom convolution
2007
        if ($this->convolve) {
2008
            //$this->log("Convolve: " . $this->convolve);
2009
            $this->imageConvolution();
2010
        }
2011
2012
        return $this;
2013
    }
2014
2015
2016
2017
    /**
2018
     * Rotate image using angle.
2019
     *
2020
     * @param float $angle        to rotate image.
2021
     * @param int   $anglebgColor to fill image with if needed.
2022
     *
2023
     * @return $this
2024
     */
2025
    public function rotate($angle, $bgColor)
2026
    {
2027
        $this->log("Rotate image " . $angle . " degrees with filler color.");
2028
2029
        $color = $this->getBackgroundColor();
2030
        $this->image = imagerotate($this->image, $angle, $color);
2031
2032
        $this->width  = imagesx($this->image);
2033
        $this->height = imagesy($this->image);
2034
2035
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2036
2037
        return $this;
2038
    }
2039
2040
2041
2042
    /**
2043
     * Rotate image using information in EXIF.
2044
     *
2045
     * @return $this
2046
     */
2047
    public function rotateExif()
2048
    {
2049
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2050
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2051
            return $this;
2052
        }
2053
2054
        $exif = exif_read_data($this->pathToImage);
2055
2056
        if (!empty($exif['Orientation'])) {
2057
            switch ($exif['Orientation']) {
2058
                case 3:
2059
                    $this->log("Autorotate 180.");
2060
                    $this->rotate(180, $this->bgColor);
2061
                    break;
2062
2063
                case 6:
2064
                    $this->log("Autorotate -90.");
2065
                    $this->rotate(-90, $this->bgColor);
2066
                    break;
2067
2068
                case 8:
2069
                    $this->log("Autorotate 90.");
2070
                    $this->rotate(90, $this->bgColor);
2071
                    break;
2072
2073
                default:
2074
                    $this->log("Autorotate ignored, unknown value as orientation.");
2075
            }
2076
        } else {
2077
            $this->log("Autorotate ignored, no orientation in EXIF.");
2078
        }
2079
2080
        return $this;
2081
    }
2082
2083
2084
2085
    /**
2086
     * Convert true color image to palette image, keeping alpha.
2087
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2088
     *
2089
     * @return void
2090
     */
2091
    public function trueColorToPalette()
2092
    {
2093
        $img = imagecreatetruecolor($this->width, $this->height);
2094
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2095
        imagecolortransparent($img, $bga);
2096
        imagefill($img, 0, 0, $bga);
2097
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2098
        imagetruecolortopalette($img, false, 255);
2099
        imagesavealpha($img, true);
2100
2101
        if (imageistruecolor($this->image)) {
2102
            $this->log("Matching colors with true color image.");
2103
            imagecolormatch($this->image, $img);
2104
        }
2105
2106
        $this->image = $img;
2107
    }
2108
2109
2110
2111
    /**
2112
     * Sharpen image using image convolution.
2113
     *
2114
     * @return $this
2115
     */
2116
    public function sharpenImage()
2117
    {
2118
        $this->imageConvolution('sharpen');
2119
        return $this;
2120
    }
2121
2122
2123
2124
    /**
2125
     * Emboss image using image convolution.
2126
     *
2127
     * @return $this
2128
     */
2129
    public function embossImage()
2130
    {
2131
        $this->imageConvolution('emboss');
2132
        return $this;
2133
    }
2134
2135
2136
2137
    /**
2138
     * Blur image using image convolution.
2139
     *
2140
     * @return $this
2141
     */
2142
    public function blurImage()
2143
    {
2144
        $this->imageConvolution('blur');
2145
        return $this;
2146
    }
2147
2148
2149
2150
    /**
2151
     * Create convolve expression and return arguments for image convolution.
2152
     *
2153
     * @param string $expression constant string which evaluates to a list of
2154
     *                           11 numbers separated by komma or such a list.
2155
     *
2156
     * @return array as $matrix (3x3), $divisor and $offset
2157
     */
2158
    public function createConvolveArguments($expression)
2159
    {
2160
        // Check of matching constant
2161
        if (isset($this->convolves[$expression])) {
2162
            $expression = $this->convolves[$expression];
2163
        }
2164
2165
        $part = explode(',', $expression);
2166
        $this->log("Creating convolution expressen: $expression");
2167
2168
        // Expect list of 11 numbers, split by , and build up arguments
2169
        if (count($part) != 11) {
2170
            throw new Exception(
2171
                "Missmatch in argument convolve. Expected comma-separated string with
2172
                11 float values. Got $expression."
2173
            );
2174
        }
2175
2176
        array_walk($part, function ($item, $key) {
2177
            if (!is_numeric($item)) {
2178
                throw new Exception("Argument to convolve expression should be float but is not.");
2179
            }
2180
        });
2181 2
2182
        return array(
2183 2
            array(
2184
                array($part[0], $part[1], $part[2]),
2185 2
                array($part[3], $part[4], $part[5]),
2186
                array($part[6], $part[7], $part[8]),
2187 2
            ),
2188 2
            $part[9],
2189 2
            $part[10],
2190 2
        );
2191
    }
2192 2
2193
2194
2195 2
    /**
2196
     * Add custom expressions (or overwrite existing) for image convolution.
2197
     *
2198 2
     * @param array $options Key value array with strings to be converted
2199
     *                       to convolution expressions.
2200
     *
2201
     * @return $this
2202
     */
2203
    public function addConvolveExpressions($options)
2204
    {
2205
        $this->convolves = array_merge($this->convolves, $options);
2206
        return $this;
2207
    }
2208
2209
2210
2211
    /**
2212
     * Image convolution.
2213
     *
2214
     * @param string $options A string with 11 float separated by comma.
2215 2
     *
2216
     * @return $this
2217 2
     */
2218 2
    public function imageConvolution($options = null)
2219 2
    {
2220 2
        // Use incoming options or use $this.
2221
        $options = $options ? $options : $this->convolve;
2222 2
2223 2
        // Treat incoming as string, split by +
2224 2
        $this->log("Convolution with '$options'");
2225
        $options = explode(":", $options);
2226 2
2227
        // Check each option if it matches constant value
2228
        foreach ($options as $option) {
2229
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2230
            imageconvolution($this->image, $matrix, $divisor, $offset);
2231
        }
2232
2233
        return $this;
2234
    }
2235 2
2236
2237 2
2238 2
    /**
2239 2
     * Set default background color between 000000-FFFFFF or if using
2240 2
     * alpha 00000000-FFFFFF7F.
2241
     *
2242 2
     * @param string $color as hex value.
2243
     *
2244
     * @return $this
2245
    */
2246
    public function setDefaultBackgroundColor($color)
2247
    {
2248
        $this->log("Setting default background color to '$color'.");
2249
2250
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2251
            throw new Exception(
2252
                "Background color needs a hex value of 6 or 8
2253
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2254
                Current value was: '$color'."
2255
            );
2256
        }
2257
2258
        $red    = hexdec(substr($color, 0, 2));
2259
        $green  = hexdec(substr($color, 2, 2));
2260
        $blue   = hexdec(substr($color, 4, 2));
2261
2262
        $alpha = (strlen($color) == 8)
2263
            ? hexdec(substr($color, 6, 2))
2264
            : null;
2265
2266
        if (($red < 0 || $red > 255)
2267
            || ($green < 0 || $green > 255)
2268
            || ($blue < 0 || $blue > 255)
2269
            || ($alpha < 0 || $alpha > 127)
2270
        ) {
2271
            throw new Exception(
2272
                "Background color out of range. Red, green blue
2273
                should be 00-FF and alpha should be 00-7F.
2274
                Current value was: '$color'."
2275
            );
2276
        }
2277
2278
        $this->bgColor = strtolower($color);
2279
        $this->bgColorDefault = array(
2280
            'red'   => $red,
2281
            'green' => $green,
2282
            'blue'  => $blue,
2283
            'alpha' => $alpha
2284 2
        );
2285
2286
        return $this;
2287 2
    }
2288
2289
2290 2
2291
    /**
2292
     * Get the background color.
2293
     *
2294
     * @param resource $img the image to work with or null if using $this->image.
2295
     *
2296
     * @return color value or null if no background color is set.
2297
    */
2298
    private function getBackgroundColor($img = null)
2299
    {
2300
        $img = isset($img) ? $img : $this->image;
2301
2302
        if ($this->bgColorDefault) {
2303
2304
            $red   = $this->bgColorDefault['red'];
2305 2
            $green = $this->bgColorDefault['green'];
2306
            $blue  = $this->bgColorDefault['blue'];
2307 2
            $alpha = $this->bgColorDefault['alpha'];
2308
2309
            if ($alpha) {
2310
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2311 2
            } else {
2312
                $color = imagecolorallocate($img, $red, $green, $blue);
2313
            }
2314
2315
            return $color;
2316 2
2317
        } else {
2318
            return 0;
2319 2
        }
2320 2
    }
2321
2322
2323 2
2324 2
    /**
2325
     * Create a image and keep transparency for png and gifs.
2326
     *
2327
     * @param int $width of the new image.
2328
     * @param int $height of the new image.
2329
     *
2330
     * @return image resource.
2331
    */
2332
    private function createImageKeepTransparency($width, $height)
2333
    {
2334
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2335
        $img = imagecreatetruecolor($width, $height);
2336
        imagealphablending($img, false);
2337
        imagesavealpha($img, true);
2338
2339
        $index = $this->image
2340
            ? imagecolortransparent($this->image)
2341
            : -1;
2342 2
2343
        if ($index != -1) {
2344
2345
            imagealphablending($img, true);
2346
            $transparent = imagecolorsforindex($this->image, $index);
2347 2
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2348 2
            imagefill($img, 0, 0, $color);
2349 2
            $index = imagecolortransparent($img, $color);
2350
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2351
2352 2
        } elseif ($this->bgColorDefault) {
2353 2
2354 2
            $color = $this->getBackgroundColor($img);
2355
            imagefill($img, 0, 0, $color);
2356
            $this->Log("Filling image with background color.");
2357 2
        }
2358
2359
        return $img;
2360
    }
2361
2362
2363
2364
    /**
2365
     * Set optimizing  and post-processing options.
2366
     *
2367
     * @param array $options with config for postprocessing with external tools.
2368
     *
2369
     * @return $this
2370 2
     */
2371
    public function setPostProcessingOptions($options)
2372
    {
2373
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2374
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2375
        } else {
2376
            $this->jpegOptimizeCmd = null;
2377
        }
2378
2379
        if (array_key_exists("png_lossy", $options)
2380
            && $options['png_lossy'] !== false) {
2381 2
            $this->pngLossy = $options['png_lossy'];
2382 2
            $this->pngLossyCmd = $options['png_lossy_cmd'];
2383
        } else {
2384 2
            $this->pngLossyCmd = null;
2385
        }
2386
2387
        if (isset($options['png_filter']) && $options['png_filter']) {
2388
            $this->pngFilterCmd = $options['png_filter_cmd'];
2389
        } else {
2390
            $this->pngFilterCmd = null;
2391
        }
2392
2393
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2394
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2395 2
        } else {
2396
            $this->pngDeflateCmd = null;
2397
        }
2398
2399
        return $this;
2400
    }
2401
2402
2403
2404
    /**
2405
     * Find out the type (file extension) for the image to be saved.
2406
     *
2407
     * @return string as image extension.
2408
     */
2409
    protected function getTargetImageExtension()
2410
    {
2411
        // switch on mimetype
2412
        if (isset($this->extension)) {
2413
            return strtolower($this->extension);
2414
        } elseif ($this->fileType === IMG_WEBP) {
2415
            return "webp";
2416
        }
2417
2418
        return substr(image_type_to_extension($this->fileType), 1);
2419
    }
2420
2421
2422
2423
    /**
2424
     * Save image.
2425
     *
2426
     * @param string  $src       as target filename.
2427
     * @param string  $base      as base directory where to store images.
2428
     * @param boolean $overwrite or not, default to always overwrite file.
2429
     *
2430
     * @return $this or false if no folder is set.
2431
     */
2432
    public function save($src = null, $base = null, $overwrite = true)
2433
    {
2434
        if (isset($src)) {
2435
            $this->setTarget($src, $base);
2436
        }
2437
2438
        if ($overwrite === false && is_file($this->cacheFileName)) {
2439
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2440
            return;
2441
        }
2442
2443
        is_writable($this->saveFolder)
2444
            or $this->raiseError('Target directory is not writable.');
2445
2446
        $type = $this->getTargetImageExtension();
2447
        $this->Log("Saving image as " . $type);
2448
        switch($type) {
2449
2450
            case 'jpeg':
2451
            case 'jpg':
2452
                // Set as interlaced progressive JPEG
2453
                if ($this->interlace) {
2454
                    $this->Log("Set JPEG image to be interlaced.");
2455
                    $res = imageinterlace($this->image, true);
2456
                }
2457
2458
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2459
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2460
2461
                // Use JPEG optimize if defined
2462
                if ($this->jpegOptimizeCmd) {
2463
                    if ($this->verbose) {
2464
                        clearstatcache();
2465
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2466
                    }
2467
                    $res = array();
2468
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2469
                    exec($cmd, $res);
2470
                    $this->log($cmd);
2471
                    $this->log($res);
2472
                }
2473
                break;
2474
2475
            case 'gif':
2476
                $this->Log("Saving image as GIF to cache.");
2477
                imagegif($this->image, $this->cacheFileName);
2478
                break;
2479
2480
            case 'webp':
2481
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2482
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2483
                break;
2484
2485
            case 'png':
2486
            default:
2487
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2488
2489
                // Turn off alpha blending and set alpha flag
2490
                imagealphablending($this->image, false);
2491
                imagesavealpha($this->image, true);
2492
                imagepng($this->image, $this->cacheFileName, $this->compress);
2493
2494
                // Use external program to process lossy PNG, if defined
2495
                $lossyEnabled = $this->pngLossy === true;
2496
                $lossySoftEnabled = $this->pngLossy === null;
2497
                $lossyActiveEnabled = $this->lossy === true;
2498
                if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) {
2499
                    if ($this->verbose) {
2500
                        clearstatcache();
2501
                        $this->log("Lossy enabled: $lossyEnabled");
2502
                        $this->log("Lossy soft enabled: $lossySoftEnabled");
2503
                        $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes.");
2504
                    }
2505
                    $res = array();
2506
                    $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName";
2507
                    exec($cmd, $res);
2508
                    $this->Log($cmd);
2509
                    $this->Log($res);
2510
                }
2511
2512
                // Use external program to filter PNG, if defined
2513
                if ($this->pngFilterCmd) {
2514
                    if ($this->verbose) {
2515
                        clearstatcache();
2516
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2517
                    }
2518
                    $res = array();
2519
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2520
                    exec($cmd, $res);
2521
                    $this->Log($cmd);
2522
                    $this->Log($res);
2523
                }
2524
2525
                // Use external program to deflate PNG, if defined
2526
                if ($this->pngDeflateCmd) {
2527
                    if ($this->verbose) {
2528
                        clearstatcache();
2529
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2530
                    }
2531
                    $res = array();
2532
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2533
                    exec($cmd, $res);
2534
                    $this->Log($cmd);
2535
                    $this->Log($res);
2536
                }
2537
                break;
2538
        }
2539
2540
        if ($this->verbose) {
2541
            clearstatcache();
2542
            $this->log("Saved image to cache.");
2543
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2544
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2545
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2546
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2547
            $index = imagecolortransparent($this->image);
2548
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2549
        }
2550
2551
        return $this;
2552
    }
2553
2554
2555
2556
    /**
2557
     * Convert image from one colorpsace/color profile to sRGB without
2558
     * color profile.
2559
     *
2560
     * @param string  $src      of image.
2561
     * @param string  $dir      as base directory where images are.
2562
     * @param string  $cache    as base directory where to store images.
2563
     * @param string  $iccFile  filename of colorprofile.
2564
     * @param boolean $useCache or not, default to always use cache.
2565
     *
2566
     * @return string | boolean false if no conversion else the converted
2567
     *                          filename.
2568
     */
2569
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2570
    {
2571
        if ($this->verbose) {
2572
            $this->log("# Converting image to sRGB colorspace.");
2573
        }
2574
2575
        if (!class_exists("Imagick")) {
2576
            $this->log(" Ignoring since Imagemagick is not installed.");
2577
            return false;
2578
        }
2579
2580
        // Prepare
2581
        $this->setSaveFolder($cache)
2582
             ->setSource($src, $dir)
2583
             ->generateFilename(null, false, 'srgb_');
2584
2585
        // Check if the cached version is accurate.
2586
        if ($useCache && is_readable($this->cacheFileName)) {
2587
            $fileTime  = filemtime($this->pathToImage);
2588
            $cacheTime = filemtime($this->cacheFileName);
2589
2590
            if ($fileTime <= $cacheTime) {
2591
                $this->log(" Using cached version: " . $this->cacheFileName);
2592
                return $this->cacheFileName;
2593
            }
2594
        }
2595
2596
        // Only covert if cachedir is writable
2597
        if (is_writable($this->saveFolder)) {
2598
            // Load file and check if conversion is needed
2599
            $image      = new Imagick($this->pathToImage);
2600
            $colorspace = $image->getImageColorspace();
2601
            $this->log(" Current colorspace: " . $colorspace);
2602
2603
            $profiles      = $image->getImageProfiles('*', false);
2604
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2605
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2606
2607
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2608
                $this->log(" Converting to sRGB.");
2609
2610
                $sRGBicc = file_get_contents($iccFile);
2611
                $image->profileImage('icc', $sRGBicc);
2612
2613
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2614
                $image->writeImage($this->cacheFileName);
2615
                return $this->cacheFileName;
2616
            }
2617
        }
2618
2619
        return false;
2620
    }
2621
2622
2623
2624
    /**
2625
     * Create a hard link, as an alias, to the cached file.
2626
     *
2627 7
     * @param string $alias where to store the link,
2628
     *                      filename without extension.
2629 7
     *
2630
     * @return $this
2631
     */
2632
    public function linkToCacheFile($alias)
2633 7
    {
2634
        if ($alias === null) {
2635
            $this->log("Ignore creating alias.");
2636
            return $this;
2637
        }
2638
2639
        if (is_readable($alias)) {
2640
            unlink($alias);
2641
        }
2642
2643
        $res = link($this->cacheFileName, $alias);
2644
2645
        if ($res) {
2646
            $this->log("Created an alias as: $alias");
2647
        } else {
2648
            $this->log("Failed to create the alias: $alias");
2649
        }
2650
2651
        return $this;
2652
    }
2653
2654
2655
2656
    /**
2657
     * Add HTTP header for output together with image.
2658
     *
2659
     * @param string $type  the header type such as "Cache-Control"
2660
     * @param string $value the value to use
2661
     *
2662
     * @return void
2663
     */
2664
    public function addHTTPHeader($type, $value)
2665
    {
2666
        $this->HTTPHeader[$type] = $value;
2667
    }
2668
2669
2670
2671
    /**
2672
     * Output image to browser using caching.
2673
     *
2674
     * @param string $file   to read and output, default is to
2675
     *                       use $this->cacheFileName
2676
     * @param string $format set to json to output file as json
2677
     *                       object with details
2678
     *
2679
     * @return void
2680
     */
2681
    public function output($file = null, $format = null)
2682
    {
2683
        if (is_null($file)) {
2684
            $file = $this->cacheFileName;
2685
        }
2686
2687
        if (is_null($format)) {
2688
            $format = $this->outputFormat;
2689
        }
2690
2691
        $this->log("### Output");
2692
        $this->log("Output format is: $format");
2693
2694
        if (!$this->verbose && $format == 'json') {
2695
            header('Content-type: application/json');
2696
            echo $this->json($file);
2697
            exit;
2698
        } elseif ($format == 'ascii') {
2699
            header('Content-type: text/plain');
2700
            echo $this->ascii($file);
2701
            exit;
2702
        }
2703
2704
        $this->log("Outputting image: $file");
2705
2706
        // Get image modification time
2707
        clearstatcache();
2708
        $lastModified = filemtime($file);
2709
        $lastModifiedFormat = "D, d M Y H:i:s";
2710
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2711
2712
        if (!$this->verbose) {
2713
            $header = "Last-Modified: $gmdate GMT";
2714
            header($header);
2715
            $this->fastTrackCache->addHeader($header);
2716
            $this->fastTrackCache->setLastModified($lastModified);
2717
        }
2718
2719
        foreach ($this->HTTPHeader as $key => $val) {
2720
            $header = "$key: $val";
2721
            header($header);
2722
            $this->fastTrackCache->addHeader($header);
2723
        }
2724
2725
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
2726
            && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2727
2728
            if ($this->verbose) {
2729
                $this->log("304 not modified");
2730
                $this->verboseOutput();
2731
                exit;
2732
            }
2733
2734
            header("HTTP/1.0 304 Not Modified");
2735
            if (CIMAGE_DEBUG) {
2736
                trace(__CLASS__ . " 304");
2737
            }
2738
2739
        } else {
2740
2741
            $this->loadImageDetails($file);
2742
            $mime = $this->getMimeType();
2743
            $size = filesize($file);
2744
2745
            if ($this->verbose) {
2746
                $this->log("Last-Modified: " . $gmdate . " GMT");
2747
                $this->log("Content-type: " . $mime);
2748
                $this->log("Content-length: " . $size);
2749
                $this->verboseOutput();
2750
2751
                if (is_null($this->verboseFileName)) {
2752
                    exit;
2753
                }
2754
            }
2755
2756
            $header = "Content-type: $mime";
2757
            header($header);
2758
            $this->fastTrackCache->addHeaderOnOutput($header);
2759
2760
            $header = "Content-length: $size";
2761
            header($header);
2762
            $this->fastTrackCache->addHeaderOnOutput($header);
2763
2764
            $this->fastTrackCache->setSource($file);
2765
            $this->fastTrackCache->writeToCache();
2766
            if (CIMAGE_DEBUG) {
2767
                trace(__CLASS__ . " 200");
2768
            }
2769
            readfile($file);
2770
        }
2771
2772
        exit;
2773
    }
2774
2775
2776
2777
    /**
2778
     * Create a JSON object from the image details.
2779
     *
2780
     * @param string $file the file to output.
2781
     *
2782
     * @return string json-encoded representation of the image.
2783
     */
2784
    public function json($file = null)
2785
    {
2786
        $file = $file ? $file : $this->cacheFileName;
2787
2788
        $details = array();
2789
2790
        clearstatcache();
2791
2792
        $details['src']       = $this->imageSrc;
2793
        $lastModified         = filemtime($this->pathToImage);
2794
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2795
2796
        $details['cache']       = basename($this->cacheFileName);
2797
        $lastModified           = filemtime($this->cacheFileName);
2798
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2799
2800
        $this->load($file);
2801
2802
        $details['filename']    = basename($file);
2803
        $details['mimeType']    = $this->getMimeType($this->fileType);
2804
        $details['width']       = $this->width;
2805
        $details['height']      = $this->height;
2806
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2807
        $details['size']        = filesize($file);
2808
        $details['colors'] = $this->colorsTotal($this->image);
2809
        $details['includedFiles'] = count(get_included_files());
2810
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2811
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2812
        $details['memoryLimit'] = ini_get('memory_limit');
2813
2814
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2815
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2816
        }
2817
2818
        if ($details['mimeType'] == 'image/png') {
2819
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2820
        }
2821
2822
        $options = null;
2823
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2824
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2825
        }
2826
2827
        return json_encode($details, $options);
2828
    }
2829
2830
2831
2832
    /**
2833
     * Set options for creating ascii version of image.
2834
     *
2835
     * @param array $options empty to use default or set options to change.
2836
     *
2837
     * @return void.
2838
     */
2839
    public function setAsciiOptions($options = array())
2840
    {
2841
        $this->asciiOptions = $options;
2842
    }
2843
2844
2845
2846
    /**
2847
     * Create an ASCII version from the image details.
2848
     *
2849
     * @param string $file the file to output.
2850
     *
2851
     * @return string ASCII representation of the image.
2852
     */
2853
    public function ascii($file = null)
2854
    {
2855
        $file = $file ? $file : $this->cacheFileName;
2856
2857
        $asciiArt = new CAsciiArt();
2858
        $asciiArt->setOptions($this->asciiOptions);
2859
        return $asciiArt->createFromFile($file);
2860
    }
2861
2862
2863
2864
    /**
2865
     * Log an event if verbose mode.
2866
     *
2867
     * @param string $message to log.
2868
     *
2869
     * @return this
2870
     */
2871
    public function log($message)
2872
    {
2873
        if ($this->verbose) {
2874
            $this->log[] = $message;
2875
        }
2876
2877
        return $this;
2878
    }
2879
2880
2881
2882
    /**
2883
     * Do verbose output to a file.
2884
     *
2885
     * @param string $fileName where to write the verbose output.
2886
     *
2887
     * @return void
2888
     */
2889
    public function setVerboseToFile($fileName)
2890
    {
2891
        $this->log("Setting verbose output to file.");
2892
        $this->verboseFileName = $fileName;
2893
    }
2894
2895
2896
2897
    /**
2898
     * Do verbose output and print out the log and the actual images.
2899
     *
2900
     * @return void
2901
     */
2902
    private function verboseOutput()
2903
    {
2904
        $log = null;
2905
        $this->log("### Summary of verbose log");
2906
        $this->log("As JSON: \n" . $this->json());
2907
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2908
        $this->log("Memory limit: " . ini_get('memory_limit'));
2909
2910
        $included = get_included_files();
2911
        $this->log("Included files: " . count($included));
2912
2913
        foreach ($this->log as $val) {
2914
            if (is_array($val)) {
2915
                foreach ($val as $val1) {
2916
                    $log .= htmlentities($val1) . '<br/>';
2917
                }
2918
            } else {
2919
                $log .= htmlentities($val) . '<br/>';
2920
            }
2921
        }
2922
2923
        if (!is_null($this->verboseFileName)) {
2924
            file_put_contents(
2925
                $this->verboseFileName,
2926
                str_replace("<br/>", "\n", $log)
2927
            );
2928
        } else {
2929
            echo <<<EOD
2930
<h1>CImage Verbose Output</h1>
2931
<pre>{$log}</pre>
2932
EOD;
2933
        }
2934
    }
2935
2936
2937
2938
    /**
2939
     * Raise error, enables to implement a selection of error methods.
2940
     *
2941
     * @param string $message the error message to display.
2942
     *
2943
     * @return void
2944
     * @throws Exception
2945
     */
2946
    private function raiseError($message)
2947
    {
2948
        throw new Exception($message);
2949
    }
2950
}
2951