Completed
Push — master ( c5de59...e59ef9 )
by Mikael
02:59
created

CImage.php (1 issue)

Upgrade to new PHP Analysis Engine

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

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

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

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

Loading history...
2735
                }
2736
            }
2737
2738
            $header = "Content-type: $mime";
2739
            header($header);
2740
            $this->fastTrackCache->addHeaderOnOutput($header);
2741
2742
            $header = "Content-length: $size";
2743
            header($header);
2744
            $this->fastTrackCache->addHeaderOnOutput($header);
2745
2746
            $this->fastTrackCache->setSource($file);
2747
            $this->fastTrackCache->writeToCache();
2748
            if (CIMAGE_DEBUG) {
2749
                trace(__CLASS__ . " 200");
2750
            }
2751
            readfile($file);
2752
        }
2753
2754
        exit;
2755
    }
2756
2757
2758
2759
    /**
2760
     * Create a JSON object from the image details.
2761
     *
2762
     * @param string $file the file to output.
2763
     *
2764
     * @return string json-encoded representation of the image.
2765
     */
2766
    public function json($file = null)
2767
    {
2768
        $file = $file ? $file : $this->cacheFileName;
2769
2770
        $details = array();
2771
2772
        clearstatcache();
2773
2774
        $details['src']       = $this->imageSrc;
2775
        $lastModified         = filemtime($this->pathToImage);
2776
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2777
2778
        $details['cache']       = basename($this->cacheFileName);
2779
        $lastModified           = filemtime($this->cacheFileName);
2780
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2781
2782
        $this->load($file);
2783
2784
        $details['filename']    = basename($file);
2785
        $details['mimeType']    = $this->getMimeType($this->fileType);
2786
        $details['width']       = $this->width;
2787
        $details['height']      = $this->height;
2788
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2789
        $details['size']        = filesize($file);
2790
        $details['colors'] = $this->colorsTotal($this->image);
2791
        $details['includedFiles'] = count(get_included_files());
2792
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2793
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2794
        $details['memoryLimit'] = ini_get('memory_limit');
2795
2796
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2797
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2798
        }
2799
2800
        if ($details['mimeType'] == 'image/png') {
2801
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2802
        }
2803
2804
        $options = null;
2805
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2806
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2807
        }
2808
2809
        return json_encode($details, $options);
2810
    }
2811
2812
2813
2814
    /**
2815
     * Set options for creating ascii version of image.
2816
     *
2817
     * @param array $options empty to use default or set options to change.
2818
     *
2819
     * @return void.
2820
     */
2821
    public function setAsciiOptions($options = array())
2822
    {
2823
        $this->asciiOptions = $options;
2824
    }
2825
2826
2827
2828
    /**
2829
     * Create an ASCII version from the image details.
2830
     *
2831
     * @param string $file the file to output.
2832
     *
2833
     * @return string ASCII representation of the image.
2834
     */
2835
    public function ascii($file = null)
2836
    {
2837
        $file = $file ? $file : $this->cacheFileName;
2838
2839
        $asciiArt = new CAsciiArt();
2840
        $asciiArt->setOptions($this->asciiOptions);
2841
        return $asciiArt->createFromFile($file);
2842
    }
2843
2844
2845
2846
    /**
2847
     * Log an event if verbose mode.
2848
     *
2849
     * @param string $message to log.
2850
     *
2851
     * @return this
2852
     */
2853
    public function log($message)
2854
    {
2855
        if ($this->verbose) {
2856
            $this->log[] = $message;
2857
        }
2858
2859
        return $this;
2860
    }
2861
2862
2863
2864
    /**
2865
     * Do verbose output to a file.
2866
     *
2867
     * @param string $fileName where to write the verbose output.
2868
     *
2869
     * @return void
2870
     */
2871
    public function setVerboseToFile($fileName)
2872
    {
2873
        $this->log("Setting verbose output to file.");
2874
        $this->verboseFileName = $fileName;
2875
    }
2876
2877
2878
2879
    /**
2880
     * Do verbose output and print out the log and the actual images.
2881
     *
2882
     * @return void
2883
     */
2884
    private function verboseOutput()
2885
    {
2886
        $log = null;
2887
        $this->log("### Summary of verbose log");
2888
        $this->log("As JSON: \n" . $this->json());
2889
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2890
        $this->log("Memory limit: " . ini_get('memory_limit'));
2891
2892
        $included = get_included_files();
2893
        $this->log("Included files: " . count($included));
2894
2895
        foreach ($this->log as $val) {
2896
            if (is_array($val)) {
2897
                foreach ($val as $val1) {
2898
                    $log .= htmlentities($val1) . '<br/>';
2899
                }
2900
            } else {
2901
                $log .= htmlentities($val) . '<br/>';
2902
            }
2903
        }
2904
2905
        if (!is_null($this->verboseFileName)) {
2906
            file_put_contents(
2907
                $this->verboseFileName,
2908
                str_replace("<br/>", "\n", $log)
2909
            );
2910
        } else {
2911
            echo <<<EOD
2912
<h1>CImage Verbose Output</h1>
2913
<pre>{$log}</pre>
2914
EOD;
2915
        }
2916
    }
2917
2918
2919
2920
    /**
2921
     * Raise error, enables to implement a selection of error methods.
2922
     *
2923
     * @param string $message the error message to display.
2924
     *
2925
     * @return void
2926
     * @throws Exception
2927
     */
2928
    private function raiseError($message)
2929
    {
2930
        throw new Exception($message);
2931
    }
2932
}
2933