Completed
Push — master ( 923837...a77936 )
by Nazar
17:40 queued 13:31
created

SimpleImage::keep_within()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 3
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package		SimpleImage class
4
 * @version		2.5.3
5
 * @author		Cory LaViska for A Beautiful Site, LLC. (http://www.abeautifulsite.net/)
6
 * @author		Nazar Mokrynskyi <[email protected]> - merging of forks, namespace support, PhpDoc editing, adaptive_resize() method, other fixes
7
 * @license		This software is licensed under the MIT license: http://opensource.org/licenses/MIT
8
 * @copyright	A Beautiful Site, LLC.
9
 *
10
 */
11
12
namespace abeautifulsite;
13
use Exception;
14
15
/**
16
 * Class SimpleImage
17
 * This class makes image manipulation in PHP as simple as possible.
18
 * @package SimpleImage
19
 *
20
 */
21
class SimpleImage {
22
23
	/**
24
	 * @var int Default output image quality
25
	 *
26
	 */
27
	public $quality = 80;
28
29
	protected $image, $filename, $original_info, $width, $height, $imagestring;
30
31
	/**
32
	 * Create instance and load an image, or create an image from scratch
33
	 *
34
	 * @param null|string	$filename	Path to image file (may be omitted to create image from scratch)
35
	 * @param int			$width		Image width (is used for creating image from scratch)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $width not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
36
	 * @param int|null		$height		If omitted - assumed equal to $width (is used for creating image from scratch)
37
	 * @param null|string	$color		Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
38
	 * 									Where red, green, blue - integers 0-255, alpha - integer 0-127<br>
39
	 * 									(is used for creating image from scratch)
40
	 *
41
	 * @return SimpleImage
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
42
	 * @throws Exception
43
	 *
44
	 */
45
	function __construct($filename = null, $width = null, $height = null, $color = null) {
46
		if ($filename) {
47
			$this->load($filename);
48
		} elseif ($width) {
49
			$this->create($width, $height, $color);
50
		}
51
		return $this;
52
	}
53
54
	/**
55
	 * Destroy image resource
56
	 *
57
	 */
58
	function __destruct() {
59
		if ($this->image) {
60
			imagedestroy($this->image);
61
		}
62
	}
63
64
	/**
65
	 * Adaptive resize
66
	 *
67
	 * This function has been deprecated and will be removed in an upcoming release. Please
68
	 * update your code to use the `thumbnail()` method instead. The arguments for both
69
	 * methods are exactly the same.
70
	 *
71
	 * @param int			$width
72
	 * @param int|null		$height	If omitted - assumed equal to $width
73
	 *
74
	 * @return SimpleImage
75
	 *
76
	 */
77
	function adaptive_resize($width, $height = null) {
78
79
		return $this->thumbnail($width, $height);
80
81
	}
82
83
	/**
84
	 * Rotates and/or flips an image automatically so the orientation will be correct (based on exif 'Orientation')
85
	 *
86
	 * @return SimpleImage
87
	 *
88
	 */
89
	function auto_orient() {
90
91
		switch ($this->original_info['exif']['Orientation']) {
92
			case 1:
93
				// Do nothing
94
				break;
95
			case 2:
96
				// Flip horizontal
97
				$this->flip('x');
98
				break;
99
			case 3:
100
				// Rotate 180 counterclockwise
101
				$this->rotate(-180);
102
				break;
103
			case 4:
104
				// vertical flip
105
				$this->flip('y');
106
				break;
107
			case 5:
108
				// Rotate 90 clockwise and flip vertically
109
				$this->flip('y');
110
				$this->rotate(90);
111
				break;
112
			case 6:
113
				// Rotate 90 clockwise
114
				$this->rotate(90);
115
				break;
116
			case 7:
117
				// Rotate 90 clockwise and flip horizontally
118
				$this->flip('x');
119
				$this->rotate(90);
120
				break;
121
			case 8:
122
				// Rotate 90 counterclockwise
123
				$this->rotate(-90);
124
				break;
125
		}
126
127
		return $this;
128
129
	}
130
131
	/**
132
	 * Best fit (proportionally resize to fit in specified width/height)
133
	 *
134
	 * Shrink the image proportionally to fit inside a $width x $height box
135
	 *
136
	 * @param int			$max_width
137
	 * @param int			$max_height
138
	 *
139
	 * @return	SimpleImage
140
	 *
141
	 */
142
	function best_fit($max_width, $max_height) {
143
144
		// If it already fits, there's nothing to do
145
		if ($this->width <= $max_width && $this->height <= $max_height) {
146
			return $this;
147
		}
148
149
		// Determine aspect ratio
150
		$aspect_ratio = $this->height / $this->width;
151
152
		// Make width fit into new dimensions
153
		if ($this->width > $max_width) {
154
			$width = $max_width;
155
			$height = $width * $aspect_ratio;
156
		} else {
157
			$width = $this->width;
158
			$height = $this->height;
159
		}
160
161
		// Make height fit into new dimensions
162
		if ($height > $max_height) {
163
			$height = $max_height;
164
			$width = $height / $aspect_ratio;
165
		}
166
167
		return $this->resize($width, $height);
168
169
	}
170
171
	/**
172
	 * Blur
173
	 *
174
	 * @param string		$type	selective|gaussian
175
	 * @param int			$passes	Number of times to apply the filter
176
	 *
177
	 * @return SimpleImage
178
	 *
179
	 */
180
	function blur($type = 'selective', $passes = 1) {
181
		switch (strtolower($type)) {
182
			case 'gaussian':
183
				$type = IMG_FILTER_GAUSSIAN_BLUR;
184
				break;
185
			default:
186
				$type = IMG_FILTER_SELECTIVE_BLUR;
187
				break;
188
		}
189
		for ($i = 0; $i < $passes; $i++) {
190
			imagefilter($this->image, $type);
191
		}
192
		return $this;
193
	}
194
195
	/**
196
	 * Brightness
197
	 *
198
	 * @param int			$level	Darkest = -255, lightest = 255
199
	 *
200
	 * @return SimpleImage
201
	 *
202
	 */
203
	function brightness($level) {
204
		imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $this->keep_within($level, -255, 255));
205
		return $this;
206
	}
207
208
	/**
209
	 * Contrast
210
	 *
211
	 * @param int			$level	Min = -100, max = 100
212
	 *
213
	 * @return SimpleImage
214
	 *
215
	 *
216
	 */
217
	function contrast($level) {
218
		imagefilter($this->image, IMG_FILTER_CONTRAST, $this->keep_within($level, -100, 100));
219
		return $this;
220
	}
221
222
	/**
223
	 * Colorize
224
	 *
225
	 * @param string		$color		Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
226
	 * 									Where red, green, blue - integers 0-255, alpha - integer 0-127
227
	 * @param float|int		$opacity	0-1
228
	 *
229
	 * @return SimpleImage
230
	 *
231
	 */
232
	function colorize($color, $opacity) {
233
		$rgba = $this->normalize_color($color);
234
		$alpha = $this->keep_within(127 - (127 * $opacity), 0, 127);
235
		imagefilter($this->image, IMG_FILTER_COLORIZE, $this->keep_within($rgba['r'], 0, 255), $this->keep_within($rgba['g'], 0, 255), $this->keep_within($rgba['b'], 0, 255), $alpha);
236
		return $this;
237
	}
238
239
	/**
240
	 * Create an image from scratch
241
	 *
242
	 * @param int			$width	Image width
243
	 * @param int|null		$height	If omitted - assumed equal to $width
244
	 * @param null|string	$color	Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
245
	 * 								Where red, green, blue - integers 0-255, alpha - integer 0-127
246
	 *
247
	 * @return SimpleImage
248
	 *
249
	 */
250
	function create($width, $height = null, $color = null) {
251
252
		$height = $height ?: $width;
253
		$this->width = $width;
254
		$this->height = $height;
255
		$this->image = imagecreatetruecolor($width, $height);
256
		$this->original_info = array(
257
			'width' => $width,
258
			'height' => $height,
259
			'orientation' => $this->get_orientation(),
260
			'exif' => null,
261
			'format' => 'png',
262
			'mime' => 'image/png'
263
		);
264
265
		if ($color) {
266
			$this->fill($color);
267
		}
268
269
		return $this;
270
271
	}
272
273
	/**
274
	 * Crop an image
275
	 *
276
	 * @param int			$x1	Left
277
	 * @param int			$y1	Top
278
	 * @param int			$x2	Right
279
	 * @param int			$y2	Bottom
280
	 *
281
	 * @return SimpleImage
282
	 *
283
	 */
284
	function crop($x1, $y1, $x2, $y2) {
285
286
		// Determine crop size
287
		if ($x2 < $x1) {
288
			list($x1, $x2) = array($x2, $x1);
289
		}
290
		if ($y2 < $y1) {
291
			list($y1, $y2) = array($y2, $y1);
292
		}
293
		$crop_width = $x2 - $x1;
294
		$crop_height = $y2 - $y1;
295
296
		// Perform crop
297
		$new = imagecreatetruecolor($crop_width, $crop_height);
298
		imagealphablending($new, false);
299
		imagesavealpha($new, true);
300
		imagecopyresampled($new, $this->image, 0, 0, $x1, $y1, $crop_width, $crop_height, $crop_width, $crop_height);
301
302
		// Update meta data
303
		$this->width = $crop_width;
304
		$this->height = $crop_height;
305
		$this->image = $new;
306
307
		return $this;
308
309
	}
310
311
	/**
312
	 * Desaturate (grayscale)
313
	 *
314
	 * @return SimpleImage
315
	 *
316
	 */
317
	function desaturate() {
318
		imagefilter($this->image, IMG_FILTER_GRAYSCALE);
319
		return $this;
320
	}
321
322
	/**
323
	 * Edge Detect
324
	 *
325
	 * @return SimpleImage
326
	 *
327
	 */
328
	function edges() {
329
		imagefilter($this->image, IMG_FILTER_EDGEDETECT);
330
		return $this;
331
	}
332
333
	/**
334
	 * Emboss
335
	 *
336
	 * @return SimpleImage
337
	 *
338
	 */
339
	function emboss() {
340
		imagefilter($this->image, IMG_FILTER_EMBOSS);
341
		return $this;
342
	}
343
344
	/**
345
	 * Fill image with color
346
	 *
347
	 * @param string		$color	Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
348
	 * 								Where red, green, blue - integers 0-255, alpha - integer 0-127
349
	 *
350
	 * @return SimpleImage
351
	 *
352
	 */
353
	function fill($color = '#000000') {
354
355
		$rgba = $this->normalize_color($color);
356
		$fill_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']);
357
		imagealphablending($this->image, false);
358
		imagesavealpha($this->image, true);
359
		imagefilledrectangle($this->image, 0, 0, $this->width, $this->height, $fill_color);
360
361
		return $this;
362
363
	}
364
365
	/**
366
	 * Fit to height (proportionally resize to specified height)
367
	 *
368
	 * @param int			$height
369
	 *
370
	 * @return SimpleImage
371
	 *
372
	 */
373
	function fit_to_height($height) {
374
375
		$aspect_ratio = $this->height / $this->width;
376
		$width = $height / $aspect_ratio;
377
378
		return $this->resize($width, $height);
379
380
	}
381
382
	/**
383
	 * Fit to width (proportionally resize to specified width)
384
	 *
385
	 * @param int			$width
386
	 *
387
	 * @return SimpleImage
388
	 *
389
	 */
390
	function fit_to_width($width) {
391
392
		$aspect_ratio = $this->height / $this->width;
393
		$height = $width * $aspect_ratio;
394
395
		return $this->resize($width, $height);
396
397
	}
398
399
	/**
400
	 * Flip an image horizontally or vertically
401
	 *
402
	 * @param string		$direction	x|y
403
	 *
404
	 * @return SimpleImage
405
	 *
406
	 */
407
	function flip($direction) {
408
409
		$new = imagecreatetruecolor($this->width, $this->height);
410
		imagealphablending($new, false);
411
		imagesavealpha($new, true);
412
413
		switch (strtolower($direction)) {
414
			case 'y':
415
				for ($y = 0; $y < $this->height; $y++) {
416
					imagecopy($new, $this->image, 0, $y, 0, $this->height - $y - 1, $this->width, 1);
417
				}
418
				break;
419
			default:
420
				for ($x = 0; $x < $this->width; $x++) {
421
					imagecopy($new, $this->image, $x, 0, $this->width - $x - 1, 0, 1, $this->height);
422
				}
423
				break;
424
		}
425
426
		$this->image = $new;
427
428
		return $this;
429
430
	}
431
432
	/**
433
	 * Get the current height
434
	 *
435
	 * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
436
	 *
437
	 */
438
	function get_height() {
439
		return $this->height;
440
	}
441
442
	/**
443
	 * Get the current orientation
444
	 *
445
	 * @return string	portrait|landscape|square
446
	 *
447
	 */
448
	function get_orientation() {
449
450
		if (imagesx($this->image) > imagesy($this->image)) {
451
			return 'landscape';
452
		}
453
454
		if (imagesx($this->image) < imagesy($this->image)) {
455
			return 'portrait';
456
		}
457
458
		return 'square';
459
460
	}
461
462
	/**
463
	 * Get info about the original image
464
	 *
465
	 * @return array <pre> array(
466
	 * 	width        => 320,
467
	 * 	height       => 200,
468
	 * 	orientation  => ['portrait', 'landscape', 'square'],
469
	 * 	exif         => array(...),
470
	 * 	mime         => ['image/jpeg', 'image/gif', 'image/png'],
471
	 * 	format       => ['jpeg', 'gif', 'png']
472
	 * )</pre>
473
	 *
474
	 */
475
	function get_original_info() {
476
		return $this->original_info;
477
	}
478
479
	/**
480
	 * Get the current width
481
	 *
482
	 * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
483
	 *
484
	 */
485
	function get_width() {
486
		return $this->width;
487
	}
488
489
	/**
490
	 * Invert
491
	 *
492
	 * @return SimpleImage
493
	 *
494
	 */
495
	function invert() {
496
		imagefilter($this->image, IMG_FILTER_NEGATE);
497
		return $this;
498
	}
499
500
	/**
501
	 * Load an image
502
	 *
503
	 * @param string		$filename	Path to image file
504
	 *
505
	 * @return SimpleImage
506
	 * @throws Exception
507
	 *
508
	 */
509
	function load($filename) {
510
511
		// Require GD library
512
		if (!extension_loaded('gd')) {
513
			throw new Exception('Required extension GD is not loaded.');
514
		}
515
		$this->filename = $filename;
516
		return $this->get_meta_data();
517
	}
518
519
	/**
520
	 * Load a base64 string as image
521
	 *
522
	 * @param string		$filename	base64 string
0 ignored issues
show
Bug introduced by
There is no parameter named $filename. 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...
523
	 *
524
	 * @return SimpleImage
525
	 *
526
	 */
527
	function load_base64($base64string) {
528
		if (!extension_loaded('gd')) {
529
			throw new Exception('Required extension GD is not loaded.');
530
		}
531
		//remove data URI scheme and spaces from base64 string then decode it
532
		$this->imagestring = base64_decode(str_replace(' ', '+',preg_replace('#^data:image/[^;]+;base64,#', '', $base64string)));
533
		$this->image = imagecreatefromstring($this->imagestring);
534
		return $this->get_meta_data();
535
	}
536
537
	/**
538
	 * Mean Remove
539
	 *
540
	 * @return SimpleImage
541
	 *
542
	 */
543
	function mean_remove() {
544
		imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
545
		return $this;
546
	}
547
548
	/**
549
	 * Changes the opacity level of the image
550
	 *
551
	 * @param float|int		$opacity	0-1
552
	 *
553
	 * @throws Exception
554
	 *
555
	 */
556
	function opacity($opacity) {
557
558
		// Determine opacity
559
		$opacity = $this->keep_within($opacity, 0, 1) * 100;
560
561
		// Make a copy of the image
562
		$copy = imagecreatetruecolor($this->width, $this->height);
563
		imagealphablending($copy, false);
564
		imagesavealpha($copy, true);
565
		imagecopy($copy, $this->image, 0, 0, 0, 0, $this->width, $this->height);
566
567
		// Create transparent layer
568
		$this->create($this->width, $this->height, array(0, 0, 0, 127));
0 ignored issues
show
Documentation introduced by
array(0, 0, 0, 127) is of type array<integer,integer,{"...nteger","3":"integer"}>, but the function expects a null|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
569
570
		// Merge with specified opacity
571
		$this->imagecopymerge_alpha($this->image, $copy, 0, 0, 0, 0, $this->width, $this->height, $opacity);
572
		imagedestroy($copy);
573
574
		return $this;
575
576
	}
577
578
	/**
579
	 * Outputs image without saving
580
	 *
581
	 * @param null|string	$format		If omitted or null - format of original file will be used, may be gif|jpg|png
582
	 * @param int|null		$quality	Output image quality in percents 0-100
583
	 *
584
	 * @throws Exception
585
	 *
586
	 */
587
	function output($format = null, $quality = null) {
588
589
		// Determine quality
590
		$quality = $quality ?: $this->quality;
591
592
		// Determine mimetype
593
		switch (strtolower($format)) {
594
			case 'gif':
595
				$mimetype = 'image/gif';
596
				break;
597
			case 'jpeg':
598
			case 'jpg':
599
				imageinterlace($this->image, true);
600
				$mimetype = 'image/jpeg';
601
				break;
602
			case 'png':
603
				$mimetype = 'image/png';
604
				break;
605
			default:
606
				$info = (empty($this->imagestring)) ? getimagesize($this->filename) : getimagesizefromstring($this->imagestring);
607
				$mimetype = $info['mime'];
608
				unset($info);
609
				break;
610
		}
611
612
		// Output the image
613
		\cs\Response::instance()->header('content-type', $mimetype);
614
		switch ($mimetype) {
615
			case 'image/gif':
616
				imagegif($this->image);
617
				break;
618
			case 'image/jpeg':
619
				imagejpeg($this->image, null, round($quality));
620
				break;
621
			case 'image/png':
622
				imagepng($this->image, null, round(9 * $quality / 100));
623
				break;
624
			default:
625
				throw new Exception('Unsupported image format: '.$this->filename);
626
				break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
627
		}
628
629
		// Since no more output can be sent, call the destructor to free up memory
630
		$this->__destruct();
631
632
	}
633
634
	/**
635
	 * Outputs image as data base64 to use as img src
636
	 *
637
	 * @param null|string	$format		If omitted or null - format of original file will be used, may be gif|jpg|png
638
	 * @param int|null		$quality	Output image quality in percents 0-100
639
	 *
640
	 * @return string
641
	 * @throws Exception
642
	 *
643
	 */
644
	function output_base64($format = null, $quality = null) {
645
646
		// Determine quality
647
		$quality = $quality ?: $this->quality;
648
649
		// Determine mimetype
650
		switch (strtolower($format)) {
651
			case 'gif':
652
				$mimetype = 'image/gif';
653
				break;
654
			case 'jpeg':
655
			case 'jpg':
656
				imageinterlace($this->image, true);
657
				$mimetype = 'image/jpeg';
658
				break;
659
			case 'png':
660
				$mimetype = 'image/png';
661
				break;
662
			default:
663
				$info = getimagesize($this->filename);
664
				$mimetype = $info['mime'];
665
				unset($info);
666
				break;
667
		}
668
669
		// Output the image
670
		ob_start();
671
		switch ($mimetype) {
672
			case 'image/gif':
673
				imagegif($this->image);
674
				break;
675
			case 'image/jpeg':
676
				imagejpeg($this->image, null, round($quality));
677
				break;
678
			case 'image/png':
679
				imagepng($this->image, null, round(9 * $quality / 100));
680
				break;
681
			default:
682
				throw new Exception('Unsupported image format: '.$this->filename);
683
				break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
684
		}
685
		$image_data = ob_get_contents();
686
		ob_end_clean();
687
688
		// Returns formatted string for img src
689
		return 'data:'.$mimetype.';base64,'.base64_encode($image_data);
690
691
	}
692
693
	/**
694
	 * Overlay
695
	 *
696
	 * Overlay an image on top of another, works with 24-bit PNG alpha-transparency
697
	 *
698
	 * @param string		$overlay		An image filename or a SimpleImage object
699
	 * @param string		$position		center|top|left|bottom|right|top left|top right|bottom left|bottom right
700
	 * @param float|int		$opacity		Overlay opacity 0-1
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $opacity a bit more specific; maybe use integer.
Loading history...
701
	 * @param int			$x_offset		Horizontal offset in pixels
702
	 * @param int			$y_offset		Vertical offset in pixels
703
	 *
704
	 * @return SimpleImage
705
	 *
706
	 */
707
	function overlay($overlay, $position = 'center', $opacity = 1, $x_offset = 0, $y_offset = 0) {
708
709
		// Load overlay image
710
		if( !($overlay instanceof SimpleImage) ) {
711
			$overlay = new SimpleImage($overlay);
712
		}
713
714
		// Convert opacity
715
		$opacity = $opacity * 100;
716
717
		// Determine position
718
		switch (strtolower($position)) {
719
			case 'top left':
720
				$x = 0 + $x_offset;
721
				$y = 0 + $y_offset;
722
				break;
723
			case 'top right':
724
				$x = $this->width - $overlay->width + $x_offset;
725
				$y = 0 + $y_offset;
726
				break;
727
			case 'top':
728
				$x = ($this->width / 2) - ($overlay->width / 2) + $x_offset;
729
				$y = 0 + $y_offset;
730
				break;
731
			case 'bottom left':
732
				$x = 0 + $x_offset;
733
				$y = $this->height - $overlay->height + $y_offset;
734
				break;
735
			case 'bottom right':
736
				$x = $this->width - $overlay->width + $x_offset;
737
				$y = $this->height - $overlay->height + $y_offset;
738
				break;
739
			case 'bottom':
740
				$x = ($this->width / 2) - ($overlay->width / 2) + $x_offset;
741
				$y = $this->height - $overlay->height + $y_offset;
742
				break;
743
			case 'left':
744
				$x = 0 + $x_offset;
745
				$y = ($this->height / 2) - ($overlay->height / 2) + $y_offset;
746
				break;
747
			case 'right':
748
				$x = $this->width - $overlay->width + $x_offset;
749
				$y = ($this->height / 2) - ($overlay->height / 2) + $y_offset;
750
				break;
751
			case 'center':
752
			default:
753
				$x = ($this->width / 2) - ($overlay->width / 2) + $x_offset;
754
				$y = ($this->height / 2) - ($overlay->height / 2) + $y_offset;
755
				break;
756
		}
757
758
		// Perform the overlay
759
		$this->imagecopymerge_alpha($this->image, $overlay->image, $x, $y, 0, 0, $overlay->width, $overlay->height, $opacity);
760
761
		return $this;
762
763
	}
764
765
	/**
766
	 * Pixelate
767
	 *
768
	 * @param int			$block_size	Size in pixels of each resulting block
769
	 *
770
	 * @return SimpleImage
771
	 *
772
	 */
773
	function pixelate($block_size = 10) {
774
		imagefilter($this->image, IMG_FILTER_PIXELATE, $block_size, true);
775
		return $this;
776
	}
777
778
	/**
779
	 * Resize an image to the specified dimensions
780
	 *
781
	 * @param int	$width
782
	 * @param int	$height
783
	 *
784
	 * @return SimpleImage
785
	 *
786
	 */
787
	function resize($width, $height) {
788
789
		// Generate new GD image
790
		$new = imagecreatetruecolor($width, $height);
791
792
		if( $this->original_info['format'] === 'gif' ) {
793
			// Preserve transparency in GIFs
794
			$transparent_index = imagecolortransparent($this->image);
795
			if ($transparent_index >= 0) {
796
	            $transparent_color = imagecolorsforindex($this->image, $transparent_index);
797
	            $transparent_index = imagecolorallocate($new, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
798
	            imagefill($new, 0, 0, $transparent_index);
799
	            imagecolortransparent($new, $transparent_index);
800
			}
801
		} else {
802
			// Preserve transparency in PNGs (benign for JPEGs)
803
			imagealphablending($new, false);
804
			imagesavealpha($new, true);
805
		}
806
807
		// Resize
808
		imagecopyresampled($new, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
809
810
		// Update meta data
811
		$this->width = $width;
812
		$this->height = $height;
813
		$this->image = $new;
814
815
		return $this;
816
817
	}
818
819
	/**
820
	 * Rotate an image
821
	 *
822
	 * @param int			$angle		0-360
823
	 * @param string		$bg_color	Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
824
	 * 									Where red, green, blue - integers 0-255, alpha - integer 0-127
825
	 *
826
	 * @return SimpleImage
827
	 *
828
	 */
829
	function rotate($angle, $bg_color = '#000000') {
830
831
		// Perform the rotation
832
		$rgba = $this->normalize_color($bg_color);
833
		$bg_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']);
834
		$new = imagerotate($this->image, -($this->keep_within($angle, -360, 360)), $bg_color);
835
		imagesavealpha($new, true);
836
		imagealphablending($new, true);
837
838
		// Update meta data
839
		$this->width = imagesx($new);
840
		$this->height = imagesy($new);
841
		$this->image = $new;
842
843
		return $this;
844
845
	}
846
847
	/**
848
	 * Save an image
849
	 *
850
	 * The resulting format will be determined by the file extension.
851
	 *
852
	 * @param null|string	$filename	If omitted - original file will be overwritten
853
	 * @param null|int		$quality	Output image quality in percents 0-100
854
	 *
855
	 * @return SimpleImage
856
	 * @throws Exception
857
	 *
858
	 */
859
	function save($filename = null, $quality = null) {
860
861
		// Determine quality, filename, and format
862
		$quality = $quality ?: $this->quality;
863
		$filename = $filename ?: $this->filename;
864
		$format = $this->file_ext($filename) ?: $this->original_info['format'];
865
866
		// Create the image
867
		switch (strtolower($format)) {
868
			case 'gif':
869
				$result = imagegif($this->image, $filename);
870
				break;
871
			case 'jpg':
872
			case 'jpeg':
873
				imageinterlace($this->image, true);
874
				$result = imagejpeg($this->image, $filename, round($quality));
875
				break;
876
			case 'png':
877
				$result = imagepng($this->image, $filename, round(9 * $quality / 100));
878
				break;
879
			default:
880
				throw new Exception('Unsupported format');
881
		}
882
883
		if (!$result) {
884
			throw new Exception('Unable to save image: ' . $filename);
885
		}
886
887
		return $this;
888
889
	}
890
891
	/**
892
	 * Sepia
893
	 *
894
	 * @return SimpleImage
895
	 *
896
	 */
897
	function sepia() {
898
		imagefilter($this->image, IMG_FILTER_GRAYSCALE);
899
		imagefilter($this->image, IMG_FILTER_COLORIZE, 100, 50, 0);
900
		return $this;
901
	}
902
903
	/**
904
	 * Sketch
905
	 *
906
	 * @return SimpleImage
907
	 *
908
	 */
909
	function sketch() {
910
		imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
911
		return $this;
912
	}
913
914
	/**
915
	 * Smooth
916
	 *
917
	 * @param int			$level	Min = -10, max = 10
918
	 *
919
	 * @return SimpleImage
920
	 *
921
	 */
922
	function smooth($level) {
923
		imagefilter($this->image, IMG_FILTER_SMOOTH, $this->keep_within($level, -10, 10));
924
		return $this;
925
	}
926
927
	/**
928
	 * Add text to an image
929
	 *
930
	 * @param string		$text
931
	 * @param string		$font_file
932
	 * @param float|int		$font_size
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $font_size a bit more specific; maybe use integer.
Loading history...
933
	 * @param string		$color
934
	 * @param string		$position
935
	 * @param int			$x_offset
936
	 * @param int			$y_offset
937
	 *
938
	 * @return SimpleImage
939
	 * @throws Exception
940
	 *
941
	 */
942
	function text($text, $font_file, $font_size = 12, $color = '#000000', $position = 'center', $x_offset = 0, $y_offset = 0) {
943
944
		// todo - this method could be improved to support the text angle
945
		$angle = 0;
946
947
		// Determine text color
948
		$rgba = $this->normalize_color($color);
949
		$color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']);
950
951
		// Determine textbox size
952
		$box = imagettfbbox($font_size, $angle, $font_file, $text);
953
		if (!$box) {
954
			throw new Exception('Unable to load font: '.$font_file);
955
		}
956
		$box_width = abs($box[6] - $box[2]);
957
		$box_height = abs($box[7] - $box[1]);
958
959
		// Determine position
960
		switch (strtolower($position)) {
961
			case 'top left':
962
				$x = 0 + $x_offset;
963
				$y = 0 + $y_offset + $box_height;
964
				break;
965
			case 'top right':
966
				$x = $this->width - $box_width + $x_offset;
967
				$y = 0 + $y_offset + $box_height;
968
				break;
969
			case 'top':
970
				$x = ($this->width / 2) - ($box_width / 2) + $x_offset;
971
				$y = 0 + $y_offset + $box_height;
972
				break;
973
			case 'bottom left':
974
				$x = 0 + $x_offset;
975
				$y = $this->height - $box_height + $y_offset + $box_height;
976
				break;
977
			case 'bottom right':
978
				$x = $this->width - $box_width + $x_offset;
979
				$y = $this->height - $box_height + $y_offset + $box_height;
980
				break;
981
			case 'bottom':
982
				$x = ($this->width / 2) - ($box_width / 2) + $x_offset;
983
				$y = $this->height - $box_height + $y_offset + $box_height;
984
				break;
985
			case 'left':
986
				$x = 0 + $x_offset;
987
				$y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset;
988
				break;
989
			case 'right';
990
				$x = $this->width - $box_width + $x_offset;
991
				$y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset;
992
				break;
993
			case 'center':
994
			default:
995
				$x = ($this->width / 2) - ($box_width / 2) + $x_offset;
996
				$y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset;
997
				break;
998
		}
999
1000
		// Add the text
1001
		imagettftext($this->image, $font_size, $angle, $x, $y, $color, $font_file, $text);
1002
1003
		return $this;
1004
1005
	}
1006
1007
	/**
1008
	 * Thumbnail
1009
	 *
1010
	 * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the
1011
	 * remaining overflow (from the center) to get the image to be the size specified. Useful for generating thumbnails.
1012
	 *
1013
	 * @param int			$width
1014
	 * @param int|null		$height	If omitted - assumed equal to $width
1015
	 *
1016
	 * @return SimpleImage
1017
	 *
1018
	 */
1019
	function thumbnail($width, $height = null) {
1020
1021
		// Determine height
1022
		$height = $height ?: $width;
1023
1024
		// Determine aspect ratios
1025
		$current_aspect_ratio = $this->height / $this->width;
1026
		$new_aspect_ratio = $height / $width;
1027
1028
		// Fit to height/width
1029
		if ($new_aspect_ratio > $current_aspect_ratio) {
1030
			$this->fit_to_height($height);
1031
		} else {
1032
			$this->fit_to_width($width);
1033
		}
1034
		$left = floor(($this->width / 2) - ($width / 2));
1035
		$top = floor(($this->height / 2) - ($height / 2));
1036
1037
		// Return trimmed image
1038
		return $this->crop($left, $top, $width + $left, $height + $top);
1039
1040
	}
1041
1042
	/**
1043
	 * Returns the file extension of the specified file
1044
	 *
1045
	 * @param string	$filename
1046
	 *
1047
	 * @return string
1048
	 *
1049
	 */
1050
	protected function file_ext($filename) {
1051
1052
		if (!preg_match('/\./', $filename)) {
1053
			return '';
1054
		}
1055
1056
		return preg_replace('/^.*\./', '', $filename);
1057
1058
	}
1059
1060
	/**
1061
	 * Get meta data of image or base64 string
1062
	 *
1063
	 * @param string|null		$imagestring	If omitted treat as a normal image
0 ignored issues
show
Bug introduced by
There is no parameter named $imagestring. 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...
1064
	 *
1065
	 * @return SimpleImage
1066
	 * @throws Exception
1067
	 *
1068
	 */
1069
	protected function get_meta_data() {
1070
		//gather meta data
1071
		if(empty($this->imagestring)) {
1072
			$info = getimagesize($this->filename);
1073
1074
			switch ($info['mime']) {
1075
				case 'image/gif':
1076
					$this->image = imagecreatefromgif($this->filename);
1077
					break;
1078
				case 'image/jpeg':
1079
					$this->image = imagecreatefromjpeg($this->filename);
1080
					break;
1081
				case 'image/png':
1082
					$this->image = imagecreatefrompng($this->filename);
1083
					break;
1084
				default:
1085
					throw new Exception('Invalid image: '.$this->filename);
1086
					break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1087
			}
1088
		} elseif (function_exists('getimagesizefromstring')) {
1089
			$info = getimagesizefromstring($this->imagestring);
1090
		} else {
1091
			throw new Exception('PHP 5.4 is required to use method getimagesizefromstring');
1092
		}
1093
1094
		$this->original_info = array(
1095
			'width' => $info[0],
1096
			'height' => $info[1],
1097
			'orientation' => $this->get_orientation(),
1098
			'exif' => function_exists('exif_read_data') && $info['mime'] === 'image/jpeg' && $this->imagestring === null ? $this->exif = @exif_read_data($this->filename) : null,
1099
			'format' => preg_replace('/^image\//', '', $info['mime']),
1100
			'mime' => $info['mime']
1101
		);
1102
		$this->width = $info[0];
1103
		$this->height = $info[1];
1104
1105
		imagesavealpha($this->image, true);
1106
		imagealphablending($this->image, true);
1107
1108
		return $this;
1109
1110
	}
1111
1112
	/**
1113
	 * Same as PHP's imagecopymerge() function, except preserves alpha-transparency in 24-bit PNGs
1114
	 *
1115
	 * @param $dst_im
1116
	 * @param $src_im
1117
	 * @param $dst_x
1118
	 * @param $dst_y
1119
	 * @param $src_x
1120
	 * @param $src_y
1121
	 * @param $src_w
1122
	 * @param $src_h
1123
	 * @param $pct
1124
	 *
1125
	 * @link http://www.php.net/manual/en/function.imagecopymerge.php#88456
1126
	 *
1127
	 */
1128
	protected function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) {
1129
1130
		// Get image width and height and percentage
1131
		$pct /= 100;
1132
		$w = imagesx($src_im);
1133
		$h = imagesy($src_im);
1134
1135
		// Turn alpha blending off
1136
		imagealphablending($src_im, false);
1137
1138
		// Find the most opaque pixel in the image (the one with the smallest alpha value)
1139
		$minalpha = 127;
1140
		for ($x = 0; $x < $w; $x++) {
1141
			for ($y = 0; $y < $h; $y++) {
1142
				$alpha = (imagecolorat($src_im, $x, $y) >> 24) & 0xFF;
1143
				if ($alpha < $minalpha) {
1144
					$minalpha = $alpha;
1145
				}
1146
			}
1147
		}
1148
1149
		// Loop through image pixels and modify alpha for each
1150
		for ($x = 0; $x < $w; $x++) {
1151
			for ($y = 0; $y < $h; $y++) {
1152
				// Get current alpha value (represents the TANSPARENCY!)
1153
				$colorxy = imagecolorat($src_im, $x, $y);
1154
				$alpha = ($colorxy >> 24) & 0xFF;
1155
				// Calculate new alpha
1156
				if ($minalpha !== 127) {
1157
					$alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha);
1158
				} else {
1159
					$alpha += 127 * $pct;
1160
				}
1161
				// Get the color index with new alpha
1162
				$alphacolorxy = imagecolorallocatealpha($src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha);
1163
				// Set pixel with the new color + opacity
1164
				if (!imagesetpixel($src_im, $x, $y, $alphacolorxy)) {
1165
					return;
1166
				}
1167
			}
1168
		}
1169
1170
		// Copy it
1171
		imagesavealpha($dst_im, true);
1172
		imagealphablending($dst_im, true);
1173
		imagesavealpha($src_im, true);
1174
		imagealphablending($src_im, true);
1175
		imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h);
1176
1177
	}
1178
1179
	/**
1180
	 * Ensures $value is always within $min and $max range.
1181
	 *
1182
	 * If lower, $min is returned. If higher, $max is returned.
1183
	 *
1184
	 * @param int|float		$value
1185
	 * @param int|float		$min
1186
	 * @param int|float		$max
1187
	 *
1188
	 * @return int|float
1189
	 *
1190
	 */
1191
	protected function keep_within($value, $min, $max) {
1192
1193
		if ($value < $min) {
1194
			return $min;
1195
		}
1196
1197
		if ($value > $max) {
1198
			return $max;
1199
		}
1200
1201
		return $value;
1202
1203
	}
1204
1205
	/**
1206
	 * Converts a hex color value to its RGB equivalent
1207
	 *
1208
	 * @param string		$color	Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
1209
	 * 								Where red, green, blue - integers 0-255, alpha - integer 0-127
1210
	 *
1211
	 * @return array|bool
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|array<string,integer|double>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1212
	 *
1213
	 */
1214
	protected function normalize_color($color) {
1215
1216
		if (is_string($color)) {
1217
1218
			$color = trim($color, '#');
1219
1220
			if (strlen($color) == 6) {
1221
				list($r, $g, $b) = array(
1222
					$color[0].$color[1],
1223
					$color[2].$color[3],
1224
					$color[4].$color[5]
1225
				);
1226
			} elseif (strlen($color) == 3) {
1227
				list($r, $g, $b) = array(
1228
					$color[0].$color[0],
1229
					$color[1].$color[1],
1230
					$color[2].$color[2]
1231
				);
1232
			} else {
1233
				return false;
1234
			}
1235
			return array(
1236
				'r' => hexdec($r),
1237
				'g' => hexdec($g),
1238
				'b' => hexdec($b),
1239
				'a' => 0
1240
			);
1241
1242
		} elseif (is_array($color) && (count($color) == 3 || count($color) == 4)) {
1243
1244
			if (isset($color['r'], $color['g'], $color['b'])) {
1245
				return array(
1246
					'r' => $this->keep_within($color['r'], 0, 255),
1247
					'g' => $this->keep_within($color['g'], 0, 255),
1248
					'b' => $this->keep_within($color['b'], 0, 255),
1249
					'a' => $this->keep_within(isset($color['a']) ? $color['a'] : 0, 0, 127)
1250
				);
1251
			} elseif (isset($color[0], $color[1], $color[2])) {
1252
				return array(
1253
					'r' => $this->keep_within($color[0], 0, 255),
1254
					'g' => $this->keep_within($color[1], 0, 255),
1255
					'b' => $this->keep_within($color[2], 0, 255),
1256
					'a' => $this->keep_within(isset($color[3]) ? $color[3] : 0, 0, 127)
1257
				);
1258
			}
1259
1260
		}
1261
		return false;
1262
	}
1263
1264
}
1265