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

codeigniter4 / CodeIgniter4 / 7298596047

22 Dec 2023 10:01AM UTC coverage: 85.049% (+0.004%) from 85.045%
7298596047

push

github

web-flow
Merge pull request #8359 from kenjis/docs-FileLocatorInterface

[4.5] fix: merge mistake

1 of 1 new or added line in 1 file covered. (100.0%)

147 existing lines in 11 files now uncovered.

19358 of 22761 relevant lines covered (85.05%)

193.82 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
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\Images\Exceptions\ImageException;
17
use CodeIgniter\Images\Image;
18
use CodeIgniter\Images\ImageHandlerInterface;
19
use Config\Images;
20
use InvalidArgumentException;
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();
81✔
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
    /**
400
     * Overlays a string of text over the image.
401
     *
402
     * Valid options:
403
     *
404
     *  - color         Text Color (hex number)
405
     *  - shadowColor   Color of the shadow (hex number)
406
     *  - hAlign        Horizontal alignment: left, center, right
407
     *  - vAlign        Vertical alignment: top, middle, bottom
408
     *  - hOffset
409
     *  - vOffset
410
     *  - fontPath
411
     *  - fontSize
412
     *  - shadowOffset
413
     *
414
     * @return $this
415
     */
416
    public function text(string $text, array $options = [])
417
    {
418
        $options                = array_merge($this->textDefaults, $options);
6✔
419
        $options['color']       = trim($options['color'], '# ');
6✔
420
        $options['shadowColor'] = trim($options['shadowColor'], '# ');
6✔
421

422
        $this->_text($text, $options);
6✔
423

424
        return $this;
6✔
425
    }
426

427
    /**
428
     * Handler-specific method for overlaying text on an image.
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
        switch ($orientation) {
477
            case 2:
2✔
478
                return $this->flip('horizontal');
2✔
479

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

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

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

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

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

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

498
            default:
499
                return $this;
2✔
500
        }
501
    }
502

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

523
            throw ImageException::forEXIFUnsupported(); // @codeCoverageIgnore
524
        }
525

526
        $exif = null; // default
4✔
527

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

537
        return $exif;
4✔
538
    }
539

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

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

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

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

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

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

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

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

597
        $xRatio = $width / $origWidth;
6✔
598
        $yRatio = $height / $origHeight;
6✔
599

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

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

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

627
        $x = $y = 0;
8✔
628

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

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

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

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

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

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

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

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

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

676
        return [
8✔
677
            $x,
8✔
678
            $y,
8✔
679
        ];
8✔
680
    }
681

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

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

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

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

722
    /**
723
     * Re-proportion Image Width/Height
724
     *
725
     * When creating thumbs, the desired width/height
726
     * can end up warping the image due to an incorrect
727
     * ratio between the full-sized image and the thumb.
728
     *
729
     * This function lets us re-proportion the width/height
730
     * if users choose to maintain the aspect ratio when resizing.
731
     *
732
     * @return void
733
     */
734
    protected function reproportion()
735
    {
736
        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✔
UNCOV
737
            return;
×
738
        }
739

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

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

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

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

774
    /**
775
     * Return image height.
776
     *
777
     * accessor for testing; not part of interface
778
     *
779
     * @return int
780
     */
781
    public function getHeight()
782
    {
783
        return ($this->resource !== null) ? $this->_getHeight() : $this->height;
48✔
784
    }
785
}
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