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