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

codeigniter4 / CodeIgniter4 / 12855700662

19 Jan 2025 05:28PM UTC coverage: 84.546% (+0.08%) from 84.469%
12855700662

push

github

web-flow
Merge pull request #9417 from codeigniter4/4.6

4.6.0 Merge code

720 of 841 new or added lines in 68 files covered. (85.61%)

8 existing lines in 6 files now uncovered.

20811 of 24615 relevant lines covered (84.55%)

191.24 hits per line

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

93.89
/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
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();
84✔
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
     * @return $this
142
     */
143
    public function withFile(string $path)
144
    {
145
        // Clear out the old resource so that
146
        // it doesn't try to use a previous image
147
        $this->resource = null;
74✔
148
        $this->verified = false;
74✔
149

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

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

156
        return $this;
72✔
157
    }
158

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

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

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

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

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

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

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

207
        return $this->image;
62✔
208
    }
209

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

221
        return $this->resource;
6✔
222
    }
223

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

235
        return $this;
2✔
236
    }
237

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

252
        $this->width  = $width;
15✔
253
        $this->height = $height;
15✔
254

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

260
        return $this->_resize($maintainRatio);
15✔
261
    }
262

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

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

285
        $result = $this->_crop();
20✔
286

287
        $this->xAxis = null;
20✔
288
        $this->yAxis = null;
20✔
289

290
        return $result;
20✔
291
    }
292

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

305
        $this->image()->imageType = $imageType;
3✔
306

307
        return $this;
3✔
308
    }
309

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

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

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

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

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

341
        return $this;
6✔
342
    }
343

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

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

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

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

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

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

388
        return $this->_flip($dir);
10✔
389
    }
390

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

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

405
        $this->_text($text, $options);
6✔
406

407
        return $this;
6✔
408
    }
409

410
    /**
411
     * Handler-specific method for overlaying text on an image.
412
     *
413
     * @param array{
414
     *     color?: string,
415
     *     shadowColor?: string,
416
     *     hAlign?: string,
417
     *     vAlign?: string,
418
     *     hOffset?: int,
419
     *     vOffset?: int,
420
     *     fontPath?: string,
421
     *     fontSize?: int,
422
     *     shadowOffset?: int,
423
     *     opacity?: float,
424
     *     padding?: int,
425
     *     withShadow?: bool|string
426
     * } $options
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
        return match ($orientation) {
2✔
475
            2       => $this->flip('horizontal'),
2✔
476
            3       => $this->rotate(180),
2✔
477
            4       => $this->rotate(180)->flip('horizontal'),
2✔
478
            5       => $this->rotate(270)->flip('horizontal'),
2✔
479
            6       => $this->rotate(270),
2✔
480
            7       => $this->rotate(90)->flip('horizontal'),
2✔
481
            8       => $this->rotate(90),
2✔
482
            default => $this,
2✔
483
        };
2✔
484
    }
485

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

506
            throw ImageException::forEXIFUnsupported(); // @codeCoverageIgnore
×
507
        }
508

509
        $exif = null; // default
4✔
510

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

520
        return $exif;
4✔
521
    }
522

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

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

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

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

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

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

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

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

580
        $xRatio = $width / $origWidth;
6✔
581
        $yRatio = $height / $origHeight;
6✔
582

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

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

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

610
        $x = $y = 0;
8✔
611

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

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

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

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

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

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

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

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

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

659
        return [
8✔
660
            $x,
8✔
661
            $y,
8✔
662
        ];
8✔
663
    }
664

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

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

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

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

NEW
704
        return null;
×
705
    }
706

707
    /**
708
     * Re-proportion Image Width/Height
709
     *
710
     * When creating thumbs, the desired width/height
711
     * can end up warping the image due to an incorrect
712
     * ratio between the full-sized image and the thumb.
713
     *
714
     * This function lets us re-proportion the width/height
715
     * if users choose to maintain the aspect ratio when resizing.
716
     *
717
     * @return void
718
     */
719
    protected function reproportion()
720
    {
721
        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✔
722
            return;
×
723
        }
724

725
        // Sanitize
726
        $this->width  = (int) $this->width;
8✔
727
        $this->height = (int) $this->height;
8✔
728

729
        if ($this->masterDim !== 'width' && $this->masterDim !== 'height') {
8✔
730
            if ($this->width > 0 && $this->height > 0) {
8✔
731
                $this->masterDim = ((($this->image()->origHeight / $this->image()->origWidth) - ($this->height / $this->width)) < 0) ? 'width' : 'height';
4✔
732
            } else {
733
                $this->masterDim = ($this->height === 0) ? 'width' : 'height';
4✔
734
            }
735
        } elseif (($this->masterDim === 'width' && $this->width === 0) || ($this->masterDim === 'height' && $this->height === 0)
×
736
        ) {
737
            return;
×
738
        }
739

740
        if ($this->masterDim === 'width') {
8✔
741
            $this->height = (int) ceil($this->width * $this->image()->origHeight / $this->image()->origWidth);
4✔
742
        } else {
743
            $this->width = (int) ceil($this->image()->origWidth * $this->height / $this->image()->origHeight);
4✔
744
        }
745
    }
746

747
    /**
748
     * Return image width.
749
     *
750
     * accessor for testing; not part of interface
751
     *
752
     * @return int
753
     */
754
    public function getWidth()
755
    {
756
        return ($this->resource !== null) ? $this->_getWidth() : $this->width;
48✔
757
    }
758

759
    /**
760
     * Return image height.
761
     *
762
     * accessor for testing; not part of interface
763
     *
764
     * @return int
765
     */
766
    public function getHeight()
767
    {
768
        return ($this->resource !== null) ? $this->_getHeight() : $this->height;
48✔
769
    }
770
}
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