Completed
Push — 2.x-dev ( 7eb1ef...8335fa )
by Oscar
02:56
created

Image::calculateClientSize()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 6
Ratio 35.29 %

Importance

Changes 3
Bugs 1 Features 2
Metric Value
c 3
b 1
f 2
dl 6
loc 17
rs 8.8571
cc 6
eloc 9
nc 4
nop 2
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
        return new static($class::createFromFile($image), $image);
36
    }
37
38
    /**
39
     * Static function to create a new Imagecow instance from a binary string.
40
     *
41
     * @param string $string  The string of the image
42
     * @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.
43
     *
44
     * @return Image
45
     */
46
    public static function fromString($string, $library = null)
47
    {
48
        $class = self::getLibraryClass($library);
49
50
        return new static($class::createFromString($string));
51
    }
52
53
    /**
54
     * Constructor.
55
     *
56
     * @param Libs\LibInterface $image
57
     * @param string            $filename Original filename (used to overwrite)
58
     */
59
    public function __construct(Libs\LibInterface $image, $filename = null)
60
    {
61
        $this->image = $image;
62
        $this->filename = $filename;
63
64
        if ($this->isAnimatedGif()) {
65
            $this->image->setAnimated(true);
66
        }
67
    }
68
69
    /**
70
     * Set the available client hints
71
     * 
72
     * @param array $hints
0 ignored issues
show
Bug introduced by
There is no parameter named $hints. 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...
73
     * 
74
     * @return self
75
     */
76
    public function setClientHints(array $clientHints) {
77
        $normalize = [];
78
79
        foreach ($clientHints as $key => $value) {
80
            $normalize[strtolower($key)] = is_null($value) ? null : (float) $value;
81
        }
82
83
        if (array_diff_key($clientHints, $this->clientHints)) {
84
            throw new \InvalidArgumentException('Invalid client hints');
85
        }
86
87
        $this->clientHints = array_replace($this->clientHints, $clientHints);
88
    }
89
90
    /**
91
     * Set a default background color used to fill in some transformation functions.
92
     *
93
     * @param array $background The color in rgb, for example: array(0,127,34)
94
     *
95
     * @return self
96
     */
97
    public function setBackground(array $background)
98
    {
99
        $this->image->setBackground($background);
100
101
        return $this;
102
    }
103
104
    /**
105
     * Define the image compression quality for jpg images.
106
     *
107
     * @param int $quality The quality (from 0 to 100)
108
     *
109
     * @deprecated Use quality instead
110
     *
111
     * @return self
112
     */
113
    public function setCompressionQuality($quality)
114
    {
115
        error_log('The method `setCompressionQuality()` is deprecated. Use `quality()` instead.');
116
117
        return $this->quality($quality);
118
    }
119
120
    /**
121
     * Get the fixed size according with the client hints
122
     * 
123
     * @param int $width
124
     * @param int $height
125
     * 
126
     * @return array
127
     */
128
    private function calculateClientSize($width, $height)
129
    {
130 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...
131
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['width'], null);
132
        }
133
134 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...
135
            return Dimmensions::getResizeDimmensions($width, $height, $this->clientHints['viewport-width'], null);
136
        }
137
138
        if ($this->clientHints['dpr'] !== null) {
139
            $width *= $this->clientHints['dpr'];
140
            $height *= $this->clientHints['dpr'];
141
        }
142
143
        return [$width, $height];
144
    }
145
146
    /**
147
     * Inverts the image vertically.
148
     *
149
     * @return self
150
     */
151
    public function flip()
152
    {
153
        $this->image->flip();
154
155
        return $this;
156
    }
157
158
    /**
159
     * Inverts the image horizontally.
160
     *
161
     * @return self
162
     */
163
    public function flop()
164
    {
165
        $this->image->flop();
166
167
        return $this;
168
    }
169
170
    /**
171
     * Saves the image in a file.
172
     *
173
     * @param string $filename Name of the file where the image will be saved. If it's not defined, The original file will be overwritten.
174
     *
175
     * @return self
176
     */
177
    public function save($filename = null)
178
    {
179
        $this->image->save($filename ?: $this->filename);
180
181
        return $this;
182
    }
183
184
    /**
185
     * Gets the image data in a string.
186
     *
187
     * @return string The image data
188
     */
189
    public function getString()
190
    {
191
        return $this->image->getString();
192
    }
193
194
    /**
195
     * Gets the mime type of the image.
196
     *
197
     * @return string The mime type
198
     */
199
    public function getMimeType()
200
    {
201
        return $this->image->getMimeType();
202
    }
203
204
    /**
205
     * Gets the width of the image.
206
     *
207
     * @return int The width in pixels
208
     */
209
    public function getWidth()
210
    {
211
        return $this->image->getWidth();
212
    }
213
214
    /**
215
     * Gets the height of the image.
216
     *
217
     * @return int The height in pixels
218
     */
219
    public function getHeight()
220
    {
221
        return $this->image->getHeight();
222
    }
223
224
    /**
225
     * Converts the image to other format.
226
     *
227
     * @param string $format The new format: png, jpg, gif
228
     *
229
     * @return self
230
     */
231
    public function format($format)
232
    {
233
        $this->image->format($format);
234
235
        return $this;
236
    }
237
238
    /**
239
     * Resizes the image maintaining the proportion (A 800x600 image resized to 400x400 becomes to 400x300).
240
     *
241
     * @param int|string $width   The max width of the image. It can be a number (pixels) or percentaje
242
     * @param int|string $height  The max height of the image. It can be a number (pixels) or percentaje
243
     * @param bool       $cover
244
     *
245
     * @return self
246
     */
247
    public function resize($width, $height = 0, $cover = false)
248
    {
249
        $imageWidth = $this->getWidth();
250
        $imageHeight = $this->getHeight();
251
252
        $width = Dimmensions::getIntegerValue($width, $imageWidth);
253
        $height = Dimmensions::getIntegerValue($height, $imageHeight);
254
255
        list($width, $height) = Dimmensions::getResizeDimmensions($imageWidth, $imageHeight, $width, $height, $cover);
256
        list($width, $height) = $this->calculateClientSize($width, $height);
257
258
        if ($width >= $imageWidth) {
259
            return $this;
260
        }
261
262
        $this->image->resize($width, $height);
263
264
        return $this;
265
    }
266
267
    /**
268
     * Crops the image.
269
     *
270
     * @param int|string $width  The new width of the image. It can be a number (pixels) or percentaje
271
     * @param int|string $height The new height of the image. It can be a 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
     * @return self
276
     */
277
    public function crop($width, $height, $x = 'center', $y = 'middle')
278
    {
279
        $imageWidth = $this->getWidth();
280
        $imageHeight = $this->getHeight();
281
282
        $width = Dimmensions::getIntegerValue($width, $imageWidth);
283
        $height = Dimmensions::getIntegerValue($height, $imageHeight);
284
285
        list($width, $height) = $this->calculateClientSize($width, $height);
286
287
        switch ($x) {
288
            case self::CROP_BALANCED:
289
            case self::CROP_ENTROPY:
290
                list($x, $y) = $this->image->getCropOffsets($width, $height, $x);
291
                break;
292
        }
293
294
        $x = Dimmensions::getPositionValue($x, $width, $imageWidth);
295
        $y = Dimmensions::getPositionValue($y, $height, $imageHeight);
296
297
        $this->image->crop($width, $height, $x, $y);
298
299
        return $this;
300
    }
301
302
    /**
303
     * Adjust the image to the given dimmensions. Resizes and crops the image maintaining the proportions.
304
     *
305
     * @param int|string $width   The new width in number (pixels) or percentaje
306
     * @param int|string $height  The new height in number (pixels) or percentaje
307
     * @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
308
     * @param int|string $y       The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
309
     *
310
     * @return self
311
     */
312
    public function resizeCrop($width, $height, $x = 'center', $y = 'middle')
313
    {
314
        $this->resize($width, $height, true);
315
        $this->crop($width, $height, $x, $y);
316
317
        return $this;
318
    }
319
320
    /**
321
     * Rotates the image.
322
     *
323
     * @param int $angle Rotation angle in degrees (anticlockwise)
324
     *
325
     * @return self
326
     */
327
    public function rotate($angle)
328
    {
329
        if (($angle = intval($angle)) !== 0) {
330
            $this->image->rotate($angle);
331
        }
332
333
        return $this;
334
    }
335
336
    /**
337
     * Define the image compression quality for jpg images.
338
     *
339
     * @param int $quality The quality (from 0 to 100)
340
     *
341
     * @return self
342
     */
343
    public function quality($quality)
344
    {
345
        $quality = intval($quality);
346
347
        if ($quality < 0) {
348
            $quality = 0;
349
        } elseif ($quality > 100) {
350
            $quality = 100;
351
        }
352
353
        $this->image->setCompressionQuality($quality);
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
     * @param string $operations The string with all operations separated by "|".
383
     *
384
     * @return self
385
     */
386
    public function transform($operations = null)
387
    {
388
        //No transform operations, resize to fix the client size
389
        if (empty($operations)) {
390
            return $this->resize($this->getWidth(), $this->getHeight());
391
        }
392
393
        $operations = self::parseOperations($operations);
394
395
        foreach ($operations as $operation) {
396
            switch ($operation['function']) {
397
                case 'crop':
398
                case 'resizecrop':
399
                    if (isset($operation['params'][2])) {
400
                        switch ($operation['params'][2]) {
401
                            case 'CROP_ENTROPY':
402
                                $operation['params'][2] = self::CROP_ENTROPY;
403
                                break;
404
405
                            case 'CROP_BALANCED':
406
                                $operation['params'][2] = self::CROP_BALANCED;
407
                                break;
408
                        }
409
                    }
410
                    break;
411
            }
412
413
            call_user_func_array([$this, $operation['function']], $operation['params']);
414
        }
415
416
        return $this;
417
    }
418
419
    /**
420
     * Send the HTTP header with the content-type, output the image data and die.
421
     */
422 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...
423
    {
424
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
425
            header('Content-Type: '.$mimetype);
426
            die($string);
427
        }
428
    }
429
430
    /**
431
     * Returns the image as base64 url.
432
     *
433
     * @return string|null
434
     */
435 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...
436
    {
437
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
438
            $string = base64_encode($string);
439
440
            return "data:{$mimetype};base64,{$string}";
441
        }
442
    }
443
444
    /**
445
     * Auto-rotate the image according with its exif data
446
     * Taken from: http://php.net/manual/en/function.exif-read-data.php#76964.
447
     *
448
     * @return self
449
     */
450
    public function autoRotate()
451
    {
452
        switch ($this->getExifData('Orientation')) {
453
            case 2:
454
                $this->flop();
455
                break;
456
457
            case 3:
458
                $this->rotate(180);
459
                break;
460
461
            case 4:
462
                $this->flip();
463
                break;
464
465
            case 5:
466
                $this->flip()->rotate(-90);
467
                break;
468
469
            case 6:
470
                $this->rotate(90);
471
                break;
472
473
            case 7:
474
                $this->flop()->rotate(-90);
475
                break;
476
477
            case 8:
478
                $this->rotate(90);
479
                break;
480
        }
481
482
        return $this;
483
    }
484
485
    /**
486
     * Check whether the image is an animated gif.
487
     *
488
     * Copied from: https://github.com/Sybio/GifFrameExtractor/blob/master/src/GifFrameExtractor/GifFrameExtractor.php#L181
489
     *
490
     * @return bool
491
     */
492
    protected function isAnimatedGif()
493
    {
494
        if (($this->getMimeType() !== 'image/gif') || $this->filename !== null || !($fh = @fopen($this->filename, 'rb'))) {
495
            return false;
496
        }
497
498
        $count = 0;
499
500
        while (!feof($fh) && $count < 2) {
501
            $chunk = fread($fh, 1024 * 100); //read 100kb at a time
502
            $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
503
        }
504
505
        fclose($fh);
506
507
        return $count > 1;
508
    }
509
510
    /**
511
     * Converts a string with operations in an array.
512
     *
513
     * @param string $operations The operations string
514
     *
515
     * @return array
516
     */
517
    private static function parseOperations($operations)
518
    {
519
        $valid_operations = ['resize', 'resizecrop', 'crop', 'format', 'quality'];
520
        $operations = explode('|', str_replace(' ', '', $operations));
521
        $return = [];
522
523
        foreach ($operations as $operations) {
524
            $params = explode(',', $operations);
525
            $function = strtolower(trim(array_shift($params)));
526
527
            if (!in_array($function, $valid_operations, true)) {
528
                throw new ImageException("The transform function '{$function}' is not valid");
529
            }
530
531
            $return[] = [
532
                'function' => $function,
533
                'params' => $params,
534
            ];
535
        }
536
537
        return $return;
538
    }
539
540
    /**
541
     * Checks the library to use and returns its class.
542
     *
543
     * @param string $library The library name (Gd, Imagick)
544
     *
545
     * @throws ImageException if the image library does not exists.
546
     *
547
     * @return string
548
     */
549
    private static function getLibraryClass($library)
550
    {
551
        if (!$library) {
552
            $library = Libs\Imagick::checkCompatibility() ? self::LIB_IMAGICK : self::LIB_GD;
553
        }
554
555
        $class = 'Imagecow\\Libs\\'.$library;
556
557
        if (!class_exists($class)) {
558
            throw new ImageException('The image library is not valid');
559
        }
560
561
        if (!$class::checkCompatibility()) {
562
            throw new ImageException("The image library '$library' is not installed in this computer");
563
        }
564
565
        return $class;
566
    }
567
}
568