Completed
Push — v3.x ( 8b6982 )
by Oscar
01:53
created

Image::resize()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
cc 3
nc 2
nop 3
1
<?php
2
3
namespace Imagecow;
4
5
use Imagecow\Utils\Dimmensions;
6
use Imagecow\Adapters\AdapterInterface;
7
use Imagecow\Adapters\GdAdapter;
8
use Imagecow\Adapters\ImagickAdapter;
9
10
final class Image
11
{
12
    const CROP_ENTROPY = 'Entropy';
13
    const CROP_BALANCED = 'Balanced';
14
    const CROP_FACE = 'Face';
15
16
    private $adapter;
17
    private $filename;
18
    private $clientHints = [
19
        'dpr' => null,
20
        'viewport-width' => null,
21
        'width' => null,
22
    ];
23
24
    /**
25
     * Create a new Imagecow instance from an image file.
26
     */
27
    public static function fromFile(string $filename, string $adapter = null): Image
28
    {
29
        $adapter = self::getAdapterClass($adapter);
30
        $image = new static($adapter::createFromFile($filename), $filename);
31
32
        if ($image->getMimeType() !== 'image/gif') {
33
            return $image;
34
        }
35
36
        $stream = fopen($filename, 'rb');
37
38
        if (self::isAnimatedGif($stream)) {
39
            $image->adapter->setAnimated(true);
40
        }
41
42
        fclose($stream);
43
44
        return $image;
45
    }
46
47
    /**
48
     * Create a new Imagecow instance from a string.
49
     */
50
    public static function fromString(string $string, string $adapter = null): Image
51
    {
52
        $adapter = self::getAdapterClass($adapter);
53
        $image = new static($adapter::createFromString($string));
54
55
        if ($image->getMimeType() !== 'image/gif') {
56
            return $image;
57
        }
58
59
        $stream = fopen('php://temp', 'r+');
60
61
        fwrite($stream, $string);
62
        rewind($stream);
63
64
        if (self::isAnimatedGif($stream)) {
65
            $image->adapter->setAnimated(true);
66
        }
67
68
        fclose($stream);
69
70
        return $image;
71
    }
72
73
    /**
74
     * Constructor.
75
     */
76
    public function __construct(AdapterInterface $adapter, string $filename = null)
77
    {
78
        $this->adapter = $adapter;
79
        $this->filename = $filename;
80
    }
81
82
    /**
83
     * Set the available client hints.
84
     */
85
    public function setClientHints(array $clientHints): self
86
    {
87
        $normalize = [];
88
89
        foreach ($clientHints as $key => $value) {
90
            $normalize[strtolower($key)] = is_null($value) ? null : (float) $value;
91
        }
92
93
        if (array_diff_key($normalize, $this->clientHints)) {
94
            throw new \InvalidArgumentException('Invalid client hints');
95
        }
96
97
        $this->clientHints = array_replace($this->clientHints, $normalize);
98
99
        return $this;
100
    }
101
102
    /**
103
     * Set a default background color used to fill in some transformation functions
104
     * in rgb format, for example: array(0,127,34)
105
     */
106
    public function setBackground(array $background): self
107
    {
108
        $this->adapter->setBackground($background);
109
110
        return $this;
111
    }
112
113
    /**
114
     * Get the fixed size according with the client hints.
115
     */
116
    private function calculateClientSize(int $width, int $height): array
117
    {
118 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...
119
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['width'], null);
120
        }
121
122 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...
123
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['viewport-width'], null);
124
        }
125
126
        if ($this->clientHints['dpr'] !== null) {
127
            $width *= $this->clientHints['dpr'];
128
            $height *= $this->clientHints['dpr'];
129
        }
130
131
        return [$width, $height];
132
    }
133
134
    /**
135
     * Inverts the image vertically.
136
     */
137
    public function flip(): self
138
    {
139
        $this->adapter->flip();
140
141
        return $this;
142
    }
143
144
    /**
145
     * Inverts the image horizontally.
146
     */
147
    public function flop(): self
148
    {
149
        $this->adapter->flop();
150
151
        return $this;
152
    }
153
154
    /**
155
     * Saves the image in a file (or override the previous opened file).
156
     */
157
    public function save(string $filename = null): self
158
    {
159
        $this->adapter->save($filename ?: $this->filename);
160
161
        return $this;
162
    }
163
164
    public function getAdapter(): AdapterInterface
165
    {
166
        return $this->adapter;
167
    }
168
169
    /**
170
     * Gets the image data in a string.
171
     */
172
    public function getString(): string
173
    {
174
        return $this->adapter->getString();
175
    }
176
177
    /**
178
     * Gets the mime type of the image.
179
     */
180
    public function getMimeType(): string
181
    {
182
        return $this->adapter->getMimeType();
183
    }
184
185
    /**
186
     * Gets the width of the image in pixels.
187
     */
188
    public function getWidth(): int
189
    {
190
        return $this->adapter->getWidth();
191
    }
192
193
    /**
194
     * Gets the height of the image in pixels.
195
     */
196
    public function getHeight(): int
197
    {
198
        return $this->adapter->getHeight();
199
    }
200
201
    /**
202
     * Converts the image to other format (png, jpg, gif or webp).
203
     */
204
    public function format($format): self
205
    {
206
        $this->adapter->format($format);
207
208
        return $this;
209
    }
210
211
    /**
212
     * Resizes the image maintaining the proportion (A 800x600 image resized to 400x400 becomes to 400x300).
213
     *
214
     * @param int|string $width  The max width of the image. It can be a number (pixels) or percentaje
215
     * @param int|string $height The max height of the image. It can be a number (pixels) or percentaje
216
     */
217
    public function resize($width, $height = 0, bool $cover = false): self
218
    {
219
        $imageWidth = $this->getWidth();
220
        $imageHeight = $this->getHeight();
221
222
        $width = Dimmensions::getIntegerValue('x', $width, $imageWidth);
223
        $height = Dimmensions::getIntegerValue('y', $height, $imageHeight);
224
225
        list($width, $height) = Dimmensions::getResizeDimmensions($imageWidth, $imageHeight, $width, $height, $cover);
226
        list($width, $height) = $this->calculateClientSize($width, $height);
227
228
        if ($width >= $imageWidth && !$cover) {
229
            return $this;
230
        }
231
232
        $this->adapter->resize($width, $height);
233
234
        return $this;
235
    }
236
237
    /**
238
     * Crops the image.
239
     *
240
     * @param int|string $width  The new width of the image. It can be a number (pixels) or percentaje
241
     * @param int|string $height The new height of the image. It can be a number (pixels) or percentaje
242
     * @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
243
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
244
     */
245
    public function crop($width, $height, $x = 'center', $y = 'middle'): self
246
    {
247
        $imageWidth = $this->getWidth();
248
        $imageHeight = $this->getHeight();
249
250
        $width = Dimmensions::getIntegerValue('x', $width, $imageWidth);
251
        $height = Dimmensions::getIntegerValue('y', $height, $imageHeight);
252
253
        list($width, $height) = $this->calculateClientSize($width, $height);
254
255
        if (in_array($x, [self::CROP_BALANCED, self::CROP_ENTROPY, self::CROP_FACE], true)) {
256
            list($x, $y) = $this->adapter->getCropOffsets($width, $height, $x);
257
        }
258
259
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
260
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
261
262
        $this->adapter->crop($width, $height, $x, $y);
263
264
        return $this;
265
    }
266
267
    /**
268
     * Adjust the image to the given dimmensions. Resizes and crops the image maintaining the proportions.
269
     *
270
     * @param int|string $width  The new width in number (pixels) or percentaje
271
     * @param int|string $height The new height in number (pixels) or percentaje
272
     * @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
273
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
274
     */
275
    public function resizeCrop($width, $height, $x = 'center', $y = 'middle'): self
276
    {
277
        $this->resize($width, $height, true);
278
        $this->crop($width, $height, $x, $y);
279
280
        return $this;
281
    }
282
283
    /**
284
     * Rotates the image (in degrees, anticlockwise).
285
     */
286
    public function rotate(int $angle): self
287
    {
288
        if (($angle = intval($angle)) !== 0) {
289
            $this->adapter->rotate($angle);
290
        }
291
292
        return $this;
293
    }
294
295
    /**
296
     * Apply blur to image
297
     */
298
    public function blur(int $loops = 4): self
299
    {
300
        $this->adapter->blur($loops);
301
302
        return $this;
303
    }
304
305
    /**
306
     * Define the image compression quality for jpg images (from 0 to 100).
307
     */
308
    public function quality(int $quality): self
309
    {
310
        $quality = intval($quality);
311
312
        if ($quality < 0) {
313
            $quality = 0;
314
        } elseif ($quality > 100) {
315
            $quality = 100;
316
        }
317
318
        $this->adapter->setCompressionQuality($quality);
319
320
        return $this;
321
    }
322
323
    /**
324
     * Add a watermark to current image.
325
     *
326
     * @param mixed  $x    Horizontal position
327
     * @param mixed  $y    Vertical position
328
     */
329
    public function watermark(Image $image, $x = 'right', $y = 'bottom'): self
330
    {
331
        $imageWidth = $this->getWidth();
332
        $imageHeight = $this->getHeight();
333
334
        $width = $image->getWidth();
335
        $height = $image->getHeight();
336
337
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
338
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
339
340
        $this->adapter->watermark($image->getImage(), $x, $y);
0 ignored issues
show
Bug introduced by
The method getImage() does not seem to exist on object<Imagecow\Image>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
341
342
        return $this;
343
    }
344
345
    /**
346
     * Add opacity to image from 0 (transparent) to 100 (opaque).
347
     */
348
    public function opacity(int $opacity): self
349
    {
350
        $this->adapter->opacity($opacity);
351
352
        return $this;
353
    }
354
355
    /**
356
     * Set the image progressive or not
357
     */
358
    public function progressive(bool $progressive = true): self
359
    {
360
        $this->adapter->setProgressive((bool) $progressive);
361
362
        return $this;
363
    }
364
365
    /**
366
     * Reads the EXIF data from a JPEG and returns an associative array
367
     * (requires the exif PHP extension enabled).
368
     *
369
     * @param null|string $key
370
     *
371
     * @return null|array
372
     */
373
    public function getExifData($key = null)
374
    {
375
        if ($this->filename !== null && ($this->getMimeType() === 'image/jpeg')) {
376
            $exif = exif_read_data($this->filename);
377
378
            if ($key !== null) {
379
                return isset($exif[$key]) ? $exif[$key] : null;
380
            }
381
382
            return $exif;
383
        }
384
    }
385
386
    /**
387
     * Transform the image executing various operations of crop, resize, resizeCrop and format.
388
     */
389
    public function transform(string $operations = null): self
390
    {
391
        //No transform operations, resize to fix the client size
392
        if (empty($operations)) {
393
            return $this->resize($this->getWidth(), $this->getHeight());
394
        }
395
396
        $operations = self::parseOperations($operations);
397
398
        foreach ($operations as $operation) {
399
            switch ($operation['function']) {
400
                case 'crop':
401
                case 'resizecrop':
402
                    if (empty($operation['params'][2])) {
403
                        break;
404
                    }
405
406
                    switch ($operation['params'][2]) {
407
                        case 'CROP_ENTROPY':
408
                            $operation['params'][2] = self::CROP_ENTROPY;
409
                            break;
410
411
                        case 'CROP_BALANCED':
412
                            $operation['params'][2] = self::CROP_BALANCED;
413
                            break;
414
415
                        case 'CROP_FACE':
416
                            $operation['params'][2] = self::CROP_FACE;
417
                            break;
418
                    }
419
420
                    break;
421
            }
422
423
            call_user_func_array([$this, $operation['function']], $operation['params']);
424
        }
425
426
        return $this;
427
    }
428
429
    /**
430
     * Send the HTTP header with the content-type, output the image data and die.
431
     */
432 View Code Duplication
    public function show()
433
    {
434
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
435
            header('Content-Type: '.$mimetype);
436
            die($string);
437
        }
438
    }
439
440
    /**
441
     * Returns the image as base64 url.
442
     *
443
     * @return string|null
444
     */
445 View Code Duplication
    public function base64()
446
    {
447
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
448
            $string = base64_encode($string);
449
450
            return "data:{$mimetype};base64,{$string}";
451
        }
452
    }
453
454
    /**
455
     * Auto-rotate the image according with its exif data
456
     * Taken from: http://php.net/manual/en/function.exif-read-data.php#76964.
457
     */
458
    public function autoRotate(): self
459
    {
460
        switch ($this->getExifData('Orientation')) {
461
            case 2:
462
                $this->flop();
463
                break;
464
465
            case 3:
466
                $this->rotate(180);
467
                break;
468
469
            case 4:
470
                $this->flip();
471
                break;
472
473
            case 5:
474
                $this->flip()->rotate(90);
475
                break;
476
477
            case 6:
478
                $this->rotate(90);
479
                break;
480
481
            case 7:
482
                $this->flop()->rotate(90);
483
                break;
484
485
            case 8:
486
                $this->rotate(-90);
487
                break;
488
        }
489
490
        return $this;
491
    }
492
493
    /**
494
     * Check whether the image is an animated gif.
495
     * Copied from: https://github.com/Sybio/GifFrameExtractor/blob/master/src/GifFrameExtractor/GifFrameExtractor.php#L181.
496
     *
497
     * @param resource A stream pointer opened by fopen()
498
     */
499
    private static function isAnimatedGif($stream): bool
500
    {
501
        $count = 0;
502
503
        while (!feof($stream) && $count < 2) {
504
            $chunk = fread($stream, 1024 * 100); //read 100kb at a time
505
            $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
506
        }
507
508
        return $count > 1;
509
    }
510
511
    /**
512
     * Converts a string with operations in an array.
513
     */
514
    private static function parseOperations(string $operations): array
515
    {
516
        $valid_operations = ['resize', 'resizecrop', 'crop', 'format', 'quality'];
517
        $operations = explode('|', str_replace(' ', '', $operations));
518
        $return = [];
519
520
        foreach ($operations as $operations) {
521
            $params = explode(',', $operations);
522
            $function = strtolower(trim(array_shift($params)));
523
524
            if (!in_array($function, $valid_operations, true)) {
525
                throw new ImageException("The transform function '{$function}' is not valid");
526
            }
527
528
            $return[] = [
529
                'function' => $function,
530
                'params' => $params,
531
            ];
532
        }
533
534
        return $return;
535
    }
536
537
    /**
538
     * Checks the library to use and returns its class.
539
     *
540
     * @throws ImageException if the image library does not exists.
541
     */
542
    private static function getAdapterClass(string $adapter = null): string
543
    {
544
        if (!$adapter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $adapter of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
545
            return ImagickAdapter::checkCompatibility() ? ImagickAdapter::class : GdAdapter::class;
546
        }
547
548
        if (!class_exists($adapter)) {
549
            throw new ImageException(sprintf('The class %s does not exists', $adapter));
550
        }
551
552
        if (!$adapter::checkCompatibility()) {
553
            throw new ImageException(sprintf('The class %s cannot be used in this computer', $adapter));
554
        }
555
556
        return $adapter;
557
    }
558
}
559