Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / image / adapter / GDImageAdapter.class.php
1 <?php
2 namespace wcf\system\image\adapter;
3 use wcf\system\exception\SystemException;
4 use wcf\util\StringUtil;
5
6 /**
7 * Image adapter for bundled GD imaging library.
8 *
9 * @author Alexander Ebert
10 * @copyright 2001-2019 WoltLab GmbH
11 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
12 * @package WoltLabSuite\Core\System\Image\Adapter
13 */
14 class GDImageAdapter implements IImageAdapter {
15 /**
16 * active color
17 * @var integer
18 */
19 protected $color = null;
20
21 /**
22 * red, green, blue data of the active color
23 * @var array
24 */
25 protected $colorData = [];
26
27 /**
28 * image height
29 * @var integer
30 */
31 protected $height = 0;
32
33 /**
34 * loaded image
35 * @var resource
36 */
37 protected $image = null;
38
39 /**
40 * image type
41 * @var integer
42 */
43 protected $type = 0;
44
45 /**
46 * image width
47 * @var integer
48 */
49 protected $width = 0;
50
51 /**
52 * GDImageAdapter constructor.
53 */
54 public function __construct() {
55 // suppress warnings like "recoverable error: Invalid SOS parameters for sequential JPEG"
56 @ini_set('gd.jpeg_ignore_warning', '1');
57 }
58
59 /**
60 * Returns whether the given image is a valid GD resource / GD object
61 *
62 * @return boolean
63 */
64 public function isImage($image) {
65 return (is_resource($image) && get_resource_type($image) === 'gd') || (is_object($image) && $image instanceof \GdImage);
66 }
67
68 /**
69 * @inheritDoc
70 */
71 public function load($image, $type = '') {
72 if (!$this->isImage($image)) {
73 throw new SystemException("Image resource is invalid.");
74 }
75
76 if (empty($type)) {
77 throw new SystemException("Image type is missing.");
78 }
79
80 $this->image = $image;
81 $this->type = $type;
82
83 $this->height = imagesy($this->image);
84 $this->width = imagesx($this->image);
85 }
86
87 /**
88 * @inheritDoc
89 */
90 public function loadFile($file) {
91 list($this->width, $this->height, $this->type) = getimagesize($file);
92
93 switch ($this->type) {
94 case IMAGETYPE_GIF:
95 $this->image = imagecreatefromgif($file);
96 break;
97
98 case IMAGETYPE_JPEG:
99 // suppress warnings and properly handle errors
100 $this->image = @imagecreatefromjpeg($file);
101 if ($this->image === false) {
102 throw new SystemException("Could not read jpeg image '".$file."'.");
103 }
104 break;
105
106 case IMAGETYPE_PNG:
107 // suppress warnings and properly handle errors
108 $this->image = @imagecreatefrompng($file);
109 if ($this->image === false) {
110 throw new SystemException("Could not read png image '".$file."'.");
111 }
112 break;
113
114 default:
115 throw new SystemException("Could not read image '".$file."', format is not recognized.");
116 break;
117 }
118 }
119
120 /**
121 * @inheritDoc
122 */
123 public function createEmptyImage($width, $height) {
124 $this->image = imagecreate($width, $height);
125 $this->type = IMAGETYPE_PNG;
126 $this->setColor(0xFF, 0xFF, 0xFF);
127 $this->color = null;
128
129 $this->width = $width;
130 $this->height = $height;
131 }
132
133 /**
134 * @inheritDoc
135 */
136 public function createThumbnail($maxWidth, $maxHeight, $preserveAspectRatio = true) {
137 $x = $y = 0;
138 $sourceWidth = $this->width;
139 $sourceHeight = $this->height;
140
141 if ($preserveAspectRatio) {
142 if ($maxWidth / $this->width < $maxHeight / $this->height) {
143 $width = $maxWidth;
144 $height = round($this->height * ($width / $this->width));
145 }
146 else {
147 $height = $maxHeight;
148 $width = round($this->width * ($height / $this->height));
149 }
150 }
151 else {
152 $width = $maxWidth;
153 $height = $maxHeight;
154
155 if ($maxWidth / $this->width < $maxHeight / $this->height) {
156 $cut = (($sourceWidth * ($maxHeight / $this->height)) - $maxWidth) / ($maxHeight / $this->height);
157 $x = ceil($cut / 2);
158 $sourceWidth = $sourceWidth - $x * 2;
159 }
160 else {
161 $cut = (($sourceHeight * ($maxWidth / $this->width)) - $maxHeight) / ($maxWidth / $this->width);
162 $y = ceil($cut / 2);
163 $sourceHeight = $sourceHeight - $y * 2;
164 }
165 }
166
167 // resize image
168 $image = imagecreatetruecolor((int) $width, (int) $height);
169 imagealphablending($image, false);
170 imagecopyresampled($image, $this->image, 0, 0, (int) $x, (int) $y, (int) $width, (int) $height, (int) $sourceWidth, (int) $sourceHeight);
171 imagesavealpha($image, true);
172
173 return $image;
174 }
175
176 /**
177 * @inheritDoc
178 */
179 public function clip($originX, $originY, $width, $height) {
180 $image = imagecreatetruecolor($width, $height);
181 imagealphablending($image, false);
182
183 imagecopy($image, $this->image, 0, 0, (int) $originX, (int) $originY, (int) $width, (int) $height);
184 imagesavealpha($image, true);
185
186 // reload image to update image resource, width and height
187 $this->load($image, $this->type);
188 }
189
190 /**
191 * @inheritDoc
192 */
193 public function resize($originX, $originY, $originWidth, $originHeight, $targetWidth = 0, $targetHeight = 0) {
194 $image = imagecreatetruecolor($targetWidth, $targetHeight);
195 imagealphablending($image, false);
196
197 imagecopyresampled($image, $this->image, 0, 0, (int) $originX, (int) $originY, (int) $targetWidth, (int) $targetHeight, (int) $originWidth, (int) $originHeight);
198 imagesavealpha($image, true);
199
200 // reload image to update image resource, width and height
201 $this->load($image, $this->type);
202 }
203
204 /**
205 * @inheritDoc
206 */
207 public function drawRectangle($startX, $startY, $endX, $endY) {
208 imagefilledrectangle($this->image, $startX, $startY, $endX, $endY, $this->color);
209 }
210
211 /**
212 * @inheritDoc
213 */
214 public function drawText($text, $x, $y, $font, $size, $opacity = 1.0) {
215 // set opacity
216 $color = imagecolorallocatealpha($this->image, $this->colorData['red'], $this->colorData['green'], $this->colorData['blue'], (1 - $opacity) * 127);
217
218 // draw text
219 imagettftext($this->image, $size, 0, $x, $y, $color, $font, $text);
220 }
221
222 /**
223 * @inheritDoc
224 */
225 public function drawTextRelative($text, $position, $margin, $offsetX, $offsetY, $font, $size, $opacity = 1.0) {
226 // split text into multiple lines
227 $lines = explode("\n", StringUtil::unifyNewlines($text));
228
229 // calc text width, height and first line height
230 $box = imagettfbbox($size, 0, $font, $text);
231 $firstLineBox = imagettfbbox($size, 0, $font, $lines[0]);
232 $textWidth = abs($box[0] - $box[2]);
233 $textHeight = abs($box[7] - $box[1]);
234 $firstLineHeight = abs($firstLineBox[7] - $firstLineBox[1]);
235
236 // calculate x coordinate
237 $x = 0;
238 switch ($position) {
239 case 'topLeft':
240 case 'middleLeft':
241 case 'bottomLeft':
242 $x = $margin;
243 break;
244
245 case 'topCenter':
246 case 'middleCenter':
247 case 'bottomCenter':
248 $x = floor(($this->getWidth() - $textWidth) / 2);
249 break;
250
251 case 'topRight':
252 case 'middleRight':
253 case 'bottomRight':
254 $x = $this->getWidth() - $textWidth - $margin;
255 break;
256 }
257
258 // calculate y coordinate
259 $y = 0;
260 switch ($position) {
261 case 'topLeft':
262 case 'topCenter':
263 case 'topRight':
264 $y = $margin + $firstLineHeight;
265 break;
266
267 case 'middleLeft':
268 case 'middleCenter':
269 case 'middleRight':
270 $y = floor(($this->getHeight() - $textHeight) / 2) + $firstLineHeight;
271 break;
272
273 case 'bottomLeft':
274 case 'bottomCenter':
275 case 'bottomRight':
276 $y = $this->getHeight() - $textHeight + $firstLineHeight - $margin;
277 break;
278 }
279
280 $this->drawText($text, $x + $offsetX, $y + $offsetY, $font, $size, $opacity);
281 }
282
283 /**
284 * @inheritDoc
285 */
286 public function textFitsImage($text, $margin, $font, $size) {
287 $box = imagettfbbox($size, 0, $font, $text);
288
289 $textWidth = abs($box[0] - $box[2]);
290 $textHeight = abs($box[7] - $box[1]);
291
292 return ($textWidth + 2 * $margin <= $this->getWidth() && $textHeight + 2 * $margin <= $this->getHeight());
293 }
294
295 /**
296 * @inheritDoc
297 */
298 public function adjustFontSize($text, $margin, $font, $size) {
299 // does nothing
300 }
301
302 /**
303 * @inheritDoc
304 */
305 public function setColor($red, $green, $blue) {
306 $this->color = imagecolorallocate($this->image, $red, $green, $blue);
307
308 // save data of the color
309 $this->colorData = [
310 'red' => $red,
311 'green' => $green,
312 'blue' => $blue
313 ];
314 }
315
316 /**
317 * @inheritDoc
318 */
319 public function hasColor() {
320 return ($this->color !== null);
321 }
322
323 /**
324 * @inheritDoc
325 */
326 public function setTransparentColor($red, $green, $blue) {
327 if ($this->type == IMAGETYPE_PNG) {
328 $color = imagecolorallocate($this->image, $red, $green, $blue);
329 imagecolortransparent($this->image, $color);
330 }
331 }
332
333 /**
334 * @inheritDoc
335 */
336 public function writeImage($image, $filename) {
337 if (!$this->isImage($image)) {
338 throw new SystemException("Given image is not a valid image resource.");
339 }
340
341 ob_start();
342
343 imagealphablending($image, false);
344 imagesavealpha($image, true);
345 if ($this->type == IMAGETYPE_GIF) {
346 imagegif($image);
347 }
348 else if ($this->type == IMAGETYPE_PNG) {
349 imagepng($image);
350 }
351 else if (function_exists('imageJPEG')) {
352 imagejpeg($image, null, 90);
353 }
354
355 $stream = ob_get_contents();
356 ob_end_clean();
357
358 file_put_contents($filename, $stream);
359 }
360
361 /**
362 * @inheritDoc
363 */
364 public function getWidth() {
365 return $this->width;
366 }
367
368 /**
369 * @inheritDoc
370 */
371 public function getHeight() {
372 return $this->height;
373 }
374
375 /**
376 * @inheritDoc
377 */
378 public function getType() {
379 return $this->type;
380 }
381
382 /**
383 * @inheritDoc
384 */
385 public function getImage() {
386 return $this->image;
387 }
388
389 /**
390 * @inheritDoc
391 */
392 public function rotate($degrees) {
393 // imagerotate interpretes degrees as counter-clockwise
394 return imagerotate($this->image, 360.0 - $degrees, ($this->color ?: 0));
395 }
396
397 /**
398 * @inheritDoc
399 */
400 public function overlayImage($file, $x, $y, $opacity) {
401 $overlayImage = new self();
402 $overlayImage->loadFile($file);
403
404 // fix PNG alpha channel handling
405 // see http://php.net/manual/en/function.imagecopymerge.php#92787
406 $cut = imagecreatetruecolor($overlayImage->getWidth(), $overlayImage->getHeight());
407 imagealphablending($cut, false);
408 imagesavealpha($cut, true);
409
410 imagecopy($cut, $this->image, 0, 0, $x, $y, $overlayImage->getWidth(), $overlayImage->getHeight());
411 imagecopy($cut, $overlayImage->image, 0, 0, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight());
412
413 $this->imagecopymerge_alpha($this->image, $cut, $x, $y, 0, 0, $overlayImage->getWidth(), $overlayImage->getHeight(), $opacity * 100);
414 }
415
416 /**
417 * `imagecopymerge` implementation with alpha support.
418 *
419 * @see http://php.net/manual/en/function.imagecopymerge.php#88456
420 *
421 * @param resource $dst_im destination image resource
422 * @param resource $src_im source image resource
423 * @param integer $dst_x x-coordinate of destination point
424 * @param integer $dst_y y-coordinate of destination point
425 * @param integer $src_x x-coordinate of source point
426 * @param integer $src_y y-coordinate of source point
427 * @param integer $src_w source width
428 * @param integer $src_h source height
429 * @param integer $pct opacity percent
430 * @return boolean
431 */
432 private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { // phpcs:ignore
433 if (!isset($pct)) {
434 return false;
435 }
436 $pct /= 100;
437 // Get image width and height
438 $w = imagesx($src_im);
439 $h = imagesy($src_im);
440 // Turn alpha blending off
441 imagealphablending($src_im, false);
442 // Find the most opaque pixel in the image (the one with the smallest alpha value)
443 $minalpha = 127;
444 for ($x = 0; $x < $w; $x++) {
445 for ($y = 0; $y < $h; $y++) {
446 $alpha = (imagecolorat($src_im, $x, $y) >> 24) & 0xFF;
447 if ($alpha < $minalpha) {
448 $minalpha = $alpha;
449 }
450 }
451 }
452 // loop through image pixels and modify alpha for each
453 for ($x = 0; $x < $w; $x++) {
454 for ($y = 0; $y < $h; $y++) {
455 // get current alpha value (represents the TANSPARENCY!)
456 $colorxy = imagecolorat($src_im, $x, $y);
457 $alpha = ($colorxy >> 24) & 0xFF;
458 // calculate new alpha
459 if ($minalpha !== 127) {
460 $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha);
461 }
462 else {
463 $alpha += 127 * $pct;
464 }
465 // get the color index with new alpha
466 $alphacolorxy = imagecolorallocatealpha($src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha);
467 // set pixel with the new color + opacity
468 if (!imagesetpixel($src_im, $x, $y, $alphacolorxy)) {
469 return false;
470 }
471 }
472 }
473 // The image copy
474 imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h);
475
476 return true;
477 }
478
479 /**
480 * @inheritDoc
481 */
482 public function overlayImageRelative($file, $position, $margin, $opacity) {
483 // does nothing
484 }
485
486 /**
487 * @inheritDoc
488 */
489 public static function isSupported() {
490 return function_exists('gd_info');
491 }
492 }