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