Completed
Push — master ( 357cf0...ac319f )
by Oscar
02:00
created

Image::isAnimatedGif()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 2 Features 2
Metric Value
c 11
b 2
f 2
dl 0
loc 11
rs 9.4285
cc 3
eloc 6
nc 2
nop 1
1
<?php
2
3
namespace Imagecow;
4
5
use Imagecow\Utils\Dimmensions;
6
7
class Image
8
{
9
    const LIB_GD = 'Gd';
10
    const LIB_IMAGICK = 'Imagick';
11
12
    const CROP_ENTROPY = 'Entropy';
13
    const CROP_BALANCED = 'Balanced';
14
15
    protected $image;
16
    protected $filename;
17
    protected $clientHints = [
18
        'dpr' => null,
19
        'viewport-width' => null,
20
        'width' => null,
21
    ];
22
23
    /**
24
     * Static function to create a new Imagecow instance from an image file.
25
     *
26
     * @param string $image   The path of the file
27
     * @param string $library The name of the image library to use (Gd or Imagick). If it's not defined, detects automatically the library to use.
28
     *
29
     * @return Image
30
     */
31
    public static function fromFile($image, $library = null)
32
    {
33
        $class = self::getLibraryClass($library);
34
35
        $image = new static($class::createFromFile($image), $image);
36
37
        if ($image->getMimeType() === 'image/gif') {
38
            $stream = fopen($image, 'rb');
39
40
            if (self::isAnimatedGif($stream)) {
41
                $image->image->setAnimated(true);
42
            }
43
44
            fclose($stream);
45
        }
46
47
        return $image;
48
    }
49
50
    /**
51
     * Static function to create a new Imagecow instance from a binary string.
52
     *
53
     * @param string $string  The string of the image
54
     * @param string $library The name of the image library to use (Gd or Imagick). If it's not defined, detects automatically the library to use.
55
     *
56
     * @return Image
57
     */
58
    public static function fromString($string, $library = null)
59
    {
60
        $class = self::getLibraryClass($library);
61
62
        $image = new static($class::createFromString($string));
63
64
        if ($image->getMimeType() === 'image/gif') {
65
            $stream = fopen('php://temp', 'r+');
66
            fwrite($stream, $string);
67
            rewind($stream);
68
69
            if (self::isAnimatedGif($stream)) {
70
                $image->image->setAnimated(true);
71
            }
72
73
            fclose($stream);
74
        }
75
76
        return $image;
77
    }
78
79
    /**
80
     * Constructor.
81
     *
82
     * @param Libs\LibInterface $image
83
     * @param string            $filename Original filename (used to overwrite)
84
     */
85
    public function __construct(Libs\LibInterface $image, $filename = null)
86
    {
87
        $this->image = $image;
88
        $this->filename = $filename;
89
    }
90
91
    /**
92
     * Set the available client hints.
93
     * 
94
     * @param array $clientHints
95
     * 
96
     * @return self
97
     */
98
    public function setClientHints(array $clientHints)
99
    {
100
        $normalize = [];
101
102
        foreach ($clientHints as $key => $value) {
103
            $normalize[strtolower($key)] = is_null($value) ? null : (float) $value;
104
        }
105
106
        if (array_diff_key($normalize, $this->clientHints)) {
107
            throw new \InvalidArgumentException('Invalid client hints');
108
        }
109
110
        $this->clientHints = array_replace($this->clientHints, $normalize);
111
112
        return $this;
113
    }
114
115
    /**
116
     * Set a default background color used to fill in some transformation functions.
117
     *
118
     * @param array $background The color in rgb, for example: array(0,127,34)
119
     *
120
     * @return self
121
     */
122
    public function setBackground(array $background)
123
    {
124
        $this->image->setBackground($background);
125
126
        return $this;
127
    }
128
129
    /**
130
     * Define the image compression quality for jpg images.
131
     *
132
     * @param int $quality The quality (from 0 to 100)
133
     *
134
     * @deprecated Use quality instead
135
     *
136
     * @return self
137
     */
138
    public function setCompressionQuality($quality)
139
    {
140
        error_log('The method `setCompressionQuality()` is deprecated. Use `quality()` instead.');
141
142
        return $this->quality($quality);
143
    }
144
145
    /**
146
     * Get the fixed size according with the client hints.
147
     * 
148
     * @param int $width
149
     * @param int $height
150
     * 
151
     * @return array
152
     */
153
    private function calculateClientSize($width, $height)
154
    {
155 View Code Duplication
        if ($this->clientHints['width'] !== null && $this->clientHints['width'] < $width) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['width'], null);
157
        }
158
159 View Code Duplication
        if ($this->clientHints['viewport-width'] !== null && $this->clientHints['viewport-width'] < $width) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['viewport-width'], null);
161
        }
162
163
        if ($this->clientHints['dpr'] !== null) {
164
            $width *= $this->clientHints['dpr'];
165
            $height *= $this->clientHints['dpr'];
166
        }
167
168
        return [$width, $height];
169
    }
170
171
    /**
172
     * Inverts the image vertically.
173
     *
174
     * @return self
175
     */
176
    public function flip()
177
    {
178
        $this->image->flip();
179
180
        return $this;
181
    }
182
183
    /**
184
     * Inverts the image horizontally.
185
     *
186
     * @return self
187
     */
188
    public function flop()
189
    {
190
        $this->image->flop();
191
192
        return $this;
193
    }
194
195
    /**
196
     * Saves the image in a file.
197
     *
198
     * @param string $filename Name of the file where the image will be saved. If it's not defined, The original file will be overwritten.
199
     *
200
     * @return self
201
     */
202
    public function save($filename = null)
203
    {
204
        $this->image->save($filename ?: $this->filename);
205
206
        return $this;
207
    }
208
209
    /**
210
     * Gets the image data in a string.
211
     *
212
     * @return string The image data
213
     */
214
    public function getString()
215
    {
216
        return $this->image->getString();
217
    }
218
219
    /**
220
     * Gets the mime type of the image.
221
     *
222
     * @return string The mime type
223
     */
224
    public function getMimeType()
225
    {
226
        return $this->image->getMimeType();
227
    }
228
229
    /**
230
     * Gets the width of the image.
231
     *
232
     * @return int The width in pixels
233
     */
234
    public function getWidth()
235
    {
236
        return $this->image->getWidth();
237
    }
238
239
    /**
240
     * Gets the height of the image.
241
     *
242
     * @return int The height in pixels
243
     */
244
    public function getHeight()
245
    {
246
        return $this->image->getHeight();
247
    }
248
249
    /**
250
     * Converts the image to other format.
251
     *
252
     * @param string $format The new format: png, jpg, gif
253
     *
254
     * @return self
255
     */
256
    public function format($format)
257
    {
258
        $this->image->format($format);
259
260
        return $this;
261
    }
262
263
    /**
264
     * Resizes the image maintaining the proportion (A 800x600 image resized to 400x400 becomes to 400x300).
265
     *
266
     * @param int|string $width  The max width of the image. It can be a number (pixels) or percentaje
267
     * @param int|string $height The max height of the image. It can be a number (pixels) or percentaje
268
     * @param bool       $cover
269
     *
270
     * @return self
271
     */
272
    public function resize($width, $height = 0, $cover = false)
273
    {
274
        $imageWidth = $this->getWidth();
275
        $imageHeight = $this->getHeight();
276
277
        $width = Dimmensions::getIntegerValue($width, $imageWidth);
278
        $height = Dimmensions::getIntegerValue($height, $imageHeight);
279
280
        list($width, $height) = Dimmensions::getResizeDimmensions($imageWidth, $imageHeight, $width, $height, $cover);
281
        list($width, $height) = $this->calculateClientSize($width, $height);
282
283
        if ($width >= $imageWidth) {
284
            return $this;
285
        }
286
287
        $this->image->resize($width, $height);
288
289
        return $this;
290
    }
291
292
    /**
293
     * Crops the image.
294
     *
295
     * @param int|string $width  The new width of the image. It can be a number (pixels) or percentaje
296
     * @param int|string $height The new height of the image. It can be a number (pixels) or percentaje
297
     * @param int|string $x      The "x" position to crop. It can be number (pixels), percentaje, [left, center, right] or one of the Image::CROP_* constants
298
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
299
     *
300
     * @return self
301
     */
302
    public function crop($width, $height, $x = 'center', $y = 'middle')
303
    {
304
        $imageWidth = $this->getWidth();
305
        $imageHeight = $this->getHeight();
306
307
        $width = Dimmensions::getIntegerValue($width, $imageWidth);
308
        $height = Dimmensions::getIntegerValue($height, $imageHeight);
309
310
        list($width, $height) = $this->calculateClientSize($width, $height);
311
312
        switch ($x) {
313
            case self::CROP_BALANCED:
314
            case self::CROP_ENTROPY:
315
                list($x, $y) = $this->image->getCropOffsets($width, $height, $x);
316
                break;
317
        }
318
319
        $x = Dimmensions::getPositionValue($x, $width, $imageWidth);
320
        $y = Dimmensions::getPositionValue($y, $height, $imageHeight);
321
322
        $this->image->crop($width, $height, $x, $y);
323
324
        return $this;
325
    }
326
327
    /**
328
     * Adjust the image to the given dimmensions. Resizes and crops the image maintaining the proportions.
329
     *
330
     * @param int|string $width  The new width in number (pixels) or percentaje
331
     * @param int|string $height The new height in number (pixels) or percentaje
332
     * @param int|string $x      The "x" position to crop. It can be number (pixels), percentaje, [left, center, right] or one of the Image::CROP_* constants
333
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
334
     *
335
     * @return self
336
     */
337
    public function resizeCrop($width, $height, $x = 'center', $y = 'middle')
338
    {
339
        $this->resize($width, $height, true);
340
        $this->crop($width, $height, $x, $y);
341
342
        return $this;
343
    }
344
345
    /**
346
     * Rotates the image.
347
     *
348
     * @param int $angle Rotation angle in degrees (anticlockwise)
349
     *
350
     * @return self
351
     */
352
    public function rotate($angle)
353
    {
354
        if (($angle = intval($angle)) !== 0) {
355
            $this->image->rotate($angle);
356
        }
357
358
        return $this;
359
    }
360
361
    /**
362
     * Define the image compression quality for jpg images.
363
     *
364
     * @param int $quality The quality (from 0 to 100)
365
     *
366
     * @return self
367
     */
368
    public function quality($quality)
369
    {
370
        $quality = intval($quality);
371
372
        if ($quality < 0) {
373
            $quality = 0;
374
        } elseif ($quality > 100) {
375
            $quality = 100;
376
        }
377
378
        $this->image->setCompressionQuality($quality);
379
380
        return $this;
381
    }
382
383
    /**
384
     * Reads the EXIF data from a JPEG and returns an associative array
385
     * (requires the exif PHP extension enabled).
386
     *
387
     * @param null|string $key
388
     *
389
     * @return null|array
390
     */
391
    public function getExifData($key = null)
392
    {
393
        if ($this->filename !== null && ($this->getMimeType() === 'image/jpeg')) {
394
            $exif = exif_read_data($this->filename);
395
396
            if ($key !== null) {
397
                return isset($exif[$key]) ? $exif[$key] : null;
398
            }
399
400
            return $exif;
401
        }
402
    }
403
404
    /**
405
     * Transform the image executing various operations of crop, resize, resizeCrop and format.
406
     *
407
     * @param string $operations The string with all operations separated by "|".
408
     *
409
     * @return self
410
     */
411
    public function transform($operations = null)
412
    {
413
        //No transform operations, resize to fix the client size
414
        if (empty($operations)) {
415
            return $this->resize($this->getWidth(), $this->getHeight());
416
        }
417
418
        $operations = self::parseOperations($operations);
419
420
        foreach ($operations as $operation) {
421
            switch ($operation['function']) {
422
                case 'crop':
423
                case 'resizecrop':
424
                    if (isset($operation['params'][2])) {
425
                        switch ($operation['params'][2]) {
426
                            case 'CROP_ENTROPY':
427
                                $operation['params'][2] = self::CROP_ENTROPY;
428
                                break;
429
430
                            case 'CROP_BALANCED':
431
                                $operation['params'][2] = self::CROP_BALANCED;
432
                                break;
433
                        }
434
                    }
435
                    break;
436
            }
437
438
            call_user_func_array([$this, $operation['function']], $operation['params']);
439
        }
440
441
        return $this;
442
    }
443
444
    /**
445
     * Send the HTTP header with the content-type, output the image data and die.
446
     */
447 View Code Duplication
    public function show()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
448
    {
449
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
450
            header('Content-Type: '.$mimetype);
451
            die($string);
452
        }
453
    }
454
455
    /**
456
     * Returns the image as base64 url.
457
     *
458
     * @return string|null
459
     */
460 View Code Duplication
    public function base64()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
461
    {
462
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
463
            $string = base64_encode($string);
464
465
            return "data:{$mimetype};base64,{$string}";
466
        }
467
    }
468
469
    /**
470
     * Auto-rotate the image according with its exif data
471
     * Taken from: http://php.net/manual/en/function.exif-read-data.php#76964.
472
     *
473
     * @return self
474
     */
475
    public function autoRotate()
476
    {
477
        switch ($this->getExifData('Orientation')) {
478
            case 2:
479
                $this->flop();
480
                break;
481
482
            case 3:
483
                $this->rotate(180);
484
                break;
485
486
            case 4:
487
                $this->flip();
488
                break;
489
490
            case 5:
491
                $this->flip()->rotate(-90);
492
                break;
493
494
            case 6:
495
                $this->rotate(90);
496
                break;
497
498
            case 7:
499
                $this->flop()->rotate(-90);
500
                break;
501
502
            case 8:
503
                $this->rotate(90);
504
                break;
505
        }
506
507
        return $this;
508
    }
509
510
    /**
511
     * Check whether the image is an animated gif.
512
     * Copied from: https://github.com/Sybio/GifFrameExtractor/blob/master/src/GifFrameExtractor/GifFrameExtractor.php#L181.
513
     *
514
     * @param resource A stream pointer opened by fopen()
515
     * 
516
     * @return bool
517
     */
518
    private static function isAnimatedGif($stream)
519
    {
520
        $count = 0;
521
522
        while (!feof($stream) && $count < 2) {
523
            $chunk = fread($stream, 1024 * 100); //read 100kb at a time
524
            $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
525
        }
526
527
        return $count > 1;
528
    }
529
530
    /**
531
     * Converts a string with operations in an array.
532
     *
533
     * @param string $operations The operations string
534
     *
535
     * @return array
536
     */
537
    private static function parseOperations($operations)
538
    {
539
        $valid_operations = ['resize', 'resizecrop', 'crop', 'format', 'quality'];
540
        $operations = explode('|', str_replace(' ', '', $operations));
541
        $return = [];
542
543
        foreach ($operations as $operations) {
544
            $params = explode(',', $operations);
545
            $function = strtolower(trim(array_shift($params)));
546
547
            if (!in_array($function, $valid_operations, true)) {
548
                throw new ImageException("The transform function '{$function}' is not valid");
549
            }
550
551
            $return[] = [
552
                'function' => $function,
553
                'params' => $params,
554
            ];
555
        }
556
557
        return $return;
558
    }
559
560
    /**
561
     * Checks the library to use and returns its class.
562
     *
563
     * @param string $library The library name (Gd, Imagick)
564
     *
565
     * @throws ImageException if the image library does not exists.
566
     *
567
     * @return string
568
     */
569
    private static function getLibraryClass($library)
570
    {
571
        if (!$library) {
572
            $library = Libs\Imagick::checkCompatibility() ? self::LIB_IMAGICK : self::LIB_GD;
573
        }
574
575
        $class = 'Imagecow\\Libs\\'.$library;
576
577
        if (!class_exists($class)) {
578
            throw new ImageException('The image library is not valid');
579
        }
580
581
        if (!$class::checkCompatibility()) {
582
            throw new ImageException("The image library '$library' is not installed in this computer");
583
        }
584
585
        return $class;
586
    }
587
}
588