Issues (31)

src/formatter/Image.php (1 issue)

Labels
Severity
1
<?php
2
declare(strict_types=1);
3
4
namespace tkanstantsin\fileupload\formatter;
5
6
use Imagine\Image\Box;
7
use Imagine\Image\BoxInterface;
8
use Imagine\Image\ImageInterface;
9
use Imagine\Image\ImagineInterface;
10
use Imagine\Image\Palette\RGB as RGBPalette;
11
use Imagine\Image\Point;
12
use tkanstantsin\fileupload\config\InvalidConfigException;
13
14
/**
15
 * Class ImageProcessor
16
 *
17
 * @todo: add min width|height options.
18
 */
19
class Image extends File
20
{
21
    /**
22
     * Like background `cover` in css.
23
     */
24
    public const RESIZE_OUTBOUND = ImageInterface::THUMBNAIL_OUTBOUND;
25
    /**
26
     * Like background `contain` in css.
27
     */
28
    public const RESIZE_INSET = ImageInterface::THUMBNAIL_INSET;
29
    /**
30
     * Means that image may be smaller than defined in config, never bigger.
31
     */
32
    public const RESIZE_INSET_KEEP_RATIO = 'inset_keep_ratio';
33
34
    public const DEFAULT_EXTENSION = 'jpg';
35
36
    /**
37
     * @see ImagineFactory::get()
38
     * @var string
39
     */
40
    public $driver = ImagineFactory::DEFAULT_DRIVER;
41
42
    /**
43
     * @var int
44
     */
45
    public $width;
46
    /**
47
     * @var int
48
     */
49
    public $height;
50
51
    /**
52
     * Used when defined only height as upper limit for width
53
     * @todo: implement in Image::createBox() method.
54
     * @var int
55
     */
56
    public $maxWidth;
57
    /**
58
     * Used when defined only widith as upper limit for height
59
     * @var int
60
     */
61
    public $maxHeight;
62
63
    /**
64
     * Used for jpg images which may be png originally and have transparency.
65
     * @var string
66
     */
67
    public $transparentBackground = 'ffffff';
68
69
    /**
70
     * @var string
71
     */
72
    public $mode = self::RESIZE_INSET;
73
    /**
74
     * Whether image must keep aspect ration when used inset mote.
75
     * Means that image would may be smaller than smaller
76
     * @todo: implement it.
77
     * @var bool
78
     */
79
    public $keepRatio = true;
80
81
    /**
82
     * @var ImagineInterface
83
     */
84
    protected $imagine;
85
86
    /**
87
     * @inheritdoc
88
     * @throws \Imagine\Exception\RuntimeException
89
     */
90
    public function init(): void
91
    {
92
        parent::init();
93
94
        $this->imagine = ImagineFactory::get($this->driver);
95
96
        if ($this->width !== null && $this->maxWidth !== null) {
97
            throw new InvalidConfigException('`width` and `maxWidth` cannot be defined at the same time');
98
        }
99
        if ($this->height !== null && $this->maxHeight !== null) {
100
            throw new InvalidConfigException('`height` and `maxHeight` cannot be defined at the same time');
101
        }
102
    }
103
104
    /**
105
     * @todo: add check for metadata with `exif_read_data` method.
106
     * @see http://php.net/manual/en/function.exif-read-data.php
107
     * @inheritdoc
108
     * @throws \Imagine\Exception\OutOfBoundsException
109
     * @throws \UnexpectedValueException
110
     * @throws \Imagine\Exception\InvalidArgumentException
111
     * @throws \Imagine\Exception\RuntimeException
112
     */
113
    protected function getContentInternal()
114
    {
115
        $image = $this->imagine->read(parent::getContentInternal());
0 ignored issues
show
It seems like parent::getContentInternal() can also be of type false; however, parameter $resource of Imagine\Image\ImagineInterface::read() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

115
        $image = $this->imagine->read(/** @scrutinizer ignore-type */ parent::getContentInternal());
Loading history...
116
        $image = $this->format($image);
117
118
        return $image->get($this->getExtension());
119
    }
120
121
    /**
122
     * @return string
123
     */
124
    protected function getExtension(): string
125
    {
126
        return mb_strtolower($this->file->getExtension() ?? self::DEFAULT_EXTENSION);
127
    }
128
129
    /**
130
     * @param ImageInterface $image
131
     * @return ImageInterface
132
     * @throws \Imagine\Exception\OutOfBoundsException
133
     * @throws \UnexpectedValueException
134
     * @throws \Imagine\Exception\RuntimeException
135
     * @throws \Imagine\Exception\InvalidArgumentException
136
     */
137
    protected function format(ImageInterface $image): ImageInterface
138
    {
139
        $image = $this->resize($image);
140
        $image = $this->setBackground($image);
141
142
        return $image;
143
    }
144
145
    /**
146
     * @param ImageInterface $image
147
     * @return ImageInterface
148
     * @throws \Imagine\Exception\RuntimeException
149
     * @throws \UnexpectedValueException
150
     * @throws \Imagine\Exception\InvalidArgumentException
151
     */
152
    protected function resize(ImageInterface $image): ImageInterface
153
    {
154
        $originBox = $image->getSize();
155
        $newBox = $this->createBox($originBox);
156
        if ($this->areBoxesEqual($originBox, $newBox)) {
157
            return $image;
158
        }
159
160
        switch ($this->mode) {
161
            case self::RESIZE_OUTBOUND:
162
            case self::RESIZE_INSET:
163
                return $image->thumbnail($newBox, $this->mode);
164
            case self::RESIZE_INSET_KEEP_RATIO:
165
                // TODO: implement new resize mode.
166
                throw new \UnexpectedValueException(sprintf('Resize mode `%s` not supported yet', $this->mode));
167
            default:
168
                throw new \UnexpectedValueException(sprintf('Image resize mode `%s` not defined', $this->mode));
169
        }
170
    }
171
172
    /**
173
     * @param BoxInterface $actualBox
174
     * @return BoxInterface
175
     */
176
    protected function createBox(BoxInterface $actualBox): BoxInterface
177
    {
178
        // TODO: check resize modes.
179
        if ($this->width !== null && $this->height !== null) {
180
            return new Box($this->width, $this->height);
181
        }
182
183
        $box = clone $actualBox;
184
        if ($this->width !== null) {
185
            $box = $box->widen($this->width);
186
        }
187
        if ($this->height !== null) {
188
            $box = $box->heighten($this->height);
189
        }
190
191
        if ($this->maxWidth !== null && $this->maxWidth < $box->getWidth()) {
192
            $box = $box->widen($this->maxWidth);
193
        }
194
        if ($this->maxHeight !== null && $this->maxHeight < $box->getHeight()) {
195
            $box = $box->heighten($this->maxHeight);
196
        }
197
198
        return $box;
199
    }
200
201
    /**
202
     * Add Image::transparentBackground color behind image.
203
     * If original image was of PNG type but stored with jpg extension, or it
204
     * must be converted
205
     *
206
     * @param ImageInterface $image
207
     * @return ImageInterface
208
     * @throws \Imagine\Exception\OutOfBoundsException
209
     * @throws \Imagine\Exception\RuntimeException
210
     * @throws \Imagine\Exception\InvalidArgumentException
211
     */
212
    protected function setBackground(ImageInterface $image): ImageInterface
213
    {
214
        if (!\in_array($this->getExtension(), ['jpg', 'jpeg'], true)) {
215
            return $image;
216
        }
217
218
        $palette = new RGBPalette();
219
        $backgroundColor = $palette->color($this->transparentBackground, 100);
220
        $background = $this->imagine->create($image->getSize(), $backgroundColor);
221
222
        return $background->paste($image, new Point(0, 0));
223
    }
224
225
    /**
226
     * @param BoxInterface $a
227
     * @param BoxInterface $b
228
     * @return bool
229
     */
230
    protected function areBoxesEqual(BoxInterface $a, BoxInterface $b): bool
231
    {
232
        return $a->getWidth() === $b->getWidth()
233
            && $a->getHeight() === $b->getHeight();
234
    }
235
}