Completed
Push — master ( d7d63e...f91d06 )
by Oscar
02:08
created

Image::watermark()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 2
Metric Value
c 4
b 0
f 2
dl 0
loc 22
rs 9.2
cc 2
eloc 13
nc 2
nop 4
1
<?php
2
3
namespace Imagecow;
4
5
use Exception;
6
use Imagecow\Utils\Dimmensions;
7
8
class Image
9
{
10
    const LIB_GD = 'Gd';
11
    const LIB_IMAGICK = 'Imagick';
12
13
    const CROP_ENTROPY = 'Entropy';
14
    const CROP_BALANCED = 'Balanced';
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
     * Gets the image data in a string.
217
     *
218
     * @return string The image data
219
     */
220
    public function getString()
221
    {
222
        return $this->image->getString();
223
    }
224
225
    /**
226
     * Gets the mime type of the image.
227
     *
228
     * @return string The mime type
229
     */
230
    public function getMimeType()
231
    {
232
        return $this->image->getMimeType();
233
    }
234
235
    /**
236
     * Gets the width of the image.
237
     *
238
     * @return int The width in pixels
239
     */
240
    public function getWidth()
241
    {
242
        return $this->image->getWidth();
243
    }
244
245
    /**
246
     * Gets the height of the image.
247
     *
248
     * @return int The height in pixels
249
     */
250
    public function getHeight()
251
    {
252
        return $this->image->getHeight();
253
    }
254
255
    /**
256
     * Converts the image to other format.
257
     *
258
     * @param string $format The new format: png, jpg, gif
259
     *
260
     * @return self
261
     */
262
    public function format($format)
263
    {
264
        $this->image->format($format);
265
266
        return $this;
267
    }
268
269
    /**
270
     * Resizes the image maintaining the proportion (A 800x600 image resized to 400x400 becomes to 400x300).
271
     *
272
     * @param int|string $width  The max width of the image. It can be a number (pixels) or percentaje
273
     * @param int|string $height The max height of the image. It can be a number (pixels) or percentaje
274
     * @param bool       $cover
275
     *
276
     * @return self
277
     */
278
    public function resize($width, $height = 0, $cover = false)
279
    {
280
        $imageWidth = $this->getWidth();
281
        $imageHeight = $this->getHeight();
282
283
        $width = Dimmensions::getIntegerValue('x', $width, $imageWidth);
284
        $height = Dimmensions::getIntegerValue('y', $height, $imageHeight);
285
286
        list($width, $height) = Dimmensions::getResizeDimmensions($imageWidth, $imageHeight, $width, $height, $cover);
287
        list($width, $height) = $this->calculateClientSize($width, $height);
288
289
        if ($width >= $imageWidth) {
290
            return $this;
291
        }
292
293
        $this->image->resize($width, $height);
294
295
        return $this;
296
    }
297
298
    /**
299
     * Crops the image.
300
     *
301
     * @param int|string $width  The new width of the image. It can be a number (pixels) or percentaje
302
     * @param int|string $height The new height of the image. It can be a number (pixels) or percentaje
303
     * @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
304
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
305
     *
306
     * @return self
307
     */
308
    public function crop($width, $height, $x = 'center', $y = 'middle')
309
    {
310
        $imageWidth = $this->getWidth();
311
        $imageHeight = $this->getHeight();
312
313
        $width = Dimmensions::getIntegerValue('x', $width, $imageWidth);
314
        $height = Dimmensions::getIntegerValue('y', $height, $imageHeight);
315
316
        list($width, $height) = $this->calculateClientSize($width, $height);
317
318
        if (($x === self::CROP_BALANCED) || ($x === self::CROP_ENTROPY)) {
319
            list($x, $y) = $this->image->getCropOffsets($width, $height, $x);
320
        }
321
322
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
323
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
324
325
        $this->image->crop($width, $height, $x, $y);
326
327
        return $this;
328
    }
329
330
    /**
331
     * Adjust the image to the given dimmensions. Resizes and crops the image maintaining the proportions.
332
     *
333
     * @param int|string $width  The new width in number (pixels) or percentaje
334
     * @param int|string $height The new height in number (pixels) or percentaje
335
     * @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
336
     * @param int|string $y      The "y" position to crop. It can be number (pixels), percentaje or [top, middle, bottom]
337
     *
338
     * @return self
339
     */
340
    public function resizeCrop($width, $height, $x = 'center', $y = 'middle')
341
    {
342
        $this->resize($width, $height, true);
343
        $this->crop($width, $height, $x, $y);
344
345
        return $this;
346
    }
347
348
    /**
349
     * Rotates the image.
350
     *
351
     * @param int $angle Rotation angle in degrees (anticlockwise)
352
     *
353
     * @return self
354
     */
355
    public function rotate($angle)
356
    {
357
        if (($angle = intval($angle)) !== 0) {
358
            $this->image->rotate($angle);
359
        }
360
361
        return $this;
362
    }
363
364
    /**
365
     * Define the image compression quality for jpg images.
366
     *
367
     * @param int $quality The quality (from 0 to 100)
368
     *
369
     * @return self
370
     */
371
    public function quality($quality)
372
    {
373
        $quality = intval($quality);
374
375
        if ($quality < 0) {
376
            $quality = 0;
377
        } elseif ($quality > 100) {
378
            $quality = 100;
379
        }
380
381
        $this->image->setCompressionQuality($quality);
382
383
        return $this;
384
    }
385
386
    /**
387
     * Add a watermark to current image
388
     *
389
     * @param string $file    Image to set as watermark
390
     * @param mixed  $x       Horizontal position
391
     * @param mixed  $y       Vertical position
392
     * @param int    $opacity Opacity value
393
     *
394
     * @return self
395
     */
396
    public function watermark($file, $x = 'right', $y = 'bottom', $opacity = 100)
397
    {
398
        $class = get_class($this->image);
399
        $watermark = $class::createFromFile($file);
400
401
        if ($opacity < 100) {
402
            $watermark->opacity((int) $opacity);
403
        }
404
405
        $imageWidth = $this->getWidth();
406
        $imageHeight = $this->getHeight();
407
408
        $width = $watermark->getWidth();
409
        $height = $watermark->getHeight();
410
411
        $x = Dimmensions::getPositionValue('x', $x, $width, $imageWidth);
412
        $y = Dimmensions::getPositionValue('y', $y, $height, $imageHeight);
413
414
        $this->image->watermark($watermark, $x, $y);
415
416
        return $this;
417
    }
418
419
    /**
420
     * Add opacity to image from 0 (transparent) to 100 (opaque)
421
     *
422
     * @param int $opacity Opacity value
423
     * 
424
     * @return self
425
     */
426
    public function opacity($opacity)
427
    {
428
        $this->image->opacity($opacity);
429
430
        return $this;
431
    }
432
433
    /**
434
     * Reads the EXIF data from a JPEG and returns an associative array
435
     * (requires the exif PHP extension enabled).
436
     *
437
     * @param null|string $key
438
     *
439
     * @return null|array
440
     */
441
    public function getExifData($key = null)
442
    {
443
        if ($this->filename !== null && ($this->getMimeType() === 'image/jpeg')) {
444
            $exif = exif_read_data($this->filename);
445
446
            if ($key !== null) {
447
                return isset($exif[$key]) ? $exif[$key] : null;
448
            }
449
450
            return $exif;
451
        }
452
    }
453
454
    /**
455
     * Transform the image executing various operations of crop, resize, resizeCrop and format.
456
     *
457
     * @param string $operations The string with all operations separated by "|".
458
     *
459
     * @return self
460
     */
461
    public function transform($operations = null)
462
    {
463
        //No transform operations, resize to fix the client size
464
        if (empty($operations)) {
465
            return $this->resize($this->getWidth(), $this->getHeight());
466
        }
467
468
        $operations = self::parseOperations($operations);
469
470
        foreach ($operations as $operation) {
471
            switch ($operation['function']) {
472
                case 'crop':
473
                case 'resizecrop':
474
                    if (empty($operation['params'][2])) {
475
                        break;
476
                    }
477
478
                    switch ($operation['params'][2]) {
479
                        case 'CROP_ENTROPY':
480
                            $operation['params'][2] = self::CROP_ENTROPY;
481
                            break;
482
483
                        case 'CROP_BALANCED':
484
                            $operation['params'][2] = self::CROP_BALANCED;
485
                            break;
486
                    }
487
488
                    break;
489
            }
490
491
            call_user_func_array([$this, $operation['function']], $operation['params']);
492
        }
493
494
        return $this;
495
    }
496
497
    /**
498
     * Send the HTTP header with the content-type, output the image data and die.
499
     */
500 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...
501
    {
502
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
503
            header('Content-Type: '.$mimetype);
504
            die($string);
505
        }
506
    }
507
508
    /**
509
     * Returns the image as base64 url.
510
     *
511
     * @return string|null
512
     */
513 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...
514
    {
515
        if (($string = $this->getString()) && ($mimetype = $this->getMimeType())) {
516
            $string = base64_encode($string);
517
518
            return "data:{$mimetype};base64,{$string}";
519
        }
520
    }
521
522
    /**
523
     * Auto-rotate the image according with its exif data
524
     * Taken from: http://php.net/manual/en/function.exif-read-data.php#76964.
525
     *
526
     * @return self
527
     */
528
    public function autoRotate()
529
    {
530
        switch ($this->getExifData('Orientation')) {
531
            case 2:
532
                $this->flop();
533
                break;
534
535
            case 3:
536
                $this->rotate(180);
537
                break;
538
539
            case 4:
540
                $this->flip();
541
                break;
542
543
            case 5:
544
                $this->flip()->rotate(-90);
545
                break;
546
547
            case 6:
548
                $this->rotate(90);
549
                break;
550
551
            case 7:
552
                $this->flop()->rotate(-90);
553
                break;
554
555
            case 8:
556
                $this->rotate(90);
557
                break;
558
        }
559
560
        return $this;
561
    }
562
563
    /**
564
     * Check whether the image is an animated gif.
565
     * Copied from: https://github.com/Sybio/GifFrameExtractor/blob/master/src/GifFrameExtractor/GifFrameExtractor.php#L181.
566
     *
567
     * @param resource A stream pointer opened by fopen()
568
     *
569
     * @return bool
570
     */
571
    private static function isAnimatedGif($stream)
572
    {
573
        $count = 0;
574
575
        while (!feof($stream) && $count < 2) {
576
            $chunk = fread($stream, 1024 * 100); //read 100kb at a time
577
            $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
578
        }
579
580
        return $count > 1;
581
    }
582
583
    /**
584
     * Converts a string with operations in an array.
585
     *
586
     * @param string $operations The operations string
587
     *
588
     * @return array
589
     */
590
    private static function parseOperations($operations)
591
    {
592
        $valid_operations = ['resize', 'resizecrop', 'crop', 'format', 'quality'];
593
        $operations = explode('|', str_replace(' ', '', $operations));
594
        $return = [];
595
596
        foreach ($operations as $operations) {
597
            $params = explode(',', $operations);
598
            $function = strtolower(trim(array_shift($params)));
599
600
            if (!in_array($function, $valid_operations, true)) {
601
                throw new ImageException("The transform function '{$function}' is not valid");
602
            }
603
604
            $return[] = [
605
                'function' => $function,
606
                'params' => $params,
607
            ];
608
        }
609
610
        return $return;
611
    }
612
613
    /**
614
     * Checks the library to use and returns its class.
615
     *
616
     * @param string $library The library name (Gd, Imagick)
617
     *
618
     * @throws ImageException if the image library does not exists.
619
     *
620
     * @return string
621
     */
622
    private static function getLibraryClass($library)
623
    {
624
        if (!$library) {
625
            $library = Libs\Imagick::checkCompatibility() ? self::LIB_IMAGICK : self::LIB_GD;
626
        }
627
628
        $class = 'Imagecow\\Libs\\'.$library;
629
630
        if (!class_exists($class)) {
631
            throw new ImageException('The image library is not valid');
632
        }
633
634
        if (!$class::checkCompatibility()) {
635
            throw new ImageException("The image library '$library' is not installed in this computer");
636
        }
637
638
        return $class;
639
    }
640
}
641