• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 7293561159

21 Dec 2023 09:55PM UTC coverage: 85.237% (+0.004%) from 85.233%
7293561159

push

github

web-flow
Merge pull request #8355 from paulbalandan/replace

Add `replace` to composer.json

18597 of 21818 relevant lines covered (85.24%)

199.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.08
/system/Images/Handlers/BaseHandler.php
1
<?php
2

3
/**
4
 * This file is part of CodeIgniter 4 framework.
5
 *
6
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11

12
namespace CodeIgniter\Images\Handlers;
13

14
use CodeIgniter\Images\Exceptions\ImageException;
15
use CodeIgniter\Images\Image;
16
use CodeIgniter\Images\ImageHandlerInterface;
17
use Config\Images;
18
use InvalidArgumentException;
19

20
/**
21
 * Base image handling implementation
22
 */
23
abstract class BaseHandler implements ImageHandlerInterface
24
{
25
    /**
26
     * Configuration settings.
27
     *
28
     * @var Images
29
     */
30
    protected $config;
31

32
    /**
33
     * The image/file instance
34
     *
35
     * @var Image
36
     */
37
    protected $image;
38

39
    /**
40
     * Whether the image file has been confirmed.
41
     *
42
     * @var bool
43
     */
44
    protected $verified = false;
45

46
    /**
47
     * Image width.
48
     *
49
     * @var int
50
     */
51
    protected $width = 0;
52

53
    /**
54
     * Image height.
55
     *
56
     * @var int
57
     */
58
    protected $height = 0;
59

60
    /**
61
     * File permission mask.
62
     *
63
     * @var int
64
     */
65
    protected $filePermissions = 0644;
66

67
    /**
68
     * X-axis.
69
     *
70
     * @var int|null
71
     */
72
    protected $xAxis = 0;
73

74
    /**
75
     * Y-axis.
76
     *
77
     * @var int|null
78
     */
79
    protected $yAxis = 0;
80

81
    /**
82
     * Master dimensioning.
83
     *
84
     * @var string
85
     */
86
    protected $masterDim = 'auto';
87

88
    /**
89
     * Default options for text watermarking.
90
     *
91
     * @var array
92
     */
93
    protected $textDefaults = [
94
        'fontPath'     => null,
95
        'fontSize'     => 16,
96
        'color'        => 'ffffff',
97
        'opacity'      => 1.0,
98
        'vAlign'       => 'bottom',
99
        'hAlign'       => 'center',
100
        'vOffset'      => 0,
101
        'hOffset'      => 0,
102
        'padding'      => 0,
103
        'withShadow'   => false,
104
        'shadowColor'  => '000000',
105
        'shadowOffset' => 3,
106
    ];
107

108
    /**
109
     * Image types with support for transparency.
110
     *
111
     * @var array
112
     */
113
    protected $supportTransparency = [
114
        IMAGETYPE_PNG,
115
        IMAGETYPE_WEBP,
116
    ];
117

118
    /**
119
     * Temporary image used by the different engines.
120
     *
121
     * @var resource|null
122
     */
123
    protected $resource;
124

125
    /**
126
     * Constructor.
127
     *
128
     * @param Images|null $config
129
     */
130
    public function __construct($config = null)
131
    {
132
        $this->config = $config ?? new Images();
81✔
133
    }
134

135
    /**
136
     * Sets another image for this handler to work on.
137
     * Keeps us from needing to continually instantiate the handler.
138
     *
139
     * @return $this
140
     */
141
    public function withFile(string $path)
142
    {
143
        // Clear out the old resource so that
144
        // it doesn't try to use a previous image
145
        $this->resource = null;
74✔
146
        $this->verified = false;
74✔
147

148
        $this->image = new Image($path, true);
74✔
149

150
        $this->image->getProperties(false);
73✔
151
        $this->width  = $this->image->origWidth;
72✔
152
        $this->height = $this->image->origHeight;
72✔
153

154
        return $this;
72✔
155
    }
156

157
    /**
158
     * Make the image resource object if needed
159
     *
160
     * @return void
161
     */
162
    abstract protected function ensureResource();
163

164
    /**
165
     * Returns the image instance.
166
     *
167
     * @return Image
168
     */
169
    public function getFile()
170
    {
171
        return $this->image;
6✔
172
    }
173

174
    /**
175
     * Verifies that a file has been supplied and it is an image.
176
     *
177
     * @return Image The image instance
178
     *
179
     * @throws ImageException
180
     */
181
    protected function image(): Image
182
    {
183
        if ($this->verified) {
63✔
184
            return $this->image;
60✔
185
        }
186

187
        // Verify withFile has been called
188
        if (empty($this->image)) {
63✔
189
            throw ImageException::forMissingImage();
1✔
190
        }
191

192
        // Verify the loaded image is an Image instance
193
        if (! $this->image instanceof Image) {
62✔
194
            throw ImageException::forInvalidPath();
×
195
        }
196

197
        // File::__construct has verified the file exists - make sure it is an image
198
        if (! is_int($this->image->imageType)) {
62✔
199
            throw ImageException::forFileNotSupported();
×
200
        }
201

202
        // Note that the image has been verified
203
        $this->verified = true;
62✔
204

205
        return $this->image;
62✔
206
    }
207

208
    /**
209
     * Returns the temporary image used during the image processing.
210
     * Good for extending the system or doing things this library
211
     * is not intended to do.
212
     *
213
     * @return resource
214
     */
215
    public function getResource()
216
    {
217
        $this->ensureResource();
6✔
218

219
        return $this->resource;
6✔
220
    }
221

222
    /**
223
     * Load the temporary image used during the image processing.
224
     * Some functions e.g. save() will only copy and not compress
225
     * your image otherwise.
226
     *
227
     * @return $this
228
     */
229
    public function withResource()
230
    {
231
        $this->ensureResource();
2✔
232

233
        return $this;
2✔
234
    }
235

236
    /**
237
     * Resize the image
238
     *
239
     * @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true.
240
     *
241
     * @return BaseHandler
242
     */
243
    public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto')
244
    {
245
        // If the target width/height match the source, then we have nothing to do here.
246
        if ($this->image()->origWidth === $width && $this->image()->origHeight === $height) {
19✔
247
            return $this;
3✔
248
        }
249

250
        $this->width  = $width;
15✔
251
        $this->height = $height;
15✔
252

253
        if ($maintainRatio) {
15✔
254
            $this->masterDim = $masterDim;
6✔
255
            $this->reproportion();
6✔
256
        }
257

258
        return $this->_resize($maintainRatio);
15✔
259
    }
260

261
    /**
262
     * Crops the image to the desired height and width. If one of the height/width values
263
     * is not provided, that value will be set the appropriate value based on offsets and
264
     * image dimensions.
265
     *
266
     * @param int|null $x X-axis coord to start cropping from the left of image
267
     * @param int|null $y Y-axis coord to start cropping from the top of image
268
     *
269
     * @return $this
270
     */
271
    public function crop(?int $width = null, ?int $height = null, ?int $x = null, ?int $y = null, bool $maintainRatio = false, string $masterDim = 'auto')
272
    {
273
        $this->width  = $width;
20✔
274
        $this->height = $height;
20✔
275
        $this->xAxis  = $x;
20✔
276
        $this->yAxis  = $y;
20✔
277

278
        if ($maintainRatio) {
20✔
279
            $this->masterDim = $masterDim;
2✔
280
            $this->reproportion();
2✔
281
        }
282

283
        $result = $this->_crop();
20✔
284

285
        $this->xAxis = null;
20✔
286
        $this->yAxis = null;
20✔
287

288
        return $result;
20✔
289
    }
290

291
    /**
292
     * Changes the stored image type to indicate the new file format to use when saving.
293
     * Does not touch the actual resource.
294
     *
295
     * @param int $imageType A PHP imageType constant, e.g. https://www.php.net/manual/en/function.image-type-to-mime-type.php
296
     *
297
     * @return $this
298
     */
299
    public function convert(int $imageType)
300
    {
301
        $this->ensureResource();
3✔
302

303
        $this->image()->imageType = $imageType;
3✔
304

305
        return $this;
3✔
306
    }
307

308
    /**
309
     * Rotates the image on the current canvas.
310
     *
311
     * @return $this
312
     */
313
    public function rotate(float $angle)
314
    {
315
        // Allowed rotation values
316
        $degs = [
8✔
317
            90.0,
8✔
318
            180.0,
8✔
319
            270.0,
8✔
320
        ];
8✔
321

322
        if (! in_array($angle, $degs, true)) {
8✔
323
            throw ImageException::forMissingAngle();
2✔
324
        }
325

326
        // cast angle as an int, for our use
327
        $angle = (int) $angle;
6✔
328

329
        // Reassign the width and height
330
        if ($angle === 90 || $angle === 270) {
6✔
331
            $temp         = $this->height;
6✔
332
            $this->width  = $this->height;
6✔
333
            $this->height = $temp;
6✔
334
        }
335

336
        // Call the Handler-specific version.
337
        $this->_rotate($angle);
6✔
338

339
        return $this;
6✔
340
    }
341

342
    /**
343
     * Flattens transparencies, default white background
344
     *
345
     * @return $this
346
     */
347
    public function flatten(int $red = 255, int $green = 255, int $blue = 255)
348
    {
349
        $this->width  = $this->image()->origWidth;
2✔
350
        $this->height = $this->image()->origHeight;
2✔
351

352
        return $this->_flatten($red, $green, $blue);
2✔
353
    }
354

355
    /**
356
     * Handler-specific method to flattening an image's transparencies.
357
     *
358
     * @return $this
359
     *
360
     * @internal
361
     */
362
    abstract protected function _flatten(int $red = 255, int $green = 255, int $blue = 255);
363

364
    /**
365
     * Handler-specific method to handle rotating an image in 90 degree increments.
366
     *
367
     * @return mixed
368
     */
369
    abstract protected function _rotate(int $angle);
370

371
    /**
372
     * Flips an image either horizontally or vertically.
373
     *
374
     * @param string $dir Either 'vertical' or 'horizontal'
375
     *
376
     * @return $this
377
     */
378
    public function flip(string $dir = 'vertical')
379
    {
380
        $dir = strtolower($dir);
12✔
381

382
        if ($dir !== 'vertical' && $dir !== 'horizontal') {
12✔
383
            throw ImageException::forInvalidDirection($dir);
2✔
384
        }
385

386
        return $this->_flip($dir);
10✔
387
    }
388

389
    /**
390
     * Handler-specific method to handle flipping an image along its
391
     * horizontal or vertical axis.
392
     *
393
     * @return $this
394
     */
395
    abstract protected function _flip(string $direction);
396

397
    /**
398
     * Overlays a string of text over the image.
399
     *
400
     * Valid options:
401
     *
402
     *  - color         Text Color (hex number)
403
     *  - shadowColor   Color of the shadow (hex number)
404
     *  - hAlign        Horizontal alignment: left, center, right
405
     *  - vAlign        Vertical alignment: top, middle, bottom
406
     *  - hOffset
407
     *  - vOffset
408
     *  - fontPath
409
     *  - fontSize
410
     *  - shadowOffset
411
     *
412
     * @return $this
413
     */
414
    public function text(string $text, array $options = [])
415
    {
416
        $options                = array_merge($this->textDefaults, $options);
6✔
417
        $options['color']       = trim($options['color'], '# ');
6✔
418
        $options['shadowColor'] = trim($options['shadowColor'], '# ');
6✔
419

420
        $this->_text($text, $options);
6✔
421

422
        return $this;
6✔
423
    }
424

425
    /**
426
     * Handler-specific method for overlaying text on an image.
427
     *
428
     * @return void
429
     */
430
    abstract protected function _text(string $text, array $options = []);
431

432
    /**
433
     * Handles the actual resizing of the image.
434
     *
435
     * @return $this
436
     */
437
    abstract public function _resize(bool $maintainRatio = false);
438

439
    /**
440
     * Crops the image.
441
     *
442
     * @return $this
443
     */
444
    abstract public function _crop();
445

446
    /**
447
     * Return image width.
448
     *
449
     * @return int
450
     */
451
    abstract public function _getWidth();
452

453
    /**
454
     * Return the height of an image.
455
     *
456
     * @return int
457
     */
458
    abstract public function _getHeight();
459

460
    /**
461
     * Reads the EXIF information from the image and modifies the orientation
462
     * so that displays correctly in the browser. This is especially an issue
463
     * with images taken by smartphones who always store the image up-right,
464
     * but set the orientation flag to display it correctly.
465
     *
466
     * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF.
467
     *
468
     * @return $this
469
     */
470
    public function reorient(bool $silent = false)
471
    {
472
        $orientation = $this->getEXIF('Orientation', $silent);
2✔
473

474
        switch ($orientation) {
475
            case 2:
2✔
476
                return $this->flip('horizontal');
2✔
477

478
            case 3:
2✔
479
                return $this->rotate(180);
2✔
480

481
            case 4:
2✔
482
                return $this->rotate(180)->flip('horizontal');
2✔
483

484
            case 5:
2✔
485
                return $this->rotate(270)->flip('horizontal');
2✔
486

487
            case 6:
2✔
488
                return $this->rotate(270);
2✔
489

490
            case 7:
2✔
491
                return $this->rotate(90)->flip('horizontal');
2✔
492

493
            case 8:
2✔
494
                return $this->rotate(90);
2✔
495

496
            default:
497
                return $this;
2✔
498
        }
499
    }
500

501
    /**
502
     * Retrieve the EXIF information from the image, if possible. Returns
503
     * an array of the information, or null if nothing can be found.
504
     *
505
     * EXIF data is only supported fr JPEG & TIFF formats.
506
     *
507
     * @param string|null $key    If specified, will only return this piece of EXIF data.
508
     * @param bool        $silent If true, will not throw our own exceptions.
509
     *
510
     * @return mixed
511
     *
512
     * @throws ImageException
513
     */
514
    public function getEXIF(?string $key = null, bool $silent = false)
515
    {
516
        if (! function_exists('exif_read_data')) {
4✔
517
            if ($silent) {
×
518
                return null;
×
519
            }
520

521
            throw ImageException::forEXIFUnsupported(); // @codeCoverageIgnore
522
        }
523

524
        $exif = null; // default
4✔
525

526
        switch ($this->image()->imageType) {
4✔
527
            case IMAGETYPE_JPEG:
4✔
528
            case IMAGETYPE_TIFF_II:
×
529
                $exif = @exif_read_data($this->image()->getPathname());
4✔
530
                if ($key !== null && is_array($exif)) {
4✔
531
                    $exif = $exif[$key] ?? false;
4✔
532
                }
533
        }
534

535
        return $exif;
4✔
536
    }
537

538
    /**
539
     * Combine cropping and resizing into a single command.
540
     *
541
     * Supported positions:
542
     *  - top-left
543
     *  - top
544
     *  - top-right
545
     *  - left
546
     *  - center
547
     *  - right
548
     *  - bottom-left
549
     *  - bottom
550
     *  - bottom-right
551
     *
552
     * @return BaseHandler
553
     */
554
    public function fit(int $width, ?int $height = null, string $position = 'center')
555
    {
556
        $origWidth  = $this->image()->origWidth;
8✔
557
        $origHeight = $this->image()->origHeight;
8✔
558

559
        [$cropWidth, $cropHeight] = $this->calcAspectRatio($width, $height, $origWidth, $origHeight);
8✔
560

561
        if ($height === null) {
8✔
562
            $height = ceil(($width / $cropWidth) * $cropHeight);
2✔
563
        }
564

565
        [$x, $y] = $this->calcCropCoords($cropWidth, $cropHeight, $origWidth, $origHeight, $position);
8✔
566

567
        return $this->crop($cropWidth, $cropHeight, $x, $y)->resize($width, $height);
8✔
568
    }
569

570
    /**
571
     * Calculate image aspect ratio.
572
     *
573
     * @param float|int      $width
574
     * @param float|int|null $height
575
     * @param float|int      $origWidth
576
     * @param float|int      $origHeight
577
     */
578
    protected function calcAspectRatio($width, $height = null, $origWidth = 0, $origHeight = 0): array
579
    {
580
        if (empty($origWidth) || empty($origHeight)) {
8✔
581
            throw new InvalidArgumentException('You must supply the parameters: origWidth, origHeight.');
×
582
        }
583

584
        // If $height is null, then we have it easy.
585
        // Calc based on full image size and be done.
586
        if ($height === null) {
8✔
587
            $height = ($width / $origWidth) * $origHeight;
2✔
588

589
            return [
2✔
590
                $width,
2✔
591
                (int) $height,
2✔
592
            ];
2✔
593
        }
594

595
        $xRatio = $width / $origWidth;
6✔
596
        $yRatio = $height / $origHeight;
6✔
597

598
        if ($xRatio > $yRatio) {
6✔
599
            return [
4✔
600
                $origWidth,
4✔
601
                (int) ($origWidth * $height / $width),
4✔
602
            ];
4✔
603
        }
604

605
        return [
3✔
606
            (int) ($origHeight * $width / $height),
3✔
607
            $origHeight,
3✔
608
        ];
3✔
609
    }
610

611
    /**
612
     * Based on the position, will determine the correct x/y coords to
613
     * crop the desired portion from the image.
614
     *
615
     * @param float|int $width
616
     * @param float|int $height
617
     * @param float|int $origWidth
618
     * @param float|int $origHeight
619
     * @param string    $position
620
     */
621
    protected function calcCropCoords($width, $height, $origWidth, $origHeight, $position): array
622
    {
623
        $position = strtolower($position);
8✔
624

625
        $x = $y = 0;
8✔
626

627
        switch ($position) {
628
            case 'top-left':
8✔
629
                $x = 0;
2✔
630
                $y = 0;
2✔
631
                break;
2✔
632

633
            case 'top':
8✔
634
                $x = floor(($origWidth - $width) / 2);
2✔
635
                $y = 0;
2✔
636
                break;
2✔
637

638
            case 'top-right':
8✔
639
                $x = $origWidth - $width;
2✔
640
                $y = 0;
2✔
641
                break;
2✔
642

643
            case 'left':
8✔
644
                $x = 0;
2✔
645
                $y = floor(($origHeight - $height) / 2);
2✔
646
                break;
2✔
647

648
            case 'center':
8✔
649
                $x = floor(($origWidth - $width) / 2);
8✔
650
                $y = floor(($origHeight - $height) / 2);
8✔
651
                break;
8✔
652

653
            case 'right':
2✔
654
                $x = ($origWidth - $width);
2✔
655
                $y = floor(($origHeight - $height) / 2);
2✔
656
                break;
2✔
657

658
            case 'bottom-left':
2✔
659
                $x = 0;
2✔
660
                $y = $origHeight - $height;
2✔
661
                break;
2✔
662

663
            case 'bottom':
2✔
664
                $x = floor(($origWidth - $width) / 2);
2✔
665
                $y = $origHeight - $height;
2✔
666
                break;
2✔
667

668
            case 'bottom-right':
2✔
669
                $x = ($origWidth - $width);
2✔
670
                $y = $origHeight - $height;
2✔
671
                break;
2✔
672
        }
673

674
        return [
8✔
675
            $x,
8✔
676
            $y,
8✔
677
        ];
8✔
678
    }
679

680
    /**
681
     * Get the version of the image library in use.
682
     *
683
     * @return string
684
     */
685
    abstract public function getVersion();
686

687
    /**
688
     * Saves any changes that have been made to file.
689
     *
690
     * Example:
691
     *    $image->resize(100, 200, true)
692
     *          ->save($target);
693
     *
694
     * @param non-empty-string|null $target
695
     *
696
     * @return bool
697
     */
698
    abstract public function save(?string $target = null, int $quality = 90);
699

700
    /**
701
     * Does the driver-specific processing of the image.
702
     *
703
     * @return mixed
704
     */
705
    abstract protected function process(string $action);
706

707
    /**
708
     * Provide access to the Image class' methods if they don't exist
709
     * on the handler itself.
710
     *
711
     * @return mixed
712
     */
713
    public function __call(string $name, array $args = [])
714
    {
715
        if (method_exists($this->image(), $name)) {
1✔
716
            return $this->image()->{$name}(...$args);
1✔
717
        }
718
    }
719

720
    /**
721
     * Re-proportion Image Width/Height
722
     *
723
     * When creating thumbs, the desired width/height
724
     * can end up warping the image due to an incorrect
725
     * ratio between the full-sized image and the thumb.
726
     *
727
     * This function lets us re-proportion the width/height
728
     * if users choose to maintain the aspect ratio when resizing.
729
     *
730
     * @return void
731
     */
732
    protected function reproportion()
733
    {
734
        if (($this->width === 0 && $this->height === 0) || $this->image()->origWidth === 0 || $this->image()->origHeight === 0 || (! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height)) || ! ctype_digit((string) $this->image()->origWidth) || ! ctype_digit((string) $this->image()->origHeight)) {
8✔
735
            return;
×
736
        }
737

738
        // Sanitize
739
        $this->width  = (int) $this->width;
8✔
740
        $this->height = (int) $this->height;
8✔
741

742
        if ($this->masterDim !== 'width' && $this->masterDim !== 'height') {
8✔
743
            if ($this->width > 0 && $this->height > 0) {
8✔
744
                $this->masterDim = ((($this->image()->origHeight / $this->image()->origWidth) - ($this->height / $this->width)) < 0) ? 'width' : 'height';
4✔
745
            } else {
746
                $this->masterDim = ($this->height === 0) ? 'width' : 'height';
8✔
747
            }
748
        } elseif (($this->masterDim === 'width' && $this->width === 0) || ($this->masterDim === 'height' && $this->height === 0)
×
749
        ) {
750
            return;
×
751
        }
752

753
        if ($this->masterDim === 'width') {
8✔
754
            $this->height = (int) ceil($this->width * $this->image()->origHeight / $this->image()->origWidth);
4✔
755
        } else {
756
            $this->width = (int) ceil($this->image()->origWidth * $this->height / $this->image()->origHeight);
4✔
757
        }
758
    }
759

760
    /**
761
     * Return image width.
762
     *
763
     * accessor for testing; not part of interface
764
     *
765
     * @return int
766
     */
767
    public function getWidth()
768
    {
769
        return ($this->resource !== null) ? $this->_getWidth() : $this->width;
48✔
770
    }
771

772
    /**
773
     * Return image height.
774
     *
775
     * accessor for testing; not part of interface
776
     *
777
     * @return int
778
     */
779
    public function getHeight()
780
    {
781
        return ($this->resource !== null) ? $this->_getHeight() : $this->height;
48✔
782
    }
783
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc