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