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

CImage.php (2 issues)

Upgrade to new PHP Analysis Engine

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

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

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
982
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
983
            $this->log(" Image filesize: " . filesize($this->pathToImage) . " bytes.");
984
            $this->log(" Image mimetype: " . $this->getMimeType());
985
        }
986
987
        return $this;
988
    }
989
990
991
992
    /**
993
     * Get mime type for image type.
994
     *
995
     * @return $this
996
     * @throws Exception
997
     */
998
    protected function getMimeType()
999
    {
1000
        if ($this->fileType === IMG_WEBP) {
1001
            return "image/webp";
1002
        }
1003
1004
        return image_type_to_mime_type($this->fileType);
1005
    }
1006
1007
1008
1009
    /**
1010
     * Init new width and height and do some sanity checks on constraints, before any
1011
     * processing can be done.
1012
     *
1013
     * @return $this
1014
     * @throws Exception
1015
     */
1016
    public function initDimensions()
1017
    {
1018
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1019
1020
        // width as %
1021
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
1022
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
1023
            $this->log("Setting new width based on % to {$this->newWidth}");
1024
        }
1025
1026
        // height as %
1027
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
1028
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
1029
            $this->log("Setting new height based on % to {$this->newHeight}");
1030
        }
1031
1032
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
1033
1034
        // width & height from aspect ratio
1035
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
1036
            if ($this->aspectRatio >= 1) {
1037
                $this->newWidth   = $this->width;
1038
                $this->newHeight  = $this->width / $this->aspectRatio;
1039
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
1040
1041
            } else {
1042
                $this->newHeight  = $this->height;
1043
                $this->newWidth   = $this->height * $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
1047
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
1048
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
1049
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
1050
1051
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
1052
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
1053
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
1054
        }
1055
1056
        // Change width & height based on dpr
1057
        if ($this->dpr != 1) {
1058
            if (!is_null($this->newWidth)) {
1059
                $this->newWidth  = round($this->newWidth * $this->dpr);
1060
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
1061
            }
1062
            if (!is_null($this->newHeight)) {
1063
                $this->newHeight = round($this->newHeight * $this->dpr);
1064
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
1065
            }
1066
        }
1067
1068
        // Check values to be within domain
1069
        is_null($this->newWidth)
1070
            or is_numeric($this->newWidth)
1071
            or $this->raiseError('Width not numeric');
1072
1073
        is_null($this->newHeight)
1074
            or is_numeric($this->newHeight)
1075
            or $this->raiseError('Height not numeric');
1076
1077
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1078
1079
        return $this;
1080
    }
1081
1082
1083
1084
    /**
1085
     * Calculate new width and height of image, based on settings.
1086
     *
1087
     * @return $this
1088
     */
1089
    public function calculateNewWidthAndHeight()
1090
    {
1091
        // Crop, use cropped width and height as base for calulations
1092
        $this->log("Calculate new width and height.");
1093
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1094
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1095
1096
        // Check if there is an area to crop off
1097
        if (isset($this->area)) {
1098
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1099
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1100
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1101
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1102
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1103
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1104
            $this->width  = $this->offset['width'];
1105
            $this->height = $this->offset['height'];
1106
            $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']}%.");
1107
            $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.");
1108
        }
1109
1110
        $width  = $this->width;
1111
        $height = $this->height;
1112
1113
        // Check if crop is set
1114
        if ($this->crop) {
1115
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1116
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1117
1118
            if ($this->crop['start_x'] == 'left') {
1119
                $this->crop['start_x'] = 0;
1120
            } elseif ($this->crop['start_x'] == 'right') {
1121
                $this->crop['start_x'] = $this->width - $width;
1122
            } elseif ($this->crop['start_x'] == 'center') {
1123
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1124
            }
1125
1126
            if ($this->crop['start_y'] == 'top') {
1127
                $this->crop['start_y'] = 0;
1128
            } elseif ($this->crop['start_y'] == 'bottom') {
1129
                $this->crop['start_y'] = $this->height - $height;
1130
            } elseif ($this->crop['start_y'] == 'center') {
1131
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1132
            }
1133
1134
            $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.");
1135
        }
1136
1137
        // Calculate new width and height if keeping aspect-ratio.
1138
        if ($this->keepRatio) {
1139
1140
            $this->log("Keep aspect ratio.");
1141
1142
            // Crop-to-fit and both new width and height are set.
1143
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1144
1145
                // Use newWidth and newHeigh as width/height, image should fit in box.
1146
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1147
1148
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1149
1150
                // Both new width and height are set.
1151
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1152
                $ratioWidth  = $width  / $this->newWidth;
1153
                $ratioHeight = $height / $this->newHeight;
1154
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1155
                $this->newWidth  = round($width  / $ratio);
1156
                $this->newHeight = round($height / $ratio);
1157
                $this->log("New width and height was set.");
1158
1159
            } elseif (isset($this->newWidth)) {
1160
1161
                // Use new width as max-width
1162
                $factor = (float)$this->newWidth / (float)$width;
1163
                $this->newHeight = round($factor * $height);
1164
                $this->log("New width was set.");
1165
1166
            } elseif (isset($this->newHeight)) {
1167
1168
                // Use new height as max-hight
1169
                $factor = (float)$this->newHeight / (float)$height;
1170
                $this->newWidth = round($factor * $width);
1171
                $this->log("New height was set.");
1172
1173
            } else {
1174
1175
                // Use existing width and height as new width and height.
1176
                $this->newWidth = $width;
1177
                $this->newHeight = $height;
1178
            }
1179
            
1180
1181
            // Get image dimensions for pre-resize image.
1182
            if ($this->cropToFit || $this->fillToFit) {
1183
1184
                // Get relations of original & target image
1185
                $ratioWidth  = $width  / $this->newWidth;
1186
                $ratioHeight = $height / $this->newHeight;
1187
1188
                if ($this->cropToFit) {
1189
1190
                    // Use newWidth and newHeigh as defined width/height,
1191
                    // image should fit the area.
1192
                    $this->log("Crop to fit.");
1193
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1194
                    $this->cropWidth  = round($width  / $ratio);
1195
                    $this->cropHeight = round($height / $ratio);
1196
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1197
1198
                } elseif ($this->fillToFit) {
1199
1200
                    // Use newWidth and newHeigh as defined width/height,
1201
                    // image should fit the area.
1202
                    $this->log("Fill to fit.");
1203
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1204
                    $this->fillWidth  = round($width  / $ratio);
1205
                    $this->fillHeight = round($height / $ratio);
1206
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1207
                }
1208
            }
1209
        }
1210
1211
        // Crop, ensure to set new width and height
1212
        if ($this->crop) {
1213
            $this->log("Crop.");
1214
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1215
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1216
        }
1217
1218
        // Fill to fit, ensure to set new width and height
1219
        /*if ($this->fillToFit) {
1220
            $this->log("FillToFit.");
1221
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1222
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1223
        }*/
1224
1225
        // No new height or width is set, use existing measures.
1226
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1227
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1228
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1229
1230
        return $this;
1231
    }
1232
1233
1234
1235
    /**
1236
     * Re-calculate image dimensions when original image dimension has changed.
1237
     *
1238
     * @return $this
1239
     */
1240
    public function reCalculateDimensions()
1241
    {
1242
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1243
1244
        $this->newWidth  = $this->newWidthOrig;
1245
        $this->newHeight = $this->newHeightOrig;
1246
        $this->crop      = $this->cropOrig;
1247
1248
        $this->initDimensions()
1249
             ->calculateNewWidthAndHeight();
1250
1251
        return $this;
1252
    }
1253
1254
1255
1256
    /**
1257
     * Set extension for filename to save as.
1258
     *
1259
     * @param string $saveas extension to save image as
1260
     *
1261
     * @return $this
1262
     */
1263
    public function setSaveAsExtension($saveAs = null)
1264
    {
1265
        if (isset($saveAs)) {
1266
            $saveAs = strtolower($saveAs);
1267
            $this->checkFileExtension($saveAs);
1268
            $this->saveAs = $saveAs;
1269
            $this->extension = $saveAs;
1270
        }
1271
1272
        $this->log("Prepare to save image as: " . $this->extension);
1273
1274
        return $this;
1275
    }
1276
1277
1278
1279
    /**
1280
     * Set JPEG quality to use when saving image
1281
     *
1282
     * @param int $quality as the quality to set.
1283
     *
1284
     * @return $this
1285
     */
1286
    public function setJpegQuality($quality = null)
1287
    {
1288
        if ($quality) {
1289
            $this->useQuality = true;
1290
        }
1291
1292
        $this->quality = isset($quality)
1293
            ? $quality
1294
            : self::JPEG_QUALITY_DEFAULT;
1295
1296
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1297
            or $this->raiseError('Quality not in range.');
1298
1299
        $this->log("Setting JPEG quality to {$this->quality}.");
1300
1301
        return $this;
1302
    }
1303
1304
1305
1306
    /**
1307
     * Set PNG compressen algorithm to use when saving image
1308
     *
1309
     * @param int $compress as the algorithm to use.
1310
     *
1311
     * @return $this
1312
     */
1313
    public function setPngCompression($compress = null)
1314
    {
1315
        if ($compress) {
1316
            $this->useCompress = true;
1317
        }
1318
1319
        $this->compress = isset($compress)
1320
            ? $compress
1321
            : self::PNG_COMPRESSION_DEFAULT;
1322
1323
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1324
            or $this->raiseError('Quality not in range.');
1325
1326
        $this->log("Setting PNG compression level to {$this->compress}.");
1327
1328
        return $this;
1329
    }
1330
1331
1332
1333
    /**
1334
     * Use original image if possible, check options which affects image processing.
1335
     *
1336
     * @param boolean $useOrig default is to use original if possible, else set to false.
1337
     *
1338
     * @return $this
1339
     */
1340
    public function useOriginalIfPossible($useOrig = true)
1341
    {
1342
        if ($useOrig
1343
            && ($this->newWidth == $this->width)
1344
            && ($this->newHeight == $this->height)
1345
            && !$this->area
1346
            && !$this->crop
1347
            && !$this->cropToFit
1348
            && !$this->fillToFit
1349
            && !$this->filters
1350
            && !$this->sharpen
1351
            && !$this->emboss
1352
            && !$this->blur
1353
            && !$this->convolve
1354
            && !$this->palette
1355
            && !$this->useQuality
1356
            && !$this->useCompress
1357
            && !$this->saveAs
1358
            && !$this->rotateBefore
1359
            && !$this->rotateAfter
1360
            && !$this->autoRotate
1361
            && !$this->bgColor
1362
            && ($this->upscale === self::UPSCALE_DEFAULT)
1363
        ) {
1364
            $this->log("Using original image.");
1365
            $this->output($this->pathToImage);
1366
        }
1367
1368
        return $this;
1369
    }
1370
1371
1372
1373
    /**
1374
     * Generate filename to save file in cache.
1375
     *
1376
     * @param string  $base      as optional basepath for storing file.
1377
     * @param boolean $useSubdir use or skip the subdir part when creating the
1378
     *                           filename.
1379
     * @param string  $prefix    to add as part of filename
1380
     *
1381
     * @return $this
1382
     */
1383
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1384
    {
1385
        $filename     = basename($this->pathToImage);
1386
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1387
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1388
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1389
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1390
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1391
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1392
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1393
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1394
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1395
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1396
1397
        $saveAs = $this->normalizeFileExtension();
1398
        $saveAs = $saveAs ? "_$saveAs" : null;
1399
1400
        $copyStrat = null;
1401
        if ($this->copyStrategy === self::RESIZE) {
1402
            $copyStrat = "_rs";
1403
        }
1404
1405
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1406
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1407
1408
        $offset = isset($this->offset)
1409
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1410
            : null;
1411
1412
        $crop = $this->crop
1413
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1414
            : null;
1415
1416
        $filters = null;
1417
        if (isset($this->filters)) {
1418
            foreach ($this->filters as $filter) {
1419
                if (is_array($filter)) {
1420
                    $filters .= "_f{$filter['id']}";
1421
                    for ($i=1; $i<=$filter['argc']; $i++) {
1422
                        $filters .= "-".$filter["arg{$i}"];
1423
                    }
1424
                }
1425
            }
1426
        }
1427
1428
        $sharpen = $this->sharpen ? 's' : null;
1429
        $emboss  = $this->emboss  ? 'e' : null;
1430
        $blur    = $this->blur    ? 'b' : null;
1431
        $palette = $this->palette ? 'p' : null;
1432
1433
        $autoRotate = $this->autoRotate ? 'ar' : null;
1434
1435
        $optimize  = $this->jpegOptimize ? 'o' : null;
1436
        $optimize .= $this->pngFilter    ? 'f' : null;
1437
        $optimize .= $this->pngDeflate   ? 'd' : null;
1438
1439
        $convolve = null;
1440
        if ($this->convolve) {
1441
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1442
        }
1443
1444
        $upscale = null;
1445
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1446
            $upscale = '_nu';
1447
        }
1448
1449
        $subdir = null;
1450
        if ($useSubdir === true) {
1451
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1452
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1453
            $subdir .= '_';
1454
        }
1455
1456
        $file = $prefix . $subdir . $filename . $width . $height
1457
            . $offset . $crop . $cropToFit . $fillToFit
1458
            . $crop_x . $crop_y . $upscale
1459
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1460
            . $optimize . $compress
1461
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1462
            . $convolve . $copyStrat . $saveAs;
1463
1464
        return $this->setTarget($file, $base);
1465
    }
1466
1467
1468
1469
    /**
1470
     * Use cached version of image, if possible.
1471
     *
1472
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1473
     *
1474
     * @return $this
1475
     */
1476
    public function useCacheIfPossible($useCache = true)
1477
    {
1478
        if ($useCache && is_readable($this->cacheFileName)) {
1479
            $fileTime   = filemtime($this->pathToImage);
1480
            $cacheTime  = filemtime($this->cacheFileName);
1481
1482
            if ($fileTime <= $cacheTime) {
1483
                if ($this->useCache) {
1484
                    if ($this->verbose) {
1485
                        $this->log("Use cached file.");
1486
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1487
                    }
1488
                    $this->output($this->cacheFileName, $this->outputFormat);
1489
                } else {
1490
                    $this->log("Cache is valid but ignoring it by intention.");
1491
                }
1492
            } else {
1493
                $this->log("Original file is modified, ignoring cache.");
1494
            }
1495
        } else {
1496
            $this->log("Cachefile does not exists or ignoring it.");
1497
        }
1498
1499
        return $this;
1500
    }
1501
1502
1503
1504
    /**
1505
     * Load image from disk. Try to load image without verbose error message,
1506
     * if fail, load again and display error messages.
1507
     *
1508
     * @param string $src of image.
1509
     * @param string $dir as base directory where images are.
1510
     *
1511
     * @return $this
1512
     *
1513
     */
1514
    public function load($src = null, $dir = null)
1515
    {
1516
        if (isset($src)) {
1517
            $this->setSource($src, $dir);
1518
        }
1519
1520
        is_readable($this->pathToImage)
1521
            or $this->raiseError('Image file does not exist.');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
1522
1523
        $imageAsString = file_get_contents($this->pathToImage);
1524
        $this->image = imagecreatefromstring($imageAsString);
1525
        if ($this->image === false) {
1526
            throw new Exception("Could not load image.");
1527
        }
1528
1529
        $this->getImageDetails();
1530
1531
1532
        /* Removed v0.7.7
1533
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1534
            $type = $this->getPngType();
1535
            $hasFewColors = imagecolorstotal($this->image);
1536
1537
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1538
                if ($this->verbose) {
1539
                    $this->log("Handle this image as a palette image.");
1540
                }
1541
                $this->palette = true;
1542
            }
1543
        }
1544
        */
1545
1546
        if ($this->verbose) {
1547
            $this->log("### Image successfully loaded from file.");
1548
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1549
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1550
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1551
            $index = imagecolortransparent($this->image);
1552
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1553
        }
1554
1555
        return $this;
1556
    }
1557
1558
1559
1560
    /**
1561
     * Get the type of PNG image.
1562
     *
1563
     * @param string $filename to use instead of default.
1564
     *
1565
     * @return int as the type of the png-image
1566
     *
1567
     */
1568
    public function getPngType($filename = null)
1569
    {
1570
        $filename = $filename ? $filename : $this->pathToImage;
1571
1572
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1573
1574
        if ($this->verbose) {
1575
            $this->log("Checking png type of: " . $filename);
1576
            $this->log($this->getPngTypeAsString($pngType));
1577
        }
1578
1579
        return $pngType;
1580
    }
1581
1582
1583
1584
    /**
1585
     * Get the type of PNG image as a verbose string.
1586
     *
1587
     * @param integer $type     to use, default is to check the type.
1588
     * @param string  $filename to use instead of default.
1589
     *
1590
     * @return int as the type of the png-image
1591
     *
1592
     */
1593
    private function getPngTypeAsString($pngType = null, $filename = null)
1594
    {
1595
        if ($filename || !$pngType) {
1596
            $pngType = $this->getPngType($filename);
1597
        }
1598
1599
        $index = imagecolortransparent($this->image);
1600
        $transparent = null;
1601
        if ($index != -1) {
1602
            $transparent = " (transparent)";
1603
        }
1604
1605
        switch ($pngType) {
1606
1607
            case self::PNG_GREYSCALE:
1608
                $text = "PNG is type 0, Greyscale$transparent";
1609
                break;
1610
1611
            case self::PNG_RGB:
1612
                $text = "PNG is type 2, RGB$transparent";
1613
                break;
1614
1615
            case self::PNG_RGB_PALETTE:
1616
                $text = "PNG is type 3, RGB with palette$transparent";
1617
                break;
1618
1619
            case self::PNG_GREYSCALE_ALPHA:
1620
                $text = "PNG is type 4, Greyscale with alpha channel";
1621
                break;
1622
1623
            case self::PNG_RGB_ALPHA:
1624
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1625
                break;
1626
1627
            default:
1628
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1629
        }
1630
1631
        return $text;
1632
    }
1633
1634
1635
1636
1637
    /**
1638
     * Calculate number of colors in an image.
1639
     *
1640
     * @param resource $im the image.
1641
     *
1642
     * @return int
1643
     */
1644
    private function colorsTotal($im)
1645
    {
1646
        if (imageistruecolor($im)) {
1647
            $this->log("Colors as true color.");
1648
            $h = imagesy($im);
1649
            $w = imagesx($im);
1650
            $c = array();
1651
            for ($x=0; $x < $w; $x++) {
1652
                for ($y=0; $y < $h; $y++) {
1653
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1654
                }
1655
            }
1656
            return count($c);
1657
        } else {
1658
            $this->log("Colors as palette.");
1659
            return imagecolorstotal($im);
1660
        }
1661
    }
1662
1663
1664
1665
    /**
1666
     * Preprocess image before rezising it.
1667
     *
1668
     * @return $this
1669
     */
1670
    public function preResize()
1671
    {
1672
        $this->log("### Pre-process before resizing");
1673
1674
        // Rotate image
1675
        if ($this->rotateBefore) {
1676
            $this->log("Rotating image.");
1677
            $this->rotate($this->rotateBefore, $this->bgColor)
1678
                 ->reCalculateDimensions();
1679
        }
1680
1681
        // Auto-rotate image
1682
        if ($this->autoRotate) {
1683
            $this->log("Auto rotating image.");
1684
            $this->rotateExif()
1685
                 ->reCalculateDimensions();
1686
        }
1687
1688
        // Scale the original image before starting
1689
        if (isset($this->scale)) {
1690
            $this->log("Scale by {$this->scale}%");
1691
            $newWidth  = $this->width * $this->scale / 100;
1692
            $newHeight = $this->height * $this->scale / 100;
1693
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1694
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1695
            $this->image = $img;
1696
            $this->width = $newWidth;
1697
            $this->height = $newHeight;
1698
        }
1699
1700
        return $this;
1701
    }
1702
1703
1704
1705
    /**
1706
     * Resize or resample the image while resizing.
1707
     *
1708
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1709
     *
1710
     * @return $this
1711
     */
1712
     public function setCopyResizeStrategy($strategy)
1713
     {
1714
         $this->copyStrategy = $strategy;
1715
         return $this;
1716
     }
1717
1718
1719
1720
    /**
1721
     * Resize and or crop the image.
1722
     *
1723
     * @return void
1724
     */
1725
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1726
    {
1727
        if($this->copyStrategy == self::RESIZE) {
1728
            $this->log("Copy by resize");
1729
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1730
        } else {
1731
            $this->log("Copy by resample");
1732
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1733
        }
1734
    }
1735
1736
1737
1738
    /**
1739
     * Resize and or crop the image.
1740
     *
1741
     * @return $this
1742
     */
1743
    public function resize()
1744
    {
1745
1746
        $this->log("### Starting to Resize()");
1747
        $this->log("Upscale = '$this->upscale'");
1748
1749
        // Only use a specified area of the image, $this->offset is defining the area to use
1750
        if (isset($this->offset)) {
1751
1752
            $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']}");
1753
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1754
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1755
            $this->image = $img;
1756
            $this->width = $this->offset['width'];
1757
            $this->height = $this->offset['height'];
1758
        }
1759
1760
        if ($this->crop) {
1761
1762
            // Do as crop, take only part of image
1763
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1764
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1765
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1766
            $this->image = $img;
1767
            $this->width = $this->crop['width'];
1768
            $this->height = $this->crop['height'];
1769
        }
1770
1771
        if (!$this->upscale) {
1772
            // Consider rewriting the no-upscale code to fit within this if-statement,
1773
            // likely to be more readable code.
1774
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1775
        }
1776
1777
        if ($this->cropToFit) {
1778
1779
            // Resize by crop to fit
1780
            $this->log("Resizing using strategy - Crop to fit");
1781
1782
            if (!$this->upscale 
1783
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1784
                $this->log("Resizing - smaller image, do not upscale.");
1785
1786
                $posX = 0;
1787
                $posY = 0;
1788
                $cropX = 0;
1789
                $cropY = 0;
1790
1791
                if ($this->newWidth > $this->width) {
1792
                    $posX = round(($this->newWidth - $this->width) / 2);
1793
                }
1794
                if ($this->newWidth < $this->width) {
1795
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1796
                }
1797
1798
                if ($this->newHeight > $this->height) {
1799
                    $posY = round(($this->newHeight - $this->height) / 2);
1800
                }
1801
                if ($this->newHeight < $this->height) {
1802
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1803
                }
1804
                $this->log(" cwidth: $this->cropWidth");
1805
                $this->log(" cheight: $this->cropHeight");
1806
                $this->log(" nwidth: $this->newWidth");
1807
                $this->log(" nheight: $this->newHeight");
1808
                $this->log(" width: $this->width");
1809
                $this->log(" height: $this->height");
1810
                $this->log(" posX: $posX");
1811
                $this->log(" posY: $posY");
1812
                $this->log(" cropX: $cropX");
1813
                $this->log(" cropY: $cropY");
1814
1815
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1816
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1817
            } else {
1818
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1819
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1820
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1821
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1822
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1823
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1824
            }
1825
1826
            $this->image = $imageResized;
1827
            $this->width = $this->newWidth;
1828
            $this->height = $this->newHeight;
1829
1830
        } elseif ($this->fillToFit) {
1831
1832
            // Resize by fill to fit
1833
            $this->log("Resizing using strategy - Fill to fit");
1834
1835
            $posX = 0;
1836
            $posY = 0;
1837
1838
            $ratioOrig = $this->width / $this->height;
1839
            $ratioNew  = $this->newWidth / $this->newHeight;
1840
1841
            // Check ratio for landscape or portrait
1842
            if ($ratioOrig < $ratioNew) {
1843
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1844
            } else {
1845
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1846
            }
1847
1848
            if (!$this->upscale
1849
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1850
            ) {
1851
1852
                $this->log("Resizing - smaller image, do not upscale.");
1853
                $posX = round(($this->newWidth - $this->width) / 2);
1854
                $posY = round(($this->newHeight - $this->height) / 2);
1855
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1856
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1857
1858
            } else {
1859
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1860
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1861
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1862
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1863
            }
1864
1865
            $this->image = $imageResized;
1866
            $this->width = $this->newWidth;
1867
            $this->height = $this->newHeight;
1868
1869
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1870
1871
            // Resize it
1872
            $this->log("Resizing, new height and/or width");
1873
1874
            if (!$this->upscale
1875
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1876
            ) {
1877
                $this->log("Resizing - smaller image, do not upscale.");
1878
1879
                if (!$this->keepRatio) {
1880
                    $this->log("Resizing - stretch to fit selected.");
1881
1882
                    $posX = 0;
1883
                    $posY = 0;
1884
                    $cropX = 0;
1885
                    $cropY = 0;
1886
1887
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1888
                        $posX = round(($this->newWidth - $this->width) / 2);
1889
                        $posY = round(($this->newHeight - $this->height) / 2);
1890
                    } elseif ($this->newWidth > $this->width) {
1891
                        $posX = round(($this->newWidth - $this->width) / 2);
1892
                        $cropY = round(($this->height - $this->newHeight) / 2);
1893
                    } elseif ($this->newHeight > $this->height) {
1894
                        $posY = round(($this->newHeight - $this->height) / 2);
1895
                        $cropX = round(($this->width - $this->newWidth) / 2);
1896
                    }
1897
1898
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1899
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1900
                    $this->image = $imageResized;
1901
                    $this->width = $this->newWidth;
1902
                    $this->height = $this->newHeight;
1903
                }
1904
            } else {
1905
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1906
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1907
                $this->image = $imageResized;
1908
                $this->width = $this->newWidth;
1909
                $this->height = $this->newHeight;
1910
            }
1911
        }
1912
1913
        return $this;
1914
    }
1915
1916
1917
1918
    /**
1919
     * Postprocess image after rezising image.
1920
     *
1921
     * @return $this
1922
     */
1923
    public function postResize()
1924
    {
1925
        $this->log("### Post-process after resizing");
1926
1927
        // Rotate image
1928
        if ($this->rotateAfter) {
1929
            $this->log("Rotating image.");
1930
            $this->rotate($this->rotateAfter, $this->bgColor);
1931
        }
1932
1933
        // Apply filters
1934
        if (isset($this->filters) && is_array($this->filters)) {
1935
1936
            foreach ($this->filters as $filter) {
1937
                $this->log("Applying filter {$filter['type']}.");
1938
1939
                switch ($filter['argc']) {
1940
1941
                    case 0:
1942
                        imagefilter($this->image, $filter['type']);
1943
                        break;
1944
1945
                    case 1:
1946
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1947
                        break;
1948
1949
                    case 2:
1950
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1951
                        break;
1952
1953
                    case 3:
1954
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1955
                        break;
1956
1957
                    case 4:
1958
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1959
                        break;
1960
                }
1961
            }
1962
        }
1963
1964
        // Convert to palette image
1965
        if ($this->palette) {
1966
            $this->log("Converting to palette image.");
1967
            $this->trueColorToPalette();
1968
        }
1969
1970
        // Blur the image
1971
        if ($this->blur) {
1972
            $this->log("Blur.");
1973
            $this->blurImage();
1974
        }
1975
1976
        // Emboss the image
1977
        if ($this->emboss) {
1978
            $this->log("Emboss.");
1979
            $this->embossImage();
1980
        }
1981
1982
        // Sharpen the image
1983
        if ($this->sharpen) {
1984
            $this->log("Sharpen.");
1985
            $this->sharpenImage();
1986
        }
1987
1988
        // Custom convolution
1989
        if ($this->convolve) {
1990
            //$this->log("Convolve: " . $this->convolve);
1991
            $this->imageConvolution();
1992
        }
1993
1994
        return $this;
1995
    }
1996
1997
1998
1999
    /**
2000
     * Rotate image using angle.
2001
     *
2002
     * @param float $angle        to rotate image.
2003
     * @param int   $anglebgColor to fill image with if needed.
2004
     *
2005
     * @return $this
2006
     */
2007
    public function rotate($angle, $bgColor)
2008
    {
2009
        $this->log("Rotate image " . $angle . " degrees with filler color.");
2010
2011
        $color = $this->getBackgroundColor();
2012
        $this->image = imagerotate($this->image, $angle, $color);
2013
2014
        $this->width  = imagesx($this->image);
2015
        $this->height = imagesy($this->image);
2016
2017
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
2018
2019
        return $this;
2020
    }
2021
2022
2023
2024
    /**
2025
     * Rotate image using information in EXIF.
2026
     *
2027
     * @return $this
2028
     */
2029
    public function rotateExif()
2030
    {
2031
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
2032
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
2033
            return $this;
2034
        }
2035
2036
        $exif = exif_read_data($this->pathToImage);
2037
2038
        if (!empty($exif['Orientation'])) {
2039
            switch ($exif['Orientation']) {
2040
                case 3:
2041
                    $this->log("Autorotate 180.");
2042
                    $this->rotate(180, $this->bgColor);
2043
                    break;
2044
2045
                case 6:
2046
                    $this->log("Autorotate -90.");
2047
                    $this->rotate(-90, $this->bgColor);
2048
                    break;
2049
2050
                case 8:
2051
                    $this->log("Autorotate 90.");
2052
                    $this->rotate(90, $this->bgColor);
2053
                    break;
2054
2055
                default:
2056
                    $this->log("Autorotate ignored, unknown value as orientation.");
2057
            }
2058
        } else {
2059
            $this->log("Autorotate ignored, no orientation in EXIF.");
2060
        }
2061
2062
        return $this;
2063
    }
2064
2065
2066
2067
    /**
2068
     * Convert true color image to palette image, keeping alpha.
2069
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
2070
     *
2071
     * @return void
2072
     */
2073
    public function trueColorToPalette()
2074
    {
2075
        $img = imagecreatetruecolor($this->width, $this->height);
2076
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2077
        imagecolortransparent($img, $bga);
2078
        imagefill($img, 0, 0, $bga);
2079
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2080
        imagetruecolortopalette($img, false, 255);
2081
        imagesavealpha($img, true);
2082
2083
        if (imageistruecolor($this->image)) {
2084
            $this->log("Matching colors with true color image.");
2085
            imagecolormatch($this->image, $img);
2086
        }
2087
2088
        $this->image = $img;
2089
    }
2090
2091
2092
2093
    /**
2094
     * Sharpen image using image convolution.
2095
     *
2096
     * @return $this
2097
     */
2098
    public function sharpenImage()
2099
    {
2100
        $this->imageConvolution('sharpen');
2101
        return $this;
2102
    }
2103
2104
2105
2106
    /**
2107
     * Emboss image using image convolution.
2108
     *
2109
     * @return $this
2110
     */
2111
    public function embossImage()
2112
    {
2113
        $this->imageConvolution('emboss');
2114
        return $this;
2115
    }
2116
2117
2118
2119
    /**
2120
     * Blur image using image convolution.
2121
     *
2122
     * @return $this
2123
     */
2124
    public function blurImage()
2125
    {
2126
        $this->imageConvolution('blur');
2127
        return $this;
2128
    }
2129
2130
2131
2132
    /**
2133
     * Create convolve expression and return arguments for image convolution.
2134
     *
2135
     * @param string $expression constant string which evaluates to a list of
2136
     *                           11 numbers separated by komma or such a list.
2137
     *
2138
     * @return array as $matrix (3x3), $divisor and $offset
2139
     */
2140
    public function createConvolveArguments($expression)
2141
    {
2142
        // Check of matching constant
2143
        if (isset($this->convolves[$expression])) {
2144
            $expression = $this->convolves[$expression];
2145
        }
2146
2147
        $part = explode(',', $expression);
2148
        $this->log("Creating convolution expressen: $expression");
2149
2150
        // Expect list of 11 numbers, split by , and build up arguments
2151
        if (count($part) != 11) {
2152
            throw new Exception(
2153
                "Missmatch in argument convolve. Expected comma-separated string with
2154
                11 float values. Got $expression."
2155
            );
2156
        }
2157
2158
        array_walk($part, function ($item, $key) {
2159
            if (!is_numeric($item)) {
2160
                throw new Exception("Argument to convolve expression should be float but is not.");
2161
            }
2162
        });
2163
2164
        return array(
2165
            array(
2166
                array($part[0], $part[1], $part[2]),
2167
                array($part[3], $part[4], $part[5]),
2168
                array($part[6], $part[7], $part[8]),
2169
            ),
2170
            $part[9],
2171
            $part[10],
2172
        );
2173
    }
2174
2175
2176
2177
    /**
2178
     * Add custom expressions (or overwrite existing) for image convolution.
2179
     *
2180
     * @param array $options Key value array with strings to be converted
2181
     *                       to convolution expressions.
2182
     *
2183
     * @return $this
2184
     */
2185
    public function addConvolveExpressions($options)
2186
    {
2187
        $this->convolves = array_merge($this->convolves, $options);
2188
        return $this;
2189
    }
2190
2191
2192
2193
    /**
2194
     * Image convolution.
2195
     *
2196
     * @param string $options A string with 11 float separated by comma.
2197
     *
2198
     * @return $this
2199
     */
2200
    public function imageConvolution($options = null)
2201
    {
2202
        // Use incoming options or use $this.
2203
        $options = $options ? $options : $this->convolve;
2204
2205
        // Treat incoming as string, split by +
2206
        $this->log("Convolution with '$options'");
2207
        $options = explode(":", $options);
2208
2209
        // Check each option if it matches constant value
2210
        foreach ($options as $option) {
2211
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2212
            imageconvolution($this->image, $matrix, $divisor, $offset);
2213
        }
2214
2215
        return $this;
2216
    }
2217
2218
2219
2220
    /**
2221
     * Set default background color between 000000-FFFFFF or if using
2222
     * alpha 00000000-FFFFFF7F.
2223
     *
2224
     * @param string $color as hex value.
2225
     *
2226
     * @return $this
2227
    */
2228
    public function setDefaultBackgroundColor($color)
2229
    {
2230
        $this->log("Setting default background color to '$color'.");
2231
2232
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2233
            throw new Exception(
2234
                "Background color needs a hex value of 6 or 8
2235
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2236
                Current value was: '$color'."
2237
            );
2238
        }
2239
2240
        $red    = hexdec(substr($color, 0, 2));
2241
        $green  = hexdec(substr($color, 2, 2));
2242
        $blue   = hexdec(substr($color, 4, 2));
2243
2244
        $alpha = (strlen($color) == 8)
2245
            ? hexdec(substr($color, 6, 2))
2246
            : null;
2247
2248
        if (($red < 0 || $red > 255)
2249
            || ($green < 0 || $green > 255)
2250
            || ($blue < 0 || $blue > 255)
2251
            || ($alpha < 0 || $alpha > 127)
2252
        ) {
2253
            throw new Exception(
2254
                "Background color out of range. Red, green blue
2255
                should be 00-FF and alpha should be 00-7F.
2256
                Current value was: '$color'."
2257
            );
2258
        }
2259
2260
        $this->bgColor = strtolower($color);
2261
        $this->bgColorDefault = array(
2262
            'red'   => $red,
2263
            'green' => $green,
2264
            'blue'  => $blue,
2265
            'alpha' => $alpha
2266
        );
2267
2268
        return $this;
2269
    }
2270
2271
2272
2273
    /**
2274
     * Get the background color.
2275
     *
2276
     * @param resource $img the image to work with or null if using $this->image.
2277
     *
2278
     * @return color value or null if no background color is set.
2279
    */
2280
    private function getBackgroundColor($img = null)
2281
    {
2282
        $img = isset($img) ? $img : $this->image;
2283
2284
        if ($this->bgColorDefault) {
2285
2286
            $red   = $this->bgColorDefault['red'];
2287
            $green = $this->bgColorDefault['green'];
2288
            $blue  = $this->bgColorDefault['blue'];
2289
            $alpha = $this->bgColorDefault['alpha'];
2290
2291
            if ($alpha) {
2292
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2293
            } else {
2294
                $color = imagecolorallocate($img, $red, $green, $blue);
2295
            }
2296
2297
            return $color;
2298
2299
        } else {
2300
            return 0;
2301
        }
2302
    }
2303
2304
2305
2306
    /**
2307
     * Create a image and keep transparency for png and gifs.
2308
     *
2309
     * @param int $width of the new image.
2310
     * @param int $height of the new image.
2311
     *
2312
     * @return image resource.
2313
    */
2314
    private function createImageKeepTransparency($width, $height)
2315
    {
2316
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2317
        $img = imagecreatetruecolor($width, $height);
2318
        imagealphablending($img, false);
2319
        imagesavealpha($img, true);
2320
2321
        $index = $this->image
2322
            ? imagecolortransparent($this->image)
2323
            : -1;
2324
2325
        if ($index != -1) {
2326
2327
            imagealphablending($img, true);
2328
            $transparent = imagecolorsforindex($this->image, $index);
2329
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2330
            imagefill($img, 0, 0, $color);
2331
            $index = imagecolortransparent($img, $color);
2332
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2333
2334
        } elseif ($this->bgColorDefault) {
2335
2336
            $color = $this->getBackgroundColor($img);
2337
            imagefill($img, 0, 0, $color);
2338
            $this->Log("Filling image with background color.");
2339
        }
2340
2341
        return $img;
2342
    }
2343
2344
2345
2346
    /**
2347
     * Set optimizing  and post-processing options.
2348
     *
2349
     * @param array $options with config for postprocessing with external tools.
2350
     *
2351
     * @return $this
2352
     */
2353
    public function setPostProcessingOptions($options)
2354
    {
2355
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2356
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2357
        } else {
2358
            $this->jpegOptimizeCmd = null;
2359
        }
2360
2361
        if (isset($options['png_filter']) && $options['png_filter']) {
2362
            $this->pngFilterCmd = $options['png_filter_cmd'];
2363
        } else {
2364
            $this->pngFilterCmd = null;
2365
        }
2366
2367
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2368
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2369
        } else {
2370
            $this->pngDeflateCmd = null;
2371
        }
2372
2373
        return $this;
2374
    }
2375
2376
2377
2378
    /**
2379
     * Find out the type (file extension) for the image to be saved.
2380
     *
2381
     * @return string as image extension.
2382
     */
2383
    protected function getTargetImageExtension()
2384
    {
2385
        // switch on mimetype
2386
        if (isset($this->extension)) {
2387
            return strtolower($this->extension);
2388
        } else {
2389
            return substr(image_type_to_extension($this->fileType), 1);
2390
        }
2391
    }
2392
2393
2394
2395
    /**
2396
     * Save image.
2397
     *
2398
     * @param string  $src       as target filename.
2399
     * @param string  $base      as base directory where to store images.
2400
     * @param boolean $overwrite or not, default to always overwrite file.
2401
     *
2402
     * @return $this or false if no folder is set.
2403
     */
2404
    public function save($src = null, $base = null, $overwrite = true)
2405
    {
2406
        if (isset($src)) {
2407
            $this->setTarget($src, $base);
2408
        }
2409
2410
        if ($overwrite === false && is_file($this->cacheFileName)) {
2411
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2412
            return;
2413
        }
2414
2415
        is_writable($this->saveFolder)
2416
            or $this->raiseError('Target directory is not writable.');
2417
2418
        $type = $this->getTargetImageExtension();
2419
        $this->Log("Saving image as " . $type);
2420
        switch($type) {
2421
2422
            case 'jpeg':
2423
            case 'jpg':
2424
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2425
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2426
2427
                // Use JPEG optimize if defined
2428
                if ($this->jpegOptimizeCmd) {
2429
                    if ($this->verbose) {
2430
                        clearstatcache();
2431
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2432
                    }
2433
                    $res = array();
2434
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2435
                    exec($cmd, $res);
2436
                    $this->log($cmd);
2437
                    $this->log($res);
2438
                }
2439
                break;
2440
2441
            case 'gif':
2442
                $this->Log("Saving image as GIF to cache.");
2443
                imagegif($this->image, $this->cacheFileName);
2444
                break;
2445
2446
            case 'webp':
2447
                $this->Log("Saving image as WEBP to cache using quality = {$this->quality}.");
2448
                imagewebp($this->image, $this->cacheFileName, $this->quality);
2449
                break;
2450
2451
            case 'png':
2452
            default:
2453
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2454
2455
                // Turn off alpha blending and set alpha flag
2456
                imagealphablending($this->image, false);
2457
                imagesavealpha($this->image, true);
2458
                imagepng($this->image, $this->cacheFileName, $this->compress);
2459
2460
                // Use external program to filter PNG, if defined
2461
                if ($this->pngFilterCmd) {
2462
                    if ($this->verbose) {
2463
                        clearstatcache();
2464
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2465
                    }
2466
                    $res = array();
2467
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2468
                    exec($cmd, $res);
2469
                    $this->Log($cmd);
2470
                    $this->Log($res);
2471
                }
2472
2473
                // Use external program to deflate PNG, if defined
2474
                if ($this->pngDeflateCmd) {
2475
                    if ($this->verbose) {
2476
                        clearstatcache();
2477
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2478
                    }
2479
                    $res = array();
2480
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2481
                    exec($cmd, $res);
2482
                    $this->Log($cmd);
2483
                    $this->Log($res);
2484
                }
2485
                break;
2486
        }
2487
2488
        if ($this->verbose) {
2489
            clearstatcache();
2490
            $this->log("Saved image to cache.");
2491
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2492
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2493
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2494
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2495
            $index = imagecolortransparent($this->image);
2496
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2497
        }
2498
2499
        return $this;
2500
    }
2501
2502
2503
2504
    /**
2505
     * Convert image from one colorpsace/color profile to sRGB without
2506
     * color profile.
2507
     *
2508
     * @param string  $src      of image.
2509
     * @param string  $dir      as base directory where images are.
2510
     * @param string  $cache    as base directory where to store images.
2511
     * @param string  $iccFile  filename of colorprofile.
2512
     * @param boolean $useCache or not, default to always use cache.
2513
     *
2514
     * @return string | boolean false if no conversion else the converted
2515
     *                          filename.
2516
     */
2517
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2518
    {
2519
        if ($this->verbose) {
2520
            $this->log("# Converting image to sRGB colorspace.");
2521
        }
2522
2523
        if (!class_exists("Imagick")) {
2524
            $this->log(" Ignoring since Imagemagick is not installed.");
2525
            return false;
2526
        }
2527
2528
        // Prepare
2529
        $this->setSaveFolder($cache)
2530
             ->setSource($src, $dir)
2531
             ->generateFilename(null, false, 'srgb_');
2532
2533
        // Check if the cached version is accurate.
2534
        if ($useCache && is_readable($this->cacheFileName)) {
2535
            $fileTime  = filemtime($this->pathToImage);
2536
            $cacheTime = filemtime($this->cacheFileName);
2537
2538
            if ($fileTime <= $cacheTime) {
2539
                $this->log(" Using cached version: " . $this->cacheFileName);
2540
                return $this->cacheFileName;
2541
            }
2542
        }
2543
2544
        // Only covert if cachedir is writable
2545
        if (is_writable($this->saveFolder)) {
2546
            // Load file and check if conversion is needed
2547
            $image      = new Imagick($this->pathToImage);
2548
            $colorspace = $image->getImageColorspace();
2549
            $this->log(" Current colorspace: " . $colorspace);
2550
2551
            $profiles      = $image->getImageProfiles('*', false);
2552
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2553
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2554
2555
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2556
                $this->log(" Converting to sRGB.");
2557
2558
                $sRGBicc = file_get_contents($iccFile);
2559
                $image->profileImage('icc', $sRGBicc);
2560
2561
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2562
                $image->writeImage($this->cacheFileName);
2563
                return $this->cacheFileName;
2564
            }
2565
        }
2566
2567
        return false;
2568
    }
2569
2570
2571
2572
    /**
2573
     * Create a hard link, as an alias, to the cached file.
2574
     *
2575
     * @param string $alias where to store the link,
2576
     *                      filename without extension.
2577
     *
2578
     * @return $this
2579
     */
2580
    public function linkToCacheFile($alias)
2581
    {
2582
        if ($alias === null) {
2583
            $this->log("Ignore creating alias.");
2584
            return $this;
2585
        }
2586
2587
        if (is_readable($alias)) {
2588
            unlink($alias);
2589
        }
2590
2591
        $res = link($this->cacheFileName, $alias);
2592
2593
        if ($res) {
2594
            $this->log("Created an alias as: $alias");
2595
        } else {
2596
            $this->log("Failed to create the alias: $alias");
2597
        }
2598
2599
        return $this;
2600
    }
2601
2602
2603
2604
    /**
2605
     * Add HTTP header for output together with image.
2606
     *
2607
     * @param string $type  the header type such as "Cache-Control"
2608
     * @param string $value the value to use
2609
     *
2610
     * @return void
2611
     */
2612
    public function addHTTPHeader($type, $value)
2613
    {
2614
        $this->HTTPHeader[$type] = $value;
2615
    }
2616
2617
2618
2619
    /**
2620
     * Output image to browser using caching.
2621
     *
2622
     * @param string $file   to read and output, default is to
2623
     *                       use $this->cacheFileName
2624
     * @param string $format set to json to output file as json
2625
     *                       object with details
2626
     *
2627
     * @return void
2628
     */
2629
    public function output($file = null, $format = null)
2630
    {
2631
        if (is_null($file)) {
2632
            $file = $this->cacheFileName;
2633
        }
2634
2635
        if (is_null($format)) {
2636
            $format = $this->outputFormat;
2637
        }
2638
2639
        $this->log("Output format is: $format");
2640
2641
        if (!$this->verbose && $format == 'json') {
2642
            header('Content-type: application/json');
2643
            echo $this->json($file);
2644
            exit;
2645
        } elseif ($format == 'ascii') {
2646
            header('Content-type: text/plain');
2647
            echo $this->ascii($file);
2648
            exit;
2649
        }
2650
2651
        $this->log("Outputting image: $file");
2652
2653
        // Get image modification time
2654
        clearstatcache();
2655
        $lastModified = filemtime($file);
2656
        $lastModifiedFormat = "D, d M Y H:i:s";
2657
        $gmdate = gmdate($lastModifiedFormat, $lastModified);
2658
2659
        if (!$this->verbose) {
2660
            $header = "Last-Modified: $gmdate GMT";
2661
            header($header);
2662
            $this->fastTrackCache->addHeader($header);
2663
            $this->fastTrackCache->setLastModified($lastModified);
2664
        }
2665
2666
        foreach ($this->HTTPHeader as $key => $val) {
2667
            $header = "$key: $val";
2668
            header($header);
2669
            $this->fastTrackCache->addHeader($header);
2670
        }
2671
2672
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2673
2674
            if ($this->verbose) {
2675
                $this->log("304 not modified");
2676
                $this->verboseOutput();
2677
                exit;
2678
            }
2679
2680
            header("HTTP/1.0 304 Not Modified");
2681
            if (CIMAGE_DEBUG) {
2682
                trace(__CLASS__ . " 304");
2683
            }
2684
2685
        } else {
2686
2687
            // Get details on image
2688
            $info = getimagesize($file);
2689
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
2690
            $mime = $info['mime'];
2691
            $size = filesize($file);
2692
2693
            if ($this->verbose) {
2694
                $this->log("Last-Modified: " . $gmdate . " GMT");
2695
                $this->log("Content-type: " . $mime);
2696
                $this->log("Content-length: " . $size);
2697
                $this->verboseOutput();
2698
2699
                if (is_null($this->verboseFileName)) {
2700
                    exit;
2701
                }
2702
            }
2703
2704
            $header = "Content-type: $mime";
2705
            header($header);
2706
            $this->fastTrackCache->addHeaderOnOutput($header);
2707
2708
            $header = "Content-length: $size";
2709
            header($header);
2710
            $this->fastTrackCache->addHeaderOnOutput($header);
2711
2712
            $this->fastTrackCache->setSource($file);
2713
            $this->fastTrackCache->writeToCache();
2714
            if (CIMAGE_DEBUG) {
2715
                trace(__CLASS__ . " 200");
2716
            }
2717
            readfile($file);
2718
        }
2719
2720
        exit;
2721
    }
2722
2723
2724
2725
    /**
2726
     * Create a JSON object from the image details.
2727
     *
2728
     * @param string $file the file to output.
2729
     *
2730
     * @return string json-encoded representation of the image.
2731
     */
2732
    public function json($file = null)
2733
    {
2734
        $file = $file ? $file : $this->cacheFileName;
2735
2736
        $details = array();
2737
2738
        clearstatcache();
2739
2740
        $details['src']       = $this->imageSrc;
2741
        $lastModified         = filemtime($this->pathToImage);
2742
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2743
2744
        $details['cache']       = basename($this->cacheFileName);
2745
        $lastModified           = filemtime($this->cacheFileName);
2746
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2747
2748
        $this->load($file);
2749
2750
        $details['filename']    = basename($file);
2751
        $details['mimeType']    = $this->getMimeType($this->fileType);
2752
        $details['width']       = $this->width;
2753
        $details['height']      = $this->height;
2754
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2755
        $details['size']        = filesize($file);
2756
        $details['colors'] = $this->colorsTotal($this->image);
2757
        $details['includedFiles'] = count(get_included_files());
2758
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2759
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2760
        $details['memoryLimit'] = ini_get('memory_limit');
2761
2762
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2763
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2764
        }
2765
2766
        if ($details['mimeType'] == 'image/png') {
2767
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2768
        }
2769
2770
        $options = null;
2771
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2772
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2773
        }
2774
2775
        return json_encode($details, $options);
2776
    }
2777
2778
2779
2780
    /**
2781
     * Set options for creating ascii version of image.
2782
     *
2783
     * @param array $options empty to use default or set options to change.
2784
     *
2785
     * @return void.
2786
     */
2787
    public function setAsciiOptions($options = array())
2788
    {
2789
        $this->asciiOptions = $options;
2790
    }
2791
2792
2793
2794
    /**
2795
     * Create an ASCII version from the image details.
2796
     *
2797
     * @param string $file the file to output.
2798
     *
2799
     * @return string ASCII representation of the image.
2800
     */
2801
    public function ascii($file = null)
2802
    {
2803
        $file = $file ? $file : $this->cacheFileName;
2804
2805
        $asciiArt = new CAsciiArt();
2806
        $asciiArt->setOptions($this->asciiOptions);
2807
        return $asciiArt->createFromFile($file);
2808
    }
2809
2810
2811
2812
    /**
2813
     * Log an event if verbose mode.
2814
     *
2815
     * @param string $message to log.
2816
     *
2817
     * @return this
2818
     */
2819
    public function log($message)
2820
    {
2821
        if ($this->verbose) {
2822
            $this->log[] = $message;
2823
        }
2824
2825
        return $this;
2826
    }
2827
2828
2829
2830
    /**
2831
     * Do verbose output to a file.
2832
     *
2833
     * @param string $fileName where to write the verbose output.
2834
     *
2835
     * @return void
2836
     */
2837
    public function setVerboseToFile($fileName)
2838
    {
2839
        $this->log("Setting verbose output to file.");
2840
        $this->verboseFileName = $fileName;
2841
    }
2842
2843
2844
2845
    /**
2846
     * Do verbose output and print out the log and the actual images.
2847
     *
2848
     * @return void
2849
     */
2850
    private function verboseOutput()
2851
    {
2852
        $log = null;
2853
        $this->log("As JSON: \n" . $this->json());
2854
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2855
        $this->log("Memory limit: " . ini_get('memory_limit'));
2856
2857
        $included = get_included_files();
2858
        $this->log("Included files: " . count($included));
2859
2860
        foreach ($this->log as $val) {
2861
            if (is_array($val)) {
2862
                foreach ($val as $val1) {
2863
                    $log .= htmlentities($val1) . '<br/>';
2864
                }
2865
            } else {
2866
                $log .= htmlentities($val) . '<br/>';
2867
            }
2868
        }
2869
2870
        if (!is_null($this->verboseFileName)) {
2871
            file_put_contents(
2872
                $this->verboseFileName,
2873
                str_replace("<br/>", "\n", $log)
2874
            );
2875
        } else {
2876
            echo <<<EOD
2877
<h1>CImage Verbose Output</h1>
2878
<pre>{$log}</pre>
2879
EOD;
2880
        }
2881
    }
2882
2883
2884
2885
    /**
2886
     * Raise error, enables to implement a selection of error methods.
2887
     *
2888
     * @param string $message the error message to display.
2889
     *
2890
     * @return void
2891
     * @throws Exception
2892
     */
2893
    private function raiseError($message)
2894
    {
2895
        throw new Exception($message);
2896
    }
2897
}
2898