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