Apply PSR-12 code style (#3886)
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / image / adapter / ImageAdapter.class.php
1 <?php
2
3 namespace wcf\system\image\adapter;
4
5 use wcf\system\exception\SystemException;
6 use wcf\util\FileUtil;
7
8 /**
9 * Wrapper for image adapters.
10 *
11 * @author Alexander Ebert
12 * @copyright 2001-2019 WoltLab GmbH
13 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
14 * @package WoltLabSuite\Core\System\Image\Adapter
15 */
16 class ImageAdapter implements IImageAdapter, IMemoryAwareImageAdapter
17 {
18 /**
19 * IImageAdapter object
20 * @var IImageAdapter
21 */
22 protected $adapter;
23
24 /**
25 * supported relative positions
26 * @var string[]
27 */
28 protected $relativePositions = [
29 'topLeft',
30 'topCenter',
31 'topRight',
32 'middleLeft',
33 'middleCenter',
34 'middleRight',
35 'bottomLeft',
36 'bottomCenter',
37 'bottomRight',
38 ];
39
40 /**
41 * Creates a new ImageAdapter instance.
42 *
43 * @param string $adapterClassName
44 */
45 public function __construct($adapterClassName)
46 {
47 $this->adapter = new $adapterClassName();
48 }
49
50 /**
51 * @inheritDoc
52 */
53 public function load($image, $type = 0)
54 {
55 $this->adapter->load($image, $type);
56 }
57
58 /**
59 * @inheritDoc
60 */
61 public function loadFile($file)
62 {
63 if (!\file_exists($file) || !\is_readable($file)) {
64 throw new SystemException("Image '" . $file . "' is not readable or does not exists.");
65 }
66
67 $this->adapter->loadFile($file);
68 }
69
70 /**
71 * @inheritDoc
72 */
73 public function createEmptyImage($width, $height)
74 {
75 $this->adapter->createEmptyImage($width, $height);
76 }
77
78 /**
79 * @inheritDoc
80 */
81 public function createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio = true)
82 {
83 if ($maxWidth > $this->getWidth() && $maxHeight > $this->getHeight()) {
84 throw new SystemException("Dimensions for thumbnail can not exceed image dimensions.");
85 }
86
87 $maxHeight = \min($maxHeight, $this->getHeight());
88 $maxWidth = \min($maxWidth, $this->getWidth());
89
90 return $this->adapter->createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio);
91 }
92
93 /**
94 * @inheritDoc
95 */
96 public function clip($originX, $originY, $width, $height)
97 {
98 // validate if coordinates and size are within bounds
99 if ($originX < 0 || $originY < 0) {
100 throw new SystemException("Clipping an image requires valid offsets, an offset below zero is invalid.");
101 }
102 if ($width <= 0 || $height <= 0) {
103 throw new SystemException(
104 "Clipping an image requires valid dimensions, width or height below or equal zero are invalid."
105 );
106 }
107 if ((($originX + $width) > $this->getWidth()) || (($originY + $height) > $this->getHeight())) {
108 throw new SystemException("Offset and dimension can not exceed image dimensions.");
109 }
110
111 $this->adapter->clip($originX, $originY, $width, $height);
112 }
113
114 /**
115 * @inheritDoc
116 */
117 public function resize($originX, $originY, $originWidth, $originHeight, $targetWidth, $targetHeight)
118 {
119 // use origin dimensions if target dimensions are both zero
120 if ($targetWidth == 0 && $targetHeight == 0) {
121 $targetWidth = $originWidth;
122 $targetHeight = $originHeight;
123 }
124
125 $this->adapter->resize($originX, $originY, $originWidth, $originHeight, $targetWidth, $targetHeight);
126 }
127
128 /**
129 * @inheritDoc
130 */
131 public function drawRectangle($startX, $startY, $endX, $endY)
132 {
133 if (!$this->adapter->hasColor()) {
134 throw new SystemException("Cannot draw a rectangle unless a color has been specified with setColor().");
135 }
136
137 $this->adapter->drawRectangle($startX, $startY, $endX, $endY);
138 }
139
140 /**
141 * @inheritDoc
142 */
143 public function drawText($text, $x, $y, $font, $size, $opacity = 1.0)
144 {
145 if (!$this->adapter->hasColor()) {
146 throw new SystemException("Cannot draw text unless a color has been specified with setColor().");
147 }
148
149 // validate opacity
150 if ($opacity < 0 || $opacity > 1) {
151 throw new SystemException("Invalid opacity value given.");
152 }
153
154 $this->adapter->drawText($text, $x, $y, $font, $size, $opacity);
155 }
156
157 /**
158 * @inheritDoc
159 */
160 public function drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity = 1.0)
161 {
162 if (!$this->adapter->hasColor()) {
163 throw new SystemException("Cannot draw text unless a color has been specified with setColor().");
164 }
165
166 // validate position
167 if (!\in_array($position, $this->relativePositions)) {
168 throw new SystemException("Unknown relative position '" . $position . "'.");
169 }
170
171 // validate margin
172 if ($margin < 0 || $margin >= $this->getHeight() / 2 || $margin >= $this->getWidth() / 2) {
173 throw new SystemException("Margin has to be positive and respect image dimensions.");
174 }
175
176 // validate opacity
177 if ($opacity < 0 || $opacity > 1) {
178 throw new SystemException("Invalid opacity value given.");
179 }
180
181 $this->adapter->drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity);
182 }
183
184 /**
185 * @inheritDoc
186 */
187 public function textFitsImage($text, $margin, $font, $size)
188 {
189 return $this->adapter->textFitsImage($text, $margin, $font, $size);
190 }
191
192 /**
193 * @inheritDoc
194 */
195 public function adjustFontSize($text, $margin, $font, $size)
196 {
197 // adjust font size
198 while ($size && !$this->textFitsImage($text, $margin, $font, $size)) {
199 $size--;
200 }
201
202 return $size;
203 }
204
205 /**
206 * @inheritDoc
207 */
208 public function setColor($red, $green, $blue)
209 {
210 $this->adapter->setColor($red, $green, $blue);
211 }
212
213 /**
214 * @inheritDoc
215 */
216 public function hasColor()
217 {
218 return $this->adapter->hasColor();
219 }
220
221 /**
222 * @inheritDoc
223 */
224 public function setTransparentColor($red, $green, $blue)
225 {
226 $this->adapter->setTransparentColor($red, $green, $blue);
227 }
228
229 /**
230 * @inheritDoc
231 */
232 public function writeImage($image, $filename = null)
233 {
234 if ($filename === null) {
235 $filename = $image;
236 $image = $this->adapter->getImage();
237 }
238
239 $this->adapter->writeImage($image, $filename);
240 }
241
242 /**
243 * @inheritDoc
244 */
245 public function getImage()
246 {
247 return $this->adapter->getImage();
248 }
249
250 /**
251 * @inheritDoc
252 */
253 public function getWidth()
254 {
255 return $this->adapter->getWidth();
256 }
257
258 /**
259 * @inheritDoc
260 */
261 public function getHeight()
262 {
263 return $this->adapter->getHeight();
264 }
265
266 /**
267 * @inheritDoc
268 */
269 public function getType()
270 {
271 return $this->adapter->getType();
272 }
273
274 /**
275 * @inheritDoc
276 */
277 public function rotate($degrees)
278 {
279 if ($degrees > 360.0 || $degrees < 0.0) {
280 throw new SystemException("Degrees must be a value between 0 and 360.");
281 }
282
283 return $this->adapter->rotate($degrees);
284 }
285
286 /**
287 * @inheritDoc
288 */
289 public function overlayImage($file, $x, $y, $opacity)
290 {
291 // validate file
292 if (!\file_exists($file)) {
293 throw new SystemException("Image '" . $file . "' does not exist.");
294 }
295
296 // validate opacity
297 if ($opacity < 0 || $opacity > 1) {
298 throw new SystemException("Invalid opacity value given.");
299 }
300
301 $this->adapter->overlayImage($file, $x, $y, $opacity);
302 }
303
304 /**
305 * @inheritDoc
306 */
307 public function overlayImageRelative($file, $position, $margin, $opacity)
308 {
309 // validate file
310 if (!\file_exists($file)) {
311 throw new SystemException("Image '" . $file . "' does not exist.");
312 }
313
314 // validate position
315 if (!\in_array($position, $this->relativePositions)) {
316 throw new SystemException("Unknown relative position '" . $position . "'.");
317 }
318
319 // validate margin
320 if ($margin < 0 || $margin >= $this->getHeight() / 2 || $margin >= $this->getWidth() / 2) {
321 throw new SystemException("Margin has to be positive and respect image dimensions.");
322 }
323
324 // validate opacity
325 if ($opacity < 0 || $opacity > 1) {
326 throw new SystemException("Invalid opacity value given.");
327 }
328
329 $adapterClassName = \get_class($this->adapter);
330
331 /** @var IImageAdapter $overlayImage */
332 $overlayImage = new $adapterClassName();
333 $overlayImage->loadFile($file);
334 $overlayHeight = $overlayImage->getHeight();
335 $overlayWidth = $overlayImage->getWidth();
336
337 // calculate y coordinate
338 $x = 0;
339 switch ($position) {
340 case 'topLeft':
341 case 'middleLeft':
342 case 'bottomLeft':
343 $x = $margin;
344 break;
345
346 case 'topCenter':
347 case 'middleCenter':
348 case 'bottomCenter':
349 $x = \floor(($this->getWidth() - $overlayWidth) / 2);
350 break;
351
352 case 'topRight':
353 case 'middleRight':
354 case 'bottomRight':
355 $x = $this->getWidth() - $overlayWidth - $margin;
356 break;
357 }
358
359 // calculate y coordinate
360 $y = 0;
361 switch ($position) {
362 case 'topLeft':
363 case 'topCenter':
364 case 'topRight':
365 $y = $margin;
366 break;
367
368 case 'middleLeft':
369 case 'middleCenter':
370 case 'middleRight':
371 $y = \floor(($this->getHeight() - $overlayHeight) / 2);
372 break;
373
374 case 'bottomLeft':
375 case 'bottomCenter':
376 case 'bottomRight':
377 $y = $this->getHeight() - $overlayHeight - $margin;
378 break;
379 }
380
381 $this->overlayImage($file, $x, $y, $opacity);
382 }
383
384 /**
385 * @inheritDoc
386 */
387 public function checkMemoryLimit($width, $height, $mimeType)
388 {
389 if ($this->adapter instanceof IMemoryAwareImageAdapter) {
390 return $this->adapter->checkMemoryLimit($width, $height, $mimeType);
391 }
392
393 $channels = $mimeType == 'image/png' ? 4 : 3;
394
395 return FileUtil::checkMemoryLimit($width * $height * $channels * 2.1);
396 }
397
398 /**
399 * @inheritDoc
400 */
401 public function saveImageAs($image, string $filename, string $type, int $quality = 100): void
402 {
403 switch ($type) {
404 case "gif":
405 case "jpg":
406 case "jpeg":
407 case "png":
408 case "webp":
409 break;
410
411 default:
412 throw new \InvalidArgumentException("Unsupported image format '{$type}'.");
413 }
414
415 if ($quality < 0 || $quality > 100) {
416 throw new \InvalidArgumentException("The quality must be an integer between 0 and 100.");
417 }
418
419 $this->adapter->saveImageAs($image, $filename, $type, $quality);
420 }
421
422 /**
423 * @inheritDoc
424 */
425 public static function isSupported()
426 {
427 return false;
428 }
429 }