Completed
Push — master ( cde8ba...32a238 )
by Mikael
03:02
created

CImage.php (4 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
    /*
384
     * Set whitelist for valid hostnames from where remote source can be
385
     * downloaded.
386
     */
387
    private $remoteHostWhitelist = null;
388
389
390
391
    /*
392
     * Do verbose logging to file by setting this to a filename.
393
     */
394
    private $verboseFileName = null;
395
396
397
398
    /*
399
     * Output to ascii can take som options as an array.
400
     */
401
    private $asciiOptions = array();
402
403
404
405
    /*
406
     * Image copy strategy, defaults to RESAMPLE.
407
     */
408
     const RESIZE = 1;
409
     const RESAMPLE = 2;
410
     private $copyStrategy = NULL;
411
412
413
414
    /**
415
     * Properties, the class is mutable and the method setOptions()
416
     * decides (partly) what properties are created.
417
     *
418
     * @todo Clean up these and check if and how they are used
419
     */
420
421
    public $keepRatio;
422
    public $cropToFit;
423
    private $cropWidth;
424
    private $cropHeight;
425
    public $crop_x;
426
    public $crop_y;
427
    public $filters;
428
    private $attr; // Calculated from source image
429
430
431
432
433
    /**
434 7
     * Constructor, can take arguments to init the object.
435
     *
436 7
     * @param string $imageSrc    filename which may contain subdirectory.
437 7
     * @param string $imageFolder path to root folder for images.
438 7
     * @param string $saveFolder  path to folder where to save the new file or null to skip saving.
439
     * @param string $saveName    name of target file when saveing.
440
     */
441
    public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null)
442
    {
443
        $this->setSource($imageSrc, $imageFolder);
444
        $this->setTarget($saveFolder, $saveName);
445
    }
446
447
448
449
    /**
450
     * Set verbose mode.
451
     *
452
     * @param boolean $mode true or false to enable and disable verbose mode,
453
     *                      default is true.
454
     *
455
     * @return $this
456
     */
457
    public function setVerbose($mode = true)
458
    {
459
        $this->verbose = $mode;
460
        return $this;
461
    }
462
463
464
465
    /**
466
     * Set save folder, base folder for saving cache files.
467 2
     *
468
     * @todo clean up how $this->saveFolder is used in other methods.
469 2
     *
470 2
     * @param string $path where to store cached files.
471
     *
472
     * @return $this
473
     */
474
    public function setSaveFolder($path)
475
    {
476
        $this->saveFolder = $path;
477
        return $this;
478
    }
479
480
481
482
    /**
483
     * Use cache or not.
484
     *
485
     * @param boolean $use true or false to use cache.
486
     *
487
     * @return $this
488
     */
489
    public function useCache($use = true)
490
    {
491
        $this->useCache = $use;
492
        return $this;
493
    }
494
495
496
497
    /**
498
     * Create and save a dummy image. Use dimensions as stated in
499 2
     * $this->newWidth, or $width or default to 100 (same for height.
500
     *
501 2
     * @param integer $width  use specified width for image dimension.
502 2
     * @param integer $height use specified width for image dimension.
503
     *
504 2
     * @return $this
505
     */
506 2
    public function createDummyImage($width = null, $height = null)
507
    {
508
        $this->newWidth  = $this->newWidth  ?: $width  ?: 100;
509
        $this->newHeight = $this->newHeight ?: $height ?: 100;
510
511
        $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
512
513
        return $this;
514
    }
515
516
517
518
    /**
519 2
     * Allow or disallow remote image download.
520
     *
521 2
     * @param boolean $allow   true or false to enable and disable.
522 2
     * @param string  $cache   path to cache dir.
523
     * @param string  $pattern to use to detect if its a remote file.
524 2
     *
525
     * @return $this
526 2
     */
527 2
    public function setRemoteDownload($allow, $cache, $pattern = null)
528 2
    {
529 2
        $this->allowRemote = $allow;
530
        $this->remoteCache = $cache;
531 2
        $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern;
532
533
        $this->log(
534
            "Set remote download to: "
535
            . ($this->allowRemote ? "true" : "false")
536
            . " using pattern "
537
            . $this->remotePattern
538
        );
539
540
        return $this;
541
    }
542
543 2
544
545 2
    /**
546 2
     * Check if the image resource is a remote file or not.
547 2
     *
548
     * @param string $src check if src is remote.
549
     *
550
     * @return boolean true if $src is a remote file, else false.
551
     */
552
    public function isRemoteSource($src)
553
    {
554
        $remote = preg_match($this->remotePattern, $src);
555
        $this->log("Detected remote image: " . ($remote ? "true" : "false"));
556
        return !!$remote;
557
    }
558
559
560 2
561
    /**
562 2
     * Set whitelist for valid hostnames from where remote source can be
563 2
     * downloaded.
564
     *
565 2
     * @param array $whitelist with regexp hostnames to allow download from.
566 2
     *
567 2
     * @return $this
568
     */
569
    public function setRemoteHostWhitelist($whitelist = null)
570
    {
571
        $this->remoteHostWhitelist = $whitelist;
572
        $this->log(
573
            "Setting remote host whitelist to: "
574
            . (is_null($whitelist) ? "null" : print_r($whitelist, 1))
575
        );
576
        return $this;
577
    }
578
579
580 3
581
    /**
582 3
     * Check if the hostname for the remote image, is on a whitelist,
583 1
     * if the whitelist is defined.
584 1
     *
585
     * @param string $src the remote source.
586
     *
587 2
     * @return boolean true if hostname on $src is in the whitelist, else false.
588 2
     */
589 2
    public function isRemoteSourceOnWhitelist($src)
590
    {
591 2
        if (is_null($this->remoteHostWhitelist)) {
592
            $this->log("Remote host on whitelist not configured - allowing.");
593 2
            return true;
594 2
        }
595 2
596
        $whitelist = new CWhitelist();
597
        $hostname = parse_url($src, PHP_URL_HOST);
598
        $allow = $whitelist->check($hostname, $this->remoteHostWhitelist);
599
600
        $this->log(
601
            "Remote host is on whitelist: "
602
            . ($allow ? "true" : "false")
603
        );
604
        return $allow;
605
    }
606
607
608
609
    /**
610
     * Check if file extension is valid as a file extension.
611
     *
612
     * @param string $extension of image file.
613
     *
614
     * @return $this
615
     */
616
    private function checkFileExtension($extension)
617
    {
618
        $valid = array('jpg', 'jpeg', 'png', 'gif');
619
620
        in_array(strtolower($extension), $valid)
621
            or $this->raiseError('Not a valid file extension.');
622
623
        return $this;
624
    }
625
626 2
627
628 2
    /**
629
     * Normalize the file extension.
630 2
     *
631
     * @param string $extension of image file or skip to use internal.
632
     *
633
     * @return string $extension as a normalized file extension.
634 2
     */
635
    private function normalizeFileExtension($extension = null)
636
    {
637
        $extension = strtolower($extension ? $extension : $this->extension);
638
639
        if ($extension == 'jpeg') {
640
                $extension = 'jpg';
641
            }
642
643
        return $extension;
644
    }
645
646
647
648
    /**
649
     * Download a remote image and return path to its local copy.
650
     *
651
     * @param string $src remote path to image.
652
     *
653
     * @return string as path to downloaded remote source.
654
     */
655
    public function downloadRemoteSource($src)
656
    {
657
        if (!$this->isRemoteSourceOnWhitelist($src)) {
658
            throw new Exception("Hostname is not on whitelist for remote sources.");
659
        }
660
661
        $remote = new CRemoteImage();
662
663
        if (!is_writable($this->remoteCache)) {
664
            $this->log("The remote cache is not writable.");
665
        }
666
667
        $remote->setCache($this->remoteCache);
668
        $remote->useCache($this->useCache);
669
        $src = $remote->download($src);
670
671
        $this->log("Remote HTTP status: " . $remote->getStatus());
672
        $this->log("Remote item is in local cache: $src");
673
        $this->log("Remote details on cache:" . print_r($remote->getDetails(), true));
674
675
        return $src;
676
    }
677
678
679
680
    /**
681
     * Set source file to use as image source.
682
     *
683
     * @param string $src of image.
684
     * @param string $dir as optional base directory where images are.
685
     *
686
     * @return $this
687
     */
688 7
    public function setSource($src, $dir = null)
689
    {
690 7
        if (!isset($src)) {
691 7
            $this->imageSrc = null;
692 7
            $this->pathToImage = null;
693 7
            return $this;
694
        }
695
696 2
        if ($this->allowRemote && $this->isRemoteSource($src)) {
697
            $src = $this->downloadRemoteSource($src);
698
            $dir = null;
699
        }
700
701 2
        if (!isset($dir)) {
702
            $dir = dirname($src);
703
            $src = basename($src);
704
        }
705
706 2
        $this->imageSrc     = ltrim($src, '/');
707 2
        $imageFolder        = rtrim($dir, '/');
708 2
        $this->pathToImage  = $imageFolder . '/' . $this->imageSrc;
709
710 2
        return $this;
711
    }
712
713
714
715
    /**
716
     * Set target file.
717
     *
718
     * @param string $src of target image.
719
     * @param string $dir as optional base directory where images are stored.
720
     *                    Uses $this->saveFolder if null.
721
     *
722
     * @return $this
723
     */
724 7
    public function setTarget($src = null, $dir = null)
725
    {
726 7
        if (!isset($src)) {
727 7
            $this->cacheFileName = null;
728 7
            return $this;
729
        }
730
731 2
        if (isset($dir)) {
732
            $this->saveFolder = rtrim($dir, '/');
733
        }
734
735 2
        $this->cacheFileName  = $this->saveFolder . '/' . $src;
736
737
        // Sanitize filename
738 2
        $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName);
739 2
        $this->log("The cache file name is: " . $this->cacheFileName);
740
741 2
        return $this;
742
    }
743
744
745
746
    /**
747
     * Get filename of target file.
748
     *
749
     * @return Boolean|String as filename of target or false if not set.
750
     */
751 2
    public function getTarget()
752
    {
753 2
        return $this->cacheFileName;
754
    }
755
756
757
758
    /**
759
     * Set options to use when processing image.
760
     *
761
     * @param array $args used when processing image.
762
     *
763
     * @return $this
764
     */
765
    public function setOptions($args)
766
    {
767
        $this->log("Set new options for processing image.");
768
769
        $defaults = array(
770
            // Options for calculate dimensions
771
            'newWidth'    => null,
772
            'newHeight'   => null,
773
            'aspectRatio' => null,
774
            'keepRatio'   => true,
775
            'cropToFit'   => false,
776
            'fillToFit'   => null,
777
            'crop'        => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0),
778
            'area'        => null, //'0,0,0,0',
779
            'upscale'     => self::UPSCALE_DEFAULT,
780
781
            // Options for caching or using original
782
            'useCache'    => true,
783
            'useOriginal' => true,
784
785
            // Pre-processing, before resizing is done
786
            'scale'        => null,
787
            'rotateBefore' => null,
788
            'autoRotate'  => false,
789
790
            // General options
791
            'bgColor'     => null,
792
793
            // Post-processing, after resizing is done
794
            'palette'     => null,
795
            'filters'     => null,
796
            'sharpen'     => null,
797
            'emboss'      => null,
798
            'blur'        => null,
799
            'convolve'       => null,
800
            'rotateAfter' => null,
801
802
            // Output format
803
            'outputFormat' => null,
804
            'dpr'          => 1,
805
        );
806
807
        // Convert crop settings from string to array
808
        if (isset($args['crop']) && !is_array($args['crop'])) {
809
            $pices = explode(',', $args['crop']);
810
            $args['crop'] = array(
811
                'width'   => $pices[0],
812
                'height'  => $pices[1],
813
                'start_x' => $pices[2],
814
                'start_y' => $pices[3],
815
            );
816
        }
817
818
        // Convert area settings from string to array
819
        if (isset($args['area']) && !is_array($args['area'])) {
820
                $pices = explode(',', $args['area']);
821
                $args['area'] = array(
822
                    'top'    => $pices[0],
823
                    'right'  => $pices[1],
824
                    'bottom' => $pices[2],
825
                    'left'   => $pices[3],
826
                );
827
        }
828
829
        // Convert filter settings from array of string to array of array
830
        if (isset($args['filters']) && is_array($args['filters'])) {
831
            foreach ($args['filters'] as $key => $filterStr) {
832
                $parts = explode(',', $filterStr);
833
                $filter = $this->mapFilter($parts[0]);
834
                $filter['str'] = $filterStr;
835
                for ($i=1; $i<=$filter['argc']; $i++) {
836
                    if (isset($parts[$i])) {
837
                        $filter["arg{$i}"] = $parts[$i];
838
                    } else {
839
                        throw new Exception(
840
                            'Missing arg to filter, review how many arguments are needed at
841
                            http://php.net/manual/en/function.imagefilter.php'
842
                        );
843
                    }
844
                }
845
                $args['filters'][$key] = $filter;
846
            }
847
        }
848
849
        // Merge default arguments with incoming and set properties.
850
        //$args = array_merge_recursive($defaults, $args);
851
        $args = array_merge($defaults, $args);
852
        foreach ($defaults as $key => $val) {
853
            $this->{$key} = $args[$key];
854
        }
855
856
        if ($this->bgColor) {
857
            $this->setDefaultBackgroundColor($this->bgColor);
858
        }
859
860
        // Save original values to enable re-calculating
861
        $this->newWidthOrig  = $this->newWidth;
862
        $this->newHeightOrig = $this->newHeight;
863
        $this->cropOrig      = $this->crop;
864
865
        return $this;
866
    }
867
868
869
870
    /**
871
     * Map filter name to PHP filter and id.
872
     *
873
     * @param string $name the name of the filter.
874
     *
875
     * @return array with filter settings
876
     * @throws Exception
877
     */
878
    private function mapFilter($name)
879
    {
880
        $map = array(
881
            'negate'          => array('id'=>0,  'argc'=>0, 'type'=>IMG_FILTER_NEGATE),
882
            'grayscale'       => array('id'=>1,  'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE),
883
            'brightness'      => array('id'=>2,  'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS),
884
            'contrast'        => array('id'=>3,  'argc'=>1, 'type'=>IMG_FILTER_CONTRAST),
885
            'colorize'        => array('id'=>4,  'argc'=>4, 'type'=>IMG_FILTER_COLORIZE),
886
            'edgedetect'      => array('id'=>5,  'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT),
887
            'emboss'          => array('id'=>6,  'argc'=>0, 'type'=>IMG_FILTER_EMBOSS),
888
            'gaussian_blur'   => array('id'=>7,  'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR),
889
            'selective_blur'  => array('id'=>8,  'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR),
890
            'mean_removal'    => array('id'=>9,  'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL),
891
            'smooth'          => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH),
892
            'pixelate'        => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE),
893
        );
894
895
        if (isset($map[$name])) {
896
            return $map[$name];
897
        } else {
898
            throw new Exception('No such filter.');
899
        }
900
    }
901
902
903
904
    /**
905
     * Load image details from original image file.
906
     *
907
     * @param string $file the file to load or null to use $this->pathToImage.
908
     *
909
     * @return $this
910
     * @throws Exception
911
     */
912
    public function loadImageDetails($file = null)
913
    {
914
        $file = $file ? $file : $this->pathToImage;
915
916
        is_readable($file)
917
            or $this->raiseError('Image file does not exist.');
918
919
        // Get details on image
920
        $info = list($this->width, $this->height, $this->fileType, $this->attr) = getimagesize($file);
921
        if (empty($info)) {
922
            throw new Exception("The file doesn't seem to be a valid image.");
923
        }
924
925
        if ($this->verbose) {
926
            $this->log("Loading image details for: {$file}");
927
            $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType}).");
928
            $this->log(" Image filesize: " . filesize($file) . " bytes.");
929
            $this->log(" Image mimetype: " . image_type_to_mime_type($this->fileType));
930
        }
931
932
        return $this;
933
    }
934
935
936
937
    /**
938
     * Init new width and height and do some sanity checks on constraints, before any
939
     * processing can be done.
940
     *
941
     * @return $this
942
     * @throws Exception
943
     */
944
    public function initDimensions()
945
    {
946
        $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
947
948
        // width as %
949
        if ($this->newWidth[strlen($this->newWidth)-1] == '%') {
950
            $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100;
951
            $this->log("Setting new width based on % to {$this->newWidth}");
952
        }
953
954
        // height as %
955
        if ($this->newHeight[strlen($this->newHeight)-1] == '%') {
956
            $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100;
957
            $this->log("Setting new height based on % to {$this->newHeight}");
958
        }
959
960
        is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range');
961
962
        // width & height from aspect ratio
963
        if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) {
964
            if ($this->aspectRatio >= 1) {
965
                $this->newWidth   = $this->width;
966
                $this->newHeight  = $this->width / $this->aspectRatio;
967
                $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}");
968
969
            } else {
970
                $this->newHeight  = $this->height;
971
                $this->newWidth   = $this->height * $this->aspectRatio;
972
                $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}");
973
            }
974
975
        } elseif ($this->aspectRatio && is_null($this->newWidth)) {
976
            $this->newWidth   = $this->newHeight * $this->aspectRatio;
977
            $this->log("Setting new width based on aspect ratio to {$this->newWidth}");
978
979
        } elseif ($this->aspectRatio && is_null($this->newHeight)) {
980
            $this->newHeight  = $this->newWidth / $this->aspectRatio;
981
            $this->log("Setting new height based on aspect ratio to {$this->newHeight}");
982
        }
983
984
        // Change width & height based on dpr
985
        if ($this->dpr != 1) {
986
            if (!is_null($this->newWidth)) {
987
                $this->newWidth  = round($this->newWidth * $this->dpr);
988
                $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}");
989
            }
990
            if (!is_null($this->newHeight)) {
991
                $this->newHeight = round($this->newHeight * $this->dpr);
992
                $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}");
993
            }
994
        }
995
996
        // Check values to be within domain
997
        is_null($this->newWidth)
998
            or is_numeric($this->newWidth)
999
            or $this->raiseError('Width not numeric');
1000
1001
        is_null($this->newHeight)
1002
            or is_numeric($this->newHeight)
1003
            or $this->raiseError('Height not numeric');
1004
1005
        $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1006
1007
        return $this;
1008
    }
1009
1010
1011
1012
    /**
1013
     * Calculate new width and height of image, based on settings.
1014
     *
1015
     * @return $this
1016
     */
1017
    public function calculateNewWidthAndHeight()
1018
    {
1019
        // Crop, use cropped width and height as base for calulations
1020
        $this->log("Calculate new width and height.");
1021
        $this->log("Original width x height is {$this->width} x {$this->height}.");
1022
        $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}.");
1023
1024
        // Check if there is an area to crop off
1025
        if (isset($this->area)) {
1026
            $this->offset['top']    = round($this->area['top'] / 100 * $this->height);
1027
            $this->offset['right']  = round($this->area['right'] / 100 * $this->width);
1028
            $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height);
1029
            $this->offset['left']   = round($this->area['left'] / 100 * $this->width);
1030
            $this->offset['width']  = $this->width - $this->offset['left'] - $this->offset['right'];
1031
            $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom'];
1032
            $this->width  = $this->offset['width'];
1033
            $this->height = $this->offset['height'];
1034
            $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']}%.");
1035
            $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.");
1036
        }
1037
1038
        $width  = $this->width;
1039
        $height = $this->height;
1040
1041
        // Check if crop is set
1042
        if ($this->crop) {
1043
            $width  = $this->crop['width']  = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width'];
1044
            $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height'];
1045
1046
            if ($this->crop['start_x'] == 'left') {
1047
                $this->crop['start_x'] = 0;
1048
            } elseif ($this->crop['start_x'] == 'right') {
1049
                $this->crop['start_x'] = $this->width - $width;
1050
            } elseif ($this->crop['start_x'] == 'center') {
1051
                $this->crop['start_x'] = round($this->width / 2) - round($width / 2);
1052
            }
1053
1054
            if ($this->crop['start_y'] == 'top') {
1055
                $this->crop['start_y'] = 0;
1056
            } elseif ($this->crop['start_y'] == 'bottom') {
1057
                $this->crop['start_y'] = $this->height - $height;
1058
            } elseif ($this->crop['start_y'] == 'center') {
1059
                $this->crop['start_y'] = round($this->height / 2) - round($height / 2);
1060
            }
1061
1062
            $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.");
1063
        }
1064
1065
        // Calculate new width and height if keeping aspect-ratio.
1066
        if ($this->keepRatio) {
1067
1068
            $this->log("Keep aspect ratio.");
1069
1070
            // Crop-to-fit and both new width and height are set.
1071
            if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) {
1072
1073
                // Use newWidth and newHeigh as width/height, image should fit in box.
1074
                $this->log("Use newWidth and newHeigh as width/height, image should fit in box.");
1075
1076
            } elseif (isset($this->newWidth) && isset($this->newHeight)) {
1077
1078
                // Both new width and height are set.
1079
                // Use newWidth and newHeigh as max width/height, image should not be larger.
1080
                $ratioWidth  = $width  / $this->newWidth;
1081
                $ratioHeight = $height / $this->newHeight;
1082
                $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight;
1083
                $this->newWidth  = round($width  / $ratio);
1084
                $this->newHeight = round($height / $ratio);
1085
                $this->log("New width and height was set.");
1086
1087
            } elseif (isset($this->newWidth)) {
1088
1089
                // Use new width as max-width
1090
                $factor = (float)$this->newWidth / (float)$width;
1091
                $this->newHeight = round($factor * $height);
1092
                $this->log("New width was set.");
1093
1094
            } elseif (isset($this->newHeight)) {
1095
1096
                // Use new height as max-hight
1097
                $factor = (float)$this->newHeight / (float)$height;
1098
                $this->newWidth = round($factor * $width);
1099
                $this->log("New height was set.");
1100
1101
            } else {
1102
1103
                // Use existing width and height as new width and height.
1104
                $this->newWidth = $width;
1105
                $this->newHeight = $height;
1106
            }
1107
            
1108
1109
            // Get image dimensions for pre-resize image.
1110
            if ($this->cropToFit || $this->fillToFit) {
1111
1112
                // Get relations of original & target image
1113
                $ratioWidth  = $width  / $this->newWidth;
1114
                $ratioHeight = $height / $this->newHeight;
1115
1116
                if ($this->cropToFit) {
1117
1118
                    // Use newWidth and newHeigh as defined width/height,
1119
                    // image should fit the area.
1120
                    $this->log("Crop to fit.");
1121
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight;
1122
                    $this->cropWidth  = round($width  / $ratio);
1123
                    $this->cropHeight = round($height / $ratio);
1124
                    $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio).");
1125
1126
                } elseif ($this->fillToFit) {
1127
1128
                    // Use newWidth and newHeigh as defined width/height,
1129
                    // image should fit the area.
1130
                    $this->log("Fill to fit.");
1131
                    $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth;
1132
                    $this->fillWidth  = round($width  / $ratio);
1133
                    $this->fillHeight = round($height / $ratio);
1134
                    $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio).");
1135
                }
1136
            }
1137
        }
1138
1139
        // Crop, ensure to set new width and height
1140
        if ($this->crop) {
1141
            $this->log("Crop.");
1142
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1143
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1144
        }
1145
1146
        // Fill to fit, ensure to set new width and height
1147
        /*if ($this->fillToFit) {
1148
            $this->log("FillToFit.");
1149
            $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']);
1150
            $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']);
1151
        }*/
1152
1153
        // No new height or width is set, use existing measures.
1154
        $this->newWidth  = round(isset($this->newWidth) ? $this->newWidth : $this->width);
1155
        $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height);
1156
        $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}.");
1157
1158
        return $this;
1159
    }
1160
1161
1162
1163
    /**
1164
     * Re-calculate image dimensions when original image dimension has changed.
1165
     *
1166
     * @return $this
1167
     */
1168
    public function reCalculateDimensions()
1169
    {
1170
        $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight);
1171
1172
        $this->newWidth  = $this->newWidthOrig;
1173
        $this->newHeight = $this->newHeightOrig;
1174
        $this->crop      = $this->cropOrig;
1175
1176
        $this->initDimensions()
1177
             ->calculateNewWidthAndHeight();
1178
1179
        return $this;
1180
    }
1181
1182
1183
1184
    /**
1185
     * Set extension for filename to save as.
1186
     *
1187
     * @param string $saveas extension to save image as
1188
     *
1189
     * @return $this
1190
     */
1191
    public function setSaveAsExtension($saveAs = null)
1192
    {
1193
        if (isset($saveAs)) {
1194
            $saveAs = strtolower($saveAs);
1195
            $this->checkFileExtension($saveAs);
1196
            $this->saveAs = $saveAs;
1197
            $this->extension = $saveAs;
1198
        }
1199
1200
        $this->log("Prepare to save image as: " . $this->extension);
1201
1202
        return $this;
1203
    }
1204
1205
1206
1207
    /**
1208
     * Set JPEG quality to use when saving image
1209
     *
1210
     * @param int $quality as the quality to set.
1211
     *
1212
     * @return $this
1213
     */
1214
    public function setJpegQuality($quality = null)
1215
    {
1216
        if ($quality) {
1217
            $this->useQuality = true;
1218
        }
1219
1220
        $this->quality = isset($quality)
1221
            ? $quality
1222
            : self::JPEG_QUALITY_DEFAULT;
1223
1224
        (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100)
1225
            or $this->raiseError('Quality not in range.');
1226
1227
        $this->log("Setting JPEG quality to {$this->quality}.");
1228
1229
        return $this;
1230
    }
1231
1232
1233
1234
    /**
1235
     * Set PNG compressen algorithm to use when saving image
1236
     *
1237
     * @param int $compress as the algorithm to use.
1238
     *
1239
     * @return $this
1240
     */
1241
    public function setPngCompression($compress = null)
1242
    {
1243
        if ($compress) {
1244
            $this->useCompress = true;
1245
        }
1246
1247
        $this->compress = isset($compress)
1248
            ? $compress
1249
            : self::PNG_COMPRESSION_DEFAULT;
1250
1251
        (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9)
1252
            or $this->raiseError('Quality not in range.');
1253
1254
        $this->log("Setting PNG compression level to {$this->compress}.");
1255
1256
        return $this;
1257
    }
1258
1259
1260
1261
    /**
1262
     * Use original image if possible, check options which affects image processing.
1263
     *
1264
     * @param boolean $useOrig default is to use original if possible, else set to false.
1265
     *
1266
     * @return $this
1267
     */
1268
    public function useOriginalIfPossible($useOrig = true)
1269
    {
1270
        if ($useOrig
1271
            && ($this->newWidth == $this->width)
1272
            && ($this->newHeight == $this->height)
1273
            && !$this->area
1274
            && !$this->crop
1275
            && !$this->cropToFit
1276
            && !$this->fillToFit
1277
            && !$this->filters
1278
            && !$this->sharpen
1279
            && !$this->emboss
1280
            && !$this->blur
1281
            && !$this->convolve
1282
            && !$this->palette
1283
            && !$this->useQuality
1284
            && !$this->useCompress
1285
            && !$this->saveAs
1286
            && !$this->rotateBefore
1287
            && !$this->rotateAfter
1288
            && !$this->autoRotate
1289
            && !$this->bgColor
1290
            && ($this->upscale === self::UPSCALE_DEFAULT)
1291
        ) {
1292
            $this->log("Using original image.");
1293
            $this->output($this->pathToImage);
1294
        }
1295
1296
        return $this;
1297
    }
1298
1299
1300
1301
    /**
1302
     * Generate filename to save file in cache.
1303
     *
1304 2
     * @param string  $base      as optional basepath for storing file.
1305
     * @param boolean $useSubdir use or skip the subdir part when creating the
1306 2
     *                           filename.
1307 2
     * @param string  $prefix    to add as part of filename
1308 2
     *
1309 2
     * @return $this
1310 2
     */
1311 2
    public function generateFilename($base = null, $useSubdir = true, $prefix = null)
1312 2
    {
1313 2
        $filename     = basename($this->pathToImage);
1314 2
        $cropToFit    = $this->cropToFit    ? '_cf'                      : null;
1315 2
        $fillToFit    = $this->fillToFit    ? '_ff'                      : null;
1316 2
        $crop_x       = $this->crop_x       ? "_x{$this->crop_x}"        : null;
1317
        $crop_y       = $this->crop_y       ? "_y{$this->crop_y}"        : null;
1318 2
        $scale        = $this->scale        ? "_s{$this->scale}"         : null;
1319 2
        $bgColor      = $this->bgColor      ? "_bgc{$this->bgColor}"     : null;
1320
        $quality      = $this->quality      ? "_q{$this->quality}"       : null;
1321 2
        $compress     = $this->compress     ? "_co{$this->compress}"     : null;
1322 2
        $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null;
1323
        $rotateAfter  = $this->rotateAfter  ? "_ra{$this->rotateAfter}"  : null;
1324
1325
        $saveAs = $this->normalizeFileExtension();
1326 2
        $saveAs = $saveAs ? "_$saveAs" : null;
1327 2
1328
        $copyStrat = null;
1329 2
        if ($this->copyStrategy === self::RESIZE) {
1330 2
            $copyStrat = "_rs";
1331 2
        }
1332
1333 2
        $width  = $this->newWidth  ? '_' . $this->newWidth  : null;
1334 2
        $height = $this->newHeight ? '_' . $this->newHeight : null;
1335 2
1336
        $offset = isset($this->offset)
1337 2
            ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left']
1338 2
            : null;
1339
1340
        $crop = $this->crop
1341
            ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y']
1342
            : null;
1343
1344
        $filters = null;
1345
        if (isset($this->filters)) {
1346
            foreach ($this->filters as $filter) {
1347
                if (is_array($filter)) {
1348
                    $filters .= "_f{$filter['id']}";
1349 2
                    for ($i=1; $i<=$filter['argc']; $i++) {
1350 2
                        $filters .= "-".$filter["arg{$i}"];
1351 2
                    }
1352 2
                }
1353
            }
1354 2
        }
1355
1356 2
        $sharpen = $this->sharpen ? 's' : null;
1357 2
        $emboss  = $this->emboss  ? 'e' : null;
1358 2
        $blur    = $this->blur    ? 'b' : null;
1359
        $palette = $this->palette ? 'p' : null;
1360 2
1361 2
        $autoRotate = $this->autoRotate ? 'ar' : null;
1362
1363
        $optimize  = $this->jpegOptimize ? 'o' : null;
1364
        $optimize .= $this->pngFilter    ? 'f' : null;
1365 2
        $optimize .= $this->pngDeflate   ? 'd' : null;
1366 2
1367
        $convolve = null;
1368
        if ($this->convolve) {
1369
            $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve);
1370 2
        }
1371 2
1372
        $upscale = null;
1373
        if ($this->upscale !== self::UPSCALE_DEFAULT) {
1374
            $upscale = '_nu';
1375
        }
1376
1377 2
        $subdir = null;
1378 2
        if ($useSubdir === true) {
1379 2
            $subdir = str_replace('/', '-', dirname($this->imageSrc));
1380 2
            $subdir = ($subdir == '.') ? '_.' : $subdir;
1381 2
            $subdir .= '_';
1382 2
        }
1383 2
1384
        $file = $prefix . $subdir . $filename . $width . $height
1385 2
            . $offset . $crop . $cropToFit . $fillToFit
1386
            . $crop_x . $crop_y . $upscale
1387
            . $quality . $filters . $sharpen . $emboss . $blur . $palette
1388
            . $optimize . $compress
1389
            . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor
1390
            . $convolve . $copyStrat . $saveAs;
1391
1392
        return $this->setTarget($file, $base);
1393
    }
1394
1395
1396
1397
    /**
1398
     * Use cached version of image, if possible.
1399
     *
1400
     * @param boolean $useCache is default true, set to false to avoid using cached object.
1401
     *
1402
     * @return $this
1403
     */
1404
    public function useCacheIfPossible($useCache = true)
1405
    {
1406
        if ($useCache && is_readable($this->cacheFileName)) {
1407
            $fileTime   = filemtime($this->pathToImage);
1408
            $cacheTime  = filemtime($this->cacheFileName);
1409
1410
            if ($fileTime <= $cacheTime) {
1411
                if ($this->useCache) {
1412
                    if ($this->verbose) {
1413
                        $this->log("Use cached file.");
1414
                        $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
1415
                    }
1416
                    $this->output($this->cacheFileName, $this->outputFormat);
1417
                } else {
1418
                    $this->log("Cache is valid but ignoring it by intention.");
1419
                }
1420
            } else {
1421
                $this->log("Original file is modified, ignoring cache.");
1422
            }
1423
        } else {
1424
            $this->log("Cachefile does not exists or ignoring it.");
1425
        }
1426
1427
        return $this;
1428
    }
1429
1430
1431
1432
    /**
1433
     * Load image from disk. Try to load image without verbose error message,
1434
     * if fail, load again and display error messages.
1435
     *
1436
     * @param string $src of image.
1437
     * @param string $dir as base directory where images are.
1438
     *
1439
     * @return $this
1440
     *
1441
     */
1442
    public function load($src = null, $dir = null)
1443
    {
1444
        if (isset($src)) {
1445
            $this->setSource($src, $dir);
1446
        }
1447
1448
        $this->loadImageDetails($this->pathToImage);
1449
1450
        $this->image = imagecreatefromstring(file_get_contents($this->pathToImage));
1451
        if ($this->image === false) {
1452
            throw new Exception("Could not load image.");
1453
        }
1454
1455
        /* Removed v0.7.7
1456
        if (image_type_to_mime_type($this->fileType) == 'image/png') {
1457
            $type = $this->getPngType();
1458
            $hasFewColors = imagecolorstotal($this->image);
1459
1460
            if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) {
1461
                if ($this->verbose) {
1462
                    $this->log("Handle this image as a palette image.");
1463
                }
1464
                $this->palette = true;
1465
            }
1466
        }
1467
        */
1468
1469
        if ($this->verbose) {
1470
            $this->log("### Image successfully loaded from file.");
1471
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
1472
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
1473
            $this->log(" Number of colors in image = " . $this->colorsTotal($this->image));
1474
            $index = imagecolortransparent($this->image);
1475
            $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
1476
        }
1477
1478
        return $this;
1479
    }
1480
1481
1482
1483
    /**
1484
     * Get the type of PNG image.
1485
     *
1486
     * @param string $filename to use instead of default.
1487
     *
1488
     * @return int as the type of the png-image
1489
     *
1490
     */
1491
    public function getPngType($filename = null)
1492
    {
1493
        $filename = $filename ? $filename : $this->pathToImage;
1494
1495
        $pngType = ord(file_get_contents($filename, false, null, 25, 1));
1496
1497
        if ($this->verbose) {
1498
            $this->log("Checking png type of: " . $filename);
1499
            $this->log($this->getPngTypeAsString($pngType));
1500
        }
1501
1502
        return $pngType;
1503
    }
1504
1505
1506
1507
    /**
1508
     * Get the type of PNG image as a verbose string.
1509
     *
1510
     * @param integer $type     to use, default is to check the type.
1511
     * @param string  $filename to use instead of default.
1512
     *
1513
     * @return int as the type of the png-image
1514
     *
1515
     */
1516
    private function getPngTypeAsString($pngType = null, $filename = null)
1517
    {
1518
        if ($filename || !$pngType) {
1519
            $pngType = $this->getPngType($filename);
1520
        }
1521
1522
        $index = imagecolortransparent($this->image);
1523
        $transparent = null;
1524
        if ($index != -1) {
1525
            $transparent = " (transparent)";
1526
        }
1527
1528
        switch ($pngType) {
1529
1530
            case self::PNG_GREYSCALE:
1531
                $text = "PNG is type 0, Greyscale$transparent";
1532
                break;
1533
1534
            case self::PNG_RGB:
1535
                $text = "PNG is type 2, RGB$transparent";
1536
                break;
1537
1538
            case self::PNG_RGB_PALETTE:
1539
                $text = "PNG is type 3, RGB with palette$transparent";
1540
                break;
1541
1542
            case self::PNG_GREYSCALE_ALPHA:
1543
                $text = "PNG is type 4, Greyscale with alpha channel";
1544
                break;
1545
1546
            case self::PNG_RGB_ALPHA:
1547
                $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)";
1548
                break;
1549
1550
            default:
1551
                $text = "PNG is UNKNOWN type, is it really a PNG image?";
1552
        }
1553
1554
        return $text;
1555
    }
1556
1557
1558
1559
1560
    /**
1561
     * Calculate number of colors in an image.
1562
     *
1563
     * @param resource $im the image.
1564
     *
1565
     * @return int
1566
     */
1567
    private function colorsTotal($im)
1568
    {
1569
        if (imageistruecolor($im)) {
1570
            $this->log("Colors as true color.");
1571
            $h = imagesy($im);
1572
            $w = imagesx($im);
1573
            $c = array();
1574
            for ($x=0; $x < $w; $x++) {
1575
                for ($y=0; $y < $h; $y++) {
1576
                    @$c['c'.imagecolorat($im, $x, $y)]++;
1577
                }
1578
            }
1579
            return count($c);
1580
        } else {
1581
            $this->log("Colors as palette.");
1582
            return imagecolorstotal($im);
1583
        }
1584
    }
1585
1586
1587
1588
    /**
1589
     * Preprocess image before rezising it.
1590
     *
1591
     * @return $this
1592
     */
1593
    public function preResize()
1594
    {
1595
        $this->log("### Pre-process before resizing");
1596
1597
        // Rotate image
1598
        if ($this->rotateBefore) {
1599
            $this->log("Rotating image.");
1600
            $this->rotate($this->rotateBefore, $this->bgColor)
1601
                 ->reCalculateDimensions();
1602
        }
1603
1604
        // Auto-rotate image
1605
        if ($this->autoRotate) {
1606
            $this->log("Auto rotating image.");
1607
            $this->rotateExif()
1608
                 ->reCalculateDimensions();
1609
        }
1610
1611
        // Scale the original image before starting
1612
        if (isset($this->scale)) {
1613
            $this->log("Scale by {$this->scale}%");
1614
            $newWidth  = $this->width * $this->scale / 100;
1615
            $newHeight = $this->height * $this->scale / 100;
1616
            $img = $this->CreateImageKeepTransparency($newWidth, $newHeight);
1617
            imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height);
1618
            $this->image = $img;
1619
            $this->width = $newWidth;
1620
            $this->height = $newHeight;
1621
        }
1622
1623
        return $this;
1624
    }
1625
1626
1627
1628
    /**
1629
     * Resize or resample the image while resizing.
1630
     *
1631
     * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE
1632
     *
1633
     * @return $this
1634
     */
1635
     public function setCopyResizeStrategy($strategy)
1636
     {
1637
         $this->copyStrategy = $strategy;
1638
         return $this;
1639
     }
1640
1641
1642
1643
    /**
1644
     * Resize and or crop the image.
1645
     *
1646
     * @return void
1647
     */
1648
    public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)
1649
    {
1650
        if($this->copyStrategy == self::RESIZE) {
1651
            $this->log("Copy by resize");
1652
            imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1653
        } else {
1654
            $this->log("Copy by resample");
1655
            imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
1656
        }
1657
    }
1658
1659
1660
1661
    /**
1662
     * Resize and or crop the image.
1663
     *
1664
     * @return $this
1665
     */
1666
    public function resize()
1667
    {
1668
1669
        $this->log("### Starting to Resize()");
1670
        $this->log("Upscale = '$this->upscale'");
1671
1672
        // Only use a specified area of the image, $this->offset is defining the area to use
1673
        if (isset($this->offset)) {
1674
1675
            $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']}");
1676
            $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']);
1677
            imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']);
1678
            $this->image = $img;
1679
            $this->width = $this->offset['width'];
1680
            $this->height = $this->offset['height'];
1681
        }
1682
1683
        if ($this->crop) {
1684
1685
            // Do as crop, take only part of image
1686
            $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}");
1687
            $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']);
1688
            imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']);
1689
            $this->image = $img;
1690
            $this->width = $this->crop['width'];
1691
            $this->height = $this->crop['height'];
1692
        }
1693
1694
        if (!$this->upscale) {
1695
            // Consider rewriting the no-upscale code to fit within this if-statement,
1696
            // likely to be more readable code.
1697
            // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch
1698
        }
1699
1700
        if ($this->cropToFit) {
1701
1702
            // Resize by crop to fit
1703
            $this->log("Resizing using strategy - Crop to fit");
1704
1705
            if (!$this->upscale 
1706
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)) {
1707
                $this->log("Resizing - smaller image, do not upscale.");
1708
1709
                //$cropX = round(($this->cropWidth/2) - ($this->width/2));
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1710
                //$cropY = round(($this->cropHeight/2) - ($this->height/2));
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1711
1712
                $posX = 0;
1713
                $posY = 0;
1714
                $cropX = 0;
1715
                $cropY = 0;
1716
1717
                if ($this->newWidth > $this->width) {
1718
                    $posX = round(($this->newWidth - $this->width) / 2);
1719
                }
1720
                if ($this->newWidth < $this->width) {
1721
                    $cropX = round(($this->width/2) - ($this->newWidth/2));
1722
                }
1723
1724
                if ($this->newHeight > $this->height) {
1725
                    $posY = round(($this->newHeight - $this->height) / 2);
1726
                }
1727
                if ($this->newHeight < $this->height) {
1728
                    $cropY = round(($this->height/2) - ($this->newHeight/2));
1729
                }
1730
                $this->log(" cwidth: $this->cropWidth");
1731
                $this->log(" cheight: $this->cropHeight");
1732
                $this->log(" nwidth: $this->newWidth");
1733
                $this->log(" nheight: $this->newHeight");
1734
                $this->log(" width: $this->width");
1735
                $this->log(" height: $this->height");
1736
                $this->log(" posX: $posX");
1737
                $this->log(" posY: $posY");
1738
                $this->log(" cropX: $cropX");
1739
                $this->log(" cropY: $cropY");
1740
1741
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1742
                //imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
0 ignored issues
show
Unused Code Comprehensibility introduced by
66% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1743
                imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1744
            } else {
1745
                $cropX = round(($this->cropWidth/2) - ($this->newWidth/2));
1746
                $cropY = round(($this->cropHeight/2) - ($this->newHeight/2));
1747
                $imgPreCrop   = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight);
1748
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1749
                $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height);
1750
                imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight);
1751
            }
1752
1753
            $this->image = $imageResized;
1754
            $this->width = $this->newWidth;
1755
            $this->height = $this->newHeight;
1756
1757
        } elseif ($this->fillToFit) {
1758
1759
            // Resize by fill to fit
1760
            $this->log("Resizing using strategy - Fill to fit");
1761
1762
            $posX = 0;
1763
            $posY = 0;
1764
1765
            $ratioOrig = $this->width / $this->height;
1766
            $ratioNew  = $this->newWidth / $this->newHeight;
1767
1768
            // Check ratio for landscape or portrait
1769
            if ($ratioOrig < $ratioNew) {
1770
                $posX = round(($this->newWidth - $this->fillWidth) / 2);
1771
            } else {
1772
                $posY = round(($this->newHeight - $this->fillHeight) / 2);
1773
            }
1774
1775
            if (!$this->upscale
1776
                && ($this->width < $this->newWidth && $this->height < $this->newHeight)
1777
            ) {
1778
1779
                $this->log("Resizing - smaller image, do not upscale.");
1780
                $posX = round(($this->newWidth - $this->width) / 2);
1781
                $posY = round(($this->newHeight - $this->height) / 2);
1782
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1783
                imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height);
1784
1785
            } else {
1786
                $imgPreFill   = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight);
1787
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1788
                $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height);
1789
                imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight);
1790
            }
1791
1792
            $this->image = $imageResized;
1793
            $this->width = $this->newWidth;
1794
            $this->height = $this->newHeight;
1795
1796
        } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) {
1797
1798
            // Resize it
1799
            $this->log("Resizing, new height and/or width");
1800
1801
            if (!$this->upscale
1802
                && ($this->width < $this->newWidth || $this->height < $this->newHeight)
1803
            ) {
1804
                $this->log("Resizing - smaller image, do not upscale.");
1805
1806
                if (!$this->keepRatio) {
1807
                    $this->log("Resizing - stretch to fit selected.");
1808
1809
                    $posX = 0;
1810
                    $posY = 0;
1811
                    $cropX = 0;
1812
                    $cropY = 0;
1813
1814
                    if ($this->newWidth > $this->width && $this->newHeight > $this->height) {
1815
                        $posX = round(($this->newWidth - $this->width) / 2);
1816
                        $posY = round(($this->newHeight - $this->height) / 2);
1817
                    } elseif ($this->newWidth > $this->width) {
1818
                        $posX = round(($this->newWidth - $this->width) / 2);
1819
                        $cropY = round(($this->height - $this->newHeight) / 2);
1820
                    } elseif ($this->newHeight > $this->height) {
1821
                        $posY = round(($this->newHeight - $this->height) / 2);
1822
                        $cropX = round(($this->width - $this->newWidth) / 2);
1823
                    }
1824
1825
                    //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY.");
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1826
                    $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1827
                    imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height);
1828
                    $this->image = $imageResized;
1829
                    $this->width = $this->newWidth;
1830
                    $this->height = $this->newHeight;
1831
                }
1832
            } else {
1833
                $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight);
1834
                $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height);
1835
                $this->image = $imageResized;
1836
                $this->width = $this->newWidth;
1837
                $this->height = $this->newHeight;
1838
            }
1839
        }
1840
1841
        return $this;
1842
    }
1843
1844
1845
1846
    /**
1847
     * Postprocess image after rezising image.
1848
     *
1849
     * @return $this
1850
     */
1851
    public function postResize()
1852
    {
1853
        $this->log("### Post-process after resizing");
1854
1855
        // Rotate image
1856
        if ($this->rotateAfter) {
1857
            $this->log("Rotating image.");
1858
            $this->rotate($this->rotateAfter, $this->bgColor);
1859
        }
1860
1861
        // Apply filters
1862
        if (isset($this->filters) && is_array($this->filters)) {
1863
1864
            foreach ($this->filters as $filter) {
1865
                $this->log("Applying filter {$filter['type']}.");
1866
1867
                switch ($filter['argc']) {
1868
1869
                    case 0:
1870
                        imagefilter($this->image, $filter['type']);
1871
                        break;
1872
1873
                    case 1:
1874
                        imagefilter($this->image, $filter['type'], $filter['arg1']);
1875
                        break;
1876
1877
                    case 2:
1878
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']);
1879
                        break;
1880
1881
                    case 3:
1882
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']);
1883
                        break;
1884
1885
                    case 4:
1886
                        imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']);
1887
                        break;
1888
                }
1889
            }
1890
        }
1891
1892
        // Convert to palette image
1893
        if ($this->palette) {
1894
            $this->log("Converting to palette image.");
1895
            $this->trueColorToPalette();
1896
        }
1897
1898
        // Blur the image
1899
        if ($this->blur) {
1900
            $this->log("Blur.");
1901
            $this->blurImage();
1902
        }
1903
1904
        // Emboss the image
1905
        if ($this->emboss) {
1906
            $this->log("Emboss.");
1907
            $this->embossImage();
1908
        }
1909
1910
        // Sharpen the image
1911
        if ($this->sharpen) {
1912
            $this->log("Sharpen.");
1913
            $this->sharpenImage();
1914
        }
1915
1916
        // Custom convolution
1917
        if ($this->convolve) {
1918
            //$this->log("Convolve: " . $this->convolve);
1919
            $this->imageConvolution();
1920
        }
1921
1922
        return $this;
1923
    }
1924
1925
1926
1927
    /**
1928
     * Rotate image using angle.
1929
     *
1930
     * @param float $angle        to rotate image.
1931
     * @param int   $anglebgColor to fill image with if needed.
1932
     *
1933
     * @return $this
1934
     */
1935
    public function rotate($angle, $bgColor)
1936
    {
1937
        $this->log("Rotate image " . $angle . " degrees with filler color.");
1938
1939
        $color = $this->getBackgroundColor();
1940
        $this->image = imagerotate($this->image, $angle, $color);
1941
1942
        $this->width  = imagesx($this->image);
1943
        $this->height = imagesy($this->image);
1944
1945
        $this->log("New image dimension width x height: " . $this->width . " x " . $this->height);
1946
1947
        return $this;
1948
    }
1949
1950
1951
1952
    /**
1953
     * Rotate image using information in EXIF.
1954
     *
1955
     * @return $this
1956
     */
1957
    public function rotateExif()
1958
    {
1959
        if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) {
1960
            $this->log("Autorotate ignored, EXIF not supported by this filetype.");
1961
            return $this;
1962
        }
1963
1964
        $exif = exif_read_data($this->pathToImage);
1965
1966
        if (!empty($exif['Orientation'])) {
1967
            switch ($exif['Orientation']) {
1968
                case 3:
1969
                    $this->log("Autorotate 180.");
1970
                    $this->rotate(180, $this->bgColor);
1971
                    break;
1972
1973
                case 6:
1974
                    $this->log("Autorotate -90.");
1975
                    $this->rotate(-90, $this->bgColor);
1976
                    break;
1977
1978
                case 8:
1979
                    $this->log("Autorotate 90.");
1980
                    $this->rotate(90, $this->bgColor);
1981
                    break;
1982
1983
                default:
1984
                    $this->log("Autorotate ignored, unknown value as orientation.");
1985
            }
1986
        } else {
1987
            $this->log("Autorotate ignored, no orientation in EXIF.");
1988
        }
1989
1990
        return $this;
1991
    }
1992
1993
1994
1995
    /**
1996
     * Convert true color image to palette image, keeping alpha.
1997
     * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
1998
     *
1999
     * @return void
2000
     */
2001
    public function trueColorToPalette()
2002
    {
2003
        $img = imagecreatetruecolor($this->width, $this->height);
2004
        $bga = imagecolorallocatealpha($img, 0, 0, 0, 127);
2005
        imagecolortransparent($img, $bga);
2006
        imagefill($img, 0, 0, $bga);
2007
        imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height);
2008
        imagetruecolortopalette($img, false, 255);
2009
        imagesavealpha($img, true);
2010
2011
        if (imageistruecolor($this->image)) {
2012
            $this->log("Matching colors with true color image.");
2013
            imagecolormatch($this->image, $img);
2014
        }
2015
2016
        $this->image = $img;
2017
    }
2018
2019
2020
2021
    /**
2022
     * Sharpen image using image convolution.
2023
     *
2024
     * @return $this
2025
     */
2026
    public function sharpenImage()
2027
    {
2028
        $this->imageConvolution('sharpen');
2029
        return $this;
2030
    }
2031
2032
2033
2034
    /**
2035
     * Emboss image using image convolution.
2036
     *
2037
     * @return $this
2038
     */
2039
    public function embossImage()
2040
    {
2041
        $this->imageConvolution('emboss');
2042
        return $this;
2043
    }
2044
2045
2046
2047
    /**
2048
     * Blur image using image convolution.
2049
     *
2050
     * @return $this
2051
     */
2052
    public function blurImage()
2053
    {
2054
        $this->imageConvolution('blur');
2055
        return $this;
2056
    }
2057
2058
2059
2060
    /**
2061
     * Create convolve expression and return arguments for image convolution.
2062
     *
2063
     * @param string $expression constant string which evaluates to a list of
2064
     *                           11 numbers separated by komma or such a list.
2065
     *
2066
     * @return array as $matrix (3x3), $divisor and $offset
2067
     */
2068
    public function createConvolveArguments($expression)
2069
    {
2070
        // Check of matching constant
2071
        if (isset($this->convolves[$expression])) {
2072
            $expression = $this->convolves[$expression];
2073
        }
2074
2075
        $part = explode(',', $expression);
2076
        $this->log("Creating convolution expressen: $expression");
2077
2078
        // Expect list of 11 numbers, split by , and build up arguments
2079
        if (count($part) != 11) {
2080
            throw new Exception(
2081
                "Missmatch in argument convolve. Expected comma-separated string with
2082
                11 float values. Got $expression."
2083
            );
2084
        }
2085
2086
        array_walk($part, function ($item, $key) {
2087
            if (!is_numeric($item)) {
2088
                throw new Exception("Argument to convolve expression should be float but is not.");
2089
            }
2090
        });
2091
2092
        return array(
2093
            array(
2094
                array($part[0], $part[1], $part[2]),
2095
                array($part[3], $part[4], $part[5]),
2096
                array($part[6], $part[7], $part[8]),
2097
            ),
2098
            $part[9],
2099
            $part[10],
2100
        );
2101
    }
2102
2103
2104
2105
    /**
2106
     * Add custom expressions (or overwrite existing) for image convolution.
2107
     *
2108
     * @param array $options Key value array with strings to be converted
2109
     *                       to convolution expressions.
2110
     *
2111
     * @return $this
2112
     */
2113
    public function addConvolveExpressions($options)
2114
    {
2115
        $this->convolves = array_merge($this->convolves, $options);
2116
        return $this;
2117
    }
2118
2119
2120
2121
    /**
2122
     * Image convolution.
2123
     *
2124
     * @param string $options A string with 11 float separated by comma.
2125
     *
2126
     * @return $this
2127
     */
2128
    public function imageConvolution($options = null)
2129
    {
2130
        // Use incoming options or use $this.
2131
        $options = $options ? $options : $this->convolve;
2132
2133
        // Treat incoming as string, split by +
2134
        $this->log("Convolution with '$options'");
2135
        $options = explode(":", $options);
2136
2137
        // Check each option if it matches constant value
2138
        foreach ($options as $option) {
2139
            list($matrix, $divisor, $offset) = $this->createConvolveArguments($option);
2140
            imageconvolution($this->image, $matrix, $divisor, $offset);
2141
        }
2142
2143
        return $this;
2144
    }
2145
2146
2147
2148
    /**
2149
     * Set default background color between 000000-FFFFFF or if using
2150
     * alpha 00000000-FFFFFF7F.
2151
     *
2152
     * @param string $color as hex value.
2153
     *
2154
     * @return $this
2155
    */
2156
    public function setDefaultBackgroundColor($color)
2157
    {
2158
        $this->log("Setting default background color to '$color'.");
2159
2160
        if (!(strlen($color) == 6 || strlen($color) == 8)) {
2161
            throw new Exception(
2162
                "Background color needs a hex value of 6 or 8
2163
                digits. 000000-FFFFFF or 00000000-FFFFFF7F.
2164
                Current value was: '$color'."
2165
            );
2166
        }
2167
2168
        $red    = hexdec(substr($color, 0, 2));
2169
        $green  = hexdec(substr($color, 2, 2));
2170
        $blue   = hexdec(substr($color, 4, 2));
2171
2172
        $alpha = (strlen($color) == 8)
2173
            ? hexdec(substr($color, 6, 2))
2174
            : null;
2175
2176
        if (($red < 0 || $red > 255)
2177
            || ($green < 0 || $green > 255)
2178
            || ($blue < 0 || $blue > 255)
2179
            || ($alpha < 0 || $alpha > 127)
2180
        ) {
2181 2
            throw new Exception(
2182
                "Background color out of range. Red, green blue
2183 2
                should be 00-FF and alpha should be 00-7F.
2184
                Current value was: '$color'."
2185 2
            );
2186
        }
2187 2
2188 2
        $this->bgColor = strtolower($color);
2189 2
        $this->bgColorDefault = array(
2190 2
            'red'   => $red,
2191
            'green' => $green,
2192 2
            'blue'  => $blue,
2193
            'alpha' => $alpha
2194
        );
2195 2
2196
        return $this;
2197
    }
2198 2
2199
2200
2201
    /**
2202
     * Get the background color.
2203
     *
2204
     * @param resource $img the image to work with or null if using $this->image.
2205
     *
2206
     * @return color value or null if no background color is set.
2207
    */
2208
    private function getBackgroundColor($img = null)
2209
    {
2210
        $img = isset($img) ? $img : $this->image;
2211
2212
        if ($this->bgColorDefault) {
2213
2214
            $red   = $this->bgColorDefault['red'];
2215 2
            $green = $this->bgColorDefault['green'];
2216
            $blue  = $this->bgColorDefault['blue'];
2217 2
            $alpha = $this->bgColorDefault['alpha'];
2218 2
2219 2
            if ($alpha) {
2220 2
                $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha);
2221
            } else {
2222 2
                $color = imagecolorallocate($img, $red, $green, $blue);
2223 2
            }
2224 2
2225
            return $color;
2226 2
2227
        } else {
2228
            return 0;
2229
        }
2230
    }
2231
2232
2233
2234
    /**
2235 2
     * Create a image and keep transparency for png and gifs.
2236
     *
2237 2
     * @param int $width of the new image.
2238 2
     * @param int $height of the new image.
2239 2
     *
2240 2
     * @return image resource.
2241
    */
2242 2
    private function createImageKeepTransparency($width, $height)
2243
    {
2244
        $this->log("Creating a new working image width={$width}px, height={$height}px.");
2245
        $img = imagecreatetruecolor($width, $height);
2246
        imagealphablending($img, false);
2247
        imagesavealpha($img, true);
2248
2249
        $index = $this->image
2250
            ? imagecolortransparent($this->image)
2251
            : -1;
2252
2253
        if ($index != -1) {
2254
2255
            imagealphablending($img, true);
2256
            $transparent = imagecolorsforindex($this->image, $index);
2257
            $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']);
2258
            imagefill($img, 0, 0, $color);
2259
            $index = imagecolortransparent($img, $color);
2260
            $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index");
2261
2262
        } elseif ($this->bgColorDefault) {
2263
2264
            $color = $this->getBackgroundColor($img);
2265
            imagefill($img, 0, 0, $color);
2266
            $this->Log("Filling image with background color.");
2267
        }
2268
2269
        return $img;
2270
    }
2271
2272
2273
2274
    /**
2275
     * Set optimizing  and post-processing options.
2276
     *
2277
     * @param array $options with config for postprocessing with external tools.
2278
     *
2279
     * @return $this
2280
     */
2281
    public function setPostProcessingOptions($options)
2282
    {
2283
        if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) {
2284 2
            $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd'];
2285
        } else {
2286
            $this->jpegOptimizeCmd = null;
2287 2
        }
2288
2289
        if (isset($options['png_filter']) && $options['png_filter']) {
2290 2
            $this->pngFilterCmd = $options['png_filter_cmd'];
2291
        } else {
2292
            $this->pngFilterCmd = null;
2293
        }
2294
2295
        if (isset($options['png_deflate']) && $options['png_deflate']) {
2296
            $this->pngDeflateCmd = $options['png_deflate_cmd'];
2297
        } else {
2298
            $this->pngDeflateCmd = null;
2299
        }
2300
2301
        return $this;
2302
    }
2303
2304
2305 2
2306
    /**
2307 2
     * Find out the type (file extension) for the image to be saved.
2308
     *
2309
     * @return string as image extension.
2310
     */
2311 2
    protected function getTargetImageExtension()
2312
    {
2313
        // switch on mimetype
2314
        if (isset($this->extension)) {
2315
            return strtolower($this->extension);
2316 2
        } else {
2317
            return substr(image_type_to_extension($this->fileType), 1);
2318
        }
2319 2
    }
2320 2
2321
2322
2323 2
    /**
2324 2
     * Save image.
2325
     *
2326
     * @param string  $src       as target filename.
2327
     * @param string  $base      as base directory where to store images.
2328
     * @param boolean $overwrite or not, default to always overwrite file.
2329
     *
2330
     * @return $this or false if no folder is set.
2331
     */
2332
    public function save($src = null, $base = null, $overwrite = true)
2333
    {
2334
        if (isset($src)) {
2335
            $this->setTarget($src, $base);
2336
        }
2337
2338
        if ($overwrite === false && is_file($this->cacheFileName)) {
2339
            $this->Log("Not overwriting file since its already exists and \$overwrite if false.");
2340
            return;
2341
        }
2342 2
2343
        is_writable($this->saveFolder)
2344
            or $this->raiseError('Target directory is not writable.');
2345
2346
        $type = $this->getTargetImageExtension();
2347 2
        $this->Log("Saving image as " . $type);
2348 2
        switch($type) {
2349 2
2350
            case 'jpeg':
2351
            case 'jpg':
2352 2
                $this->Log("Saving image as JPEG to cache using quality = {$this->quality}.");
2353 2
                imagejpeg($this->image, $this->cacheFileName, $this->quality);
2354 2
2355
                // Use JPEG optimize if defined
2356
                if ($this->jpegOptimizeCmd) {
2357 2
                    if ($this->verbose) {
2358
                        clearstatcache();
2359
                        $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes.");
2360
                    }
2361
                    $res = array();
2362
                    $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName";
2363
                    exec($cmd, $res);
2364
                    $this->log($cmd);
2365
                    $this->log($res);
2366
                }
2367
                break;
2368
2369
            case 'gif':
2370 2
                $this->Log("Saving image as GIF to cache.");
2371
                imagegif($this->image, $this->cacheFileName);
2372
                break;
2373
2374
            case 'png':
2375
            default:
2376
                $this->Log("Saving image as PNG to cache using compression = {$this->compress}.");
2377
2378
                // Turn off alpha blending and set alpha flag
2379
                imagealphablending($this->image, false);
2380
                imagesavealpha($this->image, true);
2381 2
                imagepng($this->image, $this->cacheFileName, $this->compress);
2382 2
2383
                // Use external program to filter PNG, if defined
2384 2
                if ($this->pngFilterCmd) {
2385
                    if ($this->verbose) {
2386
                        clearstatcache();
2387
                        $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes.");
2388
                    }
2389
                    $res = array();
2390
                    $cmd = $this->pngFilterCmd . " $this->cacheFileName";
2391
                    exec($cmd, $res);
2392
                    $this->Log($cmd);
2393
                    $this->Log($res);
2394
                }
2395 2
2396
                // Use external program to deflate PNG, if defined
2397
                if ($this->pngDeflateCmd) {
2398
                    if ($this->verbose) {
2399
                        clearstatcache();
2400
                        $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes.");
2401
                    }
2402
                    $res = array();
2403
                    $cmd = $this->pngDeflateCmd . " $this->cacheFileName";
2404
                    exec($cmd, $res);
2405
                    $this->Log($cmd);
2406
                    $this->Log($res);
2407
                }
2408
                break;
2409
        }
2410
2411
        if ($this->verbose) {
2412
            clearstatcache();
2413
            $this->log("Saved image to cache.");
2414
            $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes.");
2415
            $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false'));
2416
            $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image));
2417
            $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image));
2418
            $index = imagecolortransparent($this->image);
2419
            $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index");
2420
        }
2421
2422
        return $this;
2423
    }
2424
2425
2426
2427
    /**
2428
     * Convert image from one colorpsace/color profile to sRGB without
2429
     * color profile.
2430
     *
2431
     * @param string  $src      of image.
2432
     * @param string  $dir      as base directory where images are.
2433
     * @param string  $cache    as base directory where to store images.
2434
     * @param string  $iccFile  filename of colorprofile.
2435
     * @param boolean $useCache or not, default to always use cache.
2436
     *
2437
     * @return string | boolean false if no conversion else the converted
2438
     *                          filename.
2439
     */
2440
    public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true)
2441
    {
2442
        if ($this->verbose) {
2443
            $this->log("# Converting image to sRGB colorspace.");
2444
        }
2445
2446
        if (!class_exists("Imagick")) {
2447
            $this->log(" Ignoring since Imagemagick is not installed.");
2448
            return false;
2449
        }
2450
2451
        // Prepare
2452
        $this->setSaveFolder($cache)
2453
             ->setSource($src, $dir)
2454
             ->generateFilename(null, false, 'srgb_');
2455
2456
        // Check if the cached version is accurate.
2457
        if ($useCache && is_readable($this->cacheFileName)) {
2458
            $fileTime  = filemtime($this->pathToImage);
2459
            $cacheTime = filemtime($this->cacheFileName);
2460
2461
            if ($fileTime <= $cacheTime) {
2462
                $this->log(" Using cached version: " . $this->cacheFileName);
2463
                return $this->cacheFileName;
2464
            }
2465
        }
2466
2467
        // Only covert if cachedir is writable
2468
        if (is_writable($this->saveFolder)) {
2469
            // Load file and check if conversion is needed
2470
            $image      = new Imagick($this->pathToImage);
2471
            $colorspace = $image->getImageColorspace();
2472
            $this->log(" Current colorspace: " . $colorspace);
2473
2474
            $profiles      = $image->getImageProfiles('*', false);
2475
            $hasICCProfile = (array_search('icc', $profiles) !== false);
2476
            $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO"));
2477
2478
            if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) {
2479
                $this->log(" Converting to sRGB.");
2480
2481
                $sRGBicc = file_get_contents($iccFile);
2482
                $image->profileImage('icc', $sRGBicc);
2483
2484
                $image->transformImageColorspace(Imagick::COLORSPACE_SRGB);
2485
                $image->writeImage($this->cacheFileName);
2486
                return $this->cacheFileName;
2487
            }
2488
        }
2489
2490
        return false;
2491
    }
2492
2493
2494
2495
    /**
2496
     * Create a hard link, as an alias, to the cached file.
2497
     *
2498
     * @param string $alias where to store the link,
2499
     *                      filename without extension.
2500
     *
2501
     * @return $this
2502
     */
2503
    public function linkToCacheFile($alias)
2504
    {
2505
        if ($alias === null) {
2506
            $this->log("Ignore creating alias.");
2507
            return $this;
2508
        }
2509
2510
        if (is_readable($alias)) {
2511
            unlink($alias);
2512
        }
2513
2514
        $res = link($this->cacheFileName, $alias);
2515
2516
        if ($res) {
2517
            $this->log("Created an alias as: $alias");
2518
        } else {
2519
            $this->log("Failed to create the alias: $alias");
2520
        }
2521
2522
        return $this;
2523
    }
2524
2525
2526
2527
    /**
2528
     * Add HTTP header for putputting together with image.
2529
     *
2530
     * @param string $type  the header type such as "Cache-Control"
2531
     * @param string $value the value to use
2532
     *
2533
     * @return void
2534
     */
2535
    public function addHTTPHeader($type, $value)
2536
    {
2537
        $this->HTTPHeader[$type] = $value;
2538
    }
2539
2540
2541
2542
    /**
2543
     * Output image to browser using caching.
2544
     *
2545
     * @param string $file   to read and output, default is to
2546
     *                       use $this->cacheFileName
2547
     * @param string $format set to json to output file as json
2548
     *                       object with details
2549
     *
2550
     * @return void
2551
     */
2552
    public function output($file = null, $format = null)
2553
    {
2554
        if (is_null($file)) {
2555
            $file = $this->cacheFileName;
2556
        }
2557
2558
        if (is_null($format)) {
2559
            $format = $this->outputFormat;
2560
        }
2561
2562
        $this->log("Output format is: $format");
2563
2564
        if (!$this->verbose && $format == 'json') {
2565
            header('Content-type: application/json');
2566
            echo $this->json($file);
2567
            exit;
2568
        } elseif ($format == 'ascii') {
2569
            header('Content-type: text/plain');
2570
            echo $this->ascii($file);
2571
            exit;
2572
        }
2573
2574
        $this->log("Outputting image: $file");
2575
2576
        // Get image modification time
2577
        clearstatcache();
2578
        $lastModified = filemtime($file);
2579
        $gmdate = gmdate("D, d M Y H:i:s", $lastModified);
2580
2581
        if (!$this->verbose) {
2582
            header('Last-Modified: ' . $gmdate . " GMT");
2583
        }
2584
2585
        foreach($this->HTTPHeader as $key => $val) {
2586
            header("$key: $val");
2587
        }
2588
2589
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) {
2590
2591
            if ($this->verbose) {
2592
                $this->log("304 not modified");
2593
                $this->verboseOutput();
2594
                exit;
2595
            }
2596
2597
            header("HTTP/1.0 304 Not Modified");
2598
2599
        } else {
2600
2601
            // Get details on image
2602
            $info = getimagesize($file);
2603
            !empty($info) or $this->raiseError("The file doesn't seem to be an image.");
2604
            $mime = $info['mime'];
2605
            $size = filesize($file);
2606
2607
            if ($this->verbose) {
2608
                $this->log("Last-Modified: " . $gmdate . " GMT");
2609
                $this->log("Content-type: " . $mime);
2610
                $this->log("Content-length: " . $size);
2611
                $this->verboseOutput();
2612
2613
                if (is_null($this->verboseFileName)) {
2614
                    exit;
2615
                }
2616
            }
2617
2618
            header("Content-type: $mime");
2619
            header("Content-length: $size");
2620
            readfile($file);
2621
        }
2622
2623
        exit;
2624
    }
2625
2626
2627 7
2628
    /**
2629 7
     * Create a JSON object from the image details.
2630
     *
2631
     * @param string $file the file to output.
2632
     *
2633 7
     * @return string json-encoded representation of the image.
2634
     */
2635
    public function json($file = null)
2636
    {
2637
        $file = $file ? $file : $this->cacheFileName;
2638
2639
        $details = array();
2640
2641
        clearstatcache();
2642
2643
        $details['src']       = $this->imageSrc;
2644
        $lastModified         = filemtime($this->pathToImage);
2645
        $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2646
2647
        $details['cache']       = basename($this->cacheFileName);
2648
        $lastModified           = filemtime($this->cacheFileName);
2649
        $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified);
2650
2651
        $this->load($file);
2652
2653
        $details['filename']    = basename($file);
2654
        $details['mimeType']    = image_type_to_mime_type($this->fileType);
2655
        $details['width']       = $this->width;
2656
        $details['height']      = $this->height;
2657
        $details['aspectRatio'] = round($this->width / $this->height, 3);
2658
        $details['size']        = filesize($file);
2659
        $details['colors'] = $this->colorsTotal($this->image);
2660
        $details['includedFiles'] = count(get_included_files());
2661
        $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ;
2662
        $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB";
2663
        $details['memoryLimit'] = ini_get('memory_limit');
2664
2665
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
2666
            $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s";
2667
        }
2668
2669
        if ($details['mimeType'] == 'image/png') {
2670
            $details['pngType'] = $this->getPngTypeAsString(null, $file);
2671
        }
2672
2673
        $options = null;
2674
        if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) {
2675
            $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
2676
        }
2677
2678
        return json_encode($details, $options);
2679
    }
2680
2681
2682
2683
    /**
2684
     * Set options for creating ascii version of image.
2685
     *
2686
     * @param array $options empty to use default or set options to change.
2687
     *
2688
     * @return void.
2689
     */
2690
    public function setAsciiOptions($options = array())
2691
    {
2692
        $this->asciiOptions = $options;
2693
    }
2694
2695
2696
2697
    /**
2698
     * Create an ASCII version from the image details.
2699
     *
2700
     * @param string $file the file to output.
2701
     *
2702
     * @return string ASCII representation of the image.
2703
     */
2704
    public function ascii($file = null)
2705
    {
2706
        $file = $file ? $file : $this->cacheFileName;
2707
2708
        $asciiArt = new CAsciiArt();
2709
        $asciiArt->setOptions($this->asciiOptions);
2710
        return $asciiArt->createFromFile($file);
2711
    }
2712
2713
2714
2715
    /**
2716
     * Log an event if verbose mode.
2717
     *
2718
     * @param string $message to log.
2719
     *
2720
     * @return this
2721
     */
2722
    public function log($message)
2723
    {
2724
        if ($this->verbose) {
2725
            $this->log[] = $message;
2726
        }
2727
2728
        return $this;
2729
    }
2730
2731
2732
2733
    /**
2734
     * Do verbose output to a file.
2735
     *
2736
     * @param string $fileName where to write the verbose output.
2737
     *
2738
     * @return void
2739
     */
2740
    public function setVerboseToFile($fileName)
2741
    {
2742
        $this->log("Setting verbose output to file.");
2743
        $this->verboseFileName = $fileName;
2744
    }
2745
2746
2747
2748
    /**
2749
     * Do verbose output and print out the log and the actual images.
2750
     *
2751
     * @return void
2752
     */
2753
    private function verboseOutput()
2754
    {
2755
        $log = null;
2756
        $this->log("As JSON: \n" . $this->json());
2757
        $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M");
2758
        $this->log("Memory limit: " . ini_get('memory_limit'));
2759
2760
        $included = get_included_files();
2761
        $this->log("Included files: " . count($included));
2762
2763
        foreach ($this->log as $val) {
2764
            if (is_array($val)) {
2765
                foreach ($val as $val1) {
2766
                    $log .= htmlentities($val1) . '<br/>';
2767
                }
2768
            } else {
2769
                $log .= htmlentities($val) . '<br/>';
2770
            }
2771
        }
2772
2773
        if (!is_null($this->verboseFileName)) {
2774
            file_put_contents(
2775
                $this->verboseFileName,
2776
                str_replace("<br/>", "\n", $log)
2777
            );
2778
        } else {
2779
            echo <<<EOD
2780
<h1>CImage Verbose Output</h1>
2781
<pre>{$log}</pre>
2782
EOD;
2783
        }
2784
    }
2785
2786
2787
2788
    /**
2789
     * Raise error, enables to implement a selection of error methods.
2790
     *
2791
     * @param string $message the error message to display.
2792
     *
2793
     * @return void
2794
     * @throws Exception
2795
     */
2796
    private function raiseError($message)
2797
    {
2798
        throw new Exception($message);
2799
    }
2800
}
2801