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

codeigniter4 / CodeIgniter4 / 27781093072

18 Jun 2026 06:33PM UTC coverage: 88.297% (+0.005%) from 88.292%
27781093072

Pull #10319

github

web-flow
Merge 4fb8163c4 into b08160c4c
Pull Request #10319: fix: Prevent DivisionByZeroError in BaseHandler::reproportion() with float 0.0

17 of 18 new or added lines in 1 file covered. (94.44%)

2 existing lines in 1 file now uncovered.

22196 of 25138 relevant lines covered (88.3%)

211.1 hits per line

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

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

3
declare(strict_types=1);
4

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

14
namespace CodeIgniter\Images\Handlers;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17
use CodeIgniter\Images\Exceptions\ImageException;
18
use CodeIgniter\Images\Image;
19
use CodeIgniter\Images\ImageHandlerInterface;
20
use Config\Images;
21

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

34
    /**
35
     * The image/file instance
36
     *
37
     * @var Image|null
38
     */
39
    protected $image;
40

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

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

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

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

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

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

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

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

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

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

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

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

152
        $this->image = new Image($path, true);
80✔
153

154
        $this->image->getProperties(false);
79✔
155
        $this->width  = $this->image->origWidth;
78✔
156
        $this->height = $this->image->origHeight;
78✔
157

158
        return $this;
78✔
159
    }
160

161
    /**
162
     * Make the image resource object if needed
163
     *
164
     * @return void
165
     */
166
    abstract protected function ensureResource();
167

168
    /**
169
     * Returns the image instance.
170
     *
171
     * @return Image
172
     */
173
    public function getFile()
174
    {
175
        return $this->image;
7✔
176
    }
177

178
    /**
179
     * Verifies that a file has been supplied and it is an image.
180
     *
181
     * @phpstan-assert Image $this->image
182
     *
183
     * @throws ImageException
184
     */
185
    protected function image(): Image
186
    {
187
        if ($this->verified) {
69✔
188
            return $this->image;
64✔
189
        }
190

191
        // Verify withFile has been called
192
        if ($this->image === null) {
69✔
193
            throw ImageException::forMissingImage();
2✔
194
        }
195

196
        // Verify the loaded image is an Image instance
197
        if (! $this->image instanceof Image) {
67✔
198
            throw ImageException::forInvalidPath();
×
199
        }
200

201
        // File::__construct has verified the file exists - make sure it is an image
202
        if (! is_int($this->image->imageType)) {
67✔
203
            throw ImageException::forFileNotSupported();
×
204
        }
205

206
        // Note that the image has been verified
207
        $this->verified = true;
67✔
208

209
        return $this->image;
67✔
210
    }
211

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

223
        return $this->resource;
5✔
224
    }
225

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

237
        return $this;
2✔
238
    }
239

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

254
        $this->width  = $width;
16✔
255
        $this->height = $height;
16✔
256

257
        if ($maintainRatio) {
16✔
258
            $this->masterDim = $masterDim;
6✔
259
            $this->reproportion();
6✔
260
        }
261

262
        return $this->_resize($maintainRatio);
16✔
263
    }
264

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

282
        if ($maintainRatio) {
20✔
283
            $this->masterDim = $masterDim;
2✔
284
            $this->reproportion();
2✔
285
        }
286

287
        $result = $this->_crop();
20✔
288

289
        $this->xAxis = null;
20✔
290
        $this->yAxis = null;
20✔
291

292
        return $result;
20✔
293
    }
294

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

307
        $this->image()->imageType = $imageType;
3✔
308

309
        return $this;
3✔
310
    }
311

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

326
        if (! in_array($angle, $degs, true)) {
8✔
327
            throw ImageException::forMissingAngle();
2✔
328
        }
329

330
        // cast angle as an int, for our use
331
        $angle = (int) $angle;
6✔
332

333
        // Reassign the width and height
334
        if ($angle === 90 || $angle === 270) {
6✔
335
            $temp         = $this->height;
6✔
336
            $this->width  = $this->height;
6✔
337
            $this->height = $temp;
6✔
338
        }
339

340
        // Call the Handler-specific version.
341
        $this->_rotate($angle);
6✔
342

343
        return $this;
6✔
344
    }
345

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

356
        return $this->_flatten($red, $green, $blue);
2✔
357
    }
358

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

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

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

386
        if ($dir !== 'vertical' && $dir !== 'horizontal') {
12✔
387
            throw ImageException::forInvalidDirection($dir);
2✔
388
        }
389

390
        return $this->_flip($dir);
10✔
391
    }
392

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

401
    public function text(string $text, array $options = [])
402
    {
403
        $options                = array_merge($this->textDefaults, $options);
7✔
404
        $options['color']       = trim($options['color'], '# ');
7✔
405
        $options['shadowColor'] = trim($options['shadowColor'], '# ');
7✔
406

407
        $this->_text($text, $options);
7✔
408

409
        return $this;
7✔
410
    }
411

412
    /**
413
     * Handler-specific method for overlaying text on an image.
414
     *
415
     * @param array{
416
     *     color?: string,
417
     *     shadowColor?: string,
418
     *     hAlign?: string,
419
     *     vAlign?: string,
420
     *     hOffset?: int,
421
     *     vOffset?: int,
422
     *     fontPath?: string,
423
     *     fontSize?: int,
424
     *     shadowOffset?: int,
425
     *     opacity?: float,
426
     *     padding?: int,
427
     *     withShadow?: bool|string
428
     * } $options
429
     *
430
     * @return void
431
     */
432
    abstract protected function _text(string $text, array $options = []);
433

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

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

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

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

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

476
        return match ($orientation) {
2✔
477
            2       => $this->flip('horizontal'),
2✔
478
            3       => $this->rotate(180),
2✔
479
            4       => $this->rotate(180)->flip('horizontal'),
2✔
480
            5       => $this->rotate(270)->flip('horizontal'),
2✔
481
            6       => $this->rotate(270),
2✔
482
            7       => $this->rotate(90)->flip('horizontal'),
2✔
483
            8       => $this->rotate(90),
2✔
484
            default => $this,
2✔
485
        };
2✔
486
    }
487

488
    /**
489
     * Retrieve the EXIF information from the image, if possible. Returns
490
     * an array of the information, or null if nothing can be found.
491
     *
492
     * EXIF data is only supported fr JPEG & TIFF formats.
493
     *
494
     * @param string|null $key    If specified, will only return this piece of EXIF data.
495
     * @param bool        $silent If true, will not throw our own exceptions.
496
     *
497
     * @return mixed
498
     *
499
     * @throws ImageException
500
     */
501
    public function getEXIF(?string $key = null, bool $silent = false)
502
    {
503
        if (! function_exists('exif_read_data')) {
4✔
504
            if ($silent) {
×
505
                return null;
×
506
            }
507

508
            throw ImageException::forEXIFUnsupported(); // @codeCoverageIgnore
509
        }
510

511
        $exif = null; // default
4✔
512

513
        switch ($this->image()->imageType) {
4✔
514
            case IMAGETYPE_JPEG:
4✔
515
            case IMAGETYPE_TIFF_II:
×
516
                $exif = @exif_read_data($this->image()->getPathname());
4✔
517
                if ($key !== null && is_array($exif)) {
4✔
518
                    $exif = $exif[$key] ?? false;
4✔
519
                }
520
        }
521

522
        return $exif;
4✔
523
    }
524

525
    /**
526
     * Combine cropping and resizing into a single command.
527
     *
528
     * Supported positions:
529
     *  - top-left
530
     *  - top
531
     *  - top-right
532
     *  - left
533
     *  - center
534
     *  - right
535
     *  - bottom-left
536
     *  - bottom
537
     *  - bottom-right
538
     *
539
     * @return $this
540
     */
541
    public function fit(int $width, ?int $height = null, string $position = 'center')
542
    {
543
        $origWidth  = $this->image()->origWidth;
8✔
544
        $origHeight = $this->image()->origHeight;
8✔
545

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

548
        if ($height === null) {
8✔
549
            $height = (int) ceil(($width / $cropWidth) * $cropHeight);
2✔
550
        }
551

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

554
        return $this->crop($cropWidth, $cropHeight, (int) $x, (int) $y)->resize($width, $height);
8✔
555
    }
556

557
    /**
558
     * Calculate image aspect ratio.
559
     *
560
     * @param float|int      $width
561
     * @param float|int|null $height
562
     * @param float|int      $origWidth
563
     * @param float|int      $origHeight
564
     */
565
    protected function calcAspectRatio($width, $height = null, $origWidth = 0, $origHeight = 0): array
566
    {
567
        if (empty($origWidth) || empty($origHeight)) {
8✔
568
            throw new InvalidArgumentException('You must supply the parameters: origWidth, origHeight.');
×
569
        }
570

571
        // If $height is null, then we have it easy.
572
        // Calc based on full image size and be done.
573
        if ($height === null) {
8✔
574
            $height = ($width / $origWidth) * $origHeight;
2✔
575

576
            return [
2✔
577
                $width,
2✔
578
                (int) $height,
2✔
579
            ];
2✔
580
        }
581

582
        $xRatio = $width / $origWidth;
6✔
583
        $yRatio = $height / $origHeight;
6✔
584

585
        if ($xRatio > $yRatio) {
6✔
586
            return [
4✔
587
                $origWidth,
4✔
588
                (int) ($origWidth * $height / $width),
4✔
589
            ];
4✔
590
        }
591

592
        return [
3✔
593
            (int) ($origHeight * $width / $height),
3✔
594
            $origHeight,
3✔
595
        ];
3✔
596
    }
597

598
    /**
599
     * Based on the position, will determine the correct x/y coords to
600
     * crop the desired portion from the image.
601
     *
602
     * @param float|int $width
603
     * @param float|int $height
604
     * @param float|int $origWidth
605
     * @param float|int $origHeight
606
     * @param string    $position
607
     */
608
    protected function calcCropCoords($width, $height, $origWidth, $origHeight, $position): array
609
    {
610
        $position = strtolower($position);
8✔
611

612
        $x = $y = 0;
8✔
613

614
        switch ($position) {
615
            case 'top-left':
8✔
616
                $x = 0;
2✔
617
                $y = 0;
2✔
618
                break;
2✔
619

620
            case 'top':
8✔
621
                $x = floor(($origWidth - $width) / 2);
2✔
622
                $y = 0;
2✔
623
                break;
2✔
624

625
            case 'top-right':
8✔
626
                $x = $origWidth - $width;
2✔
627
                $y = 0;
2✔
628
                break;
2✔
629

630
            case 'left':
8✔
631
                $x = 0;
2✔
632
                $y = floor(($origHeight - $height) / 2);
2✔
633
                break;
2✔
634

635
            case 'center':
8✔
636
                $x = floor(($origWidth - $width) / 2);
8✔
637
                $y = floor(($origHeight - $height) / 2);
8✔
638
                break;
8✔
639

640
            case 'right':
2✔
641
                $x = ($origWidth - $width);
2✔
642
                $y = floor(($origHeight - $height) / 2);
2✔
643
                break;
2✔
644

645
            case 'bottom-left':
2✔
646
                $x = 0;
2✔
647
                $y = $origHeight - $height;
2✔
648
                break;
2✔
649

650
            case 'bottom':
2✔
651
                $x = floor(($origWidth - $width) / 2);
2✔
652
                $y = $origHeight - $height;
2✔
653
                break;
2✔
654

655
            case 'bottom-right':
2✔
656
                $x = ($origWidth - $width);
2✔
657
                $y = $origHeight - $height;
2✔
658
                break;
2✔
659
        }
660

661
        return [
8✔
662
            $x,
8✔
663
            $y,
8✔
664
        ];
8✔
665
    }
666

667
    /**
668
     * Get the version of the image library in use.
669
     *
670
     * @return string
671
     */
672
    abstract public function getVersion();
673

674
    /**
675
     * Saves any changes that have been made to file.
676
     *
677
     * Example:
678
     *    $image->resize(100, 200, true)
679
     *          ->save($target);
680
     *
681
     * @param non-empty-string|null $target
682
     *
683
     * @return bool
684
     */
685
    abstract public function save(?string $target = null, int $quality = 90);
686

687
    /**
688
     * Does the driver-specific processing of the image.
689
     *
690
     * @return mixed
691
     */
692
    abstract protected function process(string $action);
693

694
    /**
695
     * Provide access to the Image class' methods if they don't exist
696
     * on the handler itself.
697
     *
698
     * @return mixed
699
     */
700
    public function __call(string $name, array $args = [])
701
    {
702
        if (method_exists($this->image(), $name)) {
1✔
703
            return $this->image()->{$name}(...$args);
1✔
704
        }
705

706
        return null;
×
707
    }
708

709
    /**
710
     * Re-proportion Image Width/Height
711
     *
712
     * When creating thumbs, the desired width/height
713
     * can end up warping the image due to an incorrect
714
     * ratio between the full-sized image and the thumb.
715
     *
716
     * This function lets us re-proportion the width/height
717
     * if users choose to maintain the aspect ratio when resizing.
718
     *
719
     * @return void
720
     */
721
    protected function reproportion()
722
    {
723
        $image = $this->image();
9✔
724
        $origW = $image->origWidth;
9✔
725
        $origH = $image->origHeight;
9✔
726
        $w     = $this->width;
9✔
727
        $h     = $this->height;
9✔
728

729
        if ($origW === 0 || $origW === 0.0 || $origH === 0 || $origH === 0.0) {
9✔
730
            return;
1✔
731
        }
732

733
        if (($w === 0 || $w === 0.0) && ($h === 0 || $h === 0.0)) {
8✔
UNCOV
734
            return;
×
735
        }
736

737
        $w     = (int) $w;
8✔
738
        $h     = (int) $h;
8✔
739
        $origW = (int) $origW;
8✔
740
        $origH = (int) $origH;
8✔
741

742
        $this->width  = $w;
8✔
743
        $this->height = $h;
8✔
744

745
        if ($this->masterDim !== 'width' && $this->masterDim !== 'height') {
8✔
746
            $this->masterDim = ($h === 0 || ($w > 0 && (($origH / $origW) - ($h / $w) < 0))) ? 'width' : 'height';
8✔
NEW
747
        } elseif (($this->masterDim === 'width' && $w === 0) || ($this->masterDim === 'height' && $h === 0)) {
×
UNCOV
748
            return;
×
749
        }
750

751
        if ($this->masterDim === 'width') {
8✔
752
            $this->height = (int) ceil($w * $origH / $origW);
4✔
753
        } else {
754
            $this->width = (int) ceil($origW * $h / $origH);
4✔
755
        }
756
    }
757

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

770
    /**
771
     * Return image height.
772
     *
773
     * accessor for testing; not part of interface
774
     *
775
     * @return int
776
     */
777
    public function getHeight()
778
    {
779
        return ($this->resource !== null) ? $this->_getHeight() : $this->height;
50✔
780
    }
781

782
    /**
783
     * Placeholder method for implementing metadata clearing logic.
784
     *
785
     * This method should be implemented to remove or reset metadata as needed.
786
     */
787
    public function clearMetadata(): static
788
    {
789
        return $this;
1✔
790
    }
791
}
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

© 2026 Coveralls, Inc