ImageEditor::save()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
namespace Gwa\Image;
3
4
/**
5
 * Class containing methods for editing images.
6
 */
7
class ImageEditor
8
{
9
    /**
10
     * @var string
11
     */
12
    private $filepath;
13
14
    /**
15
     * @var int
16
     */
17
    private $type;
18
19
    /**
20
     * @var int
21
     */
22
    private $width;
23
24
    /**
25
     * @var int
26
     */
27
    private $height;
28
29
    /**
30
     * @var string
31
     */
32
    private $mimetype;
33
34
    /**
35
     * @var resource
36
     */
37
    private $resource;
38
39
    const DEFAULT_JPEG_QUALITY = 80;
40
41
    /**
42
     * Constuctor
43
     *
44
     * @param string $filepath Path to an existing image
45
     * @throws InvalidArgumentException
46
     * @throws Exception
47
     */
48
    public function __construct($filepath)
49
    {
50
        // make sure the GD library is installed
51
        if (!function_exists('gd_info')) {
52
            trigger_error('You do not have the GD Library installed.');
53
        }
54
55
        if (!file_exists($filepath)) {
56
            throw new \InvalidArgumentException('File does not exist: ' . $filepath);
57
        }
58
59
        if (!is_readable($filepath)) {
60
            throw new \Exception('File is not readable: ' . $filepath);
61
        }
62
63
        $this->filepath = $filepath;
64
        $this->extractFileData();
65
        $this->createResource();
66
    }
67
68
    /**
69
     * Destructor
70
     */
71
    public function __destruct()
72
    {
73
        if (is_resource($this->resource)) {
74
            imagedestroy($this->resource);
75
        }
76
    }
77
78
    /* resize ---------------- */
79
80
    /**
81
     * Resizes image to be within a maximum width and a maximum height
82
     *
83
     * @param int $maxwidth
84
     * @param int $maxheight
85
     * @return ImageEditor
86
     */
87
    public function resizeToWithin($maxwidth, $maxheight)
88
    {
89
        $dimensions = new Dimensions();
90
91
        if ($newd = $dimensions->resizeToWithin($this->width, $this->height, $maxwidth, $maxheight)) {
92
            $this->resizeToDimensions($newd);
93
        }
94
95
        return $this;
96
    }
97
98
    /**
99
     * Resizes image to an exact width and height, maintaining aspect ratio. Any overhang is cropped.
100
     * If `null` is passed as width or height, image is resized to the new width or height with maintained aspect ratio, without cropping.
101
     *
102
     * @param int|null $width
103
     * @param int|null $height
104
     * @return ImageEditor
105
     */
106
    public function resizeTo($width, $height = null)
107
    {
108
        $dimensions = new Dimensions();
109
110
        if ($newd = $dimensions->resizeTo($this->width, $this->height, $width, $height)) {
111
            $this->resizeToDimensions($newd);
112
            if ($newd->overhang) {
113
                $this->cropFromCenter($width, $height);
114
            }
115
        }
116
117
        return $this;
118
    }
119
120
    private function resizeToDimensions(\stdClass $dimensions)
121
    {
122
        $this->resizeImage($dimensions->width, $dimensions->height);
123
    }
124
125
    private function resizeImage($newwidth, $newheight)
126
    {
127
        $newimage = $this->createImage($newwidth, $newheight);
128
        imagecopyresampled(
129
            $newimage,
130
            $this->resource,
131
            0,
132
            0,
133
            0,
134
            0,
135
            $newwidth,
136
            $newheight,
137
            $this->width,
138
            $this->height
139
        );
140
        $this->setResource($newimage);
141
    }
142
143
    /* crop -------- */
144
145
    /**
146
     * Crops the current image
147
     *
148
     * @param int $x
149
     * @param int $y
150
     * @param int $width
151
     * @param int $height
152
     *
153
     * @return ImageEditor
154
     */
155
    public function crop($x, $y, $width, $height)
156
    {
157
        // check that crop is within bounds of image
158
        if ($x + $width > $this->width || $y + $height > $this->height) {
159
            throw new \InvalidArgumentException('crop out of bounds');
160
        }
161
162
        $newimage = $this->createImage($width, $height);
163
        imagecopy(
164
            $newimage,
165
            $this->resource,
166
            0,
167
            0,
168
            $x,
169
            $y,
170
            $width,
171
            $height
172
        );
173
        $this->setResource($newimage);
174
175
        return $this;
176
    }
177
178
    /**
179
     * Crops the image from the center
180
     *
181
     * @param int $width
182
     * @param int $height
183
     *
184
     * @return ImageEditor
185
     */
186
    public function cropFromCenter($width, $height)
187
    {
188
        $x = ($this->width / 2) - ($width / 2);
189
        $y = ($this->height / 2) - ($height / 2);
190
191
        return $this->crop($x, $y, $width, $height);
192
    }
193
194
    /* rotation -------- */
195
196
    /**
197
     * @return ImageEditor
198
     */
199
    public function rotateClockwise()
200
    {
201
        return $this->rotate(270);
202
    }
203
204
    /**
205
     * @return ImageEditor
206
     */
207
    public function rotateCounterClockwise()
208
    {
209
        return $this->rotate(90);
210
    }
211
212
    /**
213
     * @return ImageEditor
214
     */
215
    public function rotate180()
216
    {
217
        return $this->rotate(180);
218
    }
219
220
    /**
221
     * @param int $deg
222
     * @return ImageEditor
223
     */
224
    private function rotate($deg)
225
    {
226
        imagealphablending($this->resource, false);
227
        $this->setResource(imagerotate($this->resource, $deg, 0));
228
        return $this;
229
    }
230
231
    /* filter -------- */
232
233
    /**
234
     * @return ImageEditor
235
     */
236
    public function grayscale()
237
    {
238
        imagefilter($this->resource, IMG_FILTER_GRAYSCALE);
239
        return $this;
240
    }
241
242
    /**
243
     * @param int $red [0, +255]
244
     * @param int $green [0, +255]
245
     * @param int $blue [0, +255]
246
     * @param int $alpha [0, 1]
247
     *
248
     * @return ImageEditor
249
     */
250
    public function coloroverlay($red, $green, $blue, $alpha = 50)
251
    {
252
        imagealphablending($this->resource, true);
253
        $color = imagecolorallocatealpha($this->resource, $red, $green, $blue, $alpha * 1.27);
254
        imagefilledrectangle($this->resource, 0, 0, $this->width, $this->height, $color);
255
        imagealphablending($this->resource, false);
256
        return $this;
257
    }
258
259
    /**
260
     * @param int $red [0, 255]
261
     * @param int $green [0, 255]
262
     * @param int $blue [0, 255]
263
     * @param int $alpha [0, 127]
264
     *
265
     * @return ImageEditor
266
     */
267
    public function colorize($red, $green, $blue, $alpha = 0)
268
    {
269
        imagefilter($this->resource, IMG_FILTER_COLORIZE, $red, $green, $blue, $alpha);
270
        return $this;
271
    }
272
273
    /**
274
     * Paste another image onto this one.
275
     * @note Basically a wrapper method for http://www.php.net/manual/en/function.imagecopyresampled.php
276
     *
277
     * @param ImageEditor|string $imageeditor
278
     * @param int $dst_x
279
     * @param int $dst_y
280
     * @param int $dst_w
281
     * @param int $dst_h
282
     * @param int $src_x
283
     * @param int $src_y
284
     * @param int $src_w
285
     * @param int $src_h
286
     */
287
    public function pasteImage(
288
        $imageeditor,
289
        $dst_x = 0,
290
        $dst_y = 0,
291
        $dst_w = null,
292
        $dst_h = null,
293
        $src_x = null,
294
        $src_y = null,
295
        $src_w = null,
296
        $src_h = null
297
    ) {
298
        if (is_string($imageeditor)) {
299
            $imageeditor = new ImageEditor($imageeditor);
300
        }
301
302
        if ($src_w === null) {
303
            $src_w = $imageeditor->getWidth();
304
        }
305
        if ($src_h === null) {
306
            $src_h = $imageeditor->getHeight();
307
        }
308
        if ($dst_w === null) {
309
            $dst_w = $src_w;
310
        }
311
        if ($dst_h === null) {
312
            $dst_h = $src_h;
313
        }
314
315
        imagealphablending($this->resource, true);
316
317
        imagecopyresampled(
318
            $this->resource,
319
            $imageeditor->getResource(),
320
            $dst_x,
321
            $dst_y,
322
            $src_x,
323
            $src_y,
324
            $dst_w,
325
            $dst_h,
326
            $src_w,
327
            $src_h
328
        );
329
330
        imagealphablending($this->resource, false);
331
332
        return $this;
333
    }
334
335
    /* -------- */
336
337
    /**
338
     * "Duplicates" the image.
339
     * Basically unsets the filepath, so we have to use `saveAs()` and not `save()`.
340
     *
341
     * @return ImageEditor
342
     */
343
    public function duplicate()
344
    {
345
        $this->filepath = null;
346
        return $this;
347
    }
348
349
    /**
350
     * Saves the image
351
     *
352
     * @param int $quality 0-100 (only for jpegs)
353
     *
354
     * @return ImageEditor
355
     */
356
    public function save($quality = self::DEFAULT_JPEG_QUALITY)
357
    {
358
        if (!isset($this->filepath)) {
359
            throw new \LogicException('Use saveTo() to save an unnamed file.');
360
        }
361
        return $this->saveAs($this->filepath, $quality);
362
    }
363
364
    /**
365
     * Saves the image under a path
366
     *
367
     * @param string $filepath
368
     * @param int $type IMAGETYPE constant
369
     * @param int $quality 0-100 (only for jpegs)
370
     *
371
     * @return ImageEditor
372
     */
373
    public function saveAs($filepath, $type = null, $quality = self::DEFAULT_JPEG_QUALITY)
374
    {
375
        $this->outputImage($filepath, $type, $quality);
376
        $this->filepath = $filepath;
377
378
        return $this;
379
    }
380
381
    /**
382
     * Outputs the image with the correct header.
383
     *
384
     * @param int $quality 0-100 (only for jpegs)
385
     */
386
    public function output($quality = self::DEFAULT_JPEG_QUALITY)
387
    {
388
        header('Content-type: ' . $this->mimetype);
389
        $this->outputImage(null, $quality);
390
    }
391
392
    /**
393
     * @param string $filepath
394
     * @param int $type IMAGETYPE constant
395
     * @param int $quality 0-100 (only for jpegs)
396
     */
397
    private function outputImage($filepath = null, $type = null, $quality = self::DEFAULT_JPEG_QUALITY)
398
    {
399
        $type = !isset($type) ? $this->type : $type;
400
401
        switch ($type) {
402
            case IMAGETYPE_JPEG:
403
                imagejpeg($this->resource, $filepath, $quality);
404
                break;
405
406
            case IMAGETYPE_PNG:
407
                imagepng($this->resource, $filepath);
408
                break;
409
410
            case IMAGETYPE_GIF:
411
                imagegif($this->resource, $filepath);
412
                break;
413
        }
414
    }
415
416
    /* -------- */
417
418
    /**
419
     * Retrieves format.
420
     */
421
    private function extractFileData()
422
    {
423
        if (!$this->type = exif_imagetype($this->filepath)) {
424
            throw new \Exception('Wrong file type');
425
        }
426
427
        $mimetypes = array(
428
            IMAGETYPE_GIF  => 'image/gif',
429
            IMAGETYPE_JPEG => 'image/jpeg',
430
            IMAGETYPE_PNG  => 'image/png'
431
        );
432
433
        if (!array_key_exists($this->type, $mimetypes)) {
434
            throw new \Exception('Unsupported image type');
435
        }
436
437
        $this->mimetype = $mimetypes[$this->type];
438
    }
439
440
    private function createResource()
441
    {
442
        switch ($this->type) {
443
            case IMAGETYPE_GIF:
444
                $this->setResource(imagecreatefromgif($this->filepath));
445
                break;
446
            case IMAGETYPE_JPEG:
447
                $this->setResource(imagecreatefromjpeg($this->filepath));
448
                break;
449
            case IMAGETYPE_PNG:
450
                $this->setResource(imagecreatefrompng($this->filepath));
451
                break;
452
        }
453
    }
454
455
    /**
456
     * @param resource $resource
457
     */
458
    private function setResource($resource)
459
    {
460
        if (is_resource($this->resource)) {
461
            imagedestroy($this->resource);
462
        }
463
464
        $this->width = imagesx($resource);
465
        $this->height = imagesy($resource);
466
467
        imagealphablending($resource, false);
468
        imagesavealpha($resource, true);
469
470
        $this->resource = $resource;
471
    }
472
473
    /**
474
     * @param int $width
475
     * @param int $height
476
     *
477
     * @return resource
478
     */
479
    private function createImage($width, $height)
480
    {
481
        $resource = imagecreatetruecolor($width, $height);
482
483
        imagealphablending($resource, false);
484
        imagesavealpha($resource, true);
485
486
        return $resource;
487
    }
488
489
    /* -------- GETTER / SETTERS -------- */
490
491
    /**
492
     * Gets format of this image [IMAGETYPE_GIF|IMAGETYPE_GIF|IMAGETYPE_GIF]
493
     *
494
     * @return int
495
     */
496
    public function getType()
497
    {
498
        return $this->type;
499
    }
500
501
    /**
502
     * Gets mimetype
503
     *
504
     * @return string
505
     */
506
    public function getMimeType()
507
    {
508
        return $this->mimetype;
509
    }
510
511
    /**
512
     * Returns the width.
513
     *
514
     * @return int
515
     */
516
    public function getWidth()
517
    {
518
        return $this->width;
519
    }
520
521
    /**
522
     * Returns the height.
523
     *
524
     * @return int
525
     */
526
    public function getHeight()
527
    {
528
        return $this->height;
529
    }
530
531
    /**
532
     * @return resource
533
     */
534
    public function getResource()
535
    {
536
        return $this->resource;
537
    }
538
}
539