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

Image::getHeight()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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 clientHints(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 background(array $background): self
107
    {
108
        $this->adapter->setBackground($background);
109
110
        return $this;
111
    }
112
113
    /**
114
     * Inverts the image vertically.
115
     */
116
    public function flip(): self
117
    {
118
        $this->adapter->flip();
119
120
        return $this;
121
    }
122
123
    /**
124
     * Inverts the image horizontally.
125
     */
126
    public function flop(): self
127
    {
128
        $this->adapter->flop();
129
130
        return $this;
131
    }
132
133
    /**
134
     * Saves the image in a file (or override the previous opened file).
135
     */
136
    public function save(string $filename = null): self
137
    {
138
        $this->adapter->save($filename ?: $this->filename);
139
140
        return $this;
141
    }
142
143
    public function getAdapter(): AdapterInterface
144
    {
145
        return $this->adapter;
146
    }
147
148
    /**
149
     * Gets the image data in a string.
150
     */
151
    public function getString(): string
152
    {
153
        return $this->adapter->getString();
154
    }
155
156
    /**
157
     * Gets the mime type of the image.
158
     */
159
    public function getMimeType(): string
160
    {
161
        return $this->adapter->getMimeType();
162
    }
163
164
    /**
165
     * Gets the width of the image in pixels.
166
     */
167
    public function getWidth(int $percentage = null): int
168
    {
169
        $width = $this->adapter->getWidth();
170
171
        return isset($percentage) ? ($percentage / 100) * $width : $width;
172
    }
173
174
    /**
175
     * Gets the height of the image in pixels.
176
     */
177
    public function getHeight(int $percentage = null): int
178
    {
179
        $height = $this->adapter->getHeight();
180
181
        return isset($percentage) ? ($percentage / 100) * $height : $height;
182
    }
183
184
    /**
185
     * Converts the image to other format (png, jpg, gif or webp).
186
     */
187
    public function format($format): self
188
    {
189
        $this->adapter->format($format);
190
191
        return $this;
192
    }
193
194
    /**
195
     * Resizes the image maintaining the proportion (A 800x600 image resized to 400x400 becomes to 400x300).
196
     *
197
     * @param int|string $width  The max width of the image. It can be a number (pixels) or percentaje
198
     * @param int|string $height The max height of the image. It can be a number (pixels) or percentaje
199
     */
200
    public function resize($width, $height = 0, bool $cover = false): self
201
    {
202
        $imageWidth = $this->getWidth();
203
        $imageHeight = $this->getHeight();
204
205
        if ($percentage = self::getPercentage($width)) {
206
            $width = $this->getWidth($percentage);
207
        }
208
209
        if ($percentage = self::getPercentage($height)) {
210
            $height = $this->getHeight($percentage);
211
        }
212
213
        list($width, $height) = Dimmensions::getResizeDimmensions($imageWidth, $imageHeight, $width, $height, $cover);
214
        list($width, $height) = $this->calculateClientSize($width, $height);
215
216
        if ($width >= $imageWidth && !$cover) {
217
            return $this;
218
        }
219
220
        $this->adapter->resize($width, $height);
221
222
        return $this;
223
    }
224
225
    /**
226
     * Crops the image.
227
     *
228
     * @param int|string $width  The new width of the image. It can be a number (pixels) or percentaje
229
     * @param int|string $height The new height of the image. It can be a number (pixels) or percentaje
230
     * @param int|string $x      The "x" position to crop. It can be number (pixels), percentaje or one of the Image::CROP_* constants
231
     * @param int|string $y      The "y" position to crop. It can be number (pixels) or percentaje
232
     */
233
    public function crop($width, $height, $x = '50%', $y = '50%'): self
234
    {
235
        $imageWidth = $this->getWidth();
236
        $imageHeight = $this->getHeight();
237
238
        if ($percentage = self::getPercentage($width)) {
239
            $width = $this->getWidth($percentage);
240
        }
241
242
        if ($percentage = self::getPercentage($height)) {
243
            $height = $this->getHeight($percentage);
244
        }
245
246
        list($width, $height) = $this->calculateClientSize($width, $height);
247
248
        if (in_array($x, [self::CROP_BALANCED, self::CROP_ENTROPY, self::CROP_FACE], true)) {
249
            list($x, $y) = $this->adapter->getCropOffsets($width, $height, $x);
250
        }
251
252
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
253
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
254
255
        $this->adapter->crop($width, $height, $x, $y);
256
257
        return $this;
258
    }
259
260
    /**
261
     * Adjust the image to the given dimmensions. Resizes and crops the image maintaining the proportions.
262
     *
263
     * @param int|string $width  The new width in number (pixels) or percentaje
264
     * @param int|string $height The new height in number (pixels) or percentaje
265
     * @param int|string $x      The "x" position to crop. It can be number (pixels), percentaje or one of the Image::CROP_* constants
266
     * @param int|string $y      The "y" position to crop. It can be number (pixels) or percentaje
267
     */
268
    public function resizeCrop($width, $height, $x = '50%', $y = '50%'): self
269
    {
270
        $this->resize($width, $height, true);
271
        $this->crop($width, $height, $x, $y);
272
273
        return $this;
274
    }
275
276
    /**
277
     * Rotates the image (in degrees, anticlockwise).
278
     */
279
    public function rotate(int $angle): self
280
    {
281
        if (($angle = intval($angle)) !== 0) {
282
            $this->adapter->rotate($angle);
283
        }
284
285
        return $this;
286
    }
287
288
    /**
289
     * Apply blur to image
290
     */
291
    public function blur(int $loops = 4): self
292
    {
293
        $this->adapter->blur($loops);
294
295
        return $this;
296
    }
297
298
    /**
299
     * Define the image compression quality for jpg images (from 0 to 100).
300
     */
301
    public function quality(int $quality): self
302
    {
303
        $quality = intval($quality);
304
305
        if ($quality < 0) {
306
            $quality = 0;
307
        } elseif ($quality > 100) {
308
            $quality = 100;
309
        }
310
311
        $this->adapter->setCompressionQuality($quality);
312
313
        return $this;
314
    }
315
316
    /**
317
     * Add a watermark to current image.
318
     *
319
     * @param string|int  $x Horizontal position
320
     * @param string|int  $y Vertical position
321
     */
322
    public function watermark(Image $image, $x = '100%', $y = '100%'): self
323
    {
324
        $imageWidth = $this->getWidth();
325
        $imageHeight = $this->getHeight();
326
327
        $width = $image->getWidth();
328
        $height = $image->getHeight();
329
330
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
331
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
332
333
        $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...
334
335
        return $this;
336
    }
337
338
    /**
339
     * Add opacity to image from 0 (transparent) to 100 (opaque).
340
     */
341
    public function opacity(int $opacity): self
342
    {
343
        $this->adapter->opacity($opacity);
344
345
        return $this;
346
    }
347
348
    /**
349
     * Set the image progressive or not
350
     */
351
    public function progressive(bool $progressive = true): self
352
    {
353
        $this->adapter->setProgressive((bool) $progressive);
354
355
        return $this;
356
    }
357
358
    /**
359
     * Reads the EXIF data from a JPEG and returns an associative array
360
     * (requires the exif PHP extension enabled).
361
     *
362
     * @param null|string $key
363
     *
364
     * @return null|array
365
     */
366
    public function getExifData($key = null)
367
    {
368
        if ($this->filename !== null && ($this->getMimeType() === 'image/jpeg')) {
369
            $exif = exif_read_data($this->filename);
370
371
            if ($key !== null) {
372
                return isset($exif[$key]) ? $exif[$key] : null;
373
            }
374
375
            return $exif;
376
        }
377
    }
378
379
    /**
380
     * Transform the image executing various operations of crop, resize, resizeCrop and format.
381
     */
382
    public function transform(string $operations = null): self
383
    {
384
        //No transform operations, resize to fix the client size
385
        if (empty($operations)) {
386
            return $this->resize($this->getWidth(), $this->getHeight());
387
        }
388
389
        $operations = self::parseOperations($operations);
390
391
        foreach ($operations as $operation) {
392
            switch ($operation['function']) {
393
                case 'crop':
394
                case 'resizecrop':
395
                    if (empty($operation['params'][2])) {
396
                        break;
397
                    }
398
399
                    switch ($operation['params'][2]) {
400
                        case 'CROP_ENTROPY':
401
                            $operation['params'][2] = self::CROP_ENTROPY;
402
                            break;
403
404
                        case 'CROP_BALANCED':
405
                            $operation['params'][2] = self::CROP_BALANCED;
406
                            break;
407
408
                        case 'CROP_FACE':
409
                            $operation['params'][2] = self::CROP_FACE;
410
                            break;
411
                    }
412
413
                    break;
414
            }
415
416
            call_user_func_array([$this, $operation['function']], $operation['params']);
417
        }
418
419
        return $this;
420
    }
421
422
    /**
423
     * Send the HTTP header with the content-type, output the image data and die.
424
     */
425
    public function show()
426
    {
427
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
428
            header('Content-Type: '.$mimetype);
429
            die($string);
430
        }
431
    }
432
433
    /**
434
     * Returns the image as base64 url.
435
     *
436
     * @return string|null
437
     */
438
    public function base64()
439
    {
440
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
441
            $string = base64_encode($string);
442
443
            return "data:{$mimetype};base64,{$string}";
444
        }
445
    }
446
447
    /**
448
     * Auto-rotate the image according with its exif data
449
     * Taken from: http://php.net/manual/en/function.exif-read-data.php#76964.
450
     */
451
    public function autoRotate(): self
452
    {
453
        switch ($this->getExifData('Orientation')) {
454
            case 2:
455
                $this->flop();
456
                break;
457
458
            case 3:
459
                $this->rotate(180);
460
                break;
461
462
            case 4:
463
                $this->flip();
464
                break;
465
466
            case 5:
467
                $this->flip()->rotate(90);
468
                break;
469
470
            case 6:
471
                $this->rotate(90);
472
                break;
473
474
            case 7:
475
                $this->flop()->rotate(90);
476
                break;
477
478
            case 8:
479
                $this->rotate(-90);
480
                break;
481
        }
482
483
        return $this;
484
    }
485
486
    /**
487
     * Get the fixed size according with the client hints.
488
     */
489
    private function calculateClientSize(int $width, int $height): array
490
    {
491
        if ($this->clientHints['width'] !== null && $this->clientHints['width'] < $width) {
492
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['width'], null);
493
        }
494
495
        if ($this->clientHints['viewport-width'] !== null && $this->clientHints['viewport-width'] < $width) {
496
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['viewport-width'], null);
497
        }
498
499
        if ($this->clientHints['dpr'] !== null) {
500
            $width *= $this->clientHints['dpr'];
501
            $height *= $this->clientHints['dpr'];
502
        }
503
504
        return [$width, $height];
505
    }
506
507
    /**
508
     * Check whether the image is an animated gif.
509
     * Copied from: https://github.com/Sybio/GifFrameExtractor/blob/master/src/GifFrameExtractor/GifFrameExtractor.php#L181.
510
     *
511
     * @param resource A stream pointer opened by fopen()
512
     */
513
    private static function isAnimatedGif($stream): bool
514
    {
515
        $count = 0;
516
517
        while (!feof($stream) && $count < 2) {
518
            $chunk = fread($stream, 1024 * 100); //read 100kb at a time
519
            $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
520
        }
521
522
        return $count > 1;
523
    }
524
525
    /**
526
     * Converts a string with operations in an array.
527
     */
528
    private static function parseOperations(string $operations): array
529
    {
530
        $valid_operations = ['resize', 'resizecrop', 'crop', 'format', 'quality'];
531
        $operations = explode('|', str_replace(' ', '', $operations));
532
        $return = [];
533
534
        foreach ($operations as $operations) {
535
            $params = explode(',', $operations);
536
            $function = strtolower(trim(array_shift($params)));
537
538
            if (!in_array($function, $valid_operations, true)) {
539
                throw new ImageException("The transform function '{$function}' is not valid");
540
            }
541
542
            $return[] = [
543
                'function' => $function,
544
                'params' => $params,
545
            ];
546
        }
547
548
        return $return;
549
    }
550
551
    /**
552
     * Checks the library to use and returns its class.
553
     *
554
     * @throws ImageException if the image library does not exists.
555
     */
556
    private static function getAdapterClass(string $adapter = null): string
557
    {
558
        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...
559
            return ImagickAdapter::checkCompatibility() ? ImagickAdapter::class : GdAdapter::class;
560
        }
561
562
        if (!class_exists($adapter)) {
563
            throw new ImageException(sprintf('The class %s does not exists', $adapter));
564
        }
565
566
        if (!$adapter::checkCompatibility()) {
567
            throw new ImageException(sprintf('The class %s cannot be used in this computer', $adapter));
568
        }
569
570
        return $adapter;
571
    }
572
573
    private static function getPercentage($value): ?float
574
    {
575
        if (is_string($value)) {
576
            if (substr($value, -1) === '%') {
577
                return floatval(substr($value, 0, -1));
578
            }
579
580
            throw new ImageException(sprintf('Invalid value: %s', $value));
581
        }
582
583
        return null;
584
    }
585
586
    /**
587
     * Calculates the x/y position.
588
     *
589
     * @param string|int $position
590
     * @param int        $cropWidth
591
     *
592
     * @return int
593
     */
594
    private function getPositionX($position, int $cropWidth): int
595
    {
596
        $width = $this->getWidth();
0 ignored issues
show
Unused Code introduced by
$width is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
597
598
        if ($percentage = self::getPercentage($position)) {
599
            $position = $this->getWidth($percentage) - ($cropWidth / 100 * $percentage);
0 ignored issues
show
Unused Code introduced by
$position is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
600
        }
601
602
        //Offset
603
        $offset = isset($split[2]) ? $split[1].$split[2] : 0;
0 ignored issues
show
Bug introduced by
The variable $split seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
604
605
        if (is_numeric($offset)) {
606
            $offset = (int) $offset;
607
        } else {
608
            $offset = static::getIntegerValue($direction, $offset, $oldValue, true);
0 ignored issues
show
Bug introduced by
The variable $direction does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $oldValue does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The method getIntegerValue() 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...
609
        }
610
611
        return $value + $offset;
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
612
613
    }
614
}
615