[TASK] Decouple TemplateService->getFileName
[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 * The class \TYPO3\CMS\Frontend\ContentObject\Menu\GraphicalMenuContentObject also uses gifbuilder and here there is an interesting use since the function findLargestDims() from that class calls the init() and start() functions to find the total dimensions before starting the rendering of the images.
109 *
110 * @param array $conf TypoScript properties for the GIFBUILDER session. Stored internally in the variable ->setup
111 * @param array $data The current data record from \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer. Stored internally in the variable ->data
112 * @see ContentObjectRenderer::getImgResource(), \TYPO3\CMS\Frontend\ContentObject\Menu\GraphicalMenuContentObject::makeGifs(), \TYPO3\CMS\Frontend\ContentObject\Menu\GraphicalMenuContentObject::findLargestDims()
113 */
114 public function start($conf, $data)
115 {
116 if (is_array($conf)) {
117 $this->setup = $conf;
118 $this->data = $data;
119 $this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
120 $this->cObj->start($this->data);
121 // Hook preprocess gifbuilder conf
122 // Added by Julle for 3.8.0
123 //
124 // Let's you pre-process the gifbuilder configuration. for
125 // example you can split a string up into lines and render each
126 // line as TEXT obj, see extension julle_gifbconf
127 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_gifbuilder.php']['gifbuilder-ConfPreProcess'] ?? [] as $_funcRef) {
128 $_params = $this->setup;
129 $this->setup = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
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 $originalFile File */
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 $this->setup[$theKey . '.']['BBOX'] = $fileInfo;
209 $this->objBB[$theKey] = $fileInfo;
210 if ($conf['mask']) {
211 $maskInfo = $this->getResource($conf['mask'], $conf['mask.']);
212 if ($maskInfo) {
213 // the same selection criteria as regarding fileInfo above apply here
214 if ($maskInfo['processedFile'] instanceof ProcessedFile) {
215 $this->setup[$theKey . '.']['mask'] = $maskInfo['processedFile']->getForLocalProcessing(false);
216 } elseif (!isset($maskInfo['origFile']) && $maskInfo['originalFile'] instanceof File) {
217 /** @var $originalFile File */
218 $originalFile = $maskInfo['originalFile'];
219 $this->setup[$theKey . '.']['mask'] = $originalFile->getForLocalProcessing(false);
220 } else {
221 $this->setup[$theKey . '.']['mask'] = $maskInfo[3];
222 }
223 } else {
224 $this->setup[$theKey . '.']['mask'] = '';
225 }
226 }
227 } else {
228 unset($this->setup[$theKey . '.']);
229 }
230 break;
231 }
232 // Checks if disabled is set... (this is also done in menu.php / imgmenu!!)
233 if ($conf['if.']) {
234 $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
235 $cObj->start($this->data);
236 if (!$cObj->checkIf($conf['if.'])) {
237 unset($this->setup[$theKey]);
238 unset($this->setup[$theKey . '.']);
239 unset($this->objBB[$theKey]);
240 }
241 }
242 }
243 }
244 // Calculate offsets on elements
245 $this->setup['XY'] = $this->calcOffset($this->setup['XY']);
246 if (isset($this->setup['offset.'])) {
247 $this->setup['offset'] = $this->cObj->stdWrap($this->setup['offset'], $this->setup['offset.']);
248 }
249 $this->setup['offset'] = $this->calcOffset($this->setup['offset']);
250 if (isset($this->setup['workArea.'])) {
251 $this->setup['workArea'] = $this->cObj->stdWrap($this->setup['workArea'], $this->setup['workArea.']);
252 }
253 $this->setup['workArea'] = $this->calcOffset($this->setup['workArea']);
254 foreach ($sKeyArray as $theKey) {
255 $theValue = $this->setup[$theKey];
256 if ((int)$theKey && ($conf = $this->setup[$theKey . '.'])) {
257 switch ($theValue) {
258 case 'TEXT':
259
260 case 'IMAGE':
261 if (isset($this->setup[$theKey . '.']['offset.'])) {
262 $this->setup[$theKey . '.']['offset'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['offset'], $this->setup[$theKey . '.']['offset.']);
263 unset($this->setup[$theKey . '.']['offset.']);
264 }
265 if ($this->setup[$theKey . '.']['offset']) {
266 $this->setup[$theKey . '.']['offset'] = $this->calcOffset($this->setup[$theKey . '.']['offset']);
267 }
268 break;
269 case 'BOX':
270
271 case 'ELLIPSE':
272 if (isset($this->setup[$theKey . '.']['dimensions.'])) {
273 $this->setup[$theKey . '.']['dimensions'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['dimensions'], $this->setup[$theKey . '.']['dimensions.']);
274 unset($this->setup[$theKey . '.']['dimensions.']);
275 }
276 if ($this->setup[$theKey . '.']['dimensions']) {
277 $this->setup[$theKey . '.']['dimensions'] = $this->calcOffset($this->setup[$theKey . '.']['dimensions']);
278 }
279 break;
280 case 'WORKAREA':
281 if (isset($this->setup[$theKey . '.']['set.'])) {
282 $this->setup[$theKey . '.']['set'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['set'], $this->setup[$theKey . '.']['set.']);
283 unset($this->setup[$theKey . '.']['set.']);
284 }
285 if ($this->setup[$theKey . '.']['set']) {
286 $this->setup[$theKey . '.']['set'] = $this->calcOffset($this->setup[$theKey . '.']['set']);
287 }
288 break;
289 case 'CROP':
290 if (isset($this->setup[$theKey . '.']['crop.'])) {
291 $this->setup[$theKey . '.']['crop'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['crop'], $this->setup[$theKey . '.']['crop.']);
292 unset($this->setup[$theKey . '.']['crop.']);
293 }
294 if ($this->setup[$theKey . '.']['crop']) {
295 $this->setup[$theKey . '.']['crop'] = $this->calcOffset($this->setup[$theKey . '.']['crop']);
296 }
297 break;
298 case 'SCALE':
299 if (isset($this->setup[$theKey . '.']['width.'])) {
300 $this->setup[$theKey . '.']['width'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['width'], $this->setup[$theKey . '.']['width.']);
301 unset($this->setup[$theKey . '.']['width.']);
302 }
303 if ($this->setup[$theKey . '.']['width']) {
304 $this->setup[$theKey . '.']['width'] = $this->calcOffset($this->setup[$theKey . '.']['width']);
305 }
306 if (isset($this->setup[$theKey . '.']['height.'])) {
307 $this->setup[$theKey . '.']['height'] = $this->cObj->stdWrap($this->setup[$theKey . '.']['height'], $this->setup[$theKey . '.']['height.']);
308 unset($this->setup[$theKey . '.']['height.']);
309 }
310 if ($this->setup[$theKey . '.']['height']) {
311 $this->setup[$theKey . '.']['height'] = $this->calcOffset($this->setup[$theKey . '.']['height']);
312 }
313 break;
314 }
315 }
316 }
317 // Get trivial data
318 $XY = GeneralUtility::intExplode(',', $this->setup['XY']);
319 $maxWidth = isset($this->setup['maxWidth.']) ? (int)$this->cObj->stdWrap($this->setup['maxWidth'], $this->setup['maxWidth.']) : (int)$this->setup['maxWidth'];
320 $maxHeight = isset($this->setup['maxHeight.']) ? (int)$this->cObj->stdWrap($this->setup['maxHeight'], $this->setup['maxHeight.']) : (int)$this->setup['maxHeight'];
321 $XY[0] = MathUtility::forceIntegerInRange($XY[0], 1, $maxWidth ?: 2000);
322 $XY[1] = MathUtility::forceIntegerInRange($XY[1], 1, $maxHeight ?: 2000);
323 $this->XY = $XY;
324 $this->w = $XY[0];
325 $this->h = $XY[1];
326 $this->OFFSET = GeneralUtility::intExplode(',', $this->setup['offset']);
327 // this sets the workArea
328 $this->setWorkArea($this->setup['workArea']);
329 // this sets the default to the current;
330 $this->defaultWorkArea = $this->workArea;
331 }
332 }
333
334 /**
335 * Initiates the image file generation if ->setup is TRUE and if the file did not exist already.
336 * Gets filename from fileName() and if file exists in typo3temp/assets/images/ dir it will - of course - not be rendered again.
337 * Otherwise rendering means calling ->make(), then ->output(), then ->destroy()
338 *
339 * @return string The filename for the created GIF/PNG file. The filename will be prefixed "GB_
340 * @see make(), fileName()
341 */
342 public function gifBuild()
343 {
344 if ($this->setup) {
345 // Relative to Environment::getPublicPath()
346 $gifFileName = $this->fileName('assets/images/');
347 // File exists
348 if (!file_exists($gifFileName)) {
349 // Create temporary directory if not done:
350 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/typo3temp/assets/images/');
351 // Create file:
352 $this->make();
353 $this->output($gifFileName);
354 $this->destroy();
355 }
356 return $gifFileName;
357 }
358 return '';
359 }
360
361 /**
362 * The actual rendering of the image file.
363 * Basically sets the dimensions, the background color, the traverses the array of GIFBUILDER objects and finally setting the transparent color if defined.
364 * Creates a GDlib resource in $this->im and works on that
365 * Called by gifBuild()
366 *
367 * @access private
368 * @see gifBuild()
369 */
370 public function make()
371 {
372 // Get trivial data
373 $XY = $this->XY;
374 // Reset internal properties
375 $this->saveAlphaLayer = false;
376 // Gif-start
377 $this->im = imagecreatetruecolor($XY[0], $XY[1]);
378 $this->w = $XY[0];
379 $this->h = $XY[1];
380 // Transparent layer as background if set and requirements are met
381 if (!empty($this->setup['backColor']) && $this->setup['backColor'] === 'transparent' && !$this->setup['reduceColors'] && (empty($this->setup['format']) || $this->setup['format'] === 'png')) {
382 // Set transparency properties
383 imagesavealpha($this->im, true);
384 // Fill with a transparent background
385 $transparentColor = imagecolorallocatealpha($this->im, 0, 0, 0, 127);
386 imagefill($this->im, 0, 0, $transparentColor);
387 // Set internal properties to keep the transparency over the rendering process
388 $this->saveAlphaLayer = true;
389 // Force PNG in case no format is set
390 $this->setup['format'] = 'png';
391 $BGcols = [];
392 } else {
393 // Fill the background with the given color
394 $BGcols = $this->convertColor($this->setup['backColor']);
395 $Bcolor = imagecolorallocate($this->im, $BGcols[0], $BGcols[1], $BGcols[2]);
396 imagefilledrectangle($this->im, 0, 0, $XY[0], $XY[1], $Bcolor);
397 }
398 // Traverse the GIFBUILDER objects an render each one:
399 if (is_array($this->setup)) {
400 $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($this->setup);
401 foreach ($sKeyArray as $theKey) {
402 $theValue = $this->setup[$theKey];
403 if ((int)$theKey && ($conf = $this->setup[$theKey . '.'])) {
404 // apply stdWrap to all properties, except for TEXT objects
405 // all properties of the TEXT sub-object have already been stdWrap-ped
406 // before in ->checkTextObj()
407 if ($theValue !== 'TEXT') {
408 $isStdWrapped = [];
409 foreach ($conf as $key => $value) {
410 $parameter = rtrim($key, '.');
411 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) {
412 $conf[$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']);
413 $isStdWrapped[$parameter] = 1;
414 }
415 }
416 }
417
418 switch ($theValue) {
419 case 'IMAGE':
420 if ($conf['mask']) {
421 $this->maskImageOntoImage($this->im, $conf, $this->workArea);
422 } else {
423 $this->copyImageOntoImage($this->im, $conf, $this->workArea);
424 }
425 break;
426 case 'TEXT':
427 if (!$conf['hide']) {
428 if (is_array($conf['shadow.'])) {
429 $isStdWrapped = [];
430 foreach ($conf['shadow.'] as $key => $value) {
431 $parameter = rtrim($key, '.');
432 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) {
433 $conf['shadow.'][$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']);
434 $isStdWrapped[$parameter] = 1;
435 }
436 }
437 $this->makeShadow($this->im, $conf['shadow.'], $this->workArea, $conf);
438 }
439 if (is_array($conf['emboss.'])) {
440 $isStdWrapped = [];
441 foreach ($conf['emboss.'] as $key => $value) {
442 $parameter = rtrim($key, '.');
443 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) {
444 $conf['emboss.'][$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']);
445 $isStdWrapped[$parameter] = 1;
446 }
447 }
448 $this->makeEmboss($this->im, $conf['emboss.'], $this->workArea, $conf);
449 }
450 if (is_array($conf['outline.'])) {
451 $isStdWrapped = [];
452 foreach ($conf['outline.'] as $key => $value) {
453 $parameter = rtrim($key, '.');
454 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) {
455 $conf['outline.'][$parameter] = $this->cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']);
456 $isStdWrapped[$parameter] = 1;
457 }
458 }
459 $this->makeOutline($this->im, $conf['outline.'], $this->workArea, $conf);
460 }
461 $conf['imgMap'] = 1;
462 $this->makeText($this->im, $conf, $this->workArea);
463 }
464 break;
465 case 'OUTLINE':
466 if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) {
467 $this->makeOutline($this->im, $conf, $this->workArea, $txtConf);
468 }
469 break;
470 case 'EMBOSS':
471 if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) {
472 $this->makeEmboss($this->im, $conf, $this->workArea, $txtConf);
473 }
474 break;
475 case 'SHADOW':
476 if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) {
477 $this->makeShadow($this->im, $conf, $this->workArea, $txtConf);
478 }
479 break;
480 case 'BOX':
481 $this->makeBox($this->im, $conf, $this->workArea);
482 break;
483 case 'EFFECT':
484 $this->makeEffect($this->im, $conf);
485 break;
486 case 'ADJUST':
487 $this->adjust($this->im, $conf);
488 break;
489 case 'CROP':
490 $this->crop($this->im, $conf);
491 break;
492 case 'SCALE':
493 $this->scale($this->im, $conf);
494 break;
495 case 'WORKAREA':
496 if ($conf['set']) {
497 // this sets the workArea
498 $this->setWorkArea($conf['set']);
499 }
500 if (isset($conf['clear'])) {
501 // This sets the current to the default;
502 $this->workArea = $this->defaultWorkArea;
503 }
504 break;
505 case 'ELLIPSE':
506 $this->makeEllipse($this->im, $conf, $this->workArea);
507 break;
508 }
509 }
510 }
511 }
512 // Preserve alpha transparency
513 if (!$this->saveAlphaLayer) {
514 if ($this->setup['transparentBackground']) {
515 // Auto transparent background is set
516 $Bcolor = imagecolorclosest($this->im, $BGcols[0], $BGcols[1], $BGcols[2]);
517 imagecolortransparent($this->im, $Bcolor);
518 } elseif (is_array($this->setup['transparentColor_array'])) {
519 // Multiple transparent colors are set. This is done via the trick that all transparent colors get
520 // converted to one color and then this one gets set as transparent as png/gif can just have one
521 // transparent color.
522 $Tcolor = $this->unifyColors($this->im, $this->setup['transparentColor_array'], (int)$this->setup['transparentColor.']['closest']);
523 if ($Tcolor >= 0) {
524 imagecolortransparent($this->im, $Tcolor);
525 }
526 }
527 }
528 }
529
530 /*********************************************
531 *
532 * Various helper functions
533 *
534 ********************************************/
535 /**
536 * Initializing/Cleaning of TypoScript properties for TEXT GIFBUILDER objects
537 *
538 * 'cleans' TEXT-object; Checks fontfile and other vital setup
539 * Finds the title if its a 'variable' (instantiates a cObj and loads it with the ->data record)
540 * Performs caseshift if any.
541 *
542 * @param array $conf GIFBUILDER object TypoScript properties
543 * @return array Modified $conf array IF the "text" property is not blank
544 * @access private
545 */
546 public function checkTextObj($conf)
547 {
548 $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
549 $cObj->start($this->data);
550 $isStdWrapped = [];
551 foreach ($conf as $key => $value) {
552 $parameter = rtrim($key, '.');
553 if (!$isStdWrapped[$parameter] && isset($conf[$parameter . '.'])) {
554 $conf[$parameter] = $cObj->stdWrap($conf[$parameter], $conf[$parameter . '.']);
555 $isStdWrapped[$parameter] = 1;
556 }
557 }
558 $conf['fontFile'] = $this->checkFile($conf['fontFile']);
559 if (!$conf['fontFile']) {
560 $conf['fontFile'] = $this->checkFile('EXT:core/Resources/Private/Font/nimbus.ttf');
561 }
562 if (!$conf['iterations']) {
563 $conf['iterations'] = 1;
564 }
565 if (!$conf['fontSize']) {
566 $conf['fontSize'] = 12;
567 }
568 // If any kind of spacing applys, we cannot use angles!!
569 if ($conf['spacing'] || $conf['wordSpacing']) {
570 $conf['angle'] = 0;
571 }
572 if (!isset($conf['antiAlias'])) {
573 $conf['antiAlias'] = 1;
574 }
575 $conf['fontColor'] = trim($conf['fontColor']);
576 // Strip HTML
577 if (!$conf['doNotStripHTML']) {
578 $conf['text'] = strip_tags($conf['text']);
579 }
580 $this->combinedTextStrings[] = strip_tags($conf['text']);
581 // Max length = 100 if automatic line braks are not defined:
582 if (!isset($conf['breakWidth']) || !$conf['breakWidth']) {
583 $tlen = (int)$conf['textMaxLength'] ?: 100;
584 $conf['text'] = mb_substr($conf['text'], 0, $tlen, 'utf-8');
585 }
586 if ((string)$conf['text'] != '') {
587 // Char range map thingie:
588 $fontBaseName = PathUtility::basename($conf['fontFile']);
589 if (is_array($this->charRangeMap[$fontBaseName])) {
590 // Initialize splitRendering array:
591 if (!is_array($conf['splitRendering.'])) {
592 $conf['splitRendering.'] = [];
593 }
594 $cfgK = $this->charRangeMap[$fontBaseName]['cfgKey'];
595 // Do not impose settings if a splitRendering object already exists:
596 if (!isset($conf['splitRendering.'][$cfgK])) {
597 // Set configuration:
598 $conf['splitRendering.'][$cfgK] = 'charRange';
599 $conf['splitRendering.'][$cfgK . '.'] = $this->charRangeMap[$fontBaseName]['charMapConfig'];
600 // Multiplicator of fontsize:
601 if ($this->charRangeMap[$fontBaseName]['multiplicator']) {
602 $conf['splitRendering.'][$cfgK . '.']['fontSize'] = round($conf['fontSize'] * $this->charRangeMap[$fontBaseName]['multiplicator']);
603 }
604 // Multiplicator of pixelSpace:
605 if ($this->charRangeMap[$fontBaseName]['pixelSpace']) {
606 $travKeys = ['xSpaceBefore', 'xSpaceAfter', 'ySpaceBefore', 'ySpaceAfter'];
607 foreach ($travKeys as $pxKey) {
608 if (isset($conf['splitRendering.'][$cfgK . '.'][$pxKey])) {
609 $conf['splitRendering.'][$cfgK . '.'][$pxKey] = round($conf['splitRendering.'][$cfgK . '.'][$pxKey] * ($conf['fontSize'] / $this->charRangeMap[$fontBaseName]['pixelSpace']));
610 }
611 }
612 }
613 }
614 }
615 if (is_array($conf['splitRendering.'])) {
616 foreach ($conf['splitRendering.'] as $key => $value) {
617 if (is_array($conf['splitRendering.'][$key])) {
618 if (isset($conf['splitRendering.'][$key]['fontFile'])) {
619 $conf['splitRendering.'][$key]['fontFile'] = $this->checkFile($conf['splitRendering.'][$key]['fontFile']);
620 }
621 }
622 }
623 }
624 return $conf;
625 }
626 return null;
627 }
628
629 /**
630 * Calculation of offset using "splitCalc" and insertion of dimensions from other GIFBUILDER objects.
631 *
632 * Example:
633 * Input: 2+2, 2*3, 123, [10.w]
634 * Output: 4,6,123,45 (provided that the width of object in position 10 was 45 pixels wide)
635 *
636 * @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.
637 * @return string The resolved string with each part (separated by comma) returned separated by comma
638 * @access private
639 */
640 public function calcOffset($string)
641 {
642 $value = [];
643 $numbers = GeneralUtility::trimExplode(',', $this->calculateFunctions($string));
644 foreach ($numbers as $key => $val) {
645 if ((string)$val == (string)(int)$val) {
646 $value[$key] = (int)$val;
647 } else {
648 $value[$key] = $this->calculateValue($val);
649 }
650 }
651 $string = implode(',', $value);
652 return $string;
653 }
654
655 /**
656 * Returns an "imgResource" creating an instance of the ContentObjectRenderer class and calling ContentObjectRenderer::getImgResource
657 *
658 * @param string $file Filename value OR the string "GIFBUILDER", see documentation in TSref for the "datatype" called "imgResource
659 * @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)
660 * @return array|null Returns an array with file information from ContentObjectRenderer::getImgResource()
661 * @access private
662 * @see ContentObjectRenderer::getImgResource()
663 */
664 public function getResource($file, $fileArray)
665 {
666 if (!in_array($fileArray['ext'], $this->imageFileExt, true)) {
667 $fileArray['ext'] = $this->gifExtension;
668 }
669 /** @var ContentObjectRenderer $cObj */
670 $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
671 $cObj->start($this->data);
672 return $cObj->getImgResource($file, $fileArray);
673 }
674
675 /**
676 * Returns the reference to a "resource" in TypoScript.
677 *
678 * @param string $file The resource value.
679 * @return string|null Returns the relative filepath or null if it's invalid
680 * @access private
681 * @see TemplateService::getFileName()
682 */
683 public function checkFile($file)
684 {
685 try {
686 return GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($file);
687 } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
688 return null;
689 }
690 }
691
692 /**
693 * Calculates the GIFBUILDER output filename/path based on a serialized, hashed value of this->setup
694 * and prefixes the original filename
695 * also, the filename gets an additional prefix (max 100 characters),
696 * something like "GB_MD5HASH_myfilename_is_very_long_and_such.jpg"
697 *
698 * @param string $pre Filename prefix, eg. "GB_
699 * @return string The filepath, relative to Environment::getPublicPath()
700 * @access private
701 */
702 public function fileName($pre)
703 {
704 /** @var $basicFileFunctions \TYPO3\CMS\Core\Utility\File\BasicFileUtility */
705 $basicFileFunctions = GeneralUtility::makeInstance(BasicFileUtility::class);
706 $filePrefix = implode('_', array_merge($this->combinedTextStrings, $this->combinedFileNames));
707 $filePrefix = $basicFileFunctions->cleanFileName(ltrim($filePrefix, '.'));
708
709 // shorten prefix to avoid overly long file names
710 $filePrefix = substr($filePrefix, 0, 100);
711
712 return 'typo3temp/' . $pre . $filePrefix . '_' . GeneralUtility::shortMD5(serialize($this->setup)) . '.' . $this->extension();
713 }
714
715 /**
716 * Returns the file extension used in the filename
717 *
718 * @return string Extension; "jpg" or "gif"/"png
719 * @access private
720 */
721 public function extension()
722 {
723 switch (strtolower($this->setup['format'])) {
724 case 'jpg':
725
726 case 'jpeg':
727 return 'jpg';
728 break;
729 case 'png':
730 return 'png';
731 break;
732 case 'gif':
733 return 'gif';
734 break;
735 default:
736 return $this->gifExtension;
737 }
738 }
739
740 /**
741 * Calculates the value concerning the dimensions of objects.
742 *
743 * @param string $string The string to be calculated (e.g. "[20.h]+13")
744 * @return int The calculated value (e.g. "23")
745 * @see calcOffset()
746 */
747 protected function calculateValue($string)
748 {
749 $calculatedValue = 0;
750 $parts = GeneralUtility::splitCalc($string, '+-*/%');
751 foreach ($parts as $part) {
752 $theVal = $part[1];
753 $sign = $part[0];
754 if (((string)(int)$theVal) == ((string)$theVal)) {
755 $theVal = (int)$theVal;
756 } elseif ('[' . substr($theVal, 1, -1) . ']' == $theVal) {
757 $objParts = explode('.', substr($theVal, 1, -1));
758 $theVal = 0;
759 if (isset($this->objBB[$objParts[0]])) {
760 if ($objParts[1] === 'w') {
761 $theVal = $this->objBB[$objParts[0]][0];
762 } elseif ($objParts[1] === 'h') {
763 $theVal = $this->objBB[$objParts[0]][1];
764 } elseif ($objParts[1] === 'lineHeight') {
765 $theVal = $this->objBB[$objParts[0]][2]['lineHeight'];
766 }
767 $theVal = (int)$theVal;
768 }
769 } elseif ((float)$theVal) {
770 $theVal = (float)$theVal;
771 } else {
772 $theVal = 0;
773 }
774 if ($sign === '-') {
775 $calculatedValue -= $theVal;
776 } elseif ($sign === '+') {
777 $calculatedValue += $theVal;
778 } elseif ($sign === '/' && $theVal) {
779 $calculatedValue = $calculatedValue / $theVal;
780 } elseif ($sign === '*') {
781 $calculatedValue = $calculatedValue * $theVal;
782 } elseif ($sign === '%' && $theVal) {
783 $calculatedValue %= $theVal;
784 }
785 }
786 return round($calculatedValue);
787 }
788
789 /**
790 * Calculates special functions:
791 * + max([10.h], [20.h]) -> gets the maximum of the given values
792 *
793 * @param string $string The raw string with functions to be calculated
794 * @return string The calculated values
795 */
796 protected function calculateFunctions($string)
797 {
798 if (preg_match_all('#max\\(([^)]+)\\)#', $string, $matches)) {
799 foreach ($matches[1] as $index => $maxExpression) {
800 $string = str_replace($matches[0][$index], $this->calculateMaximum($maxExpression), $string);
801 }
802 }
803 return $string;
804 }
805
806 /**
807 * Calculates the maximum of a set of values defined like "[10.h],[20.h],1000"
808 *
809 * @param string $string The string to be used to calculate the maximum (e.g. "[10.h],[20.h],1000")
810 * @return int The maxium value of the given comma separated and calculated values
811 */
812 protected function calculateMaximum($string)
813 {
814 $parts = GeneralUtility::trimExplode(',', $this->calcOffset($string), true);
815 $maximum = !empty($parts) ? max($parts) : 0;
816 return $maximum;
817 }
818 }