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

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

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

Loading history...
2664
    }
2665
2666
2667
2668
    /**
2669
     * Create a JSON object from the image details.
2670
     *
2671
     * @param string $file the file to output.
2672
     *
2673
     * @return string json-encoded representation of the image.
2674
     */
2675
    public function json($file = null)
2676
    {
2677
        $file = $file ? $file : $this->cacheFileName;
2678
2679
        $details = array();
2680
2681
        clearstatcache();
2682
2683
        $details['src']       = $this->imageSrc;
2684
        $lastModified         = filemtime($this->pathToImage);
2685
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2686
2687
        $details['cache']       = basename($this->cacheFileName);
2688
        $lastModified           = filemtime($this->cacheFileName);
2689
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2690
2691
        $this->load($file);
2692
2693
        $details['filename']    = basename($file);
2694
        $details['mimeType']    = image_type_to_mime_type($this->fileType);
2695
        $details['width']       = $this->width;
2696
        $details['height']      = $this->height;
2697
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2698
        $details['size']        = filesize($file);
2699
        $details['colors'] = $this->colorsTotal($this->image);
2700
        $details['includedFiles'] = count(get_included_files());
2701
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2702
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2703
        $details['memoryLimit'] = ini_get('memory_limit');
2704
2705
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2706
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2707
        }
2708
2709
        if ($details['mimeType'] == 'image/png') {
2710
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2711
        }
2712
2713
        $options = null;
2714
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2715
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2716
        }
2717
2718
        return json_encode($details, $options);
2719
    }
2720
2721
2722
2723
    /**
2724
     * Set options for creating ascii version of image.
2725
     *
2726
     * @param array $options empty to use default or set options to change.
2727
     *
2728
     * @return void.
2729
     */
2730
    public function setAsciiOptions($options = array())
2731
    {
2732
        $this->asciiOptions = $options;
2733
    }
2734
2735
2736
2737
    /**
2738
     * Create an ASCII version from the image details.
2739
     *
2740
     * @param string $file the file to output.
2741
     *
2742
     * @return string ASCII representation of the image.
2743
     */
2744
    public function ascii($file = null)
2745
    {
2746
        $file = $file ? $file : $this->cacheFileName;
2747
2748
        $asciiArt = new CAsciiArt();
2749
        $asciiArt->setOptions($this->asciiOptions);
2750
        return $asciiArt->createFromFile($file);
2751
    }
2752
2753
2754
2755
    /**
2756
     * Log an event if verbose mode.
2757
     *
2758
     * @param string $message to log.
2759
     *
2760
     * @return this
2761
     */
2762
    public function log($message)
2763
    {
2764
        if ($this->verbose) {
2765
            $this->log[] = $message;
2766
        }
2767
2768
        return $this;
2769
    }
2770
2771
2772
2773
    /**
2774
     * Do verbose output to a file.
2775
     *
2776
     * @param string $fileName where to write the verbose output.
2777
     *
2778
     * @return void
2779
     */
2780
    public function setVerboseToFile($fileName)
2781
    {
2782
        $this->log("Setting verbose output to file.");
2783
        $this->verboseFileName = $fileName;
2784
    }
2785
2786
2787
2788
    /**
2789
     * Do verbose output and print out the log and the actual images.
2790
     *
2791
     * @return void
2792
     */
2793
    private function verboseOutput()
2794
    {
2795
        $log = null;
2796
        $this->log("As JSON: \n" . $this->json());
2797
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2798
        $this->log("Memory limit: " . ini_get('memory_limit'));
2799
2800
        $included = get_included_files();
2801
        $this->log("Included files: " . count($included));
2802
2803
        foreach ($this->log as $val) {
2804
            if (is_array($val)) {
2805
                foreach ($val as $val1) {
2806
                    $log .= htmlentities($val1) . '<br/>';
2807
                }
2808
            } else {
2809
                $log .= htmlentities($val) . '<br/>';
2810
            }
2811
        }
2812
2813
        if (!is_null($this->verboseFileName)) {
2814
            file_put_contents(
2815
                $this->verboseFileName,
2816
                str_replace("<br/>", "\n", $log)
2817
            );
2818
        } else {
2819
            echo <<<EOD
2820
<h1>CImage Verbose Output</h1>
2821
<pre>{$log}</pre>
2822
EOD;
2823
        }
2824
    }
2825
2826
2827
2828
    /**
2829
     * Raise error, enables to implement a selection of error methods.
2830
     *
2831
     * @param string $message the error message to display.
2832
     *
2833
     * @return void
2834
     * @throws Exception
2835
     */
2836
    private function raiseError($message)
2837
    {
2838
        throw new Exception($message);
2839
    }
2840
}
2841