Completed
Push — master ( b99dd2...fc7346 )
by Oscar
10s
created

Image::transform()   D

Complexity

Conditions 9
Paths 13

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 14
Bugs 4 Features 3
Metric Value
c 14
b 4
f 3
dl 0
loc 39
rs 4.909
cc 9
eloc 23
nc 13
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
    const CROP_FACE = 'Face';
15
16
    protected $image;
17
    protected $filename;
18
    protected $clientHints = [
19
        'dpr' => null,
20
        'viewport-width' => null,
21
        'width' => null,
22
    ];
23
24
    /**
25
     * Static function to create a new Imagecow instance from an image file.
26
     *
27
     * @param string $filename The path of the file
28
     * @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.
29
     *
30
     * @return Image
31
     */
32
    public static function fromFile($filename, $library = null)
33
    {
34
        $class = self::getLibraryClass($library);
35
36
        $image = new static($class::createFromFile($filename), $filename);
37
38
        if ($image->getMimeType() !== 'image/gif') {
39
            return $image;
40
        }
41
42
        $stream = fopen($filename, 'rb');
43
44
        if (self::isAnimatedGif($stream)) {
45
            $image->image->setAnimated(true);
46
        }
47
48
        fclose($stream);
49
50
        return $image;
51
    }
52
53
    /**
54
     * Static function to create a new Imagecow instance from a binary string.
55
     *
56
     * @param string $string  The string of the image
57
     * @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.
58
     *
59
     * @return Image
60
     */
61
    public static function fromString($string, $library = null)
62
    {
63
        $class = self::getLibraryClass($library);
64
65
        $image = new static($class::createFromString($string));
66
67
        if ($image->getMimeType() !== 'image/gif') {
68
            return $image;
69
        }
70
71
        $stream = fopen('php://temp', 'r+');
72
73
        fwrite($stream, $string);
74
        rewind($stream);
75
76
        if (self::isAnimatedGif($stream)) {
77
            $image->image->setAnimated(true);
78
        }
79
80
        fclose($stream);
81
82
        return $image;
83
    }
84
85
    /**
86
     * Constructor.
87
     *
88
     * @param Libs\LibInterface $image
89
     * @param string            $filename Original filename (used to overwrite)
90
     */
91
    public function __construct(Libs\LibInterface $image, $filename = null)
92
    {
93
        $this->image = $image;
94
        $this->filename = $filename;
95
    }
96
97
    /**
98
     * Set the available client hints.
99
     *
100
     * @param array $clientHints
101
     *
102
     * @return self
103
     */
104
    public function setClientHints(array $clientHints)
105
    {
106
        $normalize = [];
107
108
        foreach ($clientHints as $key => $value) {
109
            $normalize[strtolower($key)] = is_null($value) ? null : (float) $value;
110
        }
111
112
        if (array_diff_key($normalize, $this->clientHints)) {
113
            throw new \InvalidArgumentException('Invalid client hints');
114
        }
115
116
        $this->clientHints = array_replace($this->clientHints, $normalize);
117
118
        return $this;
119
    }
120
121
    /**
122
     * Set a default background color used to fill in some transformation functions.
123
     *
124
     * @param array $background The color in rgb, for example: array(0,127,34)
125
     *
126
     * @return self
127
     */
128
    public function setBackground(array $background)
129
    {
130
        $this->image->setBackground($background);
131
132
        return $this;
133
    }
134
135
    /**
136
     * Define the image compression quality for jpg images.
137
     *
138
     * @param int $quality The quality (from 0 to 100)
139
     *
140
     * @deprecated Use quality instead
141
     *
142
     * @return self
143
     */
144
    public function setCompressionQuality($quality)
145
    {
146
        error_log('The method `setCompressionQuality()` is deprecated. Use `quality()` instead.');
147
148
        return $this->quality($quality);
149
    }
150
151
    /**
152
     * Get the fixed size according with the client hints.
153
     *
154
     * @param int $width
155
     * @param int $height
156
     *
157
     * @return array
158
     */
159
    private function calculateClientSize($width, $height)
160
    {
161 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...
162
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['width'], null);
163
        }
164
165 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...
166
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['viewport-width'], null);
167
        }
168
169
        if ($this->clientHints['dpr'] !== null) {
170
            $width *= $this->clientHints['dpr'];
171
            $height *= $this->clientHints['dpr'];
172
        }
173
174
        return [$width, $height];
175
    }
176
177
    /**
178
     * Inverts the image vertically.
179
     *
180
     * @return self
181
     */
182
    public function flip()
183
    {
184
        $this->image->flip();
185
186
        return $this;
187
    }
188
189
    /**
190
     * Inverts the image horizontally.
191
     *
192
     * @return self
193
     */
194
    public function flop()
195
    {
196
        $this->image->flop();
197
198
        return $this;
199
    }
200
201
    /**
202
     * Saves the image in a file.
203
     *
204
     * @param string $filename Name of the file where the image will be saved. If it's not defined, The original file will be overwritten.
205
     *
206
     * @return self
207
     */
208
    public function save($filename = null)
209
    {
210
        $this->image->save($filename ?: $this->filename);
211
212
        return $this;
213
    }
214
215
    /**
216
     * Returns the image instance.
217
     *
218
     * @return Libs\LibInterface
219
     */
220
    public function getImage()
221
    {
222
        return $this->image;
223
    }
224
225
    /**
226
     * Gets the image data in a string.
227
     *
228
     * @return string The image data
229
     */
230
    public function getString()
231
    {
232
        return $this->image->getString();
233
    }
234
235
    /**
236
     * Gets the mime type of the image.
237
     *
238
     * @return string The mime type
239
     */
240
    public function getMimeType()
241
    {
242
        return $this->image->getMimeType();
243
    }
244
245
    /**
246
     * Gets the width of the image.
247
     *
248
     * @return int The width in pixels
249
     */
250
    public function getWidth()
251
    {
252
        return $this->image->getWidth();
253
    }
254
255
    /**
256
     * Gets the height of the image.
257
     *
258
     * @return int The height in pixels
259
     */
260
    public function getHeight()
261
    {
262
        return $this->image->getHeight();
263
    }
264
265
    /**
266
     * Converts the image to other format.
267
     *
268
     * @param string $format The new format: png, jpg, gif
269
     *
270
     * @return self
271
     */
272
    public function format($format)
273
    {
274
        $this->image->format($format);
275
276
        return $this;
277
    }
278
279
    /**
280
     * Resizes the image maintaining the proportion (A 800x600 image resized to 400x400 becomes to 400x300).
281
     *
282
     * @param int|string $width  The max width of the image. It can be a number (pixels) or percentaje
283
     * @param int|string $height The max height of the image. It can be a number (pixels) or percentaje
284
     * @param bool       $cover
285
     *
286
     * @return self
287
     */
288
    public function resize($width, $height = 0, $cover = false)
289
    {
290
        $imageWidth = $this->getWidth();
291
        $imageHeight = $this->getHeight();
292
293
        $width = Dimmensions::getIntegerValue('x', $width, $imageWidth);
294
        $height = Dimmensions::getIntegerValue('y', $height, $imageHeight);
295
296
        list($width, $height) = Dimmensions::getResizeDimmensions($imageWidth, $imageHeight, $width, $height, $cover);
297
        list($width, $height) = $this->calculateClientSize($width, $height);
298
299
        if ($width >= $imageWidth) {
300
            return $this;
301
        }
302
303
        $this->image->resize($width, $height);
304
305
        return $this;
306
    }
307
308
    /**
309
     * Crops the image.
310
     *
311
     * @param int|string $width  The new width of the image. It can be a number (pixels) or percentaje
312
     * @param int|string $height The new height of the image. It can be a number (pixels) or percentaje
313
     * @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
314
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
315
     *
316
     * @return self
317
     */
318
    public function crop($width, $height, $x = 'center', $y = 'middle')
319
    {
320
        $imageWidth = $this->getWidth();
321
        $imageHeight = $this->getHeight();
322
323
        $width = Dimmensions::getIntegerValue('x', $width, $imageWidth);
324
        $height = Dimmensions::getIntegerValue('y', $height, $imageHeight);
325
326
        list($width, $height) = $this->calculateClientSize($width, $height);
327
328
        if (($x === self::CROP_BALANCED) || ($x === self::CROP_ENTROPY) || ($x === self::CROP_FACE)) {
329
            list($x, $y) = $this->image->getCropOffsets($width, $height, $x);
330
        }
331
332
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
333
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
334
335
        $this->image->crop($width, $height, $x, $y);
336
337
        return $this;
338
    }
339
340
    /**
341
     * Adjust the image to the given dimmensions. Resizes and crops the image maintaining the proportions.
342
     *
343
     * @param int|string $width  The new width in number (pixels) or percentaje
344
     * @param int|string $height The new height in number (pixels) or percentaje
345
     * @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
346
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
347
     *
348
     * @return self
349
     */
350
    public function resizeCrop($width, $height, $x = 'center', $y = 'middle')
351
    {
352
        $this->resize($width, $height, true);
353
        $this->crop($width, $height, $x, $y);
354
355
        return $this;
356
    }
357
358
    /**
359
     * Rotates the image.
360
     *
361
     * @param int $angle Rotation angle in degrees (anticlockwise)
362
     *
363
     * @return self
364
     */
365
    public function rotate($angle)
366
    {
367
        if (($angle = intval($angle)) !== 0) {
368
            $this->image->rotate($angle);
369
        }
370
371
        return $this;
372
    }
373
374
    /**
375
     * Define the image compression quality for jpg images.
376
     *
377
     * @param int $quality The quality (from 0 to 100)
378
     *
379
     * @return self
380
     */
381
    public function quality($quality)
382
    {
383
        $quality = intval($quality);
384
385
        if ($quality < 0) {
386
            $quality = 0;
387
        } elseif ($quality > 100) {
388
            $quality = 100;
389
        }
390
391
        $this->image->setCompressionQuality($quality);
392
393
        return $this;
394
    }
395
396
    /**
397
     * Add a watermark to current image.
398
     *
399
     * @param string $file Image to set as watermark
0 ignored issues
show
Bug introduced by
There is no parameter named $file. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
400
     * @param mixed  $x    Horizontal position
401
     * @param mixed  $y    Vertical position
402
     *
403
     * @return self
404
     */
405
    public function watermark(Image $image, $x = 'right', $y = 'bottom')
406
    {
407
        $imageWidth = $this->getWidth();
408
        $imageHeight = $this->getHeight();
409
410
        $width = $image->getWidth();
411
        $height = $image->getHeight();
412
413
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
414
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
415
416
        $this->image->watermark($image->getImage(), $x, $y);
417
418
        return $this;
419
    }
420
421
    /**
422
     * Add opacity to image from 0 (transparent) to 100 (opaque).
423
     *
424
     * @param int $opacity Opacity value
425
     *
426
     * @return self
427
     */
428
    public function opacity($opacity)
429
    {
430
        $this->image->opacity($opacity);
431
432
        return $this;
433
    }
434
435
    /**
436
     * Reads the EXIF data from a JPEG and returns an associative array
437
     * (requires the exif PHP extension enabled).
438
     *
439
     * @param null|string $key
440
     *
441
     * @return null|array
442
     */
443
    public function getExifData($key = null)
444
    {
445
        if ($this->filename !== null && ($this->getMimeType() === 'image/jpeg')) {
446
            $exif = exif_read_data($this->filename);
447
448
            if ($key !== null) {
449
                return isset($exif[$key]) ? $exif[$key] : null;
450
            }
451
452
            return $exif;
453
        }
454
    }
455
456
    /**
457
     * Transform the image executing various operations of crop, resize, resizeCrop and format.
458
     *
459
     * @param string $operations The string with all operations separated by "|".
460
     *
461
     * @return self
462
     */
463
    public function transform($operations = null)
464
    {
465
        //No transform operations, resize to fix the client size
466
        if (empty($operations)) {
467
            return $this->resize($this->getWidth(), $this->getHeight());
468
        }
469
470
        $operations = self::parseOperations($operations);
471
472
        foreach ($operations as $operation) {
473
            switch ($operation['function']) {
474
                case 'crop':
475
                case 'resizecrop':
476
                    if (empty($operation['params'][2])) {
477
                        break;
478
                    }
479
480
                    switch ($operation['params'][2]) {
481
                        case 'CROP_ENTROPY':
482
                            $operation['params'][2] = self::CROP_ENTROPY;
483
                            break;
484
485
                        case 'CROP_BALANCED':
486
                            $operation['params'][2] = self::CROP_BALANCED;
487
                            break;
488
489
                        case 'CROP_FACE':
490
                            $operation['params'][2] = self::CROP_FACE;
491
                            break;
492
                    }
493
494
                    break;
495
            }
496
497
            call_user_func_array([$this, $operation['function']], $operation['params']);
498
        }
499
500
        return $this;
501
    }
502
503
    /**
504
     * Send the HTTP header with the content-type, output the image data and die.
505
     */
506 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...
507
    {
508
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
509
            header('Content-Type: '.$mimetype);
510
            die($string);
511
        }
512
    }
513
514
    /**
515
     * Returns the image as base64 url.
516
     *
517
     * @return string|null
518
     */
519 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...
520
    {
521
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
522
            $string = base64_encode($string);
523
524
            return "data:{$mimetype};base64,{$string}";
525
        }
526
    }
527
528
    /**
529
     * Auto-rotate the image according with its exif data
530
     * Taken from: http://php.net/manual/en/function.exif-read-data.php#76964.
531
     *
532
     * @return self
533
     */
534
    public function autoRotate()
535
    {
536
        switch ($this->getExifData('Orientation')) {
537
            case 2:
538
                $this->flop();
539
                break;
540
541
            case 3:
542
                $this->rotate(180);
543
                break;
544
545
            case 4:
546
                $this->flip();
547
                break;
548
549
            case 5:
550
                $this->flip()->rotate(-90);
551
                break;
552
553
            case 6:
554
                $this->rotate(90);
555
                break;
556
557
            case 7:
558
                $this->flop()->rotate(-90);
559
                break;
560
561
            case 8:
562
                $this->rotate(90);
563
                break;
564
        }
565
566
        return $this;
567
    }
568
569
    /**
570
     * Check whether the image is an animated gif.
571
     * Copied from: https://github.com/Sybio/GifFrameExtractor/blob/master/src/GifFrameExtractor/GifFrameExtractor.php#L181.
572
     *
573
     * @param resource A stream pointer opened by fopen()
574
     *
575
     * @return bool
576
     */
577
    private static function isAnimatedGif($stream)
578
    {
579
        $count = 0;
580
581
        while (!feof($stream) && $count < 2) {
582
            $chunk = fread($stream, 1024 * 100); //read 100kb at a time
583
            $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
584
        }
585
586
        return $count > 1;
587
    }
588
589
    /**
590
     * Converts a string with operations in an array.
591
     *
592
     * @param string $operations The operations string
593
     *
594
     * @return array
595
     */
596
    private static function parseOperations($operations)
597
    {
598
        $valid_operations = ['resize', 'resizecrop', 'crop', 'format', 'quality'];
599
        $operations = explode('|', str_replace(' ', '', $operations));
600
        $return = [];
601
602
        foreach ($operations as $operations) {
603
            $params = explode(',', $operations);
604
            $function = strtolower(trim(array_shift($params)));
605
606
            if (!in_array($function, $valid_operations, true)) {
607
                throw new ImageException("The transform function '{$function}' is not valid");
608
            }
609
610
            $return[] = [
611
                'function' => $function,
612
                'params' => $params,
613
            ];
614
        }
615
616
        return $return;
617
    }
618
619
    /**
620
     * Checks the library to use and returns its class.
621
     *
622
     * @param string $library The library name (Gd, Imagick)
623
     *
624
     * @throws ImageException if the image library does not exists.
625
     *
626
     * @return string
627
     */
628
    private static function getLibraryClass($library)
629
    {
630
        if (!$library) {
631
            $library = Libs\Imagick::checkCompatibility() ? self::LIB_IMAGICK : self::LIB_GD;
632
        }
633
634
        $class = 'Imagecow\\Libs\\'.$library;
635
636
        if (!class_exists($class)) {
637
            throw new ImageException('The image library is not valid');
638
        }
639
640
        if (!$class::checkCompatibility()) {
641
            throw new ImageException("The image library '$library' is not installed in this computer");
642
        }
643
644
        return $class;
645
    }
646
}
647