[DOCS] Fix wrong event class name in AssetRenderer events example
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Imaging / GraphicalFunctions.php
1 <?php
2
3 /*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16 namespace TYPO3\CMS\Core\Imaging;
17
18 use TYPO3\CMS\Core\Cache\CacheManager;
19 use TYPO3\CMS\Core\Charset\CharsetConverter;
20 use TYPO3\CMS\Core\Core\Environment;
21 use TYPO3\CMS\Core\Type\File\ImageInfo;
22 use TYPO3\CMS\Core\Utility\ArrayUtility;
23 use TYPO3\CMS\Core\Utility\CommandUtility;
24 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Core\Utility\MathUtility;
27 use TYPO3\CMS\Core\Utility\PathUtility;
28 use TYPO3\CMS\Core\Utility\StringUtility;
29
30 /**
31 * Standard graphical functions
32 *
33 * Class contains a bunch of cool functions for manipulating graphics with GDlib/Freetype and ImageMagick.
34 * VERY OFTEN used with gifbuilder that extends this class and provides a TypoScript API to using these functions
35 */
36 class GraphicalFunctions
37 {
38 /**
39 * If set, the frame pointer is appended to the filenames.
40 *
41 * @var bool
42 */
43 public $addFrameSelection = true;
44
45 /**
46 * This should be changed to 'png' if you want this class to read/make PNG-files instead!
47 *
48 * @var string
49 */
50 public $gifExtension = 'gif';
51
52 /**
53 * File formats supported by gdlib. This variable get's filled in "init" method
54 *
55 * @var array
56 */
57 protected $gdlibExtensions = [];
58
59 /**
60 * defines the RGB colorspace to use
61 *
62 * @var string
63 */
64 protected $colorspace = 'RGB';
65
66 /**
67 * colorspace names allowed
68 *
69 * @var array
70 */
71 protected $allowedColorSpaceNames = [
72 'CMY',
73 'CMYK',
74 'Gray',
75 'HCL',
76 'HSB',
77 'HSL',
78 'HWB',
79 'Lab',
80 'LCH',
81 'LMS',
82 'Log',
83 'Luv',
84 'OHTA',
85 'Rec601Luma',
86 'Rec601YCbCr',
87 'Rec709Luma',
88 'Rec709YCbCr',
89 'RGB',
90 'sRGB',
91 'Transparent',
92 'XYZ',
93 'YCbCr',
94 'YCC',
95 'YIQ',
96 'YCbCr',
97 'YUV'
98 ];
99
100 /**
101 * 16777216 Colors is the maximum value for PNG, JPEG truecolor images (24-bit, 8-bit / Channel)
102 *
103 * @var int
104 */
105 public $truecolorColors = 16777215;
106
107 /**
108 * Allowed file extensions perceived as images by TYPO3.
109 * List should be set to 'gif,png,jpeg,jpg' if IM is not available.
110 *
111 * @var array
112 */
113 protected $imageFileExt = ['gif', 'jpg', 'jpeg', 'png', 'tif', 'bmp', 'tga', 'pcx', 'ai', 'pdf'];
114
115 /**
116 * Web image extensions (can be shown by a webbrowser)
117 *
118 * @var array
119 */
120 protected $webImageExt = ['gif', 'jpg', 'jpeg', 'png'];
121
122 /**
123 * Enable ImageMagick effects, disabled by default as IM5+ effects slow down the image generation
124 *
125 * @var bool
126 */
127 protected $processorEffectsEnabled = false;
128
129 /**
130 * @var array
131 */
132 public $cmds = [
133 'jpg' => '',
134 'jpeg' => '',
135 'gif' => '',
136 'png' => ''
137 ];
138
139 /**
140 * Whether ImageMagick/GraphicsMagick is enabled or not
141 * @var bool
142 */
143 protected $processorEnabled;
144
145 /**
146 * @var bool
147 */
148 protected $mayScaleUp = true;
149
150 /**
151 * Filename prefix for images scaled in imageMagickConvert()
152 *
153 * @var string
154 */
155 public $filenamePrefix = '';
156
157 /**
158 * Forcing the output filename of imageMagickConvert() to this value. However after calling imageMagickConvert() it will be set blank again.
159 *
160 * @var string
161 */
162 public $imageMagickConvert_forceFileNameBody = '';
163
164 /**
165 * This flag should always be FALSE. If set TRUE, imageMagickConvert will always write a new file to the tempdir! Used for debugging.
166 *
167 * @var bool
168 */
169 public $dontCheckForExistingTempFile = false;
170
171 /**
172 * Prevents imageMagickConvert() from compressing the gif-files with self::gifCompress()
173 *
174 * @var bool
175 */
176 public $dontCompress = false;
177
178 /**
179 * For debugging only.
180 * Filenames will not be based on mtime and only filename (not path) will be used.
181 * This key is also included in the hash of the filename...
182 *
183 * @var string
184 */
185 public $alternativeOutputKey = '';
186
187 /**
188 * All ImageMagick commands executed is stored in this array for tracking. Used by the Install Tools Image section
189 *
190 * @var array
191 */
192 public $IM_commands = [];
193
194 /**
195 * @var array
196 */
197 public $workArea = [];
198
199 /**
200 * Preserve the alpha transparency layer of read PNG images
201 *
202 * @var bool
203 */
204 protected $saveAlphaLayer = false;
205
206 /**
207 * ImageMagick scaling command; "-auto-orient -geometry" or "-auto-orient -sample". Used in makeText() and imageMagickConvert()
208 *
209 * @var string
210 */
211 public $scalecmd = '-auto-orient -geometry';
212
213 /**
214 * Used by v5_blur() to simulate 10 continuous steps of blurring
215 *
216 * @var string
217 */
218 protected $im5fx_blurSteps = '1x2,2x2,3x2,4x3,5x3,5x4,6x4,7x5,8x5,9x5';
219
220 /**
221 * Used by v5_sharpen() to simulate 10 continuous steps of sharpening.
222 *
223 * @var string
224 */
225 protected $im5fx_sharpenSteps = '1x2,2x2,3x2,2x3,3x3,4x3,3x4,4x4,4x5,5x5';
226
227 /**
228 * This is the limit for the number of pixels in an image before it will be rendered as JPG instead of GIF/PNG
229 *
230 * @var int
231 */
232 protected $pixelLimitGif = 10000;
233
234 /**
235 * Array mapping HTML color names to RGB values.
236 *
237 * @var array
238 */
239 protected $colMap = [
240 'aqua' => [0, 255, 255],
241 'black' => [0, 0, 0],
242 'blue' => [0, 0, 255],
243 'fuchsia' => [255, 0, 255],
244 'gray' => [128, 128, 128],
245 'green' => [0, 128, 0],
246 'lime' => [0, 255, 0],
247 'maroon' => [128, 0, 0],
248 'navy' => [0, 0, 128],
249 'olive' => [128, 128, 0],
250 'purple' => [128, 0, 128],
251 'red' => [255, 0, 0],
252 'silver' => [192, 192, 192],
253 'teal' => [0, 128, 128],
254 'yellow' => [255, 255, 0],
255 'white' => [255, 255, 255]
256 ];
257
258 /**
259 * Charset conversion object:
260 *
261 * @var CharsetConverter
262 */
263 protected $csConvObj;
264
265 /**
266 * @var int
267 */
268 protected $jpegQuality = 85;
269
270 /**
271 * @var string
272 */
273 public $map = '';
274
275 /**
276 * This holds the operational setup.
277 * Basically this is a TypoScript array with properties.
278 *
279 * @var array
280 */
281 public $setup = [];
282
283 /**
284 * @var int
285 */
286 public $w = 0;
287
288 /**
289 * @var int
290 */
291 public $h = 0;
292
293 /**
294 * @var array
295 */
296 protected $OFFSET;
297
298 /**
299 * @var resource
300 */
301 protected $im;
302
303 /**
304 * Reads configuration information from $GLOBALS['TYPO3_CONF_VARS']['GFX']
305 * and sets some values in internal variables.
306 */
307 public function __construct()
308 {
309 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
310 if (function_exists('imagecreatefromjpeg') && function_exists('imagejpeg')) {
311 $this->gdlibExtensions[] = 'jpg';
312 $this->gdlibExtensions[] = 'jpeg';
313 }
314 if (function_exists('imagecreatefrompng') && function_exists('imagepng')) {
315 $this->gdlibExtensions[] = 'png';
316 }
317 if (function_exists('imagecreatefromgif') && function_exists('imagegif')) {
318 $this->gdlibExtensions[] = 'gif';
319 }
320
321 if ($gfxConf['processor_colorspace'] && in_array($gfxConf['processor_colorspace'], $this->allowedColorSpaceNames, true)) {
322 $this->colorspace = $gfxConf['processor_colorspace'];
323 }
324
325 $this->processorEnabled = (bool)$gfxConf['processor_enabled'];
326 // Setting default JPG parameters:
327 $this->jpegQuality = MathUtility::forceIntegerInRange($gfxConf['jpg_quality'], 10, 100, 85);
328 $this->addFrameSelection = (bool)$gfxConf['processor_allowFrameSelection'];
329 if ($gfxConf['gdlib_png']) {
330 $this->gifExtension = 'png';
331 }
332 $this->imageFileExt = GeneralUtility::trimExplode(',', $gfxConf['imagefile_ext']);
333
334 // Boolean. This is necessary if using ImageMagick 5+.
335 // Effects in Imagemagick 5+ tends to render very slowly!!
336 // - therefore must be disabled in order not to perform sharpen, blurring and such.
337 $this->cmds['jpg'] = $this->cmds['jpeg'] = '-colorspace ' . $this->colorspace . ' -quality ' . $this->jpegQuality;
338
339 // ... but if 'processor_effects' is set, enable effects
340 if ($gfxConf['processor_effects']) {
341 $this->processorEffectsEnabled = true;
342 $this->cmds['jpg'] .= $this->v5_sharpen(10);
343 $this->cmds['jpeg'] .= $this->v5_sharpen(10);
344 }
345 // Secures that images are not scaled up.
346 $this->mayScaleUp = (bool)$gfxConf['processor_allowUpscaling'];
347 $this->csConvObj = GeneralUtility::makeInstance(CharsetConverter::class);
348 }
349
350 /*************************************************
351 *
352 * Layering images / "IMAGE" GIFBUILDER object
353 *
354 *************************************************/
355 /**
356 * Implements the "IMAGE" GIFBUILDER object, when the "mask" property is TRUE.
357 * It reads the two images defined by $conf['file'] and $conf['mask'] and copies the $conf['file'] onto the input image pointer image using the $conf['mask'] as a grayscale mask
358 * The operation involves ImageMagick for combining.
359 *
360 * @param resource $im GDlib image pointer
361 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
362 * @param array $workArea The current working area coordinates.
363 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
364 */
365 public function maskImageOntoImage(&$im, $conf, $workArea)
366 {
367 if ($conf['file'] && $conf['mask']) {
368 $imgInf = pathinfo($conf['file']);
369 $imgExt = strtolower($imgInf['extension']);
370 if (!in_array($imgExt, $this->gdlibExtensions, true)) {
371 $BBimage = $this->imageMagickConvert($conf['file'], $this->gifExtension);
372 } else {
373 $BBimage = $this->getImageDimensions($conf['file']);
374 }
375 $maskInf = pathinfo($conf['mask']);
376 $maskExt = strtolower($maskInf['extension']);
377 if (!in_array($maskExt, $this->gdlibExtensions, true)) {
378 $BBmask = $this->imageMagickConvert($conf['mask'], $this->gifExtension);
379 } else {
380 $BBmask = $this->getImageDimensions($conf['mask']);
381 }
382 if ($BBimage && $BBmask) {
383 $w = imagesx($im);
384 $h = imagesy($im);
385 $tmpStr = $this->randomName();
386 $theImage = $tmpStr . '_img.' . $this->gifExtension;
387 $theDest = $tmpStr . '_dest.' . $this->gifExtension;
388 $theMask = $tmpStr . '_mask.' . $this->gifExtension;
389 // Prepare overlay image
390 $cpImg = $this->imageCreateFromFile($BBimage[3]);
391 $destImg = imagecreatetruecolor($w, $h);
392 // Preserve alpha transparency
393 if ($this->saveAlphaLayer) {
394 imagesavealpha($destImg, true);
395 $Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
396 imagefill($destImg, 0, 0, $Bcolor);
397 } else {
398 $Bcolor = imagecolorallocate($destImg, 0, 0, 0);
399 imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
400 }
401 $this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
402 $this->ImageWrite($destImg, $theImage);
403 imagedestroy($cpImg);
404 imagedestroy($destImg);
405 // Prepare mask image
406 $cpImg = $this->imageCreateFromFile($BBmask[3]);
407 $destImg = imagecreatetruecolor($w, $h);
408 if ($this->saveAlphaLayer) {
409 imagesavealpha($destImg, true);
410 $Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
411 imagefill($destImg, 0, 0, $Bcolor);
412 } else {
413 $Bcolor = imagecolorallocate($destImg, 0, 0, 0);
414 imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
415 }
416 $this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
417 $this->ImageWrite($destImg, $theMask);
418 imagedestroy($cpImg);
419 imagedestroy($destImg);
420 // Mask the images
421 $this->ImageWrite($im, $theDest);
422 // Let combineExec handle maskNegation
423 $this->combineExec($theDest, $theImage, $theMask, $theDest);
424 // The main image is loaded again...
425 $backIm = $this->imageCreateFromFile($theDest);
426 // ... and if nothing went wrong we load it onto the old one.
427 if ($backIm) {
428 if (!$this->saveAlphaLayer) {
429 imagecolortransparent($backIm, -1);
430 }
431 $im = $backIm;
432 }
433 // Unlink files from process
434 unlink($theDest);
435 unlink($theImage);
436 unlink($theMask);
437 }
438 }
439 }
440
441 /**
442 * Implements the "IMAGE" GIFBUILDER object, when the "mask" property is FALSE (using only $conf['file'])
443 *
444 * @param resource $im GDlib image pointer
445 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
446 * @param array $workArea The current working area coordinates.
447 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
448 * @see maskImageOntoImage()
449 */
450 public function copyImageOntoImage(&$im, $conf, $workArea)
451 {
452 if ($conf['file']) {
453 if (!in_array($conf['BBOX'][2], $this->gdlibExtensions, true)) {
454 $conf['BBOX'] = $this->imageMagickConvert($conf['BBOX'][3], $this->gifExtension);
455 $conf['file'] = $conf['BBOX'][3];
456 }
457 $cpImg = $this->imageCreateFromFile($conf['file']);
458 $this->copyGifOntoGif($im, $cpImg, $conf, $workArea);
459 imagedestroy($cpImg);
460 }
461 }
462
463 /**
464 * Copies two GDlib image pointers onto each other, using TypoScript configuration from $conf and the input $workArea definition.
465 *
466 * @param resource $im GDlib image pointer, destination (bottom image)
467 * @param resource $cpImg GDlib image pointer, source (top image)
468 * @param array $conf TypoScript array with the properties for the IMAGE GIFBUILDER object. Only used for the "tile" property value.
469 * @param array $workArea Work area
470 * @internal
471 */
472 public function copyGifOntoGif(&$im, $cpImg, $conf, $workArea)
473 {
474 $cpW = imagesx($cpImg);
475 $cpH = imagesy($cpImg);
476 $tile = GeneralUtility::intExplode(',', $conf['tile']);
477 $tile[0] = MathUtility::forceIntegerInRange($tile[0], 1, 20);
478 $tile[1] = MathUtility::forceIntegerInRange($tile[1], 1, 20);
479 $cpOff = $this->objPosition($conf, $workArea, [$cpW * $tile[0], $cpH * $tile[1]]);
480 for ($xt = 0; $xt < $tile[0]; $xt++) {
481 $Xstart = $cpOff[0] + $cpW * $xt;
482 // If this image is inside of the workArea, then go on
483 if ($Xstart + $cpW > $workArea[0]) {
484 // X:
485 if ($Xstart < $workArea[0]) {
486 $cpImgCutX = $workArea[0] - $Xstart;
487 $Xstart = $workArea[0];
488 } else {
489 $cpImgCutX = 0;
490 }
491 $w = $cpW - $cpImgCutX;
492 if ($Xstart > $workArea[0] + $workArea[2] - $w) {
493 $w = $workArea[0] + $workArea[2] - $Xstart;
494 }
495 // If this image is inside of the workArea, then go on
496 if ($Xstart < $workArea[0] + $workArea[2]) {
497 // Y:
498 for ($yt = 0; $yt < $tile[1]; $yt++) {
499 $Ystart = $cpOff[1] + $cpH * $yt;
500 // If this image is inside of the workArea, then go on
501 if ($Ystart + $cpH > $workArea[1]) {
502 if ($Ystart < $workArea[1]) {
503 $cpImgCutY = $workArea[1] - $Ystart;
504 $Ystart = $workArea[1];
505 } else {
506 $cpImgCutY = 0;
507 }
508 $h = $cpH - $cpImgCutY;
509 if ($Ystart > $workArea[1] + $workArea[3] - $h) {
510 $h = $workArea[1] + $workArea[3] - $Ystart;
511 }
512 // If this image is inside of the workArea, then go on
513 if ($Ystart < $workArea[1] + $workArea[3]) {
514 $this->imagecopyresized($im, $cpImg, $Xstart, $Ystart, $cpImgCutX, $cpImgCutY, $w, $h, $w, $h);
515 }
516 }
517 }
518 }
519 }
520 }
521 }
522
523 /**
524 * Alternative function for using the similar PHP function imagecopyresized(). Used for GD2 only.
525 *
526 * OK, the reason for this stupid fix is the following story:
527 * GD1.x was capable of copying two images together and combining their palettes! GD2 is apparently not.
528 * With GD2 only the palette of the dest-image is used which mostly results in totally black images when trying to
529 * copy a color-ful image onto the destination.
530 * The GD2-fix is to
531 * 1) Create a blank TRUE-COLOR image
532 * 2) Copy the destination image onto that one
533 * 3) Then do the actual operation; Copying the source (top image) onto that
534 * 4) ... and return the result pointer.
535 * 5) Reduce colors (if we do not, the result may become strange!)
536 * It works, but the resulting images is now a true-color PNG which may be very large.
537 * So, why not use 'imagetruecolortopalette ($im, TRUE, 256)' - well because it does NOT WORK! So simple is that.
538 *
539 * @param resource $dstImg Destination image
540 * @param resource $srcImg Source image
541 * @param int $dstX Destination x-coordinate
542 * @param int $dstY Destination y-coordinate
543 * @param int $srcX Source x-coordinate
544 * @param int $srcY Source y-coordinate
545 * @param int $dstWidth Destination width
546 * @param int $dstHeight Destination height
547 * @param int $srcWidth Source width
548 * @param int $srcHeight Source height
549 * @internal
550 */
551 public function imagecopyresized(&$dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight)
552 {
553 if (!$this->saveAlphaLayer) {
554 // Make true color image
555 $tmpImg = imagecreatetruecolor(imagesx($dstImg), imagesy($dstImg));
556 // Copy the source image onto that
557 imagecopyresized($tmpImg, $dstImg, 0, 0, 0, 0, imagesx($dstImg), imagesy($dstImg), imagesx($dstImg), imagesy($dstImg));
558 // Then copy the source image onto that (the actual operation!)
559 imagecopyresized($tmpImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
560 // Set the destination image
561 $dstImg = $tmpImg;
562 } else {
563 imagecopyresized($dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
564 }
565 }
566
567 /********************************
568 *
569 * Text / "TEXT" GIFBUILDER object
570 *
571 ********************************/
572 /**
573 * Implements the "TEXT" GIFBUILDER object
574 *
575 * @param resource $im GDlib image pointer
576 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
577 * @param array $workArea The current working area coordinates.
578 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
579 */
580 public function makeText(&$im, $conf, $workArea)
581 {
582 // Spacing
583 [$spacing, $wordSpacing] = $this->calcWordSpacing($conf);
584 // Position
585 $txtPos = $this->txtPosition($conf, $workArea, $conf['BBOX']);
586 $theText = $conf['text'];
587 if ($conf['imgMap'] && is_array($conf['imgMap.'])) {
588 $this->addToMap($this->calcTextCordsForMap($conf['BBOX'][2], $txtPos, $conf['imgMap.']), $conf['imgMap.']);
589 }
590 if (!$conf['hideButCreateMap']) {
591 // Font Color:
592 $cols = $this->convertColor($conf['fontColor']);
593 // NiceText is calculated
594 if (!$conf['niceText']) {
595 $Fcolor = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
596 // antiAliasing is setup:
597 $Fcolor = $conf['antiAlias'] ? $Fcolor : -$Fcolor;
598 for ($a = 0; $a < $conf['iterations']; $a++) {
599 // If any kind of spacing applies, we use this function:
600 if ($spacing || $wordSpacing) {
601 $this->SpacedImageTTFText($im, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.']);
602 } else {
603 $this->renderTTFText($im, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'], $conf);
604 }
605 }
606 } else {
607 // NICETEXT::
608 // options anti_aliased and iterations is NOT available when doing this!!
609 $w = imagesx($im);
610 $h = imagesy($im);
611 $tmpStr = $this->randomName();
612 $fileMenu = $tmpStr . '_menuNT.' . $this->gifExtension;
613 $fileColor = $tmpStr . '_colorNT.' . $this->gifExtension;
614 $fileMask = $tmpStr . '_maskNT.' . $this->gifExtension;
615 // Scalefactor
616 $sF = MathUtility::forceIntegerInRange($conf['niceText.']['scaleFactor'], 2, 5);
617 $newW = (int)ceil($sF * imagesx($im));
618 $newH = (int)ceil($sF * imagesy($im));
619 // Make mask
620 $maskImg = imagecreatetruecolor($newW, $newH);
621 $Bcolor = imagecolorallocate($maskImg, 255, 255, 255);
622 imagefilledrectangle($maskImg, 0, 0, $newW, $newH, $Bcolor);
623 $Fcolor = imagecolorallocate($maskImg, 0, 0, 0);
624 // If any kind of spacing applies, we use this function:
625 if ($spacing || $wordSpacing) {
626 $this->SpacedImageTTFText($maskImg, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.'], $sF);
627 } else {
628 $this->renderTTFText($maskImg, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'], $conf, $sF);
629 }
630 $this->ImageWrite($maskImg, $fileMask);
631 imagedestroy($maskImg);
632 // Downscales the mask
633 if (!$this->processorEffectsEnabled) {
634 $command = trim($this->scalecmd . ' ' . $w . 'x' . $h . '! -negate');
635 } else {
636 $command = trim($conf['niceText.']['before'] . ' ' . $this->scalecmd . ' ' . $w . 'x' . $h . '! ' . $conf['niceText.']['after'] . ' -negate');
637 if ($conf['niceText.']['sharpen']) {
638 $command .= $this->v5_sharpen($conf['niceText.']['sharpen']);
639 }
640 }
641 $this->imageMagickExec($fileMask, $fileMask, $command);
642 // Make the color-file
643 $colorImg = imagecreatetruecolor($w, $h);
644 $Ccolor = imagecolorallocate($colorImg, $cols[0], $cols[1], $cols[2]);
645 imagefilledrectangle($colorImg, 0, 0, $w, $h, $Ccolor);
646 $this->ImageWrite($colorImg, $fileColor);
647 imagedestroy($colorImg);
648 // The mask is applied
649 // The main pictures is saved temporarily
650 $this->ImageWrite($im, $fileMenu);
651 $this->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
652 // The main image is loaded again...
653 $backIm = $this->imageCreateFromFile($fileMenu);
654 // ... and if nothing went wrong we load it onto the old one.
655 if ($backIm) {
656 if (!$this->saveAlphaLayer) {
657 imagecolortransparent($backIm, -1);
658 }
659 $im = $backIm;
660 }
661 // Deleting temporary files;
662 unlink($fileMenu);
663 unlink($fileColor);
664 unlink($fileMask);
665 }
666 }
667 }
668
669 /**
670 * Calculates text position for printing the text onto the image based on configuration like alignment and workarea.
671 *
672 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
673 * @param array $workArea Work area definition
674 * @param array $BB Bounding box information, was set in \TYPO3\CMS\Frontend\Imaging\GifBuilder::start()
675 * @return array [0]=x, [1]=y, [2]=w, [3]=h
676 * @internal
677 * @see makeText()
678 */
679 public function txtPosition($conf, $workArea, $BB)
680 {
681 $angle = (int)$conf['angle'] / 180 * M_PI;
682 $conf['angle'] = 0;
683 $straightBB = $this->calcBBox($conf);
684 // offset, align, valign, workarea
685 // [0]=x, [1]=y, [2]=w, [3]=h
686 $result = [];
687 $result[2] = $BB[0];
688 $result[3] = $BB[1];
689 $w = $workArea[2];
690 switch ($conf['align']) {
691 case 'right':
692
693 case 'center':
694 $factor = abs(cos($angle));
695 $sign = cos($angle) < 0 ? -1 : 1;
696 $len1 = $sign * $factor * $straightBB[0];
697 $len2 = $sign * $BB[0];
698 $result[0] = $w - ceil($len2 * $factor + (1 - $factor) * $len1);
699 $factor = abs(sin($angle));
700 $sign = sin($angle) < 0 ? -1 : 1;
701 $len1 = $sign * $factor * $straightBB[0];
702 $len2 = $sign * $BB[1];
703 $result[1] = ceil($len2 * $factor + (1 - $factor) * $len1);
704 break;
705 }
706 switch ($conf['align']) {
707 case 'right':
708 break;
709 case 'center':
710 $result[0] = round($result[0] / 2);
711 $result[1] = round($result[1] / 2);
712 break;
713 default:
714 $result[0] = 0;
715 $result[1] = 0;
716 }
717 $result = $this->applyOffset($result, GeneralUtility::intExplode(',', $conf['offset']));
718 $result = $this->applyOffset($result, $workArea);
719 return $result;
720 }
721
722 /**
723 * Calculates bounding box information for the TEXT GIFBUILDER object.
724 *
725 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
726 * @return array Array with three keys [0]/[1] being x/y and [2] being the bounding box array
727 * @internal
728 * @see txtPosition()
729 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::start()
730 */
731 public function calcBBox($conf)
732 {
733 $sF = $this->getTextScalFactor($conf);
734 [$spacing, $wordSpacing] = $this->calcWordSpacing($conf, $sF);
735 $theText = $conf['text'];
736 $charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $theText, $conf['splitRendering.'], $sF);
737 $theBBoxInfo = $charInf;
738 if ($conf['angle']) {
739 $xArr = [$charInf[0], $charInf[2], $charInf[4], $charInf[6]];
740 $yArr = [$charInf[1], $charInf[3], $charInf[5], $charInf[7]];
741 $x = max($xArr) - min($xArr);
742 $y = max($yArr) - min($yArr);
743 } else {
744 $x = $charInf[2] - $charInf[0];
745 $y = $charInf[1] - $charInf[7];
746 }
747 // Set original lineHeight (used by line breaks):
748 $theBBoxInfo['lineHeight'] = $y;
749 if (!empty($conf['lineHeight'])) {
750 $theBBoxInfo['lineHeight'] = (int)$conf['lineHeight'];
751 }
752
753 // If any kind of spacing applys, we use this function:
754 if ($spacing || $wordSpacing) {
755 $x = 0;
756 if (!$spacing && $wordSpacing) {
757 $bits = explode(' ', $theText);
758 foreach ($bits as $word) {
759 $word .= ' ';
760 $wordInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $word, $conf['splitRendering.'], $sF);
761 $wordW = $wordInf[2] - $wordInf[0];
762 $x += $wordW + $wordSpacing;
763 }
764 } else {
765 $utf8Chars = $this->csConvObj->utf8_to_numberarray($theText);
766 // For each UTF-8 char, do:
767 foreach ($utf8Chars as $char) {
768 $charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $char, $conf['splitRendering.'], $sF);
769 $charW = $charInf[2] - $charInf[0];
770 $x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
771 }
772 }
773 } elseif (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($conf['text'], $conf) > $conf['breakWidth']) {
774 $maxWidth = 0;
775 $currentWidth = 0;
776 $breakWidth = $conf['breakWidth'];
777 $breakSpace = $this->getBreakSpace($conf, $theBBoxInfo);
778 $wordPairs = $this->getWordPairsForLineBreak($conf['text']);
779 // Iterate through all word pairs:
780 foreach ($wordPairs as $index => $wordPair) {
781 $wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
782 if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
783 $currentWidth += $wordWidth;
784 } else {
785 $maxWidth = max($maxWidth, $currentWidth);
786 $y += $breakSpace;
787 // Restart:
788 $currentWidth = $wordWidth;
789 }
790 }
791 $x = max($maxWidth, $currentWidth) * $sF;
792 }
793 if ($sF > 1) {
794 $x = ceil($x / $sF);
795 $y = ceil($y / $sF);
796 if (is_array($theBBoxInfo)) {
797 foreach ($theBBoxInfo as &$value) {
798 $value = ceil($value / $sF);
799 }
800 unset($value);
801 }
802 }
803 return [$x, $y, $theBBoxInfo];
804 }
805
806 /**
807 * Adds an <area> tag to the internal variable $this->map which is used to accumulate the content for an ImageMap
808 *
809 * @param array $cords Coordinates for a polygon image map as created by ->calcTextCordsForMap()
810 * @param array $conf Configuration for "imgMap." property of a TEXT GIFBUILDER object.
811 * @internal
812 * @see makeText()
813 * @see calcTextCordsForMap()
814 */
815 public function addToMap($cords, $conf)
816 {
817 $this->map .= '<area shape="poly" coords="' . implode(',', $cords) . '"'
818 . ' href="' . htmlspecialchars($conf['url']) . '"'
819 . ($conf['target'] ? ' target="' . htmlspecialchars($conf['target']) . '"' : '')
820 . ((string)$conf['titleText'] !== '' ? ' title="' . htmlspecialchars($conf['titleText']) . '"' : '')
821 . ' alt="' . htmlspecialchars($conf['altText']) . '" />';
822 }
823
824 /**
825 * Calculating the coordinates for a TEXT string on an image map. Used in an <area> tag
826 *
827 * @param array $cords Coordinates (from BBOX array)
828 * @param array $offset Offset array
829 * @param array $conf Configuration for "imgMap." property of a TEXT GIFBUILDER object.
830 * @return array
831 * @internal
832 * @see makeText()
833 * @see calcTextCordsForMap()
834 */
835 public function calcTextCordsForMap($cords, $offset, $conf)
836 {
837 $newCords = [];
838 $pars = GeneralUtility::intExplode(',', $conf['explode'] . ',');
839 $newCords[0] = $cords[0] + $offset[0] - $pars[0];
840 $newCords[1] = $cords[1] + $offset[1] + $pars[1];
841 $newCords[2] = $cords[2] + $offset[0] + $pars[0];
842 $newCords[3] = $cords[3] + $offset[1] + $pars[1];
843 $newCords[4] = $cords[4] + $offset[0] + $pars[0];
844 $newCords[5] = $cords[5] + $offset[1] - $pars[1];
845 $newCords[6] = $cords[6] + $offset[0] - $pars[0];
846 $newCords[7] = $cords[7] + $offset[1] - $pars[1];
847 return $newCords;
848 }
849
850 /**
851 * Printing text onto an image like the PHP function imageTTFText does but in addition it offers options for spacing of letters and words.
852 * Spacing is done by printing one char at a time and this means that the spacing is rather uneven and probably not very nice.
853 * See
854 *
855 * @param resource $im (See argument for PHP function imageTTFtext())
856 * @param int $fontSize (See argument for PHP function imageTTFtext())
857 * @param int $angle (See argument for PHP function imageTTFtext())
858 * @param int $x (See argument for PHP function imageTTFtext())
859 * @param int $y (See argument for PHP function imageTTFtext())
860 * @param int $Fcolor (See argument for PHP function imageTTFtext())
861 * @param string $fontFile (See argument for PHP function imageTTFtext())
862 * @param string $text (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
863 * @param int $spacing The spacing of letters in pixels
864 * @param int $wordSpacing The spacing of words in pixels
865 * @param array $splitRenderingConf Array
866 * @param int $sF Scale factor
867 * @internal
868 */
869 public function SpacedImageTTFText(&$im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $text, $spacing, $wordSpacing, $splitRenderingConf, $sF = 1)
870 {
871 $spacing *= $sF;
872 $wordSpacing *= $sF;
873 if (!$spacing && $wordSpacing) {
874 $bits = explode(' ', $text);
875 foreach ($bits as $word) {
876 $word .= ' ';
877 $wordInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $word, $splitRenderingConf, $sF);
878 $wordW = $wordInf[2] - $wordInf[0];
879 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $word, $splitRenderingConf, $sF);
880 $x += $wordW + $wordSpacing;
881 }
882 } else {
883 $utf8Chars = $this->csConvObj->utf8_to_numberarray($text);
884 // For each UTF-8 char, do:
885 foreach ($utf8Chars as $char) {
886 $charInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $char, $splitRenderingConf, $sF);
887 $charW = $charInf[2] - $charInf[0];
888 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $char, $splitRenderingConf, $sF);
889 $x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
890 }
891 }
892 }
893
894 /**
895 * Function that finds the right fontsize that will render the textstring within a certain width
896 *
897 * @param array $conf The TypoScript properties of the TEXT GIFBUILDER object
898 * @return int The new fontSize
899 * @internal
900 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::start()
901 */
902 public function fontResize($conf)
903 {
904 // You have to use +calc options like [10.h] in 'offset' to get the right position of your text-image, if you use +calc in XY height!!!!
905 $maxWidth = (int)$conf['maxWidth'];
906 [$spacing, $wordSpacing] = $this->calcWordSpacing($conf);
907 if ($maxWidth) {
908 // If any kind of spacing applys, we use this function:
909 if ($spacing || $wordSpacing) {
910 return $conf['fontSize'];
911 }
912 do {
913 // Determine bounding box.
914 $bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $conf['text'], $conf['splitRendering.']);
915 if ($conf['angle'] < 0) {
916 $pixelWidth = abs($bounds[4] - $bounds[0]);
917 } elseif ($conf['angle'] > 0) {
918 $pixelWidth = abs($bounds[2] - $bounds[6]);
919 } else {
920 $pixelWidth = abs($bounds[4] - $bounds[6]);
921 }
922 // Size is fine, exit:
923 if ($pixelWidth <= $maxWidth) {
924 break;
925 }
926 $conf['fontSize']--;
927 } while ($conf['fontSize'] > 1);
928 }
929 return $conf['fontSize'];
930 }
931
932 /**
933 * Wrapper for ImageTTFBBox
934 *
935 * @param int $fontSize (See argument for PHP function ImageTTFBBox())
936 * @param int $angle (See argument for PHP function ImageTTFBBox())
937 * @param string $fontFile (See argument for PHP function ImageTTFBBox())
938 * @param string $string (See argument for PHP function ImageTTFBBox())
939 * @param array $splitRendering Split-rendering configuration
940 * @param int $sF Scale factor
941 * @return array Information array.
942 */
943 public function ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $string, $splitRendering, $sF = 1)
944 {
945 // Initialize:
946 $offsetInfo = [];
947 $stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
948 // Traverse string parts:
949 foreach ($stringParts as $strCfg) {
950 $fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
951 if (is_readable($fontFile)) {
952 // Calculate Bounding Box for part.
953 $calc = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $fontFile, $strCfg['str']);
954 // Calculate offsets:
955 if (empty($offsetInfo)) {
956 // First run, just copy over.
957 $offsetInfo = $calc;
958 } else {
959 $offsetInfo[2] += $calc[2] - $calc[0] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
960 $offsetInfo[3] += $calc[3] - $calc[1] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
961 $offsetInfo[4] += $calc[4] - $calc[6] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
962 $offsetInfo[5] += $calc[5] - $calc[7] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
963 }
964 } else {
965 debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFBBoxWrapper()');
966 }
967 }
968 return $offsetInfo;
969 }
970
971 /**
972 * Wrapper for ImageTTFText
973 *
974 * @param resource $im (See argument for PHP function imageTTFtext())
975 * @param int $fontSize (See argument for PHP function imageTTFtext())
976 * @param int $angle (See argument for PHP function imageTTFtext())
977 * @param int $x (See argument for PHP function imageTTFtext())
978 * @param int $y (See argument for PHP function imageTTFtext())
979 * @param int $color (See argument for PHP function imageTTFtext())
980 * @param string $fontFile (See argument for PHP function imageTTFtext())
981 * @param string $string (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
982 * @param array $splitRendering Split-rendering configuration
983 * @param int $sF Scale factor
984 */
985 public function ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF = 1)
986 {
987 // Initialize:
988 $stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
989 $x = (int)ceil($sF * $x);
990 $y = (int)ceil($sF * $y);
991 // Traverse string parts:
992 foreach ($stringParts as $i => $strCfg) {
993 // Initialize:
994 $colorIndex = $color;
995 // Set custom color if any (only when niceText is off):
996 if ($strCfg['color'] && $sF == 1) {
997 $cols = $this->convertColor($strCfg['color']);
998 $colorIndex = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
999 $colorIndex = $color >= 0 ? $colorIndex : -$colorIndex;
1000 }
1001 // Setting xSpaceBefore
1002 if ($i) {
1003 $x += (int)$strCfg['xSpaceBefore'];
1004 $y -= (int)$strCfg['ySpaceBefore'];
1005 }
1006 $fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
1007 if (is_readable($fontFile)) {
1008 // Render part:
1009 imagettftext($im, $this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $x, $y, $colorIndex, $fontFile, $strCfg['str']);
1010 // Calculate offset to apply:
1011 $wordInf = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, GeneralUtility::getFileAbsFileName($strCfg['fontFile']), $strCfg['str']);
1012 $x += $wordInf[2] - $wordInf[0] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceAfter'];
1013 $y += $wordInf[5] - $wordInf[7] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceAfter'];
1014 } else {
1015 debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFTextWrapper()');
1016 }
1017 }
1018 }
1019
1020 /**
1021 * Splitting a string for ImageTTFBBox up into an array where each part has its own configuration options.
1022 *
1023 * @param string $string UTF-8 string
1024 * @param array $splitRendering Split-rendering configuration from GIFBUILDER TEXT object.
1025 * @param int $fontSize Current fontsize
1026 * @param string $fontFile Current font file
1027 * @return array Array with input string splitted according to configuration
1028 */
1029 public function splitString($string, $splitRendering, $fontSize, $fontFile)
1030 {
1031 // Initialize by setting the whole string and default configuration as the first entry.
1032 $result = [];
1033 $result[] = [
1034 'str' => $string,
1035 'fontSize' => $fontSize,
1036 'fontFile' => $fontFile
1037 ];
1038 // Traverse the split-rendering configuration:
1039 // Splitting will create more entries in $result with individual configurations.
1040 if (is_array($splitRendering)) {
1041 $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($splitRendering);
1042 // Traverse configured options:
1043 foreach ($sKeyArray as $key) {
1044 $cfg = $splitRendering[$key . '.'];
1045 // Process each type of split rendering keyword:
1046 switch ((string)$splitRendering[$key]) {
1047 case 'highlightWord':
1048 if ((string)$cfg['value'] !== '') {
1049 $newResult = [];
1050 // Traverse the current parts of the result array:
1051 foreach ($result as $part) {
1052 // Explode the string value by the word value to highlight:
1053 $explodedParts = explode($cfg['value'], $part['str']);
1054 foreach ($explodedParts as $c => $expValue) {
1055 if ((string)$expValue !== '') {
1056 $newResult[] = array_merge($part, ['str' => $expValue]);
1057 }
1058 if ($c + 1 < count($explodedParts)) {
1059 $newResult[] = [
1060 'str' => $cfg['value'],
1061 'fontSize' => $cfg['fontSize'] ?: $part['fontSize'],
1062 'fontFile' => $cfg['fontFile'] ?: $part['fontFile'],
1063 'color' => $cfg['color'],
1064 'xSpaceBefore' => $cfg['xSpaceBefore'],
1065 'xSpaceAfter' => $cfg['xSpaceAfter'],
1066 'ySpaceBefore' => $cfg['ySpaceBefore'],
1067 'ySpaceAfter' => $cfg['ySpaceAfter']
1068 ];
1069 }
1070 }
1071 }
1072 // Set the new result as result array:
1073 if (!empty($newResult)) {
1074 $result = $newResult;
1075 }
1076 }
1077 break;
1078 case 'charRange':
1079 if ((string)$cfg['value'] !== '') {
1080 // Initialize range:
1081 $ranges = GeneralUtility::trimExplode(',', $cfg['value'], true);
1082 foreach ($ranges as $i => $rangeDef) {
1083 $ranges[$i] = GeneralUtility::intExplode('-', (string)$ranges[$i]);
1084 if (!isset($ranges[$i][1])) {
1085 $ranges[$i][1] = $ranges[$i][0];
1086 }
1087 }
1088 $newResult = [];
1089 // Traverse the current parts of the result array:
1090 foreach ($result as $part) {
1091 // Initialize:
1092 $currentState = -1;
1093 $bankAccum = '';
1094 // Explode the string value by the word value to highlight:
1095 $utf8Chars = $this->csConvObj->utf8_to_numberarray($part['str']);
1096 foreach ($utf8Chars as $utfChar) {
1097 // Find number and evaluate position:
1098 $uNumber = (int)$this->csConvObj->utf8CharToUnumber($utfChar);
1099 $inRange = 0;
1100 foreach ($ranges as $rangeDef) {
1101 if ($uNumber >= $rangeDef[0] && (!$rangeDef[1] || $uNumber <= $rangeDef[1])) {
1102 $inRange = 1;
1103 break;
1104 }
1105 }
1106 if ($currentState == -1) {
1107 $currentState = $inRange;
1108 }
1109 // Initialize first char
1110 // Switch bank:
1111 if ($inRange != $currentState && $uNumber !== 9 && $uNumber !== 10 && $uNumber !== 13 && $uNumber !== 32) {
1112 // Set result:
1113 if ($bankAccum !== '') {
1114 $newResult[] = [
1115 'str' => $bankAccum,
1116 'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1117 'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1118 'color' => $currentState ? $cfg['color'] : '',
1119 'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
1120 'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
1121 'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
1122 'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : ''
1123 ];
1124 }
1125 // Initialize new settings:
1126 $currentState = $inRange;
1127 $bankAccum = '';
1128 }
1129 // Add char to bank:
1130 $bankAccum .= $utfChar;
1131 }
1132 // Set result for FINAL part:
1133 if ($bankAccum !== '') {
1134 $newResult[] = [
1135 'str' => $bankAccum,
1136 'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1137 'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1138 'color' => $currentState ? $cfg['color'] : '',
1139 'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
1140 'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
1141 'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
1142 'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : ''
1143 ];
1144 }
1145 }
1146 // Set the new result as result array:
1147 if (!empty($newResult)) {
1148 $result = $newResult;
1149 }
1150 }
1151 break;
1152 }
1153 }
1154 }
1155 return $result;
1156 }
1157
1158 /**
1159 * Calculates the spacing and wordSpacing values
1160 *
1161 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
1162 * @param int $scaleFactor TypoScript value from eg $conf['niceText.']['scaleFactor']
1163 * @return array Array with two keys [0]/[1] being array($spacing,$wordSpacing)
1164 * @internal
1165 * @see calcBBox()
1166 */
1167 public function calcWordSpacing($conf, $scaleFactor = 1)
1168 {
1169 $spacing = (int)$conf['spacing'];
1170 $wordSpacing = (int)$conf['wordSpacing'];
1171 $wordSpacing = $wordSpacing ?: $spacing * 2;
1172 $spacing *= $scaleFactor;
1173 $wordSpacing *= $scaleFactor;
1174 return [$spacing, $wordSpacing];
1175 }
1176
1177 /**
1178 * Calculates and returns the niceText.scaleFactor
1179 *
1180 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
1181 * @return int TypoScript value from eg $conf['niceText.']['scaleFactor']
1182 * @internal
1183 */
1184 public function getTextScalFactor($conf)
1185 {
1186 if (!$conf['niceText']) {
1187 $sF = 1;
1188 } else {
1189 // NICETEXT::
1190 $sF = MathUtility::forceIntegerInRange($conf['niceText.']['scaleFactor'], 2, 5);
1191 }
1192 return $sF;
1193 }
1194
1195 /**
1196 * Renders a regular text and takes care of a possible line break automatically.
1197 *
1198 * @param resource $im (See argument for PHP function imageTTFtext())
1199 * @param int $fontSize (See argument for PHP function imageTTFtext())
1200 * @param int $angle (See argument for PHP function imageTTFtext())
1201 * @param int $x (See argument for PHP function imageTTFtext())
1202 * @param int $y (See argument for PHP function imageTTFtext())
1203 * @param int $color (See argument for PHP function imageTTFtext())
1204 * @param string $fontFile (See argument for PHP function imageTTFtext())
1205 * @param string $string (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
1206 * @param array $splitRendering Split-rendering configuration
1207 * @param array $conf The configuration
1208 * @param int $sF Scale factor
1209 */
1210 protected function renderTTFText(&$im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $conf, $sF = 1)
1211 {
1212 if (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($string, $conf) > $conf['breakWidth']) {
1213 $phrase = '';
1214 $currentWidth = 0;
1215 $breakWidth = $conf['breakWidth'];
1216 $breakSpace = $this->getBreakSpace($conf);
1217 $wordPairs = $this->getWordPairsForLineBreak($string);
1218 // Iterate through all word pairs:
1219 foreach ($wordPairs as $index => $wordPair) {
1220 $wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
1221 if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
1222 $currentWidth += $wordWidth;
1223 $phrase .= $wordPair;
1224 } else {
1225 // Render the current phrase that is below breakWidth:
1226 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
1227 // Calculate the news height offset:
1228 $y += $breakSpace;
1229 // Restart the phrase:
1230 $currentWidth = $wordWidth;
1231 $phrase = $wordPair;
1232 }
1233 }
1234 // Render the remaining phrase:
1235 if ($currentWidth) {
1236 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
1237 }
1238 } else {
1239 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF);
1240 }
1241 }
1242
1243 /**
1244 * Gets the word pairs used for automatic line breaks.
1245 *
1246 * @param string $string
1247 * @return array
1248 */
1249 protected function getWordPairsForLineBreak($string)
1250 {
1251 $wordPairs = [];
1252 $wordsArray = preg_split('#([- .,!:]+)#', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
1253 $wordsArray = is_array($wordsArray) ? $wordsArray : [];
1254 $wordsCount = count($wordsArray);
1255 for ($index = 0; $index < $wordsCount; $index += 2) {
1256 $wordPairs[] = $wordsArray[$index] . $wordsArray[$index + 1];
1257 }
1258 return $wordPairs;
1259 }
1260
1261 /**
1262 * Gets the rendered text width
1263 *
1264 * @param string $text
1265 * @param array $conf
1266 * @return int
1267 */
1268 protected function getRenderedTextWidth($text, $conf)
1269 {
1270 $bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $text, $conf['splitRendering.']);
1271 if ($conf['angle'] < 0) {
1272 $pixelWidth = abs($bounds[4] - $bounds[0]);
1273 } elseif ($conf['angle'] > 0) {
1274 $pixelWidth = abs($bounds[2] - $bounds[6]);
1275 } else {
1276 $pixelWidth = abs($bounds[4] - $bounds[6]);
1277 }
1278 return $pixelWidth;
1279 }
1280
1281 /**
1282 * Gets the break space for each new line.
1283 *
1284 * @param array $conf TypoScript configuration for the currently rendered object
1285 * @param array $boundingBox The bounding box the the currently rendered object
1286 * @return int The break space
1287 */
1288 protected function getBreakSpace($conf, array $boundingBox = null)
1289 {
1290 if (!isset($boundingBox)) {
1291 $boundingBox = $this->calcBBox($conf);
1292 $boundingBox = $boundingBox[2];
1293 }
1294 if (isset($conf['breakSpace']) && $conf['breakSpace']) {
1295 $breakSpace = $boundingBox['lineHeight'] * $conf['breakSpace'];
1296 } else {
1297 $breakSpace = $boundingBox['lineHeight'];
1298 }
1299 return $breakSpace;
1300 }
1301
1302 /*********************************************
1303 *
1304 * Other GIFBUILDER objects related to TEXT
1305 *
1306 *********************************************/
1307 /**
1308 * Implements the "OUTLINE" GIFBUILDER object / property for the TEXT object
1309 *
1310 * @param resource $im GDlib image pointer
1311 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1312 * @param array $workArea The current working area coordinates.
1313 * @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
1314 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1315 * @see makeText()
1316 */
1317 public function makeOutline(&$im, $conf, $workArea, $txtConf)
1318 {
1319 $thickness = (int)$conf['thickness'];
1320 if ($thickness) {
1321 $txtConf['fontColor'] = $conf['color'];
1322 $outLineDist = MathUtility::forceIntegerInRange($thickness, 1, 2);
1323 for ($b = 1; $b <= $outLineDist; $b++) {
1324 if ($b == 1) {
1325 $it = 8;
1326 } else {
1327 $it = 16;
1328 }
1329 $outL = $this->circleOffset($b, $it);
1330 for ($a = 0; $a < $it; $a++) {
1331 $this->makeText($im, $txtConf, $this->applyOffset($workArea, $outL[$a]));
1332 }
1333 }
1334 }
1335 }
1336
1337 /**
1338 * Creates some offset values in an array used to simulate a circularly applied outline around TEXT
1339 *
1340 * access private
1341 *
1342 * @param int $distance Distance
1343 * @param int $iterations Iterations.
1344 * @return array
1345 * @see makeOutline()
1346 */
1347 public function circleOffset($distance, $iterations)
1348 {
1349 $res = [];
1350 if ($distance && $iterations) {
1351 for ($a = 0; $a < $iterations; $a++) {
1352 $yOff = round(sin(2 * M_PI / $iterations * ($a + 1)) * 100 * $distance);
1353 if ($yOff) {
1354 $yOff = (int)(ceil(abs($yOff / 100)) * ($yOff / abs($yOff)));
1355 }
1356 $xOff = round(cos(2 * M_PI / $iterations * ($a + 1)) * 100 * $distance);
1357 if ($xOff) {
1358 $xOff = (int)(ceil(abs($xOff / 100)) * ($xOff / abs($xOff)));
1359 }
1360 $res[$a] = [$xOff, $yOff];
1361 }
1362 }
1363 return $res;
1364 }
1365
1366 /**
1367 * Implements the "EMBOSS" GIFBUILDER object / property for the TEXT object
1368 *
1369 * @param resource $im GDlib image pointer
1370 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1371 * @param array $workArea The current working area coordinates.
1372 * @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
1373 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1374 * @see makeShadow()
1375 */
1376 public function makeEmboss(&$im, $conf, $workArea, $txtConf)
1377 {
1378 $conf['color'] = $conf['highColor'];
1379 $this->makeShadow($im, $conf, $workArea, $txtConf);
1380 $newOffset = GeneralUtility::intExplode(',', $conf['offset']);
1381 $newOffset[0] *= -1;
1382 $newOffset[1] *= -1;
1383 $conf['offset'] = implode(',', $newOffset);
1384 $conf['color'] = $conf['lowColor'];
1385 $this->makeShadow($im, $conf, $workArea, $txtConf);
1386 }
1387
1388 /**
1389 * Implements the "SHADOW" GIFBUILDER object / property for the TEXT object
1390 * The operation involves ImageMagick for combining.
1391 *
1392 * @param resource $im GDlib image pointer
1393 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1394 * @param array $workArea The current working area coordinates.
1395 * @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
1396 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1397 * @see makeText()
1398 * @see makeEmboss()
1399 */
1400 public function makeShadow(&$im, $conf, $workArea, $txtConf)
1401 {
1402 $workArea = $this->applyOffset($workArea, GeneralUtility::intExplode(',', $conf['offset']));
1403 $blurRate = MathUtility::forceIntegerInRange((int)$conf['blur'], 0, 99);
1404 // No effects if ImageMagick ver. 5+
1405 if (!$blurRate || !$this->processorEffectsEnabled) {
1406 $txtConf['fontColor'] = $conf['color'];
1407 $this->makeText($im, $txtConf, $workArea);
1408 } else {
1409 $w = imagesx($im);
1410 $h = imagesy($im);
1411 // Area around the blur used for cropping something
1412 $blurBorder = 3;
1413 $tmpStr = $this->randomName();
1414 $fileMenu = $tmpStr . '_menu.' . $this->gifExtension;
1415 $fileColor = $tmpStr . '_color.' . $this->gifExtension;
1416 $fileMask = $tmpStr . '_mask.' . $this->gifExtension;
1417 // BlurColor Image laves
1418 $blurColImg = imagecreatetruecolor($w, $h);
1419 $bcols = $this->convertColor($conf['color']);
1420 $Bcolor = imagecolorallocate($blurColImg, $bcols[0], $bcols[1], $bcols[2]);
1421 imagefilledrectangle($blurColImg, 0, 0, $w, $h, $Bcolor);
1422 $this->ImageWrite($blurColImg, $fileColor);
1423 imagedestroy($blurColImg);
1424 // The mask is made: BlurTextImage
1425 $blurTextImg = imagecreatetruecolor($w + $blurBorder * 2, $h + $blurBorder * 2);
1426 // Black background
1427 $Bcolor = imagecolorallocate($blurTextImg, 0, 0, 0);
1428 imagefilledrectangle($blurTextImg, 0, 0, $w + $blurBorder * 2, $h + $blurBorder * 2, $Bcolor);
1429 $txtConf['fontColor'] = 'white';
1430 $blurBordArr = [$blurBorder, $blurBorder];
1431 $this->makeText($blurTextImg, $txtConf, $this->applyOffset($workArea, $blurBordArr));
1432 // Dump to temporary file
1433 $this->ImageWrite($blurTextImg, $fileMask);
1434 // Destroy
1435 imagedestroy($blurTextImg);
1436 $command = $this->v5_blur($blurRate + 1);
1437 $this->imageMagickExec($fileMask, $fileMask, $command . ' +matte');
1438 // The mask is loaded again
1439 $blurTextImg_tmp = $this->imageCreateFromFile($fileMask);
1440 // If nothing went wrong we continue with the blurred mask
1441 if ($blurTextImg_tmp) {
1442 // Cropping the border from the mask
1443 $blurTextImg = imagecreatetruecolor($w, $h);
1444 $this->imagecopyresized($blurTextImg, $blurTextImg_tmp, 0, 0, $blurBorder, $blurBorder, $w, $h, $w, $h);
1445 // Destroy the temporary mask
1446 imagedestroy($blurTextImg_tmp);
1447 // Adjust the mask
1448 $intensity = 40;
1449 if ($conf['intensity']) {
1450 $intensity = MathUtility::forceIntegerInRange($conf['intensity'], 0, 100);
1451 }
1452 $intensity = (int)ceil(255 - $intensity / 100 * 255);
1453 $this->inputLevels($blurTextImg, 0, $intensity);
1454 $opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 0, 100);
1455 if ($opacity && $opacity < 100) {
1456 $high = (int)ceil(255 * $opacity / 100);
1457 // Reducing levels as the opacity demands
1458 $this->outputLevels($blurTextImg, 0, $high);
1459 }
1460 // Dump the mask again
1461 $this->ImageWrite($blurTextImg, $fileMask);
1462 // Destroy the mask
1463 imagedestroy($blurTextImg);
1464 // The pictures are combined
1465 // The main pictures is saved temporarily
1466 $this->ImageWrite($im, $fileMenu);
1467 $this->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
1468 // The main image is loaded again...
1469 $backIm = $this->imageCreateFromFile($fileMenu);
1470 // ... and if nothing went wrong we load it onto the old one.
1471 if ($backIm) {
1472 if (!$this->saveAlphaLayer) {
1473 imagecolortransparent($backIm, -1);
1474 }
1475 $im = $backIm;
1476 }
1477 }
1478 // Deleting temporary files;
1479 unlink($fileMenu);
1480 unlink($fileColor);
1481 unlink($fileMask);
1482 }
1483 }
1484
1485 /****************************
1486 *
1487 * Other GIFBUILDER objects
1488 *
1489 ****************************/
1490 /**
1491 * Implements the "BOX" GIFBUILDER object
1492 *
1493 * @param resource $im GDlib image pointer
1494 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1495 * @param array $workArea The current working area coordinates.
1496 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1497 */
1498 public function makeBox(&$im, $conf, $workArea)
1499 {
1500 $cords = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
1501 $conf['offset'] = $cords[0] . ',' . $cords[1];
1502 $cords = $this->objPosition($conf, $workArea, [$cords[2], $cords[3]]);
1503 $cols = $this->convertColor($conf['color']);
1504 $opacity = 0;
1505 if (isset($conf['opacity'])) {
1506 // conversion:
1507 // PHP 0 = opaque, 127 = transparent
1508 // TYPO3 100 = opaque, 0 = transparent
1509 $opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 1, 100, 1);
1510 $opacity = (int)abs($opacity - 100);
1511 $opacity = (int)round(127 * $opacity / 100);
1512 }
1513 $tmpColor = imagecolorallocatealpha($im, $cols[0], $cols[1], $cols[2], $opacity);
1514 imagefilledrectangle($im, $cords[0], $cords[1], $cords[0] + $cords[2] - 1, $cords[1] + $cords[3] - 1, $tmpColor);
1515 }
1516
1517 /**
1518 * Implements the "Ellipse" GIFBUILDER object
1519 * Example Typoscript:
1520 * file = GIFBUILDER
1521 * file {
1522 * XY = 200,200
1523 * format = jpg
1524 * quality = 100
1525 * 10 = ELLIPSE
1526 * 10.dimensions = 100,100,50,50
1527 * 10.color = red
1528 *
1529 * $workArea = X,Y
1530 * $conf['dimensions'] = offset x, offset y, width of ellipse, height of ellipse
1531 *
1532 * @param resource $im GDlib image pointer
1533 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1534 * @param array $workArea The current working area coordinates.
1535 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1536 */
1537 public function makeEllipse(&$im, array $conf, array $workArea)
1538 {
1539 $ellipseConfiguration = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
1540 // Ellipse offset inside workArea (x/y)
1541 $conf['offset'] = $ellipseConfiguration[0] . ',' . $ellipseConfiguration[1];
1542 // @see objPosition
1543 $imageCoordinates = $this->objPosition($conf, $workArea, [$ellipseConfiguration[2], $ellipseConfiguration[3]]);
1544 $color = $this->convertColor($conf['color']);
1545 $fillingColor = imagecolorallocate($im, $color[0], $color[1], $color[2]);
1546 imagefilledellipse($im, $imageCoordinates[0], $imageCoordinates[1], $imageCoordinates[2], $imageCoordinates[3], $fillingColor);
1547 }
1548
1549 /**
1550 * Implements the "EFFECT" GIFBUILDER object
1551 * The operation involves ImageMagick for applying effects
1552 *
1553 * @param resource $im GDlib image pointer
1554 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1555 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1556 * @see applyImageMagickToPHPGif()
1557 */
1558 public function makeEffect(&$im, $conf)
1559 {
1560 $commands = $this->IMparams($conf['value']);
1561 if ($commands) {
1562 $this->applyImageMagickToPHPGif($im, $commands);
1563 }
1564 }
1565
1566 /**
1567 * Creating ImageMagick parameters from TypoScript property
1568 *
1569 * @param string $setup A string with effect keywords=value pairs separated by "|
1570 * @return string ImageMagick prepared parameters.
1571 * @internal
1572 * @see makeEffect()
1573 */
1574 public function IMparams($setup)
1575 {
1576 if (!trim($setup)) {
1577 return '';
1578 }
1579 $effects = explode('|', $setup);
1580 $commands = '';
1581 foreach ($effects as $val) {
1582 $pairs = explode('=', $val, 2);
1583 $value = trim($pairs[1]);
1584 $effect = strtolower(trim($pairs[0]));
1585 switch ($effect) {
1586 case 'gamma':
1587 $commands .= ' -gamma ' . (float)$value;
1588 break;
1589 case 'blur':
1590 if ($this->processorEffectsEnabled) {
1591 $commands .= $this->v5_blur((int)$value);
1592 }
1593 break;
1594 case 'sharpen':
1595 if ($this->processorEffectsEnabled) {
1596 $commands .= $this->v5_sharpen((int)$value);
1597 }
1598 break;
1599 case 'rotate':
1600 $commands .= ' -rotate ' . MathUtility::forceIntegerInRange((int)$value, 0, 360);
1601 break;
1602 case 'solarize':
1603 $commands .= ' -solarize ' . MathUtility::forceIntegerInRange((int)$value, 0, 99);
1604 break;
1605 case 'swirl':
1606 $commands .= ' -swirl ' . MathUtility::forceIntegerInRange((int)$value, 0, 1000);
1607 break;
1608 case 'wave':
1609 $params = GeneralUtility::intExplode(',', $value);
1610 $commands .= ' -wave ' . MathUtility::forceIntegerInRange($params[0], 0, 99) . 'x' . MathUtility::forceIntegerInRange($params[1], 0, 99);
1611 break;
1612 case 'charcoal':
1613 $commands .= ' -charcoal ' . MathUtility::forceIntegerInRange((int)$value, 0, 100);
1614 break;
1615 case 'gray':
1616 $commands .= ' -colorspace GRAY';
1617 break;
1618 case 'edge':
1619 $commands .= ' -edge ' . MathUtility::forceIntegerInRange((int)$value, 0, 99);
1620 break;
1621 case 'emboss':
1622 $commands .= ' -emboss';
1623 break;
1624 case 'flip':
1625 $commands .= ' -flip';
1626 break;
1627 case 'flop':
1628 $commands .= ' -flop';
1629 break;
1630 case 'colors':
1631 $commands .= ' -colors ' . MathUtility::forceIntegerInRange((int)$value, 2, 255);
1632 break;
1633 case 'shear':
1634 $commands .= ' -shear ' . MathUtility::forceIntegerInRange((int)$value, -90, 90);
1635 break;
1636 case 'invert':
1637 $commands .= ' -negate';
1638 break;
1639 }
1640 }
1641 return $commands;
1642 }
1643
1644 /**
1645 * Implements the "ADJUST" GIFBUILDER object
1646 *
1647 * @param resource $im GDlib image pointer
1648 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1649 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1650 * @see autoLevels()
1651 * @see outputLevels()
1652 * @see inputLevels()
1653 */
1654 public function adjust(&$im, $conf)
1655 {
1656 $setup = $conf['value'];
1657 if (!trim($setup)) {
1658 return;
1659 }
1660 $effects = explode('|', $setup);
1661 foreach ($effects as $val) {
1662 $pairs = explode('=', $val, 2);
1663 $value = trim($pairs[1]);
1664 $effect = strtolower(trim($pairs[0]));
1665 switch ($effect) {
1666 case 'inputlevels':
1667 // low,high
1668 $params = GeneralUtility::intExplode(',', $value);
1669 $this->inputLevels($im, $params[0], $params[1]);
1670 break;
1671 case 'outputlevels':
1672 $params = GeneralUtility::intExplode(',', $value);
1673 $this->outputLevels($im, $params[0], $params[1]);
1674 break;
1675 case 'autolevels':
1676 $this->autolevels($im);
1677 break;
1678 }
1679 }
1680 }
1681
1682 /**
1683 * Implements the "CROP" GIFBUILDER object
1684 *
1685 * @param resource $im GDlib image pointer
1686 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1687 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1688 */
1689 public function crop(&$im, $conf)
1690 {
1691 // Clears workArea to total image
1692 $this->setWorkArea('');
1693 $cords = GeneralUtility::intExplode(',', $conf['crop'] . ',,,');
1694 $conf['offset'] = $cords[0] . ',' . $cords[1];
1695 $cords = $this->objPosition($conf, $this->workArea, [$cords[2], $cords[3]]);
1696 $newIm = imagecreatetruecolor($cords[2], $cords[3]);
1697 $cols = $this->convertColor($conf['backColor'] ?: $this->setup['backColor']);
1698 $Bcolor = imagecolorallocate($newIm, $cols[0], $cols[1], $cols[2]);
1699 imagefilledrectangle($newIm, 0, 0, $cords[2], $cords[3], $Bcolor);
1700 $newConf = [];
1701 $workArea = [0, 0, $cords[2], $cords[3]];
1702 if ($cords[0] < 0) {
1703 $workArea[0] = abs($cords[0]);
1704 } else {
1705 $newConf['offset'] = -$cords[0];
1706 }
1707 if ($cords[1] < 0) {
1708 $workArea[1] = abs($cords[1]);
1709 } else {
1710 $newConf['offset'] .= ',' . -$cords[1];
1711 }
1712 $this->copyGifOntoGif($newIm, $im, $newConf, $workArea);
1713 $im = $newIm;
1714 $this->w = imagesx($im);
1715 $this->h = imagesy($im);
1716 // Clears workArea to total image
1717 $this->setWorkArea('');
1718 }
1719
1720 /**
1721 * Implements the "SCALE" GIFBUILDER object
1722 *
1723 * @param resource $im GDlib image pointer
1724 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1725 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1726 */
1727 public function scale(&$im, $conf)
1728 {
1729 if ($conf['width'] || $conf['height'] || $conf['params']) {
1730 $tmpStr = $this->randomName();
1731 $theFile = $tmpStr . '.' . $this->gifExtension;
1732 $this->ImageWrite($im, $theFile);
1733 $theNewFile = $this->imageMagickConvert($theFile, $this->gifExtension, $conf['width'], $conf['height'], $conf['params']);
1734 $tmpImg = $this->imageCreateFromFile($theNewFile[3]);
1735 if ($tmpImg) {
1736 imagedestroy($im);
1737 $im = $tmpImg;
1738 $this->w = imagesx($im);
1739 $this->h = imagesy($im);
1740 // Clears workArea to total image
1741 $this->setWorkArea('');
1742 }
1743 unlink($theFile);
1744 if ($theNewFile[3] && $theNewFile[3] != $theFile) {
1745 unlink($theNewFile[3]);
1746 }
1747 }
1748 }
1749
1750 /**
1751 * Implements the "WORKAREA" GIFBUILDER object when setting it
1752 * Setting internal working area boundaries (->workArea)
1753 *
1754 * @param string $workArea Working area dimensions, comma separated
1755 * @internal
1756 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1757 */
1758 public function setWorkArea($workArea)
1759 {
1760 $this->workArea = GeneralUtility::intExplode(',', $workArea);
1761 $this->workArea = $this->applyOffset($this->workArea, $this->OFFSET);
1762 if (!$this->workArea[2]) {
1763 $this->workArea[2] = $this->w;
1764 }
1765 if (!$this->workArea[3]) {
1766 $this->workArea[3] = $this->h;
1767 }
1768 }
1769
1770 /*************************
1771 *
1772 * Adjustment functions
1773 *
1774 ************************/
1775 /**
1776 * Apply auto-levels to input image pointer
1777 *
1778 * @param resource $im GDlib Image Pointer
1779 */
1780 public function autolevels(&$im)
1781 {
1782 $totalCols = imagecolorstotal($im);
1783 $grayArr = [];
1784 for ($c = 0; $c < $totalCols; $c++) {
1785 $cols = imagecolorsforindex($im, $c);
1786 $grayArr[] = round(($cols['red'] + $cols['green'] + $cols['blue']) / 3);
1787 }
1788 $min = min($grayArr);
1789 $max = max($grayArr);
1790 $delta = $max - $min;
1791 if ($delta) {
1792 for ($c = 0; $c < $totalCols; $c++) {
1793 $cols = imagecolorsforindex($im, $c);
1794 $cols['red'] = floor(($cols['red'] - $min) / $delta * 255);
1795 $cols['green'] = floor(($cols['green'] - $min) / $delta * 255);
1796 $cols['blue'] = floor(($cols['blue'] - $min) / $delta * 255);
1797 imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1798 }
1799 }
1800 }
1801
1802 /**
1803 * Apply output levels to input image pointer (decreasing contrast)
1804 *
1805 * @param resource $im GDlib Image Pointer
1806 * @param int $low The "low" value (close to 0)
1807 * @param int $high The "high" value (close to 255)
1808 * @param bool $swap If swap, then low and high are swapped. (Useful for negated masks...)
1809 */
1810 public function outputLevels(&$im, $low, $high, $swap = false)
1811 {
1812 if ($low < $high) {
1813 $low = MathUtility::forceIntegerInRange($low, 0, 255);
1814 $high = MathUtility::forceIntegerInRange($high, 0, 255);
1815 if ($swap) {
1816 $temp = $low;
1817 $low = 255 - $high;
1818 $high = 255 - $temp;
1819 }
1820 $delta = $high - $low;
1821 $totalCols = imagecolorstotal($im);
1822 for ($c = 0; $c < $totalCols; $c++) {
1823 $cols = imagecolorsforindex($im, $c);
1824 $cols['red'] = $low + floor($cols['red'] / 255 * $delta);
1825 $cols['green'] = $low + floor($cols['green'] / 255 * $delta);
1826 $cols['blue'] = $low + floor($cols['blue'] / 255 * $delta);
1827 imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1828 }
1829 }
1830 }
1831
1832 /**
1833 * Apply input levels to input image pointer (increasing contrast)
1834 *
1835 * @param resource $im GDlib Image Pointer
1836 * @param int $low The "low" value (close to 0)
1837 * @param int $high The "high" value (close to 255)
1838 */
1839 public function inputLevels(&$im, $low, $high)
1840 {
1841 if ($low < $high) {
1842 $low = MathUtility::forceIntegerInRange($low, 0, 255);
1843 $high = MathUtility::forceIntegerInRange($high, 0, 255);
1844 $delta = $high - $low;
1845 $totalCols = imagecolorstotal($im);
1846 for ($c = 0; $c < $totalCols; $c++) {
1847 $cols = imagecolorsforindex($im, $c);
1848 $cols['red'] = MathUtility::forceIntegerInRange((int)(($cols['red'] - $low) / $delta * 255), 0, 255);
1849 $cols['green'] = MathUtility::forceIntegerInRange((int)(($cols['green'] - $low) / $delta * 255), 0, 255);
1850 $cols['blue'] = MathUtility::forceIntegerInRange((int)(($cols['blue'] - $low) / $delta * 255), 0, 255);
1851 imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1852 }
1853 }
1854 }
1855
1856 /**
1857 * Reduce colors in image using IM and create a palette based image if possible (<=256 colors)
1858 *
1859 * @param string $file Image file to reduce
1860 * @param int $cols Number of colors to reduce the image to.
1861 * @return string Reduced file
1862 */
1863 public function IMreduceColors($file, $cols)
1864 {
1865 $fI = GeneralUtility::split_fileref($file);
1866 $ext = strtolower($fI['fileext']);
1867 $result = $this->randomName() . '.' . $ext;
1868 $reduce = MathUtility::forceIntegerInRange($cols, 0, $ext === 'gif' ? 256 : $this->truecolorColors, 0);
1869 if ($reduce > 0) {
1870 $params = ' -colors ' . $reduce;
1871 if ($reduce <= 256) {
1872 $params .= ' -type Palette';
1873 }
1874 $prefix = $ext === 'png' && $reduce <= 256 ? 'png8:' : '';
1875 $this->imageMagickExec($file, $prefix . $result, $params);
1876 if ($result) {
1877 return $result;
1878 }
1879 }
1880 return '';
1881 }
1882
1883 /*********************************
1884 *
1885 * GIFBUILDER Helper functions
1886 *
1887 *********************************/
1888 /**
1889 * Returns the IM command for sharpening with ImageMagick 5
1890 * Uses $this->im5fx_sharpenSteps for translation of the factor to an actual command.
1891 *
1892 * @param int $factor The sharpening factor, 0-100 (effectively in 10 steps)
1893 * @return string The sharpening command, eg. " -sharpen 3x4
1894 * @see makeText()
1895 * @see IMparams()
1896 * @see v5_blur()
1897 */
1898 public function v5_sharpen($factor)
1899 {
1900 $factor = MathUtility::forceIntegerInRange((int)ceil($factor / 10), 0, 10);
1901 $sharpenArr = explode(',', ',' . $this->im5fx_sharpenSteps);
1902 $sharpenF = trim($sharpenArr[$factor]);
1903 if ($sharpenF) {
1904 return ' -sharpen ' . $sharpenF;
1905 }
1906 return '';
1907 }
1908
1909 /**
1910 * Returns the IM command for blurring with ImageMagick 5.
1911 * Uses $this->im5fx_blurSteps for translation of the factor to an actual command.
1912 *
1913 * @param int $factor The blurring factor, 0-100 (effectively in 10 steps)
1914 * @return string The blurring command, eg. " -blur 3x4
1915 * @see makeText()
1916 * @see IMparams()
1917 * @see v5_sharpen()
1918 */
1919 public function v5_blur($factor)
1920 {
1921 $factor = MathUtility::forceIntegerInRange((int)ceil($factor / 10), 0, 10);
1922 $blurArr = explode(',', ',' . $this->im5fx_blurSteps);
1923 $blurF = trim($blurArr[$factor]);
1924 if ($blurF) {
1925 return ' -blur ' . $blurF;
1926 }
1927 return '';
1928 }
1929
1930 /**
1931 * Returns a random filename prefixed with "temp_" and then 32 char md5 hash (without extension).
1932 * Used by functions in this class to create truly temporary files for the on-the-fly processing. These files will most likely be deleted right away.
1933 *
1934 * @return string
1935 */
1936 public function randomName()
1937 {
1938 GeneralUtility::mkdir_deep(Environment::getVarPath() . '/transient/');
1939 return Environment::getVarPath() . '/transient/' . md5(StringUtility::getUniqueId());
1940 }
1941
1942 /**
1943 * Applies offset value to coordinated in $cords.
1944 * Basically the value of key 0/1 of $OFFSET is added to keys 0/1 of $cords
1945 *
1946 * @param array $cords Integer coordinates in key 0/1
1947 * @param array $OFFSET Offset values in key 0/1
1948 * @return array Modified $cords array
1949 */
1950 public function applyOffset($cords, $OFFSET)
1951 {
1952 $cords[0] = (int)$cords[0] + (int)$OFFSET[0];
1953 $cords[1] = (int)$cords[1] + (int)$OFFSET[1];
1954 return $cords;
1955 }
1956
1957 /**
1958 * Converts a "HTML-color" TypoScript datatype to RGB-values.
1959 * Default is 0,0,0
1960 *
1961 * @param string $string "HTML-color" data type string, eg. 'red', '#ffeedd' or '255,0,255'. You can also add a modifying operator afterwards. There are two options: "255,0,255 : 20" - will add 20 to values, result is "255,20,255". Or "255,0,255 : *1.23" which will multiply all RGB values with 1.23
1962 * @return array RGB values in key 0/1/2 of the array
1963 */
1964 public function convertColor($string)
1965 {
1966 $col = [];
1967 $cParts = explode(':', $string, 2);
1968 // Finding the RGB definitions of the color:
1969 $string = $cParts[0];
1970 if (strpos($string, '#') !== false) {
1971 $string = preg_replace('/[^A-Fa-f0-9]*/', '', $string) ?? '';
1972 $col[] = hexdec(substr($string, 0, 2));
1973 $col[] = hexdec(substr($string, 2, 2));
1974 $col[] = hexdec(substr($string, 4, 2));
1975 } elseif (strpos($string, ',') !== false) {
1976 $string = preg_replace('/[^,0-9]*/', '', $string) ?? '';
1977 $strArr = explode(',', $string);
1978 $col[] = (int)$strArr[0];
1979 $col[] = (int)$strArr[1];
1980 $col[] = (int)$strArr[2];
1981 } else {
1982 $string = strtolower(trim($string));
1983 if ($this->colMap[$string]) {
1984 $col = $this->colMap[$string];
1985 } else {
1986 $col = [0, 0, 0];
1987 }
1988 }
1989 // ... and possibly recalculating the value
1990 if (trim($cParts[1])) {
1991 $cParts[1] = trim($cParts[1]);
1992 if ($cParts[1][0] === '*') {
1993 $val = (float)substr($cParts[1], 1);
1994 $col[0] = MathUtility::forceIntegerInRange((int)($col[0] * $val), 0, 255);
1995 $col[1] = MathUtility::forceIntegerInRange((int)($col[1] * $val), 0, 255);
1996 $col[2] = MathUtility::forceIntegerInRange((int)($col[2] * $val), 0, 255);
1997 } else {
1998 $val = (int)$cParts[1];
1999 $col[0] = MathUtility::forceIntegerInRange((int)($col[0] + $val), 0, 255);
2000 $col[1] = MathUtility::forceIntegerInRange((int)($col[1] + $val), 0, 255);
2001 $col[2] = MathUtility::forceIntegerInRange((int)($col[2] + $val), 0, 255);
2002 }
2003 }
2004 return $col;
2005 }
2006
2007 /**
2008 * Create an array with object position/boundaries based on input TypoScript configuration (such as the "align" property is used), the work area definition and $BB array
2009 *
2010 * @param array $conf TypoScript configuration for a GIFBUILDER object
2011 * @param array $workArea Workarea definition
2012 * @param array $BB BB (Bounding box) array. Not just used for TEXT objects but also for others
2013 * @return array [0]=x, [1]=y, [2]=w, [3]=h
2014 * @internal
2015 * @see copyGifOntoGif()
2016 * @see makeBox()
2017 * @see crop()
2018 */
2019 public function objPosition($conf, $workArea, $BB)
2020 {
2021 // offset, align, valign, workarea
2022 $result = [];
2023 $result[2] = $BB[0];
2024 $result[3] = $BB[1];
2025 $w = $workArea[2];
2026 $h = $workArea[3];
2027 $align = explode(',', $conf['align']);
2028 $align[0] = strtolower(substr(trim($align[0]), 0, 1));
2029 $align[1] = strtolower(substr(trim($align[1]), 0, 1));
2030 switch ($align[0]) {
2031 case 'r':
2032 $result[0] = $w - $result[2];
2033 break;
2034 case 'c':
2035 $result[0] = round(($w - $result[2]) / 2);
2036 break;
2037 default:
2038 $result[0] = 0;
2039 }
2040 switch ($align[1]) {
2041 case 'b':
2042 // y pos
2043 $result[1] = $h - $result[3];
2044 break;
2045 case 'c':
2046 $result[1] = round(($h - $result[3]) / 2);
2047 break;
2048 default:
2049 $result[1] = 0;
2050 }
2051 $result = $this->applyOffset($result, GeneralUtility::intExplode(',', $conf['offset']));
2052 $result = $this->applyOffset($result, $workArea);
2053 return $result;
2054 }
2055
2056 /***********************************
2057 *
2058 * Scaling, Dimensions of images
2059 *
2060 ***********************************/
2061 /**
2062 * Converts $imagefile to another file in temp-dir of type $newExt (extension).
2063 *
2064 * @param string $imagefile The image filepath
2065 * @param string $newExt New extension, eg. "gif", "png", "jpg", "tif". If $newExt is NOT set, the new imagefile will be of the original format. If newExt = 'WEB' then one of the web-formats is applied.
2066 * @param string $w Width. $w / $h is optional. If only one is given the image is scaled proportionally. If an 'm' exists in the $w or $h and if both are present the $w and $h is regarded as the Maximum w/h and the proportions will be kept
2067 * @param string $h Height. See $w
2068 * @param string $params Additional ImageMagick parameters.
2069 * @param string $frame Refers to which frame-number to select in the image. '' or 0 will select the first frame, 1 will select the next and so on...
2070 * @param array $options An array with options passed to getImageScale (see this function).
2071 * @param bool $mustCreate If set, then another image than the input imagefile MUST be returned. Otherwise you can risk that the input image is good enough regarding measures etc and is of course not rendered to a new, temporary file in typo3temp/. But this option will force it to.
2072 * @return array|null [0]/[1] is w/h, [2] is file extension and [3] is the filename.
2073 * @see getImageScale()
2074 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getImgResource()
2075 * @see maskImageOntoImage()
2076 * @see copyImageOntoImage()
2077 * @see scale()
2078 */
2079 public function imageMagickConvert($imagefile, $newExt = '', $w = '', $h = '', $params = '', $frame = '', $options = [], $mustCreate = false)
2080 {
2081 if (!$this->processorEnabled) {
2082 // Returning file info right away
2083 return $this->getImageDimensions($imagefile);
2084 }
2085 $info = $this->getImageDimensions($imagefile);
2086 if (!$info) {
2087 return null;
2088 }
2089
2090 $newExt = strtolower(trim($newExt));
2091 // If no extension is given the original extension is used
2092 if (!$newExt) {
2093 $newExt = $info[2];
2094 }
2095 if ($newExt === 'web') {
2096 if (in_array($info[2], $this->webImageExt, true)) {
2097 $newExt = $info[2];
2098 } else {
2099 $newExt = $this->gif_or_jpg($info[2], $info[0], $info[1]);
2100 if (!$params) {
2101 $params = $this->cmds[$newExt];
2102 }
2103 }
2104 }
2105 if (!in_array($newExt, $this->imageFileExt, true)) {
2106 return null;
2107 }
2108
2109 $data = $this->getImageScale($info, $w, $h, $options);
2110 $w = $data['origW'];
2111 $h = $data['origH'];
2112 // If no conversion should be performed
2113 // this flag is TRUE if the width / height does NOT dictate
2114 // the image to be scaled!! (that is if no width / height is
2115 // given or if the destination w/h matches the original image
2116 // dimensions or if the option to not scale the image is set)
2117 $noScale = !$w && !$h || $data[0] == $info[0] && $data[1] == $info[1] || !empty($options['noScale']);
2118 if ($noScale && !$data['crs'] && !$params && !$frame && $newExt == $info[2] && !$mustCreate) {
2119 // Set the new width and height before returning,
2120 // if the noScale option is set
2121 if (!empty($options['noScale'])) {
2122 $info[0] = $data[0];
2123 $info[1] = $data[1];
2124 }
2125 $info[3] = $imagefile;
2126 return $info;
2127 }
2128 $info[0] = $data[0];
2129 $info[1] = $data[1];
2130 $frame = $this->addFrameSelection ? (int)$frame : 0;
2131 if (!$params) {
2132 $params = $this->cmds[$newExt];
2133 }
2134 // Cropscaling:
2135 if ($data['crs']) {
2136 if (!$data['origW']) {
2137 $data['origW'] = $data[0];
2138 }
2139 if (!$data['origH']) {
2140 $data['origH'] = $data[1];
2141 }
2142 $offsetX = (int)(($data[0] - $data['origW']) * ($data['cropH'] + 100) / 200);
2143 $offsetY = (int)(($data[1] - $data['origH']) * ($data['cropV'] + 100) / 200);
2144 $params .= ' -crop ' . $data['origW'] . 'x' . $data['origH'] . '+' . $offsetX . '+' . $offsetY . '! +repage';
2145 }
2146 $command = $this->scalecmd . ' ' . $info[0] . 'x' . $info[1] . '! ' . $params . ' ';
2147 // re-apply colorspace-setting for the resulting image so colors don't appear to dark (sRGB instead of RGB)
2148 $command .= ' -colorspace ' . $this->colorspace;
2149 $cropscale = $data['crs'] ? 'crs-V' . $data['cropV'] . 'H' . $data['cropH'] : '';
2150 if ($this->alternativeOutputKey) {
2151 $theOutputName = GeneralUtility::shortMD5($command . $cropscale . PathUtility::basename($imagefile) . $this->alternativeOutputKey . '[' . $frame . ']');
2152 } else {
2153 $theOutputName = GeneralUtility::shortMD5($command . $cropscale . $imagefile . filemtime($imagefile) . '[' . $frame . ']');
2154 }
2155 if ($this->imageMagickConvert_forceFileNameBody) {
2156 $theOutputName = $this->imageMagickConvert_forceFileNameBody;
2157 $this->imageMagickConvert_forceFileNameBody = '';
2158 }
2159 // Making the temporary filename
2160 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/typo3temp/assets/images/');
2161 $output = Environment::getPublicPath() . '/typo3temp/assets/images/' . $this->filenamePrefix . $theOutputName . '.' . $newExt;
2162 if ($this->dontCheckForExistingTempFile || !file_exists($output)) {
2163 $this->imageMagickExec($imagefile, $output, $command, $frame);
2164 }
2165 if (file_exists($output)) {
2166 $info[3] = $output;
2167 $info[2] = $newExt;
2168 // params might change some image data!
2169 if ($params) {
2170 $info = $this->getImageDimensions($info[3]);
2171 }
2172 if ($info[2] == $this->gifExtension && !$this->dontCompress) {
2173 // Compress with IM (lzw) or GD (rle) (Workaround for the absence of lzw-compression in GD)
2174 self::gifCompress($info[3], '');
2175 }
2176 return $info;
2177 }
2178 return null;
2179 }
2180
2181 /**
2182 * Gets the input image dimensions.
2183 *
2184 * @param string $imageFile The image filepath
2185 * @return array|null Returns an array where [0]/[1] is w/h, [2] is extension and [3] is the filename.
2186 * @see imageMagickConvert()
2187 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getImgResource()
2188 */
2189 public function getImageDimensions($imageFile)
2190 {
2191 $returnArr = null;
2192 preg_match('/([^\\.]*)$/', $imageFile, $reg);
2193 if (file_exists($imageFile) && in_array(strtolower($reg[0]), $this->imageFileExt, true)) {
2194 $returnArr = $this->getCachedImageDimensions($imageFile);
2195 if (!$returnArr) {
2196 $imageInfoObject = GeneralUtility::makeInstance(ImageInfo::class, $imageFile);
2197 if ($imageInfoObject->getWidth()) {
2198 $returnArr = [
2199 $imageInfoObject->getWidth(),
2200 $imageInfoObject->getHeight(),
2201 strtolower($reg[0]),
2202 $imageFile
2203 ];
2204 $this->cacheImageDimensions($returnArr);
2205 }
2206 }
2207 }
2208 return $returnArr;
2209 }
2210
2211 /**
2212 * Caches the result of the getImageDimensions function into the database. Does not check if the file exists.
2213 *
2214 * @param array $identifyResult Result of the getImageDimensions function
2215 *
2216 * @return bool always TRUE
2217 */
2218 public function cacheImageDimensions(array $identifyResult)
2219 {
2220 $filePath = $identifyResult[3];
2221 $statusHash = $this->generateStatusHashForImageFile($filePath);
2222 $identifier = $this->generateCacheKeyForImageFile($filePath);
2223
2224 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache */
2225 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('imagesizes');
2226 $imageDimensions = [
2227 'hash' => $statusHash,
2228 'imagewidth' => $identifyResult[0],
2229 'imageheight' => $identifyResult[1],
2230 ];
2231 $cache->set($identifier, $imageDimensions);
2232
2233 return true;
2234 }
2235
2236 /**
2237 * Fetches the cached image dimensions from the cache. Does not check if the image file exists.
2238 *
2239 * @param string $filePath Image file path, relative to public web path
2240 *
2241 * @return array|bool an array where [0]/[1] is w/h, [2] is extension and [3] is the file name,
2242 * or FALSE for a cache miss
2243 */
2244 public function getCachedImageDimensions($filePath)
2245 {
2246 $statusHash = $this->generateStatusHashForImageFile($filePath);
2247 $identifier = $this->generateCacheKeyForImageFile($filePath);
2248 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache */
2249 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('imagesizes');
2250 $cachedImageDimensions = $cache->get($identifier);
2251 if (!isset($cachedImageDimensions['hash'])) {
2252 return false;
2253 }
2254
2255 if ($cachedImageDimensions['hash'] !== $statusHash) {
2256 // The file has changed. Delete the cache entry.
2257 $cache->remove($identifier);
2258 $result = false;
2259 } else {
2260 preg_match('/([^\\.]*)$/', $filePath, $imageExtension);
2261 $result = [
2262 (int)$cachedImageDimensions['imagewidth'],
2263 (int)$cachedImageDimensions['imageheight'],
2264 strtolower($imageExtension[0]),
2265 $filePath
2266 ];
2267 }
2268
2269 return $result;
2270 }
2271
2272 /**
2273 * Creates the key for the image dimensions cache for an image file.
2274 *
2275 * This method does not check if the image file actually exists.
2276 *
2277 * @param string $filePath Image file path, relative to public web path
2278 *
2279 * @return string the hash key (an SHA1 hash), will not be empty
2280 */
2281 protected function generateCacheKeyForImageFile($filePath)
2282 {
2283 return sha1($filePath);
2284 }
2285
2286 /**
2287 * Creates the status hash to check whether a file has been changed.
2288 *
2289 * @param string $filePath Image file path, relative to public web path
2290 *
2291 * @return string the status hash (an SHA1 hash)
2292 */
2293 protected function generateStatusHashForImageFile($filePath)
2294 {
2295 $fileStatus = stat($filePath);
2296
2297 return sha1($fileStatus['mtime'] . $fileStatus['size']);
2298 }
2299
2300 /**
2301 * Get numbers for scaling the image based on input
2302 *
2303 * @param array $info Current image information: Width, Height etc.
2304 * @param string $w "required" width
2305 * @param string $h "required" height
2306 * @param array $options Options: Keys are like "maxW", "maxH", "minW", "minH
2307 * @return array
2308 * @internal
2309 * @see imageMagickConvert()
2310 */
2311 public function getImageScale($info, $w, $h, $options)
2312 {
2313 $out = [];
2314 $max = strpos($w . $h, 'm') !== false ? 1 : 0;
2315 if (strpos($w . $h, 'c') !== false) {
2316 $out['cropH'] = (int)substr((string)strstr($w, 'c'), 1);
2317 $out['cropV'] = (int)substr((string)strstr($h, 'c'), 1);
2318 $crs = true;
2319 } else {
2320 $crs = false;
2321 }
2322 $out['crs'] = $crs;
2323 $w = (int)$w;
2324 $h = (int)$h;
2325 // If there are max-values...
2326 if (!empty($options['maxW'])) {
2327 // If width is given...
2328 if ($w) {
2329 if ($w > $options['maxW']) {
2330 $w = $options['maxW'];
2331 // Height should follow
2332 $max = 1;
2333 }
2334 } else {
2335 if ($info[0] > $options['maxW']) {
2336 $w = $options['maxW'];
2337 // Height should follow
2338 $max = 1;
2339 }
2340 }
2341 }
2342 if (!empty($options['maxH'])) {
2343 // If height is given...
2344 if ($h) {
2345 if ($h > $options['maxH']) {
2346 $h = $options['maxH'];
2347 // Height should follow
2348 $max = 1;
2349 }
2350 } else {
2351 // Changed [0] to [1] 290801
2352 if ($info[1] > $options['maxH']) {
2353 $h = $options['maxH'];
2354 // Height should follow
2355 $max = 1;
2356 }
2357 }
2358 }
2359 $out['origW'] = $w;
2360 $out['origH'] = $h;
2361 $out['max'] = $max;
2362 if (!$this->mayScaleUp) {
2363 if ($w > $info[0]) {
2364 $w = $info[0];
2365 }
2366 if ($h > $info[1]) {
2367 $h = $info[1];
2368 }
2369 }
2370 // If scaling should be performed. Check that input "info" array will not cause division-by-zero
2371 if (($w || $h) && $info[0] && $info[1]) {
2372 if ($w && !$h) {
2373 $info[1] = ceil($info[1] * ($w / $info[0]));
2374 $info[0] = $w;
2375 }
2376 if (!$w && $h) {
2377 $info[0] = ceil($info[0] * ($h / $info[1]));
2378 $info[1] = $h;
2379 }
2380 if ($w && $h) {
2381 if ($max) {
2382 $ratio = $info[0] / $info[1];
2383 if ($h * $ratio > $w) {
2384 $h = round($w / $ratio);
2385 } else {
2386 $w = round($h * $ratio);
2387 }
2388 }
2389 if ($crs) {
2390 $ratio = $info[0] / $info[1];
2391 if ($h * $ratio < $w) {
2392 $h = round($w / $ratio);
2393 } else {
2394 $w = round($h * $ratio);
2395 }
2396 }
2397 $info[0] = $w;
2398 $info[1] = $h;
2399 }
2400 }
2401 $out[0] = $info[0];
2402 $out[1] = $info[1];
2403 // Set minimum-measures!
2404 if (isset($options['minW']) && $out[0] < $options['minW']) {
2405 if (($max || $crs) && $out[0]) {
2406 $out[1] = round($out[1] * $options['minW'] / $out[0]);
2407 }
2408 $out[0] = $options['minW'];
2409 }
2410 if (isset($options['minH']) && $out[1] < $options['minH']) {
2411 if (($max || $crs) && $out[1]) {
2412 $out[0] = round($out[0] * $options['minH'] / $out[1]);
2413 }
2414 $out[1] = $options['minH'];
2415 }
2416 return $out;
2417 }
2418
2419 /***********************************
2420 *
2421 * ImageMagick API functions
2422 *
2423 ***********************************/
2424 /**
2425 * Call the identify command
2426 *
2427 * @param string $imagefile The relative to public web path image filepath
2428 * @return array|null Returns an array where [0]/[1] is w/h, [2] is extension, [3] is the filename and [4] the real image type identified by ImageMagick.
2429 */
2430 public function imageMagickIdentify($imagefile)
2431 {
2432 if (!$this->processorEnabled) {
2433 return null;
2434 }
2435
2436 $result = $this->executeIdentifyCommandForImageFile($imagefile);
2437 if ($result) {
2438 [$width, $height, $fileExtension, $fileType] = explode(' ', $result);
2439 if ((int)$width && (int)$height) {
2440 return [$width, $height, strtolower($fileExtension), $imagefile, strtolower($fileType)];
2441 }
2442 }
2443 return null;
2444 }
2445
2446 /**
2447 * Internal function to execute an IM command fetching information on an image
2448 *
2449 * @param string $imageFile the absolute path to the image
2450 * @return string|null the raw result of the identify command.
2451 */
2452 protected function executeIdentifyCommandForImageFile(string $imageFile): ?string
2453 {
2454 $frame = $this->addFrameSelection ? 0 : null;
2455 $cmd = CommandUtility::imageMagickCommand(
2456 'identify',
2457 '-format "%w %h %e %m" ' . ImageMagickFile::fromFilePath($imageFile, $frame)
2458 );
2459 $returnVal = [];
2460 CommandUtility::exec($cmd, $returnVal);
2461 $result = array_pop($returnVal);
2462 $this->IM_commands[] = ['identify', $cmd, $result];
2463 return $result;
2464 }
2465
2466 /**
2467 * Executes an ImageMagick "convert" on two filenames, $input and $output using $params before them.
2468 * Can be used for many things, mostly scaling and effects.
2469 *
2470 * @param string $input The relative to public web path image filepath, input file (read from)
2471 * @param string $output The relative to public web path image filepath, output filename (written to)
2472 * @param string $params ImageMagick parameters
2473 * @param int $frame Optional, refers to which frame-number to select in the image. '' or 0
2474 * @return string The result of a call to PHP function "exec()
2475 */
2476 public function imageMagickExec($input, $output, $params, $frame = 0)
2477 {
2478 if (!$this->processorEnabled) {
2479 return '';
2480 }
2481 // If addFrameSelection is set in the Install Tool, a frame number is added to
2482 // select a specific page of the image (by default this will be the first page)
2483 $frame = $this->addFrameSelection ? (int)$frame : null;
2484 $cmd = CommandUtility::imageMagickCommand(
2485 'convert',
2486 $params
2487 . ' ' . ImageMagickFile::fromFilePath($input, $frame)
2488 . ' ' . CommandUtility::escapeShellArgument($output)
2489 );
2490 $this->IM_commands[] = [$output, $cmd];
2491 $ret = CommandUtility::exec($cmd);
2492 // Change the permissions of the file
2493 GeneralUtility::fixPermissions($output);
2494 return $ret;
2495 }
2496
2497 /**
2498 * Executes an ImageMagick "combine" (or composite in newer times) on four filenames - $input, $overlay and $mask as input files and $output as the output filename (written to)
2499 * Can be used for many things, mostly scaling and effects.
2500 *
2501 * @param string $input The relative to public web path image filepath, bottom file
2502 * @param string $overlay The relative to public web path image filepath, overlay file (top)
2503 * @param string $mask The relative to public web path image filepath, the mask file (grayscale)
2504 * @param string $output The relative to public web path image filepath, output filename (written to)
2505 * @return string
2506 */
2507 public function combineExec($input, $overlay, $mask, $output)
2508 {
2509 if (!$this->processorEnabled) {
2510 return '';
2511 }
2512 $theMask = $this->randomName() . '.' . $this->gifExtension;
2513 // +matte = no alpha layer in output
2514 $this->imageMagickExec($mask, $theMask, '-colorspace GRAY +matte');
2515
2516 $parameters = '-compose over'
2517 . ' -quality ' . $this->jpegQuality
2518 . ' +matte '
2519 . ImageMagickFile::fromFilePath($input) . ' '
2520 . ImageMagickFile::fromFilePath($overlay) . ' '
2521 . ImageMagickFile::fromFilePath($theMask) . ' '
2522 . CommandUtility::escapeShellArgument($output);
2523 $cmd = CommandUtility::imageMagickCommand('combine', $parameters);
2524 $this->IM_commands[] = [$output, $cmd];
2525 $ret = CommandUtility::exec($cmd);
2526 // Change the permissions of the file
2527 GeneralUtility::fixPermissions($output);
2528 if (is_file($theMask)) {
2529 @unlink($theMask);
2530 }
2531 return $ret;
2532 }
2533
2534 /**
2535 * Compressing a GIF file if not already LZW compressed.
2536 * This function is a workaround for the fact that ImageMagick and/or GD does not compress GIF-files to their minimum size (that is RLE or no compression used)
2537 *
2538 * The function takes a file-reference, $theFile, and saves it again through GD or ImageMagick in order to compress the file
2539 * GIF:
2540 * If $type is not set, the compression is done with ImageMagick (provided that $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path_lzw'] is pointing to the path of a lzw-enabled version of 'convert') else with GD (should be RLE-enabled!)
2541 * If $type is set to either 'IM' or 'GD' the compression is done with ImageMagick and GD respectively
2542 * PNG:
2543 * No changes.
2544 *
2545 * $theFile is expected to be a valid GIF-file!
2546 * The function returns a code for the operation.
2547 *
2548 * @param string $theFile Filepath
2549 * @param string $type See description of function
2550 * @return string Returns "GD" if GD was used, otherwise "IM" if ImageMagick was used. If nothing done at all, it returns empty string.
2551 */
2552 public static function gifCompress($theFile, $type)
2553 {
2554 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
2555 if (!$gfxConf['gif_compress'] || strtolower(substr($theFile, -4, 4)) !== '.gif') {
2556 return '';
2557 }
2558
2559 if (($type === 'IM' || !$type) && $gfxConf['processor_enabled'] && $gfxConf['processor_path_lzw']) {
2560 // Use temporary file to prevent problems with read and write lock on same file on network file systems
2561 $temporaryName = PathUtility::dirname($theFile) . '/' . md5(StringUtility::getUniqueId()) . '.gif';
2562 // Rename could fail, if a simultaneous thread is currently working on the same thing
2563 if (@rename($theFile, $temporaryName)) {
2564 $cmd = CommandUtility::imageMagickCommand(
2565 'convert',
2566 ImageMagickFile::fromFilePath($temporaryName) . ' ' . CommandUtility::escapeShellArgument($theFile),
2567 $gfxConf['processor_path_lzw']
2568 );
2569 CommandUtility::exec($cmd);
2570 unlink($temporaryName);
2571 }
2572 $returnCode = 'IM';
2573 if (@is_file($theFile)) {
2574 GeneralUtility::fixPermissions($theFile);
2575 }
2576 } elseif (($type === 'GD' || !$type) && $gfxConf['gdlib'] && !$gfxConf['gdlib_png']) {
2577 $tempImage = imagecreatefromgif($theFile);
2578 imagegif($tempImage, $theFile);
2579 imagedestroy($tempImage);
2580 $returnCode = 'GD';
2581 if (@is_file($theFile)) {
2582 GeneralUtility::fixPermissions($theFile);
2583 }
2584 } else {
2585 $returnCode = '';
2586 }
2587
2588 return $returnCode;
2589 }
2590
2591 /**
2592 * Returns filename of the png/gif version of the input file (which can be png or gif).
2593 * If input file type does not match the wanted output type a conversion is made and temp-filename returned.
2594 *
2595 * @param string $theFile Filepath of image file
2596 * @param bool $output_png If TRUE, then input file is converted to PNG, otherwise to GIF
2597 * @return string|null If the new image file exists, its filepath is returned
2598 */
2599 public static function readPngGif($theFile, $output_png = false)
2600 {
2601 if (!$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'] || !@is_file($theFile)) {
2602 return null;
2603 }
2604
2605 $ext = strtolower(substr($theFile, -4, 4));
2606 if ((string)$ext === '.png' && $output_png || (string)$ext === '.gif' && !$output_png) {
2607 return $theFile;
2608 }
2609
2610 if (!@is_dir(Environment::getPublicPath() . '/typo3temp/assets/images/')) {
2611 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/typo3temp/assets/images/');
2612 }
2613 $newFile = Environment::getPublicPath() . '/typo3temp/assets/images/' . md5($theFile . '|' . filemtime($theFile)) . ($output_png ? '.png' : '.gif');
2614 $cmd = CommandUtility::imageMagickCommand(
2615 'convert',
2616 ImageMagickFile::fromFilePath($theFile)
2617 . ' ' . CommandUtility::escapeShellArgument($newFile),
2618 $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']
2619 );
2620 CommandUtility::exec($cmd);
2621 if (@is_file($newFile)) {
2622 GeneralUtility::fixPermissions($newFile);
2623 return $newFile;
2624 }
2625 return null;
2626 }
2627
2628 /***********************************
2629 *
2630 * Various IO functions
2631 *
2632 ***********************************/
2633
2634 /**
2635 * Applies an ImageMagick parameter to a GDlib image pointer resource by writing the resource to file, performing an IM operation upon it and reading back the result into the ImagePointer.
2636 *
2637 * @param resource $im The image pointer (reference)
2638 * @param string $command The ImageMagick parameters. Like effects, scaling etc.
2639 */
2640 public function applyImageMagickToPHPGif(&$im, $command)
2641 {
2642 $tmpStr = $this->randomName();
2643 $theFile = $tmpStr . '.' . $this->gifExtension;
2644 $this->ImageWrite($im, $theFile);
2645 $this->imageMagickExec($theFile, $theFile, $command);
2646 $tmpImg = $this->imageCreateFromFile($theFile);
2647 if ($tmpImg) {
2648 imagedestroy($im);
2649 $im = $tmpImg;
2650 $this->w = imagesx($im);
2651 $this->h = imagesy($im);
2652 }
2653 unlink($theFile);
2654 }
2655
2656 /**
2657 * Returns an image extension for an output image based on the number of pixels of the output and the file extension of the original file.
2658 * For example: If the number of pixels exceeds $this->pixelLimitGif (normally 10000) then it will be a "jpg" string in return.
2659 *
2660 * @param string $type The file extension, lowercase.
2661 * @param int $w The width of the output image.
2662 * @param int $h The height of the output image.
2663 * @return string The filename, either "jpg" or "gif"/"png" (whatever $this->gifExtension is set to.)
2664 */
2665 public function gif_or_jpg($type, $w, $h)
2666 {
2667 if ($type === 'ai' || $w * $h < $this->pixelLimitGif) {
2668 return $this->gifExtension;
2669 }
2670 return 'jpg';
2671 }
2672
2673 /**
2674 * Writing the internal image pointer, $this->im, to file based on the extension of the input filename
2675 * Used in GIFBUILDER
2676 * Uses $this->setup['reduceColors'] for gif/png images and $this->setup['quality'] for jpg images to reduce size/quality if needed.
2677 *
2678 * @param string $file The filename to write to.
2679 * @return string Returns input filename
2680 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::gifBuild()
2681 */
2682 public function output($file)
2683 {
2684 if ($file) {
2685 $reg = [];
2686 preg_match('/([^\\.]*)$/', $file, $reg);
2687 $ext = strtolower($reg[0]);
2688 switch ($ext) {
2689 case 'gif':
2690 case 'png':
2691 if ($this->ImageWrite($this->im, $file)) {
2692 // ImageMagick operations
2693 if ($this->setup['reduceColors']) {
2694 $reduced = $this->IMreduceColors($file, MathUtility::forceIntegerInRange($this->setup['reduceColors'], 256, $this->truecolorColors, 256));
2695 if ($reduced) {
2696 @copy($reduced, $file);
2697 @unlink($reduced);
2698 }
2699 }
2700 // Compress with IM! (adds extra compression, LZW from ImageMagick)
2701 // (Workaround for the absence of lzw-compression in GD)
2702 self::gifCompress($file, 'IM');
2703 }
2704 break;
2705 case 'jpg':
2706 case 'jpeg':
2707 // Use the default
2708 $quality = 0;
2709 if ($this->setup['quality']) {
2710 $quality = MathUtility::forceIntegerInRange($this->setup['quality'], 10, 100);
2711 }
2712 $this->ImageWrite($this->im, $file, $quality);
2713 break;
2714 }
2715 }
2716 return $file;
2717 }
2718
2719 /**
2720 * Destroy internal image pointer, $this->im
2721 *
2722 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::gifBuild()
2723 */
2724 public function destroy()
2725 {
2726 imagedestroy($this->im);
2727 }
2728
2729 /**
2730 * Returns Image Tag for input image information array.
2731 *
2732 * @param array $imgInfo Image information array, key 0/1 is width/height and key 3 is the src value
2733 * @return string Image tag for the input image information array.
2734 */
2735 public function imgTag($imgInfo)
2736 {
2737 return '<img src="' . $imgInfo[3] . '" width="' . $imgInfo[0] . '" height="' . $imgInfo[1] . '" border="0" alt="" />';
2738 }
2739
2740 /**
2741 * Writes the input GDlib image pointer to file
2742 *
2743 * @param resource $destImg The GDlib image resource pointer
2744 * @param string $theImage The filename to write to
2745 * @param int $quality The image quality (for JPEGs)
2746 * @return bool The output of either imageGif, imagePng or imageJpeg based on the filename to write
2747 * @see maskImageOntoImage()
2748 * @see scale()
2749 * @see output()
2750 */
2751 public function ImageWrite($destImg, $theImage, $quality = 0)
2752 {
2753 imageinterlace($destImg, 0);
2754 $ext = strtolower(substr($theImage, (int)strrpos($theImage, '.') + 1));
2755 $result = false;
2756 switch ($ext) {
2757 case 'jpg':
2758 case 'jpeg':
2759 if (function_exists('imagejpeg')) {
2760 if ($quality === 0) {
2761 $quality = $this->jpegQuality;
2762 }
2763 $result = imagejpeg($destImg, $theImage, $quality);
2764 }
2765 break;
2766 case 'gif':
2767 if (function_exists('imagegif')) {
2768 imagetruecolortopalette($destImg, true, 256);
2769 $result = imagegif($destImg, $theImage);
2770 }
2771 break;
2772 case 'png':
2773 if (function_exists('imagepng')) {
2774 $result = imagepng($destImg, $theImage);
2775 }
2776 break;
2777 }
2778 if ($result) {
2779 GeneralUtility::fixPermissions($theImage);
2780 }
2781 return $result;
2782 }
2783
2784 /**
2785 * Creates a new GDlib image resource based on the input image filename.
2786 * If it fails creating an image from the input file a blank gray image with the dimensions of the input image will be created instead.
2787 *
2788 * @param string $sourceImg Image filename
2789 * @return resource Image Resource pointer
2790 */
2791 public function imageCreateFromFile($sourceImg)
2792 {
2793 $imgInf = pathinfo($sourceImg);
2794 $ext = strtolower($imgInf['extension']);
2795 switch ($ext) {
2796 case 'gif':
2797 if (function_exists('imagecreatefromgif')) {
2798 return imagecreatefromgif($sourceImg);
2799 }
2800 break;
2801 case 'png':
2802 if (function_exists('imagecreatefrompng')) {
2803 $imageHandle = imagecreatefrompng($sourceImg);
2804 if ($this->saveAlphaLayer) {
2805 imagesavealpha($imageHandle, true);
2806 }
2807 return $imageHandle;
2808 }
2809 break;
2810 case 'jpg':
2811 case 'jpeg':
2812 if (function_exists('imagecreatefromjpeg')) {
2813 return imagecreatefromjpeg($sourceImg);
2814 }
2815 break;
2816 }
2817 // If non of the above:
2818 $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $sourceImg);
2819 $im = imagecreatetruecolor($imageInfo->getWidth(), $imageInfo->getHeight());
2820 $Bcolor = imagecolorallocate($im, 128, 128, 128);
2821 imagefilledrectangle($im, 0, 0, $imageInfo->getWidth(), $imageInfo->getHeight(), $Bcolor);
2822 return $im;
2823 }
2824
2825 /**
2826 * Returns the HEX color value for an RGB color array
2827 *
2828 * @param array $color RGB color array
2829 * @return string HEX color value
2830 */
2831 public function hexColor($color)
2832 {
2833 $r = dechex($color[0]);
2834 if (strlen($r) < 2) {
2835 $r = '0' . $r;
2836 }
2837 $g = dechex($color[1]);
2838 if (strlen($g) < 2) {
2839 $g = '0' . $g;
2840 }
2841 $b = dechex($color[2]);
2842 if (strlen($b) < 2) {
2843 $b = '0' . $b;
2844 }
2845 return '#' . $r . $g . $b;
2846 }
2847
2848 /**
2849 * Unifies all colors given in the colArr color array to the first color in the array.
2850 *
2851 * @param resource $img Image resource
2852 * @param array $colArr Array containing RGB color arrays
2853 * @param bool $closest
2854 * @return int The index of the unified color
2855 */
2856 public function unifyColors(&$img, $colArr, $closest = false)
2857 {
2858 $retCol = -1;
2859 if (is_array($colArr) && !empty($colArr) && function_exists('imagepng') && function_exists('imagecreatefrompng')) {
2860 $firstCol = array_shift($colArr);
2861 $firstColArr = $this->convertColor($firstCol);
2862 $origName = $preName = $this->randomName() . '.png';
2863 $postName = $this->randomName() . '.png';
2864 $tmpImg = null;
2865 if (count($colArr) > 1) {
2866 $this->ImageWrite($img, $preName);
2867 $firstCol = $this->hexColor($firstColArr);
2868 foreach ($colArr as $transparentColor) {
2869 $transparentColor = $this->convertColor($transparentColor);
2870 $transparentColor = $this->hexColor($transparentColor);
2871 $cmd = '-fill "' . $firstCol . '" -opaque "' . $transparentColor . '"';
2872 $this->imageMagickExec($preName, $postName, $cmd);
2873 $preName = $postName;
2874 }
2875 $this->imageMagickExec($postName, $origName, '');
2876 if (@is_file($origName)) {
2877 $tmpImg = $this->imageCreateFromFile($origName);
2878 }
2879 } else {
2880 $tmpImg = $img;
2881 }
2882 if ($tmpImg) {
2883 $img = $tmpImg;
2884 if ($closest) {
2885 $retCol = imagecolorclosest($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
2886 } else {
2887 $retCol = imagecolorexact($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
2888 }
2889 }
2890 // Unlink files from process
2891 if ($origName) {
2892 @unlink($origName);
2893 }
2894 if ($postName) {
2895 @unlink($postName);
2896 }
2897 }
2898 return $retCol;
2899 }
2900
2901 /**
2902 * Creates error image based on gfx/notfound_thumb.png
2903 * Requires GD lib enabled, otherwise it will exit with the three
2904 * textstrings outputted as text. Outputs the image stream to browser and exits!
2905 *
2906 * @param string $filename Name of the file
2907 * @param string $textline1 Text line 1
2908 * @param string $textline2 Text line 2
2909 * @param string $textline3 Text line 3
2910 * @throws \RuntimeException
2911 */
2912 public function getTemporaryImageWithText($filename, $textline1, $textline2, $textline3)
2913 {
2914 if (empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib'])) {
2915 throw new \RuntimeException('TYPO3 Fatal Error: No gdlib. ' . $textline1 . ' ' . $textline2 . ' ' . $textline3, 1270853952);
2916 }
2917 // Creates the basis for the error image
2918 $basePath = ExtensionManagementUtility::extPath('core') . 'Resources/Public/Images/';
2919 if (!empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'])) {
2920 $im = imagecreatefrompng($basePath . 'NotFound.png');
2921 } else {
2922 $im = imagecreatefromgif($basePath . 'NotFound.gif');
2923 }
2924 // Sets background color and print color.
2925 $white = imagecolorallocate($im, 255, 255, 255);
2926 $black = imagecolorallocate($im, 0, 0, 0);
2927 // Prints the text strings with the build-in font functions of GD
2928 $x = 0;
2929 $font = 0;
2930 if ($textline1) {
2931 imagefilledrectangle($im, $x, 9, 56, 16, $white);
2932 imagestring($im, $font, $x, 9, $textline1, $black);
2933 }
2934 if ($textline2) {
2935 imagefilledrectangle($im, $x, 19, 56, 26, $white);
2936 imagestring($im, $font, $x, 19, $textline2, $black);
2937 }
2938 if ($textline3) {
2939 imagefilledrectangle($im, $x, 29, 56, 36, $white);
2940 imagestring($im, $font, $x, 29, substr($textline3, -14), $black);
2941 }
2942 // Outputting the image stream and exit
2943 if (!empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'])) {
2944 imagepng($im, $filename);
2945 } else {
2946 imagegif($im, $filename);
2947 }
2948 }
2949
2950 /**
2951 * Function to compensate for DPI resolution.
2952 * FreeType 2 always has 96 dpi, so it is hard-coded at this place.
2953 *
2954 * @param float $fontSize font size for freetype function call
2955 * @return float compensated font size based on 96 dpi
2956 */
2957 protected function compensateFontSizeiBasedOnFreetypeDpi($fontSize)
2958 {
2959 return $fontSize / 96.0 * 72;
2960 }
2961 }