Completed
Push — master ( 80eb51...d7d63e )
by Oscar
7s
created

Image::watermark()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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