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 16namespace TYPO3\CMS\Frontend\Imaging; 17 18use TYPO3\CMS\Core\Context\Context; 19use TYPO3\CMS\Core\Context\FileProcessingAspect; 20use TYPO3\CMS\Core\Core\Environment; 21use TYPO3\CMS\Core\Imaging\GraphicalFunctions; 22use TYPO3\CMS\Core\Resource\Exception; 23use TYPO3\CMS\Core\Resource\File; 24use TYPO3\CMS\Core\Resource\ProcessedFile; 25use TYPO3\CMS\Core\Utility\ArrayUtility; 26use TYPO3\CMS\Core\Utility\File\BasicFileUtility; 27use TYPO3\CMS\Core\Utility\GeneralUtility; 28use TYPO3\CMS\Core\Utility\MathUtility; 29use TYPO3\CMS\Core\Utility\PathUtility; 30use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; 31use TYPO3\CMS\Frontend\Resource\FilePathSanitizer; 32 33/** 34 * GIFBUILDER 35 * 36 * Generating gif/png-files from TypoScript 37 * Used by the menu-objects and imgResource in TypoScript. 38 * 39 * This class allows for advanced rendering of images with various layers of images, text and graphical primitives. 40 * The concept is known from TypoScript as "GIFBUILDER" where you can define a "numerical array" (TypoScript term as well) of "GIFBUILDER OBJECTS" (like "TEXT", "IMAGE", etc.) and they will be rendered onto an image one by one. 41 * The name "GIFBUILDER" comes from the time where GIF was the only file format supported. PNG is just as well to create today (configured with TYPO3_CONF_VARS[GFX]) 42 * Not all instances of this class is truly building gif/png files by layers; You may also see the class instantiated for the purpose of using the scaling functions in the parent class. 43 * 44 * Here is an example of how to use this class (from tslib_content.php, function getImgResource): 45 * 46 * $gifCreator = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Imaging\GifBuilder::class); 47 * $gifCreator->init(); 48 * $theImage=''; 49 * if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) { 50 * $gifCreator->start($fileArray, $this->data); 51 * $theImage = $gifCreator->gifBuild(); 52 * } 53 * return $gifCreator->getImageDimensions($theImage); 54 */ 55class GifBuilder extends GraphicalFunctions 56{ 57 /** 58 * Contains all text strings used on this image 59 * 60 * @var array 61 */ 62 public $combinedTextStrings = []; 63 64 /** 65 * Contains all filenames (basename without extension) used on this image 66 * 67 * @var array 68 */ 69 public $combinedFileNames = []; 70 71 /** 72 * This is the array from which data->field: [key] is fetched. So this is the current record! 73 * 74 * @var array 75 */ 76 public $data = []; 77 78 /** 79 * @var array 80 */ 81 public $objBB = []; 82 83 /** 84 * @var string 85 */ 86 public $myClassName = 'gifbuilder'; 87 88 /** 89 * @var array 90 */ 91 public $charRangeMap = []; 92 93 /** 94 * @var int[] 95 */ 96 public $XY = []; 97 98 /** 99 * @var ContentObjectRenderer 100 */ 101 public $cObj; 102 103 /** 104 * @var array 105 */ 106 public $defaultWorkArea = []; 107 108 /** 109 * Initialization of the GIFBUILDER objects, in particular TEXT and IMAGE. This includes finding the bounding box, setting dimensions and offset values before the actual rendering is started. 110 * Modifies the ->setup, ->objBB internal arrays 111 * Should be called after the ->init() function which initializes the parent class functions/variables in general. 112 * 113 * @param array $conf TypoScript properties for the GIFBUILDER session. Stored internally in the variable ->setup 114 * @param array $data The current data record from \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer. Stored internally in the variable ->data 115 * @see ContentObjectRenderer::getImgResource() 116 */ 117 public function start($conf, $data) 118 { 119 if (is_array($conf)) { 120 $this->setup = $conf; 121 $this->data = $data; 122 $this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 123 $this->cObj->start($this->data); 124 // Hook preprocess gifbuilder conf 125 // Added by Julle for 3.8.0 126 // 127 // Let's you pre-process the gifbuilder configuration. for 128 // example you can split a string up into lines and render each 129 // line as TEXT obj, see extension julle_gifbconf 130 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_gifbuilder.php']['gifbuilder-ConfPreProcess'] ?? [] as $_funcRef) { 131 $_params = $this->setup; 132 $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction 133 $this->setup = GeneralUtility::callUserFunction($_funcRef, $_params, $ref); 134 } 135 // Initializing global Char Range Map 136 $this->charRangeMap = []; 137 if (is_array($GLOBALS['TSFE']->tmpl->setup['_GIFBUILDER.']['charRangeMap.'])) { 138 foreach ($GLOBALS['TSFE']->tmpl->setup['_GIFBUILDER.']['charRangeMap.'] as $cRMcfgkey => $cRMcfg) { 139 if (is_array($cRMcfg)) { 140 // Initializing: 141 $cRMkey = $GLOBALS['TSFE']->tmpl->setup['_GIFBUILDER.']['charRangeMap.'][substr($cRMcfgkey, 0, -1)]; 142 $this->charRangeMap[$cRMkey] = []; 143 $this->charRangeMap[$cRMkey]['charMapConfig'] = $cRMcfg['charMapConfig.']; 144 $this->charRangeMap[$cRMkey]['cfgKey'] = substr($cRMcfgkey, 0, -1); 145 $this->charRangeMap[$cRMkey]['multiplicator'] = (double)$cRMcfg['fontSizeMultiplicator']; 146 $this->charRangeMap[$cRMkey]['pixelSpace'] = (int)$cRMcfg['pixelSpaceFontSizeRef']; 147 } 148 } 149 } 150 // Getting sorted list of TypoScript keys from setup. 151 $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($this->setup); 152 // Setting the background color, passing it through stdWrap 153 if ($conf['backColor.'] || $conf['backColor']) { 154 $this->setup['backColor'] = isset($this->setup['backColor.']) ? trim($this->cObj->stdWrap($this->setup['backColor'], $this->setup['backColor.'])) : $this->setup['backColor']; 155 } 156 if (!$this->setup['backColor']) { 157 $this->setup['backColor'] = 'white'; 158 } 159 if ($conf['transparentColor.'] || $conf['transparentColor']) { 160 $this->setup['transparentColor_array'] = isset($this->setup['transparentColor.']) ? explode('|', trim($this->cObj->stdWrap($this->setup['transparentColor'], $this->setup['transparentColor.']))) : explode('|', trim($this->setup['transparentColor'])); 161 } 162 if (isset($this->setup['transparentBackground.'])) { 163 $this->setup['transparentBackground'] = $this->cObj->stdWrap($this->setup['transparentBackground'], $this->setup['transparentBackground.']); 164 } 165 if (isset($this->setup['reduceColors.'])) { 166 $this->setup['reduceColors'] = $this->cObj->stdWrap($this->setup['reduceColors'], $this->setup['reduceColors.']); 167 } 168 // Set default dimensions 169 if (isset($this->setup['XY.'])) { 170 $this->setup['XY'] = $this->cObj->stdWrap($this->setup['XY'], $this->setup['XY.']); 171 } 172 if (!$this->setup['XY']) { 173 $this->setup['XY'] = '120,50'; 174 } 175 // Checking TEXT and IMAGE objects for files. If any errors the objects are cleared. 176 // The Bounding Box for the objects is stored in an array 177 foreach ($sKeyArray as $theKey) { 178 $theValue = $this->setup[$theKey]; 179 if ((int)$theKey && ($conf = $this->setup[$theKey . '.'])) { 180 // Swipes through TEXT and IMAGE-objects 181 switch ($theValue) { 182 case 'TEXT': 183 if ($this->setup[$theKey . '.'] = $this->checkTextObj($conf)) { 184 // Adjust font width if max size is set: 185 $maxWidth = isset($this->setup[$theKey . '.']['maxWidth.']) ? $this->cObj->stdWrap($this->setup[$theKey . '.']['maxWidth'], $this->setup[$theKey . '.']['maxWidth.']) : $this->setup[$theKey . '.']['maxWidth']; 186 if ($maxWidth) { 187 $this->setup[$theKey . '.']['fontSize'] = $this->fontResize($this->setup[$theKey . '.']); 188 } 189 // Calculate bounding box: 190 $txtInfo = $this->calcBBox($this->setup[$theKey . '.']); 191 $this->setup[$theKey . '.']['BBOX'] = $txtInfo; 192 $this->objBB[$theKey] = $txtInfo; 193 $this->setup[$theKey . '.']['imgMap'] = 0; 194 } 195 break; 196 case 'IMAGE': 197 $fileInfo = $this->getResource($conf['file'], $conf['file.']); 198 if ($fileInfo) { 199 $this->combinedFileNames[] = preg_replace('/\\.[[:alnum:]]+$/', '', PathUtility::basename($fileInfo[3])); 200 if ($fileInfo['processedFile'] instanceof ProcessedFile) { 201 // Use processed file, if a FAL file has been processed by GIFBUILDER (e.g. scaled/cropped) 202 $this->setup[$theKey . '.']['file'] = $fileInfo['processedFile']->getForLocalProcessing(false); 203 } elseif (!isset($fileInfo['origFile']) && $fileInfo['originalFile'] instanceof File) { 204 // Use FAL file with getForLocalProcessing to circumvent problems with umlauts, if it is a FAL file (origFile not set) 205 /** @var File $originalFile */ 206 $originalFile = $fileInfo['originalFile']; 207 $this->setup[$theKey . '.']['file'] = $originalFile->getForLocalProcessing(false); 208 } else { 209 // Use normal path from fileInfo if it is a non-FAL file (even non-FAL files have originalFile set, but only non-FAL files have origFile set) 210 $this->setup[$theKey . '.']['file'] = $fileInfo[3]; 211 } 212 213 // only pass necessary parts of fileInfo further down, to not incorporate facts as 214 // CropScaleMask runs in this request, that may not occur in subsequent calls and change 215 // the md5 of the generated file name 216 $essentialFileInfo = $fileInfo; 217 unset($essentialFileInfo['originalFile'], $essentialFileInfo['processedFile']); 218 219 $this->setup[$theKey . '.']['BBOX'] = $essentialFileInfo; 220 $this->objBB[$theKey] = $essentialFileInfo; 221 if ($conf['mask']) { 222 $maskInfo = $this->getResource($conf['mask'], $conf['mask.']); 223 if ($maskInfo) { 224 // the same selection criteria as regarding fileInfo above apply here 225 if ($maskInfo['processedFile'] instanceof ProcessedFile) { 226 $this->setup[$theKey . '.']['mask'] = $maskInfo['processedFile']->getForLocalProcessing(false); 227 } elseif (!isset($maskInfo['origFile']) && $maskInfo['originalFile'] instanceof File) { 228 /** @var File $originalFile */ 229 $originalFile = $maskInfo['originalFile']; 230 $this->setup[$theKey . '.']['mask'] = $originalFile->getForLocalProcessing(false); 231 } else { 232 $this->setup[$theKey . '.']['mask'] = $maskInfo[3]; 233 } 234 } else { 235 $this->setup[$theKey . '.']['mask'] = ''; 236 } 237 } 238 } else { 239 unset($this->setup[$theKey . '.']); 240 } 241 break; 242 } 243 // Checks if disabled is set... (this is also done in menu.php / imgmenu!!) 244 if ($conf['if.']) { 245 $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 246 $cObj->start($this->data); 247 if (!$cObj->checkIf($conf['if.'])) { 248 unset($this->setup[$theKey]); 249 unset($this->setup[$theKey . '.']); 250 unset($this->objBB[$theKey]); 251 } 252 } 253 } 254 } 255 // Calculate offsets on elements 256 $this->setup['XY'] = $this->calcOffset($this->setup['XY']); 257 if (isset($this->setup['offset.'])) { 258 $this->setup['offset'] = $this->cObj->stdWrap($this->setup['offset'], $this->setup['offset.']); 259 } 260 $this->setup['offset'] = $this->calcOffset($this->setup['offset']); 261 if (isset($this->setup['workArea.'])) { 262 $this->setup['workArea'] = $this->cObj->stdWrap($this->setup['workArea'], $this->setup['workArea.']); 263 } 264 $this->setup['workArea'] = $this->calcOffset($this->setup['workArea']); 265 foreach ($sKeyArray as $theKey) { 266 $theValue = $this->setup[$theKey]; 267 if ((int)$theKey && $this->setup[$theKey . '.']) { 268 switch ($theValue) { 269 case 'TEXT': 270 271 case 'IMAGE': 272 if (isset($this->setup[$theKey . '.']['offset.'])) { 273 $this->setup[$theKey . '.']['offset'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['offset'], $this->setup[$theKey . '.']['offset.']); 274 unset($this->setup[$theKey . '.']['offset.']); 275 } 276 if ($this->setup[$theKey . '.']['offset']) { 277 $this->setup[$theKey . '.']['offset'] = $this->calcOffset($this->setup[$theKey . '.']['offset']); 278 } 279 break; 280 case 'BOX': 281 282 case 'ELLIPSE': 283 if (isset($this->setup[$theKey . '.']['dimensions.'])) { 284 $this->setup[$theKey . '.']['dimensions'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['dimensions'], $this->setup[$theKey . '.']['dimensions.']); 285 unset($this->setup[$theKey . '.']['dimensions.']); 286 } 287 if ($this->setup[$theKey . '.']['dimensions']) { 288 $this->setup[$theKey . '.']['dimensions'] = $this->calcOffset($this->setup[$theKey . '.']['dimensions']); 289 } 290 break; 291 case 'WORKAREA': 292 if (isset($this->setup[$theKey . '.']['set.'])) { 293 $this->setup[$theKey . '.']['set'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['set'], $this->setup[$theKey . '.']['set.']); 294 unset($this->setup[$theKey . '.']['set.']); 295 } 296 if ($this->setup[$theKey . '.']['set']) { 297 $this->setup[$theKey . '.']['set'] = $this->calcOffset($this->setup[$theKey . '.']['set']); 298 } 299 break; 300 case 'CROP': 301 if (isset($this->setup[$theKey . '.']['crop.'])) { 302 $this->setup[$theKey . '.']['crop'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['crop'], $this->setup[$theKey . '.']['crop.']); 303 unset($this->setup[$theKey . '.']['crop.']); 304 } 305 if ($this->setup[$theKey . '.']['crop']) { 306 $this->setup[$theKey . '.']['crop'] = $this->calcOffset($this->setup[$theKey . '.']['crop']); 307 } 308 break; 309 case 'SCALE': 310 if (isset($this->setup[$theKey . '.']['width.'])) { 311 $this->setup[$theKey . '.']['width'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['width'], $this->setup[$theKey . '.']['width.']); 312 unset($this->setup[$theKey . '.']['width.']); 313 } 314 if ($this->setup[$theKey . '.']['width']) { 315 $this->setup[$theKey . '.']['width'] = $this->calcOffset($this->setup[$theKey . '.']['width']); 316 } 317 if (isset($this->setup[$theKey . '.']['height.'])) { 318 $this->setup[$theKey . '.']['height'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['height'], $this->setup[$theKey . '.']['height.']); 319 unset($this->setup[$theKey . '.']['height.']); 320 } 321 if ($this->setup[$theKey . '.']['height']) { 322 $this->setup[$theKey . '.']['height'] = $this->calcOffset($this->setup[$theKey . '.']['height']); 323 } 324 break; 325 } 326 } 327 } 328 // Get trivial data 329 $XY = GeneralUtility::intExplode(',', $this->setup['XY']); 330 $maxWidth = isset($this->setup['maxWidth.']) ? (int)$this->cObj->stdWrap($this->setup['maxWidth'], $this->setup['maxWidth.']) : (int)$this->setup['maxWidth']; 331 $maxHeight = isset($this->setup['maxHeight.']) ? (int)$this->cObj->stdWrap($this->setup['maxHeight'], $this->setup['maxHeight.']) : (int)$this->setup['maxHeight']; 332 $XY[0] = MathUtility::forceIntegerInRange($XY[0], 1, $maxWidth ?: 2000); 333 $XY[1] = MathUtility::forceIntegerInRange($XY[1], 1, $maxHeight ?: 2000); 334 $this->XY = $XY; 335 $this->w = $XY[0]; 336 $this->h = $XY[1]; 337 $this->OFFSET = GeneralUtility::intExplode(',', $this->setup['offset']); 338 // this sets the workArea 339 $this->setWorkArea($this->setup['workArea']); 340 // this sets the default to the current; 341 $this->defaultWorkArea = $this->workArea; 342 } 343 } 344 345 /** 346 * Initiates the image file generation if ->setup is TRUE and if the file did not exist already. 347 * Gets filename from fileName() and if file exists in typo3temp/assets/images/ dir it will - of course - not be rendered again. 348 * Otherwise rendering means calling ->make(), then ->output(), then ->destroy() 349 * 350 * @return string The filename for the created GIF/PNG file. The filename will be prefixed "GB_ 351 * @see make() 352 * @see fileName() 353 */ 354 public function gifBuild() 355 { 356 if ($this->setup) { 357 // Relative to Environment::getPublicPath() 358 $gifFileName = $this->fileName('assets/images/'); 359 // File exists 360 if (!file_exists($gifFileName)) { 361 // Create temporary directory if not done: 362 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/typo3temp/assets/images/'); 363 // Create file: 364 $this->make(); 365 $this->output($gifFileName); 366 $this->destroy(); 367 } 368 return $gifFileName; 369 } 370 return ''; 371 } 372 373 /** 374 * The actual rendering of the image file. 375 * Basically sets the dimensions, the background color, the traverses the array of GIFBUILDER objects and finally setting the transparent color if defined. 376 * Creates a GDlib resource in $this->im and works on that 377 * Called by gifBuild() 378 * 379 * @internal 380 * @see gifBuild() 381 */ 382 public function make() 383 { 384 // Get trivial data 385 $XY = $this->XY; 386 // Reset internal properties 387 $this->saveAlphaLayer = false; 388 // Gif-start 389 $im = imagecreatetruecolor($XY[0], $XY[1]); 390 if ($im === false) { 391 throw new \RuntimeException('imagecreatetruecolor returned false', 1598350445); 392 } 393 $this->im = $im; 394 $this->w = $XY[0]; 395 $this->h = $XY[1]; 396 // Transparent layer as background if set and requirements are met 397 if (!empty($this->setup['backColor']) && $this->setup['backColor'] === 'transparent' && !$this->setup['reduceColors'] && (empty($this->setup['format']) || $this->setup['format'] === 'png')) { 398 // Set transparency properties 399 imagesavealpha($this->im, true); 400 // Fill with a transparent background 401 $transparentColor = imagecolorallocatealpha($this->im, 0, 0, 0, 127); 402 imagefill($this->im, 0, 0, $transparentColor); 403 // Set internal properties to keep the transparency over the rendering process 404 $this->saveAlphaLayer = true; 405 // Force PNG in case no format is set 406 $this->setup['format'] = 'png'; 407 $BGcols = []; 408 } else { 409 // Fill the background with the given color 410 $BGcols = $this->convertColor($this->setup['backColor']); 411 $Bcolor = imagecolorallocate($this->im, $BGcols[0], $BGcols[1], $BGcols[2]); 412 imagefilledrectangle($this->im, 0, 0, $XY[0], $XY[1], $Bcolor); 413 } 414 // Traverse the GIFBUILDER objects and render each one: 415 if (is_array($this->setup)) { 416 $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($this->setup); 417 foreach ($sKeyArray as $theKey) { 418 $theValue = $this->setup[$theKey]; 419 if ((int)$theKey && ($conf = $this->setup[$theKey . '.'])) { 420 // apply stdWrap to all properties, except for TEXT objects 421 // all properties of the TEXT sub-object have already been stdWrap-ped 422 // before in ->checkTextObj() 423 if ($theValue !== 'TEXT') { 424 $isStdWrapped = []; 425 foreach ($conf as $key => $value) { 426 $parameter = rtrim($key, '.'); 427 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) { 428 $conf[$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']); 429 $isStdWrapped[$parameter] = 1; 430 } 431 } 432 } 433 434 switch ($theValue) { 435 case 'IMAGE': 436 if ($conf['mask']) { 437 $this->maskImageOntoImage($this->im, $conf, $this->workArea); 438 } else { 439 $this->copyImageOntoImage($this->im, $conf, $this->workArea); 440 } 441 break; 442 case 'TEXT': 443 if (!$conf['hide']) { 444 if (is_array($conf['shadow.'])) { 445 $isStdWrapped = []; 446 foreach ($conf['shadow.'] as $key => $value) { 447 $parameter = rtrim($key, '.'); 448 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) { 449 $conf['shadow.'][$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']); 450 $isStdWrapped[$parameter] = 1; 451 } 452 } 453 $this->makeShadow($this->im, $conf['shadow.'], $this->workArea, $conf); 454 } 455 if (is_array($conf['emboss.'])) { 456 $isStdWrapped = []; 457 foreach ($conf['emboss.'] as $key => $value) { 458 $parameter = rtrim($key, '.'); 459 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) { 460 $conf['emboss.'][$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']); 461 $isStdWrapped[$parameter] = 1; 462 } 463 } 464 $this->makeEmboss($this->im, $conf['emboss.'], $this->workArea, $conf); 465 } 466 if (is_array($conf['outline.'])) { 467 $isStdWrapped = []; 468 foreach ($conf['outline.'] as $key => $value) { 469 $parameter = rtrim($key, '.'); 470 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) { 471 $conf['outline.'][$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']); 472 $isStdWrapped[$parameter] = 1; 473 } 474 } 475 $this->makeOutline($this->im, $conf['outline.'], $this->workArea, $conf); 476 } 477 $conf['imgMap'] = 1; 478 $this->makeText($this->im, $conf, $this->workArea); 479 } 480 break; 481 case 'OUTLINE': 482 if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) { 483 $this->makeOutline($this->im, $conf, $this->workArea, $txtConf); 484 } 485 break; 486 case 'EMBOSS': 487 if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) { 488 $this->makeEmboss($this->im, $conf, $this->workArea, $txtConf); 489 } 490 break; 491 case 'SHADOW': 492 if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) { 493 $this->makeShadow($this->im, $conf, $this->workArea, $txtConf); 494 } 495 break; 496 case 'BOX': 497 $this->makeBox($this->im, $conf, $this->workArea); 498 break; 499 case 'EFFECT': 500 $this->makeEffect($this->im, $conf); 501 break; 502 case 'ADJUST': 503 $this->adjust($this->im, $conf); 504 break; 505 case 'CROP': 506 $this->crop($this->im, $conf); 507 break; 508 case 'SCALE': 509 $this->scale($this->im, $conf); 510 break; 511 case 'WORKAREA': 512 if ($conf['set']) { 513 // this sets the workArea 514 $this->setWorkArea($conf['set']); 515 } 516 if (isset($conf['clear'])) { 517 // This sets the current to the default; 518 $this->workArea = $this->defaultWorkArea; 519 } 520 break; 521 case 'ELLIPSE': 522 $this->makeEllipse($this->im, $conf, $this->workArea); 523 break; 524 } 525 } 526 } 527 } 528 // Preserve alpha transparency 529 if (!$this->saveAlphaLayer) { 530 if ($this->setup['transparentBackground']) { 531 // Auto transparent background is set 532 $Bcolor = imagecolorclosest($this->im, $BGcols[0], $BGcols[1], $BGcols[2]); 533 imagecolortransparent($this->im, $Bcolor); 534 } elseif (is_array($this->setup['transparentColor_array'])) { 535 // Multiple transparent colors are set. This is done via the trick that all transparent colors get 536 // converted to one color and then this one gets set as transparent as png/gif can just have one 537 // transparent color. 538 $Tcolor = $this->unifyColors($this->im, $this->setup['transparentColor_array'], (bool)$this->setup['transparentColor.']['closest']); 539 if ($Tcolor >= 0) { 540 imagecolortransparent($this->im, $Tcolor); 541 } 542 } 543 } 544 } 545 546 /********************************************* 547 * 548 * Various helper functions 549 * 550 ********************************************/ 551 /** 552 * Initializing/Cleaning of TypoScript properties for TEXT GIFBUILDER objects 553 * 554 * 'cleans' TEXT-object; Checks fontfile and other vital setup 555 * Finds the title if its a 'variable' (instantiates a cObj and loads it with the ->data record) 556 * Performs caseshift if any. 557 * 558 * @param array $conf GIFBUILDER object TypoScript properties 559 * @return array Modified $conf array IF the "text" property is not blank 560 * @internal 561 */ 562 public function checkTextObj($conf) 563 { 564 $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 565 $cObj->start($this->data); 566 $isStdWrapped = []; 567 foreach ($conf as $key => $value) { 568 $parameter = rtrim($key, '.'); 569 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) { 570 $conf[$parameter] = $cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']); 571 $isStdWrapped[$parameter] = 1; 572 } 573 } 574 575 if (!is_null($conf['fontFile'])) { 576 $conf['fontFile'] = $this->checkFile($conf['fontFile']); 577 } 578 if (!$conf['fontFile']) { 579 $conf['fontFile'] = $this->checkFile('EXT:core/Resources/Private/Font/nimbus.ttf'); 580 } 581 if (!$conf['iterations']) { 582 $conf['iterations'] = 1; 583 } 584 if (!$conf['fontSize']) { 585 $conf['fontSize'] = 12; 586 } 587 // If any kind of spacing applies, we cannot use angles!! 588 if ($conf['spacing'] || $conf['wordSpacing']) { 589 $conf['angle'] = 0; 590 } 591 if (!isset($conf['antiAlias'])) { 592 $conf['antiAlias'] = 1; 593 } 594 $conf['fontColor'] = trim($conf['fontColor']); 595 // Strip HTML 596 if (!$conf['doNotStripHTML']) { 597 $conf['text'] = strip_tags($conf['text']); 598 } 599 $this->combinedTextStrings[] = strip_tags($conf['text']); 600 // Max length = 100 if automatic line braks are not defined: 601 if (!isset($conf['breakWidth']) || !$conf['breakWidth']) { 602 $tlen = (int)$conf['textMaxLength'] ?: 100; 603 $conf['text'] = mb_substr($conf['text'], 0, $tlen, 'utf-8'); 604 } 605 if ((string)$conf['text'] != '') { 606 // Char range map thingie: 607 $fontBaseName = PathUtility::basename($conf['fontFile']); 608 if (is_array($this->charRangeMap[$fontBaseName])) { 609 // Initialize splitRendering array: 610 if (!is_array($conf['splitRendering.'])) { 611 $conf['splitRendering.'] = []; 612 } 613 $cfgK = $this->charRangeMap[$fontBaseName]['cfgKey']; 614 // Do not impose settings if a splitRendering object already exists: 615 if (!isset($conf['splitRendering.'][$cfgK])) { 616 // Set configuration: 617 $conf['splitRendering.'][$cfgK] = 'charRange'; 618 $conf['splitRendering.'][$cfgK . '.'] = $this->charRangeMap[$fontBaseName]['charMapConfig']; 619 // Multiplicator of fontsize: 620 if ($this->charRangeMap[$fontBaseName]['multiplicator']) { 621 $conf['splitRendering.'][$cfgK . '.']['fontSize'] = round($conf['fontSize'] * $this->charRangeMap[$fontBaseName]['multiplicator']); 622 } 623 // Multiplicator of pixelSpace: 624 if ($this->charRangeMap[$fontBaseName]['pixelSpace']) { 625 $travKeys = ['xSpaceBefore', 'xSpaceAfter', 'ySpaceBefore', 'ySpaceAfter']; 626 foreach ($travKeys as $pxKey) { 627 if (isset($conf['splitRendering.'][$cfgK . '.'][$pxKey])) { 628 $conf['splitRendering.'][$cfgK . '.'][$pxKey] = round($conf['splitRendering.'][$cfgK . '.'][$pxKey] * ($conf['fontSize'] / $this->charRangeMap[$fontBaseName]['pixelSpace'])); 629 } 630 } 631 } 632 } 633 } 634 if (is_array($conf['splitRendering.'])) { 635 foreach ($conf['splitRendering.'] as $key => $value) { 636 if (is_array($conf['splitRendering.'][$key])) { 637 if (isset($conf['splitRendering.'][$key]['fontFile'])) { 638 $conf['splitRendering.'][$key]['fontFile'] = $this->checkFile($conf['splitRendering.'][$key]['fontFile']); 639 } 640 } 641 } 642 } 643 return $conf; 644 } 645 return null; 646 } 647 648 /** 649 * Calculation of offset using "splitCalc" and insertion of dimensions from other GIFBUILDER objects. 650 * 651 * Example: 652 * Input: 2+2, 2*3, 123, [10.w] 653 * Output: 4,6,123,45 (provided that the width of object in position 10 was 45 pixels wide) 654 * 655 * @param string $string The string to resolve/calculate the result of. The string is divided by a comma first and each resulting part is calculated into an integer. 656 * @return string The resolved string with each part (separated by comma) returned separated by comma 657 * @internal 658 */ 659 public function calcOffset($string) 660 { 661 $value = []; 662 $numbers = GeneralUtility::trimExplode(',', $this->calculateFunctions($string)); 663 foreach ($numbers as $key => $val) { 664 if ((string)$val == (string)(int)$val) { 665 $value[$key] = (int)$val; 666 } else { 667 $value[$key] = $this->calculateValue($val); 668 } 669 } 670 $string = implode(',', $value); 671 return $string; 672 } 673 674 /** 675 * Returns an "imgResource" creating an instance of the ContentObjectRenderer class and calling ContentObjectRenderer::getImgResource 676 * 677 * @param string $file Filename value OR the string "GIFBUILDER", see documentation in TSref for the "datatype" called "imgResource 678 * @param array $fileArray TypoScript properties passed to the function. Either GIFBUILDER properties or imgResource properties, depending on the value of $file (whether that is "GIFBUILDER" or a file reference) 679 * @return array|null Returns an array with file information from ContentObjectRenderer::getImgResource() 680 * @internal 681 * @see ContentObjectRenderer::getImgResource() 682 */ 683 public function getResource($file, $fileArray) 684 { 685 $context = GeneralUtility::makeInstance(Context::class); 686 $deferProcessing = !$context->hasAspect('fileProcessing') || $context->getPropertyFromAspect('fileProcessing', 'deferProcessing'); 687 $context->setAspect('fileProcessing', new FileProcessingAspect(false)); 688 try { 689 if (!in_array($fileArray['ext'], $this->imageFileExt, true)) { 690 $fileArray['ext'] = $this->gifExtension; 691 } 692 /** @var ContentObjectRenderer $cObj */ 693 $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 694 $cObj->start($this->data); 695 return $cObj->getImgResource($file, $fileArray); 696 } finally { 697 $context->setAspect('fileProcessing', new FileProcessingAspect($deferProcessing)); 698 } 699 } 700 701 /** 702 * Returns the reference to a "resource" in TypoScript. 703 * 704 * @param string $file The resource value. 705 * @return string|null Returns the relative filepath or null if it's invalid 706 * @internal 707 * @see TemplateService::getFileName() 708 */ 709 public function checkFile($file) 710 { 711 try { 712 return GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($file); 713 } catch (Exception $e) { 714 return null; 715 } 716 } 717 718 /** 719 * Calculates the GIFBUILDER output filename/path based on a serialized, hashed value of this->setup 720 * and prefixes the original filename 721 * also, the filename gets an additional prefix (max 100 characters), 722 * something like "GB_MD5HASH_myfilename_is_very_long_and_such.jpg" 723 * 724 * @param string $pre Filename prefix, eg. "GB_ 725 * @return string The filepath, relative to Environment::getPublicPath() 726 * @internal 727 */ 728 public function fileName($pre) 729 { 730 $basicFileFunctions = GeneralUtility::makeInstance(BasicFileUtility::class); 731 $filePrefix = implode('_', array_merge($this->combinedTextStrings, $this->combinedFileNames)); 732 $filePrefix = $basicFileFunctions->cleanFileName(ltrim($filePrefix, '.')); 733 734 // shorten prefix to avoid overly long file names 735 $filePrefix = substr($filePrefix, 0, 100); 736 737 // Only take relevant parameters to ease the pain for json_encode and make the final string short 738 // so shortMD5 is not as slow. see https://forge.typo3.org/issues/64158 739 $hashInputForFileName = [ 740 array_keys($this->setup), 741 $filePrefix, 742 $this->im, 743 $this->w, 744 $this->h, 745 $this->map, 746 $this->workArea, 747 $this->combinedTextStrings, 748 $this->combinedFileNames, 749 $this->data 750 ]; 751 return 'typo3temp/' . $pre . $filePrefix . '_' . GeneralUtility::shortMD5((string)json_encode($hashInputForFileName)) . '.' . $this->extension(); 752 } 753 754 /** 755 * Returns the file extension used in the filename 756 * 757 * @return string Extension; "jpg" or "gif"/"png 758 * @internal 759 */ 760 public function extension() 761 { 762 switch (strtolower($this->setup['format'])) { 763 case 'jpg': 764 case 'jpeg': 765 return 'jpg'; 766 case 'png': 767 return 'png'; 768 case 'gif': 769 return 'gif'; 770 default: 771 return $this->gifExtension; 772 } 773 } 774 775 /** 776 * Calculates the value concerning the dimensions of objects. 777 * 778 * @param string $string The string to be calculated (e.g. "[20.h]+13") 779 * @return int The calculated value (e.g. "23") 780 * @see calcOffset() 781 */ 782 protected function calculateValue($string) 783 { 784 $calculatedValue = 0; 785 $parts = GeneralUtility::splitCalc($string, '+-*/%'); 786 foreach ($parts as $part) { 787 $theVal = $part[1]; 788 $sign = $part[0]; 789 if (((string)(int)$theVal) == ((string)$theVal)) { 790 $theVal = (int)$theVal; 791 } elseif ('[' . substr($theVal, 1, -1) . ']' == $theVal) { 792 $objParts = explode('.', substr($theVal, 1, -1)); 793 $theVal = 0; 794 if (isset($this->objBB[$objParts[0]])) { 795 if ($objParts[1] === 'w') { 796 $theVal = $this->objBB[$objParts[0]][0]; 797 } elseif ($objParts[1] === 'h') { 798 $theVal = $this->objBB[$objParts[0]][1]; 799 } elseif ($objParts[1] === 'lineHeight') { 800 $theVal = $this->objBB[$objParts[0]][2]['lineHeight']; 801 } 802 $theVal = (int)$theVal; 803 } 804 } elseif ((float)$theVal) { 805 $theVal = (float)$theVal; 806 } else { 807 $theVal = 0; 808 } 809 if ($sign === '-') { 810 $calculatedValue -= $theVal; 811 } elseif ($sign === '+') { 812 $calculatedValue += $theVal; 813 } elseif ($sign === '/' && $theVal) { 814 $calculatedValue = $calculatedValue / $theVal; 815 } elseif ($sign === '*') { 816 $calculatedValue = $calculatedValue * $theVal; 817 } elseif ($sign === '%' && $theVal) { 818 $calculatedValue %= $theVal; 819 } 820 } 821 return round($calculatedValue); 822 } 823 824 /** 825 * Calculates special functions: 826 * + max([10.h], [20.h]) -> gets the maximum of the given values 827 * 828 * @param string $string The raw string with functions to be calculated 829 * @return string The calculated values 830 */ 831 protected function calculateFunctions($string) 832 { 833 if (preg_match_all('#max\\(([^)]+)\\)#', $string, $matches)) { 834 foreach ($matches[1] as $index => $maxExpression) { 835 $string = str_replace($matches[0][$index], (string)$this->calculateMaximum($maxExpression), $string); 836 } 837 } 838 return $string; 839 } 840 841 /** 842 * Calculates the maximum of a set of values defined like "[10.h],[20.h],1000" 843 * 844 * @param string $string The string to be used to calculate the maximum (e.g. "[10.h],[20.h],1000") 845 * @return int The maximum value of the given comma separated and calculated values 846 */ 847 protected function calculateMaximum($string) 848 { 849 $parts = GeneralUtility::trimExplode(',', $this->calcOffset($string), true); 850 $maximum = !empty($parts) ? max($parts) : 0; 851 return $maximum; 852 } 853} 854