[TASK] Deprecation free Serializable usage in ObjectStorage
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Imaging / GraphicalFunctions.php
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
16 namespace TYPO3\CMS\Core\Imaging;
17
18 use TYPO3\CMS\Core\Cache\CacheManager;
19 use TYPO3\CMS\Core\Charset\CharsetConverter;
20 use TYPO3\CMS\Core\Core\Environment;
21 use TYPO3\CMS\Core\Type\File\ImageInfo;
22 use TYPO3\CMS\Core\Utility\ArrayUtility;
23 use TYPO3\CMS\Core\Utility\CommandUtility;
24 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Core\Utility\MathUtility;
27 use TYPO3\CMS\Core\Utility\PathUtility;
28 use TYPO3\CMS\Core\Utility\StringUtility;
29
30 /**
31 * Standard graphical functions
32 *
33 * Class contains a bunch of cool functions for manipulating graphics with GDlib/Freetype and ImageMagick.
34 * VERY OFTEN used with gifbuilder that extends this class and provides a TypoScript API to using these functions
35 */
36 class GraphicalFunctions
37 {
38 /**
39 * If set, the frame pointer is appended to the filenames.
40 *
41 * @var bool
42 */
43 public $addFrameSelection = true;
44
45 /**
46 * This should be changed to 'png' if you want this class to read/make PNG-files instead!
47 *
48 * @var string
49 */
50 public $gifExtension = 'gif';
51
52 /**
53 * File formats supported by gdlib. This variable get's filled in "init" method
54 *
55 * @var array
56 */
57 protected $gdlibExtensions = [];
58
59 /**
60 * defines the RGB colorspace to use
61 *
62 * @var string
63 */
64 protected $colorspace = 'RGB';
65
66 /**
67 * colorspace names allowed
68 *
69 * @var array
70 */
71 protected $allowedColorSpaceNames = [
72 'CMY',
73 'CMYK',
74 'Gray',
75 'HCL',
76 'HSB',
77 'HSL',
78 'HWB',
79 'Lab',
80 'LCH',
81 'LMS',
82 'Log',
83 'Luv',
84 'OHTA',
85 'Rec601Luma',
86 'Rec601YCbCr',
87 'Rec709Luma',
88 'Rec709YCbCr',
89 'RGB',
90 'sRGB',
91 'Transparent',
92 'XYZ',
93 'YCbCr',
94 'YCC',
95 'YIQ',
96 'YCbCr',
97 'YUV',
98 ];
99
100 /**
101 * 16777216 Colors is the maximum value for PNG, JPEG truecolor images (24-bit, 8-bit / Channel)
102 *
103 * @var int
104 */
105 public $truecolorColors = 16777215;
106
107 /**
108 * Allowed file extensions perceived as images by TYPO3.
109 * List should be set to 'gif,png,jpeg,jpg' if IM is not available.
110 *
111 * @var array
112 */
113 protected $imageFileExt = ['gif', 'jpg', 'jpeg', 'png', 'tif', 'bmp', 'tga', 'pcx', 'ai', 'pdf', 'webp'];
114
115 /**
116 * Web image extensions (can be shown by a webbrowser)
117 *
118 * @var array
119 */
120 protected $webImageExt = ['gif', 'jpg', 'jpeg', 'png'];
121
122 /**
123 * Enable ImageMagick effects, disabled by default as IM5+ effects slow down the image generation
124 *
125 * @var bool
126 */
127 protected $processorEffectsEnabled = false;
128
129 /**
130 * @var array
131 */
132 public $cmds = [
133 'jpg' => '',
134 'jpeg' => '',
135 'gif' => '',
136 'png' => '',
137 ];
138
139 /**
140 * Whether ImageMagick/GraphicsMagick is enabled or not
141 * @var bool
142 */
143 protected $processorEnabled;
144
145 /**
146 * @var bool
147 */
148 protected $mayScaleUp = true;
149
150 /**
151 * Filename prefix for images scaled in imageMagickConvert()
152 *
153 * @var string
154 */
155 public $filenamePrefix = '';
156
157 /**
158 * Forcing the output filename of imageMagickConvert() to this value. However after calling imageMagickConvert() it will be set blank again.
159 *
160 * @var string
161 */
162 public $imageMagickConvert_forceFileNameBody = '';
163
164 /**
165 * This flag should always be FALSE. If set TRUE, imageMagickConvert will always write a new file to the tempdir! Used for debugging.
166 *
167 * @var bool
168 */
169 public $dontCheckForExistingTempFile = false;
170
171 /**
172 * Prevents imageMagickConvert() from compressing the gif-files with self::gifCompress()
173 *
174 * @var bool
175 */
176 public $dontCompress = false;
177
178 /**
179 * For debugging only.
180 * Filenames will not be based on mtime and only filename (not path) will be used.
181 * This key is also included in the hash of the filename...
182 *
183 * @var string
184 */
185 public $alternativeOutputKey = '';
186
187 /**
188 * All ImageMagick commands executed is stored in this array for tracking. Used by the Install Tools Image section
189 *
190 * @var array
191 */
192 public $IM_commands = [];
193
194 /**
195 * @var array
196 */
197 public $workArea = [];
198
199 /**
200 * Preserve the alpha transparency layer of read PNG images
201 *
202 * @var bool
203 */
204 protected $saveAlphaLayer = false;
205
206 /**
207 * ImageMagick scaling command; "-auto-orient -geometry" or "-auto-orient -sample". Used in makeText() and imageMagickConvert()
208 *
209 * @var string
210 */
211 public $scalecmd = '-auto-orient -geometry';
212
213 /**
214 * Used by v5_blur() to simulate 10 continuous steps of blurring
215 *
216 * @var string
217 */
218 protected $im5fx_blurSteps = '1x2,2x2,3x2,4x3,5x3,5x4,6x4,7x5,8x5,9x5';
219
220 /**
221 * Used by v5_sharpen() to simulate 10 continuous steps of sharpening.
222 *
223 * @var string
224 */
225 protected $im5fx_sharpenSteps = '1x2,2x2,3x2,2x3,3x3,4x3,3x4,4x4,4x5,5x5';
226
227 /**
228 * This is the limit for the number of pixels in an image before it will be rendered as JPG instead of GIF/PNG
229 *
230 * @var int
231 */
232 protected $pixelLimitGif = 10000;
233
234 /**
235 * Array mapping HTML color names to RGB values.
236 *
237 * @var array
238 */
239 protected $colMap = [
240 'aqua' => [0, 255, 255],
241 'black' => [0, 0, 0],
242 'blue' => [0, 0, 255],
243 'fuchsia' => [255, 0, 255],
244 'gray' => [128, 128, 128],
245 'green' => [0, 128, 0],
246 'lime' => [0, 255, 0],
247 'maroon' => [128, 0, 0],
248 'navy' => [0, 0, 128],
249 'olive' => [128, 128, 0],
250 'purple' => [128, 0, 128],
251 'red' => [255, 0, 0],
252 'silver' => [192, 192, 192],
253 'teal' => [0, 128, 128],
254 'yellow' => [255, 255, 0],
255 'white' => [255, 255, 255],
256 ];
257
258 /**
259 * Charset conversion object:
260 *
261 * @var CharsetConverter
262 */
263 protected $csConvObj;
264
265 /**
266 * @var int
267 */
268 protected $jpegQuality = 85;
269
270 /**
271 * @var string
272 */
273 public $map = '';
274
275 /**
276 * This holds the operational setup.
277 * Basically this is a TypoScript array with properties.
278 *
279 * @var array
280 */
281 public $setup = [];
282
283 /**
284 * @var int
285 */
286 public $w = 0;
287
288 /**
289 * @var int
290 */
291 public $h = 0;
292
293 /**
294 * @var array
295 */
296 protected $OFFSET;
297
298 /**
299 * @var resource|\GdImage
300 */
301 protected $im;
302
303 /**
304 * Reads configuration information from $GLOBALS['TYPO3_CONF_VARS']['GFX']
305 * and sets some values in internal variables.
306 */
307 public function __construct()
308 {
309 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
310 if (function_exists('imagecreatefromjpeg') && function_exists('imagejpeg')) {
311 $this->gdlibExtensions[] = 'jpg';
312 $this->gdlibExtensions[] = 'jpeg';
313 }
314 if (function_exists('imagecreatefrompng') && function_exists('imagepng')) {
315 $this->gdlibExtensions[] = 'png';
316 }
317 if (function_exists('imagecreatefromgif') && function_exists('imagegif')) {
318 $this->gdlibExtensions[] = 'gif';
319 }
320
321 if ($gfxConf['processor_colorspace'] && in_array($gfxConf['processor_colorspace'], $this->allowedColorSpaceNames, true)) {
322 $this->colorspace = $gfxConf['processor_colorspace'];
323 }
324
325 $this->processorEnabled = (bool)$gfxConf['processor_enabled'];
326 // Setting default JPG parameters:
327 $this->jpegQuality = MathUtility::forceIntegerInRange($gfxConf['jpg_quality'], 10, 100, 85);
328 $this->addFrameSelection = (bool)$gfxConf['processor_allowFrameSelection'];
329 if ($gfxConf['gdlib_png']) {
330 $this->gifExtension = 'png';
331 }
332 $this->imageFileExt = GeneralUtility::trimExplode(',', $gfxConf['imagefile_ext']);
333
334 // Boolean. This is necessary if using ImageMagick 5+.
335 // Effects in Imagemagick 5+ tends to render very slowly!!
336 // - therefore must be disabled in order not to perform sharpen, blurring and such.
337 $this->cmds['jpg'] = $this->cmds['jpeg'] = '-colorspace ' . $this->colorspace . ' -quality ' . $this->jpegQuality;
338
339 // ... but if 'processor_effects' is set, enable effects
340 if ($gfxConf['processor_effects']) {
341 $this->processorEffectsEnabled = true;
342 $this->cmds['jpg'] .= $this->v5_sharpen(10);
343 $this->cmds['jpeg'] .= $this->v5_sharpen(10);
344 }
345 // Secures that images are not scaled up.
346 $this->mayScaleUp = (bool)$gfxConf['processor_allowUpscaling'];
347 $this->csConvObj = GeneralUtility::makeInstance(CharsetConverter::class);
348 }
349
350 /*************************************************
351 *
352 * Layering images / "IMAGE" GIFBUILDER object
353 *
354 *************************************************/
355 /**
356 * Implements the "IMAGE" GIFBUILDER object, when the "mask" property is TRUE.
357 * It reads the two images defined by $conf['file'] and $conf['mask'] and copies the $conf['file'] onto the input image pointer image using the $conf['mask'] as a grayscale mask
358 * The operation involves ImageMagick for combining.
359 *
360 * @param resource $im GDlib image pointer
361 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
362 * @param array $workArea The current working area coordinates.
363 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
364 */
365 public function maskImageOntoImage(&$im, $conf, $workArea)
366 {
367 if ($conf['file'] && $conf['mask']) {
368 $imgInf = pathinfo($conf['file']);
369 $imgExt = strtolower($imgInf['extension']);
370 if (!in_array($imgExt, $this->gdlibExtensions, true)) {
371 $BBimage = $this->imageMagickConvert($conf['file'], $this->gifExtension);
372 } else {
373 $BBimage = $this->getImageDimensions($conf['file']);
374 }
375 $maskInf = pathinfo($conf['mask']);
376 $maskExt = strtolower($maskInf['extension']);
377 if (!in_array($maskExt, $this->gdlibExtensions, true)) {
378 $BBmask = $this->imageMagickConvert($conf['mask'], $this->gifExtension);
379 } else {
380 $BBmask = $this->getImageDimensions($conf['mask']);
381 }
382 if ($BBimage && $BBmask) {
383 $w = imagesx($im);
384 $h = imagesy($im);
385 $tmpStr = $this->randomName();
386 $theImage = $tmpStr . '_img.' . $this->gifExtension;
387 $theDest = $tmpStr . '_dest.' . $this->gifExtension;
388 $theMask = $tmpStr . '_mask.' . $this->gifExtension;
389 // Prepare overlay image
390 $cpImg = $this->imageCreateFromFile($BBimage[3]);
391 $destImg = imagecreatetruecolor($w, $h);
392 // Preserve alpha transparency
393 if ($this->saveAlphaLayer) {
394 imagesavealpha($destImg, true);
395 $Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
396 imagefill($destImg, 0, 0, $Bcolor);
397 } else {
398 $Bcolor = imagecolorallocate($destImg, 0, 0, 0);
399 imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
400 }
401 $this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
402 $this->ImageWrite($destImg, $theImage);
403 imagedestroy($cpImg);
404 imagedestroy($destImg);
405 // Prepare mask image
406 $cpImg = $this->imageCreateFromFile($BBmask[3]);
407 $destImg = imagecreatetruecolor($w, $h);
408 if ($this->saveAlphaLayer) {
409 imagesavealpha($destImg, true);
410 $Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
411 imagefill($destImg, 0, 0, $Bcolor);
412 } else {
413 $Bcolor = imagecolorallocate($destImg, 0, 0, 0);
414 imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
415 }
416 $this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
417 $this->ImageWrite($destImg, $theMask);
418 imagedestroy($cpImg);
419 imagedestroy($destImg);
420 // Mask the images
421 $this->ImageWrite($im, $theDest);
422 // Let combineExec handle maskNegation
423 $this->combineExec($theDest, $theImage, $theMask, $theDest);
424 // The main image is loaded again...
425 $backIm = $this->imageCreateFromFile($theDest);
426 // ... and if nothing went wrong we load it onto the old one.
427 if ($backIm) {
428 if (!$this->saveAlphaLayer) {
429 imagecolortransparent($backIm, -1);
430 }
431 $im = $backIm;
432 }
433 // Unlink files from process
434 unlink($theDest);
435 unlink($theImage);
436 unlink($theMask);
437 }
438 }
439 }
440
441 /**
442 * Implements the "IMAGE" GIFBUILDER object, when the "mask" property is FALSE (using only $conf['file'])
443 *
444 * @param resource $im GDlib image pointer
445 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
446 * @param array $workArea The current working area coordinates.
447 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
448 * @see maskImageOntoImage()
449 */
450 public function copyImageOntoImage(&$im, $conf, $workArea)
451 {
452 if ($conf['file']) {
453 if (!in_array($conf['BBOX'][2], $this->gdlibExtensions, true)) {
454 $conf['BBOX'] = $this->imageMagickConvert($conf['BBOX'][3], $this->gifExtension);
455 $conf['file'] = $conf['BBOX'][3];
456 }
457 $cpImg = $this->imageCreateFromFile($conf['file']);
458 $this->copyGifOntoGif($im, $cpImg, $conf, $workArea);
459 imagedestroy($cpImg);
460 }
461 }
462
463 /**
464 * Copies two GDlib image pointers onto each other, using TypoScript configuration from $conf and the input $workArea definition.
465 *
466 * @param resource $im GDlib image pointer, destination (bottom image)
467 * @param resource $cpImg GDlib image pointer, source (top image)
468 * @param array $conf TypoScript array with the properties for the IMAGE GIFBUILDER object. Only used for the "tile" property value.
469 * @param array $workArea Work area
470 * @internal
471 */
472 public function copyGifOntoGif(&$im, $cpImg, $conf, $workArea)
473 {
474 $cpW = imagesx($cpImg);
475 $cpH = imagesy($cpImg);
476 $tile = GeneralUtility::intExplode(',', $conf['tile'] ?? '');
477 $tile[0] = MathUtility::forceIntegerInRange($tile[0], 1, 20);
478 $tile[1] = MathUtility::forceIntegerInRange($tile[1] ?? 0, 1, 20);
479 $cpOff = $this->objPosition($conf, $workArea, [$cpW * $tile[0], $cpH * $tile[1]]);
480 for ($xt = 0; $xt < $tile[0]; $xt++) {
481 $Xstart = $cpOff[0] + $cpW * $xt;
482 // If this image is inside of the workArea, then go on
483 if ($Xstart + $cpW > $workArea[0]) {
484 // X:
485 if ($Xstart < $workArea[0]) {
486 $cpImgCutX = $workArea[0] - $Xstart;
487 $Xstart = $workArea[0];
488 } else {
489 $cpImgCutX = 0;
490 }
491 $w = $cpW - $cpImgCutX;
492 if ($Xstart > $workArea[0] + $workArea[2] - $w) {
493 $w = $workArea[0] + $workArea[2] - $Xstart;
494 }
495 // If this image is inside of the workArea, then go on
496 if ($Xstart < $workArea[0] + $workArea[2]) {
497 // Y:
498 for ($yt = 0; $yt < $tile[1]; $yt++) {
499 $Ystart = $cpOff[1] + $cpH * $yt;
500 // If this image is inside of the workArea, then go on
501 if ($Ystart + $cpH > $workArea[1]) {
502 if ($Ystart < $workArea[1]) {
503 $cpImgCutY = $workArea[1] - $Ystart;
504 $Ystart = $workArea[1];
505 } else {
506 $cpImgCutY = 0;
507 }
508 $h = $cpH - $cpImgCutY;
509 if ($Ystart > $workArea[1] + $workArea[3] - $h) {
510 $h = $workArea[1] + $workArea[3] - $Ystart;
511 }
512 // If this image is inside of the workArea, then go on
513 if ($Ystart < $workArea[1] + $workArea[3]) {
514 $this->imagecopyresized($im, $cpImg, $Xstart, $Ystart, $cpImgCutX, $cpImgCutY, $w, $h, $w, $h);
515 }
516 }
517 }
518 }
519 }
520 }
521 }
522
523 /**
524 * Alternative function for using the similar PHP function imagecopyresized(). Used for GD2 only.
525 *
526 * OK, the reason for this stupid fix is the following story:
527 * GD1.x was capable of copying two images together and combining their palettes! GD2 is apparently not.
528 * With GD2 only the palette of the dest-image is used which mostly results in totally black images when trying to
529 * copy a color-ful image onto the destination.
530 * The GD2-fix is to
531 * 1) Create a blank TRUE-COLOR image
532 * 2) Copy the destination image onto that one
533 * 3) Then do the actual operation; Copying the source (top image) onto that
534 * 4) ... and return the result pointer.
535 * 5) Reduce colors (if we do not, the result may become strange!)
536 * It works, but the resulting images is now a true-color PNG which may be very large.
537 * So, why not use 'imagetruecolortopalette ($im, TRUE, 256)' - well because it does NOT WORK! So simple is that.
538 *
539 * @param resource $dstImg Destination image
540 * @param resource $srcImg Source image
541 * @param int $dstX Destination x-coordinate
542 * @param int $dstY Destination y-coordinate
543 * @param int $srcX Source x-coordinate
544 * @param int $srcY Source y-coordinate
545 * @param int $dstWidth Destination width
546 * @param int $dstHeight Destination height
547 * @param int $srcWidth Source width
548 * @param int $srcHeight Source height
549 * @internal
550 */
551 public function imagecopyresized(&$dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight)
552 {
553 if (!$this->saveAlphaLayer) {
554 // Make true color image
555 $tmpImg = imagecreatetruecolor(imagesx($dstImg), imagesy($dstImg));
556 // Copy the source image onto that
557 imagecopyresized($tmpImg, $dstImg, 0, 0, 0, 0, imagesx($dstImg), imagesy($dstImg), imagesx($dstImg), imagesy($dstImg));
558 // Then copy the source image onto that (the actual operation!)
559 imagecopyresized($tmpImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
560 // Set the destination image
561 $dstImg = $tmpImg;
562 } else {
563 imagecopyresized($dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
564 }
565 }
566
567 /********************************
568 *
569 * Text / "TEXT" GIFBUILDER object
570 *
571 ********************************/
572 /**
573 * Implements the "TEXT" GIFBUILDER object
574 *
575 * @param resource $im GDlib image pointer
576 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
577 * @param array $workArea The current working area coordinates.
578 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
579 */
580 public function makeText(&$im, $conf, $workArea)
581 {
582 // Spacing
583 [$spacing, $wordSpacing] = $this->calcWordSpacing($conf);
584 // Position
585 $txtPos = $this->txtPosition($conf, $workArea, $conf['BBOX']);
586 $theText = $conf['text'] ?? '';
587 if (($conf['imgMap'] ?? false) && is_array($conf['imgMap.'] ?? null)) {
588 $this->addToMap($this->calcTextCordsForMap($conf['BBOX'][2], $txtPos, $conf['imgMap.']), $conf['imgMap.']);
589 }
590 if (!($conf['hideButCreateMap'] ?? false)) {
591 // Font Color:
592 $cols = $this->convertColor($conf['fontColor']);
593 // NiceText is calculated
594 if (!($conf['niceText'] ?? false)) {
595 $Fcolor = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
596 // antiAliasing is setup:
597 $Fcolor = $conf['antiAlias'] ? $Fcolor : -$Fcolor;
598 for ($a = 0; $a < $conf['iterations']; $a++) {
599 // If any kind of spacing applies, we use this function:
600 if ($spacing || $wordSpacing) {
601 $this->SpacedImageTTFText($im, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.']);
602 } else {
603 $this->renderTTFText($im, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'] ?? [], $conf);
604 }
605 }
606 } else {
607 // NICETEXT::
608 // options anti_aliased and iterations is NOT available when doing this!!
609 $w = imagesx($im);
610 $h = imagesy($im);
611 $tmpStr = $this->randomName();
612 $fileMenu = $tmpStr . '_menuNT.' . $this->gifExtension;
613 $fileColor = $tmpStr . '_colorNT.' . $this->gifExtension;
614 $fileMask = $tmpStr . '_maskNT.' . $this->gifExtension;
615 // Scalefactor
616 $sF = MathUtility::forceIntegerInRange(($conf['niceText.']['scaleFactor'] ?? 2), 2, 5);
617 $newW = (int)ceil($sF * imagesx($im));
618 $newH = (int)ceil($sF * imagesy($im));
619 // Make mask
620 $maskImg = imagecreatetruecolor($newW, $newH);
621 $Bcolor = imagecolorallocate($maskImg, 255, 255, 255);
622 imagefilledrectangle($maskImg, 0, 0, $newW, $newH, $Bcolor);
623 $Fcolor = imagecolorallocate($maskImg, 0, 0, 0);
624 // If any kind of spacing applies, we use this function:
625 if ($spacing || $wordSpacing) {
626 $this->SpacedImageTTFText($maskImg, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.'], $sF);
627 } else {
628 $this->renderTTFText($maskImg, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'] ?? [], $conf, $sF);
629 }
630 $this->ImageWrite($maskImg, $fileMask);
631 imagedestroy($maskImg);
632 // Downscales the mask
633 if (!$this->processorEffectsEnabled) {
634 $command = trim($this->scalecmd . ' ' . $w . 'x' . $h . '! -negate');
635 } else {
636 $command = trim(($conf['niceText.']['before'] ?? '') . ' ' . $this->scalecmd . ' ' . $w . 'x' . $h . '! ' . ($conf['niceText.']['after'] ?? '') . ' -negate');
637 if (isset($conf['niceText.']['sharpen'])) {
638 $command .= $this->v5_sharpen($conf['niceText.']['sharpen']);
639 }
640 }
641 $this->imageMagickExec($fileMask, $fileMask, $command);
642 // Make the color-file
643 $colorImg = imagecreatetruecolor($w, $h);
644 $Ccolor = imagecolorallocate($colorImg, $cols[0], $cols[1], $cols[2]);
645 imagefilledrectangle($colorImg, 0, 0, $w, $h, $Ccolor);
646 $this->ImageWrite($colorImg, $fileColor);
647 imagedestroy($colorImg);
648 // The mask is applied
649 // The main pictures is saved temporarily
650 $this->ImageWrite($im, $fileMenu);
651 $this->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
652 // The main image is loaded again...
653 $backIm = $this->imageCreateFromFile($fileMenu);
654 // ... and if nothing went wrong we load it onto the old one.
655 if ($backIm) {
656 if (!$this->saveAlphaLayer) {
657 imagecolortransparent($backIm, -1);
658 }
659 $im = $backIm;
660 }
661 // Deleting temporary files;
662 unlink($fileMenu);
663 unlink($fileColor);
664 unlink($fileMask);
665 }
666 }
667 }
668
669 /**
670 * Calculates text position for printing the text onto the image based on configuration like alignment and workarea.
671 *
672 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
673 * @param array $workArea Work area definition
674 * @param array $BB Bounding box information, was set in \TYPO3\CMS\Frontend\Imaging\GifBuilder::start()
675 * @return array [0]=x, [1]=y, [2]=w, [3]=h
676 * @internal
677 * @see makeText()
678 */
679 public function txtPosition($conf, $workArea, $BB)
680 {
681 $angle = (int)($conf['angle'] ?? 0) / 180 * M_PI;
682 $conf['angle'] = 0;
683 $straightBB = $this->calcBBox($conf);
684 // offset, align, valign, workarea
685 // [0]=x, [1]=y, [2]=w, [3]=h
686 $result = [];
687 $result[2] = $BB[0];
688 $result[3] = $BB[1];
689 $w = $workArea[2];
690 $alignment = $conf['align'] ?? '';
691 switch ($alignment) {
692 case 'right':
693
694 case 'center':
695 $factor = abs(cos($angle));
696 $sign = cos($angle) < 0 ? -1 : 1;
697 $len1 = $sign * $factor * $straightBB[0];
698 $len2 = $sign * $BB[0];
699 $result[0] = $w - ceil($len2 * $factor + (1 - $factor) * $len1);
700 $factor = abs(sin($angle));
701 $sign = sin($angle) < 0 ? -1 : 1;
702 $len1 = $sign * $factor * $straightBB[0];
703 $len2 = $sign * $BB[1];
704 $result[1] = ceil($len2 * $factor + (1 - $factor) * $len1);
705 break;
706 }
707 switch ($alignment) {
708 case 'right':
709 break;
710 case 'center':
711 $result[0] = round($result[0] / 2);
712 $result[1] = round($result[1] / 2);
713 break;
714 default:
715 $result[0] = 0;
716 $result[1] = 0;
717 }
718 $result = $this->applyOffset($result, GeneralUtility::intExplode(',', $conf['offset'] ?? ''));
719 $result = $this->applyOffset($result, $workArea);
720 return $result;
721 }
722
723 /**
724 * Calculates bounding box information for the TEXT GIFBUILDER object.
725 *
726 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
727 * @return array Array with three keys [0]/[1] being x/y and [2] being the bounding box array
728 * @internal
729 * @see txtPosition()
730 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::start()
731 */
732 public function calcBBox($conf)
733 {
734 $sF = $this->getTextScalFactor($conf);
735 [$spacing, $wordSpacing] = $this->calcWordSpacing($conf, $sF);
736 $theText = $conf['text'];
737 $charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'] ?? 0, $conf['fontFile'], $theText, ($conf['splitRendering.'] ?? []), $sF);
738 $theBBoxInfo = $charInf;
739 if ($conf['angle'] ?? false) {
740 $xArr = [$charInf[0], $charInf[2], $charInf[4], $charInf[6]];
741 $yArr = [$charInf[1], $charInf[3], $charInf[5], $charInf[7]];
742 $x = max($xArr) - min($xArr);
743 $y = max($yArr) - min($yArr);
744 } else {
745 $x = $charInf[2] - $charInf[0];
746 $y = $charInf[1] - $charInf[7];
747 }
748 // Set original lineHeight (used by line breaks):
749 $theBBoxInfo['lineHeight'] = $y;
750 if (!empty($conf['lineHeight'])) {
751 $theBBoxInfo['lineHeight'] = (int)$conf['lineHeight'];
752 }
753
754 // If any kind of spacing applys, we use this function:
755 if ($spacing || $wordSpacing) {
756 $x = 0;
757 if (!$spacing && $wordSpacing) {
758 $bits = explode(' ', $theText);
759 foreach ($bits as $word) {
760 $word .= ' ';
761 $wordInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $word, $conf['splitRendering.'], $sF);
762 $wordW = $wordInf[2] - $wordInf[0];
763 $x += $wordW + $wordSpacing;
764 }
765 } else {
766 $utf8Chars = $this->csConvObj->utf8_to_numberarray($theText);
767 // For each UTF-8 char, do:
768 foreach ($utf8Chars as $char) {
769 $charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $char, $conf['splitRendering.'], $sF);
770 $charW = $charInf[2] - $charInf[0];
771 $x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
772 }
773 }
774 } elseif (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($conf['text'], $conf) > $conf['breakWidth']) {
775 $maxWidth = 0;
776 $currentWidth = 0;
777 $breakWidth = $conf['breakWidth'];
778 $breakSpace = $this->getBreakSpace($conf, $theBBoxInfo);
779 $wordPairs = $this->getWordPairsForLineBreak($conf['text']);
780 // Iterate through all word pairs:
781 foreach ($wordPairs as $index => $wordPair) {
782 $wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
783 if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
784 $currentWidth += $wordWidth;
785 } else {
786 $maxWidth = max($maxWidth, $currentWidth);
787 $y += $breakSpace;
788 // Restart:
789 $currentWidth = $wordWidth;
790 }
791 }
792 $x = max($maxWidth, $currentWidth) * $sF;
793 }
794 if ($sF > 1) {
795 $x = ceil($x / $sF);
796 $y = ceil($y / $sF);
797 if (is_array($theBBoxInfo)) {
798 foreach ($theBBoxInfo as &$value) {
799 $value = ceil($value / $sF);
800 }
801 unset($value);
802 }
803 }
804 return [$x, $y, $theBBoxInfo];
805 }
806
807 /**
808 * Adds an <area> tag to the internal variable $this->map which is used to accumulate the content for an ImageMap
809 *
810 * @param array $cords Coordinates for a polygon image map as created by ->calcTextCordsForMap()
811 * @param array $conf Configuration for "imgMap." property of a TEXT GIFBUILDER object.
812 * @internal
813 * @see makeText()
814 * @see calcTextCordsForMap()
815 */
816 public function addToMap($cords, $conf)
817 {
818 $this->map .= '<area shape="poly" coords="' . implode(',', $cords) . '"'
819 . ' href="' . htmlspecialchars($conf['url']) . '"'
820 . ($conf['target'] ? ' target="' . htmlspecialchars($conf['target']) . '"' : '')
821 . ((string)$conf['titleText'] !== '' ? ' title="' . htmlspecialchars($conf['titleText']) . '"' : '')
822 . ' alt="' . htmlspecialchars($conf['altText']) . '" />';
823 }
824
825 /**
826 * Calculating the coordinates for a TEXT string on an image map. Used in an <area> tag
827 *
828 * @param array $cords Coordinates (from BBOX array)
829 * @param array $offset Offset array
830 * @param array $conf Configuration for "imgMap." property of a TEXT GIFBUILDER object.
831 * @return array
832 * @internal
833 * @see makeText()
834 * @see calcTextCordsForMap()
835 */
836 public function calcTextCordsForMap($cords, $offset, $conf)
837 {
838 $newCords = [];
839 $pars = GeneralUtility::intExplode(',', $conf['explode'] . ',');
840 $newCords[0] = $cords[0] + $offset[0] - $pars[0];
841 $newCords[1] = $cords[1] + $offset[1] + $pars[1];
842 $newCords[2] = $cords[2] + $offset[0] + $pars[0];
843 $newCords[3] = $cords[3] + $offset[1] + $pars[1];
844 $newCords[4] = $cords[4] + $offset[0] + $pars[0];
845 $newCords[5] = $cords[5] + $offset[1] - $pars[1];
846 $newCords[6] = $cords[6] + $offset[0] - $pars[0];
847 $newCords[7] = $cords[7] + $offset[1] - $pars[1];
848 return $newCords;
849 }
850
851 /**
852 * Printing text onto an image like the PHP function imageTTFText does but in addition it offers options for spacing of letters and words.
853 * Spacing is done by printing one char at a time and this means that the spacing is rather uneven and probably not very nice.
854 * See
855 *
856 * @param resource $im (See argument for PHP function imageTTFtext())
857 * @param int $fontSize (See argument for PHP function imageTTFtext())
858 * @param int $angle (See argument for PHP function imageTTFtext())
859 * @param int $x (See argument for PHP function imageTTFtext())
860 * @param int $y (See argument for PHP function imageTTFtext())
861 * @param int $Fcolor (See argument for PHP function imageTTFtext())
862 * @param string $fontFile (See argument for PHP function imageTTFtext())
863 * @param string $text (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
864 * @param int $spacing The spacing of letters in pixels
865 * @param int $wordSpacing The spacing of words in pixels
866 * @param array $splitRenderingConf Array
867 * @param int $sF Scale factor
868 * @internal
869 */
870 public function SpacedImageTTFText(&$im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $text, $spacing, $wordSpacing, $splitRenderingConf, $sF = 1)
871 {
872 $spacing *= $sF;
873 $wordSpacing *= $sF;
874 if (!$spacing && $wordSpacing) {
875 $bits = explode(' ', $text);
876 foreach ($bits as $word) {
877 $word .= ' ';
878 $wordInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $word, $splitRenderingConf, $sF);
879 $wordW = $wordInf[2] - $wordInf[0];
880 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $word, $splitRenderingConf, $sF);
881 $x += $wordW + $wordSpacing;
882 }
883 } else {
884 $utf8Chars = $this->csConvObj->utf8_to_numberarray($text);
885 // For each UTF-8 char, do:
886 foreach ($utf8Chars as $char) {
887 $charInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $char, $splitRenderingConf, $sF);
888 $charW = $charInf[2] - $charInf[0];
889 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $char, $splitRenderingConf, $sF);
890 $x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
891 }
892 }
893 }
894
895 /**
896 * Function that finds the right fontsize that will render the textstring within a certain width
897 *
898 * @param array $conf The TypoScript properties of the TEXT GIFBUILDER object
899 * @return int The new fontSize
900 * @internal
901 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::start()
902 */
903 public function fontResize($conf)
904 {
905 // You have to use +calc options like [10.h] in 'offset' to get the right position of your text-image, if you use +calc in XY height!!!!
906 $maxWidth = (int)$conf['maxWidth'];
907 [$spacing, $wordSpacing] = $this->calcWordSpacing($conf);
908 if ($maxWidth) {
909 // If any kind of spacing applys, we use this function:
910 if ($spacing || $wordSpacing) {
911 return $conf['fontSize'];
912 }
913 do {
914 // Determine bounding box.
915 $bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $conf['text'], $conf['splitRendering.']);
916 if ($conf['angle'] < 0) {
917 $pixelWidth = abs($bounds[4] - $bounds[0]);
918 } elseif ($conf['angle'] > 0) {
919 $pixelWidth = abs($bounds[2] - $bounds[6]);
920 } else {
921 $pixelWidth = abs($bounds[4] - $bounds[6]);
922 }
923 // Size is fine, exit:
924 if ($pixelWidth <= $maxWidth) {
925 break;
926 }
927 $conf['fontSize']--;
928 } while ($conf['fontSize'] > 1);
929 }
930 return $conf['fontSize'];
931 }
932
933 /**
934 * Wrapper for ImageTTFBBox
935 *
936 * @param int $fontSize (See argument for PHP function ImageTTFBBox())
937 * @param int $angle (See argument for PHP function ImageTTFBBox())
938 * @param string $fontFile (See argument for PHP function ImageTTFBBox())
939 * @param string $string (See argument for PHP function ImageTTFBBox())
940 * @param array $splitRendering Split-rendering configuration
941 * @param int $sF Scale factor
942 * @return array Information array.
943 */
944 public function ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $string, $splitRendering, $sF = 1)
945 {
946 // Initialize:
947 $offsetInfo = [];
948 $stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
949 // Traverse string parts:
950 foreach ($stringParts as $strCfg) {
951 $fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
952 if (is_readable($fontFile)) {
953 // Calculate Bounding Box for part.
954 $calc = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $fontFile, $strCfg['str']);
955 // Calculate offsets:
956 if (empty($offsetInfo)) {
957 // First run, just copy over.
958 $offsetInfo = $calc;
959 } else {
960 $offsetInfo[2] += $calc[2] - $calc[0] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
961 $offsetInfo[3] += $calc[3] - $calc[1] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
962 $offsetInfo[4] += $calc[4] - $calc[6] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
963 $offsetInfo[5] += $calc[5] - $calc[7] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
964 }
965 } else {
966 debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFBBoxWrapper()');
967 }
968 }
969 return $offsetInfo;
970 }
971
972 /**
973 * Wrapper for ImageTTFText
974 *
975 * @param resource $im (See argument for PHP function imageTTFtext())
976 * @param int $fontSize (See argument for PHP function imageTTFtext())
977 * @param int $angle (See argument for PHP function imageTTFtext())
978 * @param int $x (See argument for PHP function imageTTFtext())
979 * @param int $y (See argument for PHP function imageTTFtext())
980 * @param int $color (See argument for PHP function imageTTFtext())
981 * @param string $fontFile (See argument for PHP function imageTTFtext())
982 * @param string $string (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
983 * @param array $splitRendering Split-rendering configuration
984 * @param int $sF Scale factor
985 */
986 public function ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF = 1)
987 {
988 // Initialize:
989 $stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
990 $x = (int)ceil($sF * $x);
991 $y = (int)ceil($sF * $y);
992 // Traverse string parts:
993 foreach ($stringParts as $i => $strCfg) {
994 // Initialize:
995 $colorIndex = $color;
996 // Set custom color if any (only when niceText is off):
997 if (($strCfg['color'] ?? false) && $sF == 1) {
998 $cols = $this->convertColor($strCfg['color']);
999 $colorIndex = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
1000 $colorIndex = $color >= 0 ? $colorIndex : -$colorIndex;
1001 }
1002 // Setting xSpaceBefore
1003 if ($i) {
1004 $x += (int)$strCfg['xSpaceBefore'];
1005 $y -= (int)$strCfg['ySpaceBefore'];
1006 }
1007 $fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
1008 if (is_readable($fontFile)) {
1009 // Render part:
1010 imagettftext($im, $this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $x, $y, $colorIndex, $fontFile, $strCfg['str']);
1011 // Calculate offset to apply:
1012 $wordInf = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, GeneralUtility::getFileAbsFileName($strCfg['fontFile']), $strCfg['str']);
1013 $x += $wordInf[2] - $wordInf[0] + (int)($splitRendering['compX'] ?? 0) + (int)($strCfg['xSpaceAfter'] ?? 0);
1014 $y += $wordInf[5] - $wordInf[7] - (int)($splitRendering['compY'] ?? 0) - (int)($strCfg['ySpaceAfter'] ?? 0);
1015 } else {
1016 debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFTextWrapper()');
1017 }
1018 }
1019 }
1020
1021 /**
1022 * Splitting a string for ImageTTFBBox up into an array where each part has its own configuration options.
1023 *
1024 * @param string $string UTF-8 string
1025 * @param array $splitRendering Split-rendering configuration from GIFBUILDER TEXT object.
1026 * @param int $fontSize Current fontsize
1027 * @param string $fontFile Current font file
1028 * @return array Array with input string splitted according to configuration
1029 */
1030 public function splitString($string, $splitRendering, $fontSize, $fontFile)
1031 {
1032 // Initialize by setting the whole string and default configuration as the first entry.
1033 $result = [];
1034 $result[] = [
1035 'str' => $string,
1036 'fontSize' => $fontSize,
1037 'fontFile' => $fontFile,
1038 ];
1039 // Traverse the split-rendering configuration:
1040 // Splitting will create more entries in $result with individual configurations.
1041 if (is_array($splitRendering)) {
1042 $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($splitRendering);
1043 // Traverse configured options:
1044 foreach ($sKeyArray as $key) {
1045 $cfg = $splitRendering[$key . '.'];
1046 // Process each type of split rendering keyword:
1047 switch ((string)$splitRendering[$key]) {
1048 case 'highlightWord':
1049 if ((string)$cfg['value'] !== '') {
1050 $newResult = [];
1051 // Traverse the current parts of the result array:
1052 foreach ($result as $part) {
1053 // Explode the string value by the word value to highlight:
1054 $explodedParts = explode($cfg['value'], $part['str']);
1055 foreach ($explodedParts as $c => $expValue) {
1056 if ((string)$expValue !== '') {
1057 $newResult[] = array_merge($part, ['str' => $expValue]);
1058 }
1059 if ($c + 1 < count($explodedParts)) {
1060 $newResult[] = [
1061 'str' => $cfg['value'],
1062 'fontSize' => $cfg['fontSize'] ?: $part['fontSize'],
1063 'fontFile' => $cfg['fontFile'] ?: $part['fontFile'],
1064 'color' => $cfg['color'],
1065 'xSpaceBefore' => $cfg['xSpaceBefore'],
1066 'xSpaceAfter' => $cfg['xSpaceAfter'],
1067 'ySpaceBefore' => $cfg['ySpaceBefore'],
1068 'ySpaceAfter' => $cfg['ySpaceAfter'],
1069 ];
1070 }
1071 }
1072 }
1073 // Set the new result as result array:
1074 if (!empty($newResult)) {
1075 $result = $newResult;
1076 }
1077 }
1078 break;
1079 case 'charRange':
1080 if ((string)$cfg['value'] !== '') {
1081 // Initialize range:
1082 $ranges = GeneralUtility::trimExplode(',', $cfg['value'], true);
1083 foreach ($ranges as $i => $rangeDef) {
1084 $ranges[$i] = GeneralUtility::intExplode('-', (string)$ranges[$i]);
1085 if (!isset($ranges[$i][1])) {
1086 $ranges[$i][1] = $ranges[$i][0];
1087 }
1088 }
1089 $newResult = [];
1090 // Traverse the current parts of the result array:
1091 foreach ($result as $part) {
1092 // Initialize:
1093 $currentState = -1;
1094 $bankAccum = '';
1095 // Explode the string value by the word value to highlight:
1096 $utf8Chars = $this->csConvObj->utf8_to_numberarray($part['str']);
1097 foreach ($utf8Chars as $utfChar) {
1098 // Find number and evaluate position:
1099 $uNumber = (int)$this->csConvObj->utf8CharToUnumber($utfChar);
1100 $inRange = 0;
1101 foreach ($ranges as $rangeDef) {
1102 if ($uNumber >= $rangeDef[0] && (!$rangeDef[1] || $uNumber <= $rangeDef[1])) {
1103 $inRange = 1;
1104 break;
1105 }
1106 }
1107 if ($currentState == -1) {
1108 $currentState = $inRange;
1109 }
1110 // Initialize first char
1111 // Switch bank:
1112 if ($inRange != $currentState && $uNumber !== 9 && $uNumber !== 10 && $uNumber !== 13 && $uNumber !== 32) {
1113 // Set result:
1114 if ($bankAccum !== '') {
1115 $newResult[] = [
1116 'str' => $bankAccum,
1117 'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1118 'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1119 'color' => $currentState ? $cfg['color'] : '',
1120 'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
1121 'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
1122 'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
1123 'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : '',
1124 ];
1125 }
1126 // Initialize new settings:
1127 $currentState = $inRange;
1128 $bankAccum = '';
1129 }
1130 // Add char to bank:
1131 $bankAccum .= $utfChar;
1132 }
1133 // Set result for FINAL part:
1134 if ($bankAccum !== '') {
1135 $newResult[] = [
1136 'str' => $bankAccum,
1137 'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1138 'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1139 'color' => $currentState ? $cfg['color'] : '',
1140 'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
1141 'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
1142 'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
1143 'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : '',
1144 ];
1145 }
1146 }
1147 // Set the new result as result array:
1148 if (!empty($newResult)) {
1149 $result = $newResult;
1150 }
1151 }
1152 break;
1153 }
1154 }
1155 }
1156 return $result;
1157 }
1158
1159 /**
1160 * Calculates the spacing and wordSpacing values
1161 *
1162 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
1163 * @param int $scaleFactor TypoScript value from eg $conf['niceText.']['scaleFactor']
1164 * @return array Array with two keys [0]/[1] being array($spacing,$wordSpacing)
1165 * @internal
1166 * @see calcBBox()
1167 */
1168 public function calcWordSpacing($conf, $scaleFactor = 1)
1169 {
1170 $spacing = (int)($conf['spacing'] ?? 0);
1171 $wordSpacing = (int)($conf['wordSpacing'] ?? 0);
1172 $wordSpacing = $wordSpacing ?: $spacing * 2;
1173 $spacing *= $scaleFactor;
1174 $wordSpacing *= $scaleFactor;
1175 return [$spacing, $wordSpacing];
1176 }
1177
1178 /**
1179 * Calculates and returns the niceText.scaleFactor
1180 *
1181 * @param array $conf TypoScript array for the TEXT GIFBUILDER object
1182 * @return int TypoScript value from eg $conf['niceText.']['scaleFactor']
1183 * @internal
1184 */
1185 public function getTextScalFactor($conf)
1186 {
1187 if (!($conf['niceText'] ?? false)) {
1188 $sF = 1;
1189 } else {
1190 // NICETEXT::
1191 $sF = MathUtility::forceIntegerInRange(($conf['niceText.']['scaleFactor'] ?? 2), 2, 5);
1192 }
1193 return $sF;
1194 }
1195
1196 /**
1197 * @param array $imageFileExt
1198 * @internal Only used for ext:install, not part of TYPO3 Core API.
1199 */
1200 public function setImageFileExt(array $imageFileExt): void
1201 {
1202 $this->imageFileExt = $imageFileExt;
1203 }
1204
1205 /**
1206 * Renders a regular text and takes care of a possible line break automatically.
1207 *
1208 * @param resource $im (See argument for PHP function imageTTFtext())
1209 * @param int $fontSize (See argument for PHP function imageTTFtext())
1210 * @param int $angle (See argument for PHP function imageTTFtext())
1211 * @param int $x (See argument for PHP function imageTTFtext())
1212 * @param int $y (See argument for PHP function imageTTFtext())
1213 * @param int $color (See argument for PHP function imageTTFtext())
1214 * @param string $fontFile (See argument for PHP function imageTTFtext())
1215 * @param string $string (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
1216 * @param array $splitRendering Split-rendering configuration
1217 * @param array $conf The configuration
1218 * @param int $sF Scale factor
1219 */
1220 protected function renderTTFText(&$im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $conf, $sF = 1)
1221 {
1222 if (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($string, $conf) > $conf['breakWidth']) {
1223 $phrase = '';
1224 $currentWidth = 0;
1225 $breakWidth = $conf['breakWidth'];
1226 $breakSpace = $this->getBreakSpace($conf);
1227 $wordPairs = $this->getWordPairsForLineBreak($string);
1228 // Iterate through all word pairs:
1229 foreach ($wordPairs as $index => $wordPair) {
1230 $wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
1231 if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
1232 $currentWidth += $wordWidth;
1233 $phrase .= $wordPair;
1234 } else {
1235 // Render the current phrase that is below breakWidth:
1236 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
1237 // Calculate the news height offset:
1238 $y += $breakSpace;
1239 // Restart the phrase:
1240 $currentWidth = $wordWidth;
1241 $phrase = $wordPair;
1242 }
1243 }
1244 // Render the remaining phrase:
1245 if ($currentWidth) {
1246 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
1247 }
1248 } else {
1249 $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF);
1250 }
1251 }
1252
1253 /**
1254 * Gets the word pairs used for automatic line breaks.
1255 *
1256 * @param string $string
1257 * @return array
1258 */
1259 protected function getWordPairsForLineBreak($string)
1260 {
1261 $wordPairs = [];
1262 $wordsArray = preg_split('#([- .,!:]+)#', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
1263 $wordsArray = is_array($wordsArray) ? $wordsArray : [];
1264 $wordsCount = count($wordsArray);
1265 for ($index = 0; $index < $wordsCount; $index += 2) {
1266 $wordPairs[] = $wordsArray[$index] . $wordsArray[$index + 1];
1267 }
1268 return $wordPairs;
1269 }
1270
1271 /**
1272 * Gets the rendered text width
1273 *
1274 * @param string $text
1275 * @param array $conf
1276 * @return int
1277 */
1278 protected function getRenderedTextWidth($text, $conf)
1279 {
1280 $bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $text, $conf['splitRendering.']);
1281 if ($conf['angle'] < 0) {
1282 $pixelWidth = abs($bounds[4] - $bounds[0]);
1283 } elseif ($conf['angle'] > 0) {
1284 $pixelWidth = abs($bounds[2] - $bounds[6]);
1285 } else {
1286 $pixelWidth = abs($bounds[4] - $bounds[6]);
1287 }
1288 return $pixelWidth;
1289 }
1290
1291 /**
1292 * Gets the break space for each new line.
1293 *
1294 * @param array $conf TypoScript configuration for the currently rendered object
1295 * @param array $boundingBox The bounding box the the currently rendered object
1296 * @return int The break space
1297 */
1298 protected function getBreakSpace($conf, array $boundingBox = null)
1299 {
1300 if (!isset($boundingBox)) {
1301 $boundingBox = $this->calcBBox($conf);
1302 $boundingBox = $boundingBox[2];
1303 }
1304 if (isset($conf['breakSpace']) && $conf['breakSpace']) {
1305 $breakSpace = $boundingBox['lineHeight'] * $conf['breakSpace'];
1306 } else {
1307 $breakSpace = $boundingBox['lineHeight'];
1308 }
1309 return $breakSpace;
1310 }
1311
1312 /*********************************************
1313 *
1314 * Other GIFBUILDER objects related to TEXT
1315 *
1316 *********************************************/
1317 /**
1318 * Implements the "OUTLINE" GIFBUILDER object / property for the TEXT object
1319 *
1320 * @param resource $im GDlib image pointer
1321 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1322 * @param array $workArea The current working area coordinates.
1323 * @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
1324 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1325 * @see makeText()
1326 */
1327 public function makeOutline(&$im, $conf, $workArea, $txtConf)
1328 {
1329 $thickness = (int)$conf['thickness'];
1330 if ($thickness) {
1331 $txtConf['fontColor'] = $conf['color'];
1332 $outLineDist = MathUtility::forceIntegerInRange($thickness, 1, 2);
1333 for ($b = 1; $b <= $outLineDist; $b++) {
1334 if ($b == 1) {
1335 $it = 8;
1336 } else {
1337 $it = 16;
1338 }
1339 $outL = $this->circleOffset($b, $it);
1340 for ($a = 0; $a < $it; $a++) {
1341 $this->makeText($im, $txtConf, $this->applyOffset($workArea, $outL[$a]));
1342 }
1343 }
1344 }
1345 }
1346
1347 /**
1348 * Creates some offset values in an array used to simulate a circularly applied outline around TEXT
1349 *
1350 * access private
1351 *
1352 * @param int $distance Distance
1353 * @param int $iterations Iterations.
1354 * @return array
1355 * @see makeOutline()
1356 */
1357 public function circleOffset($distance, $iterations)
1358 {
1359 $res = [];
1360 if ($distance && $iterations) {
1361 for ($a = 0; $a < $iterations; $a++) {
1362 $yOff = round(sin(2 * M_PI / $iterations * ($a + 1)) * 100 * $distance);
1363 if ($yOff) {
1364 $yOff = (int)(ceil(abs($yOff / 100)) * ($yOff / abs($yOff)));
1365 }
1366 $xOff = round(cos(2 * M_PI / $iterations * ($a + 1)) * 100 * $distance);
1367 if ($xOff) {
1368 $xOff = (int)(ceil(abs($xOff / 100)) * ($xOff / abs($xOff)));
1369 }
1370 $res[$a] = [$xOff, $yOff];
1371 }
1372 }
1373 return $res;
1374 }
1375
1376 /**
1377 * Implements the "EMBOSS" GIFBUILDER object / property for the TEXT object
1378 *
1379 * @param resource $im GDlib image pointer
1380 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1381 * @param array $workArea The current working area coordinates.
1382 * @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
1383 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1384 * @see makeShadow()
1385 */
1386 public function makeEmboss(&$im, $conf, $workArea, $txtConf)
1387 {
1388 $conf['color'] = $conf['highColor'];
1389 $this->makeShadow($im, $conf, $workArea, $txtConf);
1390 $newOffset = GeneralUtility::intExplode(',', $conf['offset']);
1391 $newOffset[0] *= -1;
1392 $newOffset[1] *= -1;
1393 $conf['offset'] = implode(',', $newOffset);
1394 $conf['color'] = $conf['lowColor'];
1395 $this->makeShadow($im, $conf, $workArea, $txtConf);
1396 }
1397
1398 /**
1399 * Implements the "SHADOW" GIFBUILDER object / property for the TEXT object
1400 * The operation involves ImageMagick for combining.
1401 *
1402 * @param resource $im GDlib image pointer
1403 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1404 * @param array $workArea The current working area coordinates.
1405 * @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
1406 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1407 * @see makeText()
1408 * @see makeEmboss()
1409 */
1410 public function makeShadow(&$im, $conf, $workArea, $txtConf)
1411 {
1412 $workArea = $this->applyOffset($workArea, GeneralUtility::intExplode(',', $conf['offset']));
1413 $blurRate = MathUtility::forceIntegerInRange((int)$conf['blur'], 0, 99);
1414 // No effects if ImageMagick ver. 5+
1415 if (!$blurRate || !$this->processorEffectsEnabled) {
1416 $txtConf['fontColor'] = $conf['color'];
1417 $this->makeText($im, $txtConf, $workArea);
1418 } else {
1419 $w = imagesx($im);
1420 $h = imagesy($im);
1421 // Area around the blur used for cropping something
1422 $blurBorder = 3;
1423 $tmpStr = $this->randomName();
1424 $fileMenu = $tmpStr . '_menu.' . $this->gifExtension;
1425 $fileColor = $tmpStr . '_color.' . $this->gifExtension;
1426 $fileMask = $tmpStr . '_mask.' . $this->gifExtension;
1427 // BlurColor Image laves
1428 $blurColImg = imagecreatetruecolor($w, $h);
1429 $bcols = $this->convertColor($conf['color']);
1430 $Bcolor = imagecolorallocate($blurColImg, $bcols[0], $bcols[1], $bcols[2]);
1431 imagefilledrectangle($blurColImg, 0, 0, $w, $h, $Bcolor);
1432 $this->ImageWrite($blurColImg, $fileColor);
1433 imagedestroy($blurColImg);
1434 // The mask is made: BlurTextImage
1435 $blurTextImg = imagecreatetruecolor($w + $blurBorder * 2, $h + $blurBorder * 2);
1436 // Black background
1437 $Bcolor = imagecolorallocate($blurTextImg, 0, 0, 0);
1438 imagefilledrectangle($blurTextImg, 0, 0, $w + $blurBorder * 2, $h + $blurBorder * 2, $Bcolor);
1439 $txtConf['fontColor'] = 'white';
1440 $blurBordArr = [$blurBorder, $blurBorder];
1441 $this->makeText($blurTextImg, $txtConf, $this->applyOffset($workArea, $blurBordArr));
1442 // Dump to temporary file
1443 $this->ImageWrite($blurTextImg, $fileMask);
1444 // Destroy
1445 imagedestroy($blurTextImg);
1446 $command = $this->v5_blur($blurRate + 1);
1447 $this->imageMagickExec($fileMask, $fileMask, $command . ' +matte');
1448 // The mask is loaded again
1449 $blurTextImg_tmp = $this->imageCreateFromFile($fileMask);
1450 // If nothing went wrong we continue with the blurred mask
1451 if ($blurTextImg_tmp) {
1452 // Cropping the border from the mask
1453 $blurTextImg = imagecreatetruecolor($w, $h);
1454 $this->imagecopyresized($blurTextImg, $blurTextImg_tmp, 0, 0, $blurBorder, $blurBorder, $w, $h, $w, $h);
1455 // Destroy the temporary mask
1456 imagedestroy($blurTextImg_tmp);
1457 // Adjust the mask
1458 $intensity = 40;
1459 if ($conf['intensity'] ?? false) {
1460 $intensity = MathUtility::forceIntegerInRange($conf['intensity'], 0, 100);
1461 }
1462 $intensity = (int)ceil(255 - $intensity / 100 * 255);
1463 $this->inputLevels($blurTextImg, 0, $intensity);
1464 $opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 0, 100);
1465 if ($opacity && $opacity < 100) {
1466 $high = (int)ceil(255 * $opacity / 100);
1467 // Reducing levels as the opacity demands
1468 $this->outputLevels($blurTextImg, 0, $high);
1469 }
1470 // Dump the mask again
1471 $this->ImageWrite($blurTextImg, $fileMask);
1472 // Destroy the mask
1473 imagedestroy($blurTextImg);
1474 // The pictures are combined
1475 // The main pictures is saved temporarily
1476 $this->ImageWrite($im, $fileMenu);
1477 $this->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
1478 // The main image is loaded again...
1479 $backIm = $this->imageCreateFromFile($fileMenu);
1480 // ... and if nothing went wrong we load it onto the old one.
1481 if ($backIm) {
1482 if (!$this->saveAlphaLayer) {
1483 imagecolortransparent($backIm, -1);
1484 }
1485 $im = $backIm;
1486 }
1487 }
1488 // Deleting temporary files;
1489 unlink($fileMenu);
1490 unlink($fileColor);
1491 unlink($fileMask);
1492 }
1493 }
1494
1495 /****************************
1496 *
1497 * Other GIFBUILDER objects
1498 *
1499 ****************************/
1500 /**
1501 * Implements the "BOX" GIFBUILDER object
1502 *
1503 * @param resource $im GDlib image pointer
1504 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1505 * @param array $workArea The current working area coordinates.
1506 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1507 */
1508 public function makeBox(&$im, $conf, $workArea)
1509 {
1510 $cords = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
1511 $conf['offset'] = $cords[0] . ',' . $cords[1];
1512 $cords = $this->objPosition($conf, $workArea, [$cords[2], $cords[3]]);
1513 $cols = $this->convertColor($conf['color'] ?? '');
1514 $opacity = 0;
1515 if (isset($conf['opacity'])) {
1516 // conversion:
1517 // PHP 0 = opaque, 127 = transparent
1518 // TYPO3 100 = opaque, 0 = transparent
1519 $opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 1, 100, 1);
1520 $opacity = (int)abs($opacity - 100);
1521 $opacity = (int)round(127 * $opacity / 100);
1522 }
1523 $tmpColor = imagecolorallocatealpha($im, $cols[0], $cols[1], $cols[2], $opacity);
1524 imagefilledrectangle($im, $cords[0], $cords[1], $cords[0] + $cords[2] - 1, $cords[1] + $cords[3] - 1, $tmpColor);
1525 }
1526
1527 /**
1528 * Implements the "Ellipse" GIFBUILDER object
1529 * Example Typoscript:
1530 * file = GIFBUILDER
1531 * file {
1532 * XY = 200,200
1533 * format = jpg
1534 * quality = 100
1535 * 10 = ELLIPSE
1536 * 10.dimensions = 100,100,50,50
1537 * 10.color = red
1538 *
1539 * $workArea = X,Y
1540 * $conf['dimensions'] = offset x, offset y, width of ellipse, height of ellipse
1541 *
1542 * @param resource $im GDlib image pointer
1543 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1544 * @param array $workArea The current working area coordinates.
1545 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1546 */
1547 public function makeEllipse(&$im, array $conf, array $workArea)
1548 {
1549 $ellipseConfiguration = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
1550 // Ellipse offset inside workArea (x/y)
1551 $conf['offset'] = $ellipseConfiguration[0] . ',' . $ellipseConfiguration[1];
1552 // @see objPosition
1553 $imageCoordinates = $this->objPosition($conf, $workArea, [$ellipseConfiguration[2], $ellipseConfiguration[3]]);
1554 $color = $this->convertColor($conf['color'] ?? '');
1555 $fillingColor = imagecolorallocate($im, $color[0], $color[1], $color[2]);
1556 imagefilledellipse($im, $imageCoordinates[0], $imageCoordinates[1], $imageCoordinates[2], $imageCoordinates[3], $fillingColor);
1557 }
1558
1559 /**
1560 * Implements the "EFFECT" GIFBUILDER object
1561 * The operation involves ImageMagick for applying effects
1562 *
1563 * @param resource $im GDlib image pointer
1564 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1565 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1566 * @see applyImageMagickToPHPGif()
1567 */
1568 public function makeEffect(&$im, $conf)
1569 {
1570 $commands = $this->IMparams($conf['value']);
1571 if ($commands) {
1572 $this->applyImageMagickToPHPGif($im, $commands);
1573 }
1574 }
1575
1576 /**
1577 * Creating ImageMagick parameters from TypoScript property
1578 *
1579 * @param string $setup A string with effect keywords=value pairs separated by "|
1580 * @return string ImageMagick prepared parameters.
1581 * @internal
1582 * @see makeEffect()
1583 */
1584 public function IMparams($setup)
1585 {
1586 if (!trim($setup)) {
1587 return '';
1588 }
1589 $effects = explode('|', $setup);
1590 $commands = '';
1591 foreach ($effects as $val) {
1592 $pairs = explode('=', $val, 2);
1593 $value = trim($pairs[1] ?? '');
1594 $effect = strtolower(trim($pairs[0]));
1595 switch ($effect) {
1596 case 'gamma':
1597 $commands .= ' -gamma ' . (float)$value;
1598 break;
1599 case 'blur':
1600 if ($this->processorEffectsEnabled) {
1601 $commands .= $this->v5_blur((int)$value);
1602 }
1603 break;
1604 case 'sharpen':
1605 if ($this->processorEffectsEnabled) {
1606 $commands .= $this->v5_sharpen((int)$value);
1607 }
1608 break;
1609 case 'rotate':
1610 $commands .= ' -rotate ' . MathUtility::forceIntegerInRange((int)$value, 0, 360);
1611 break;
1612 case 'solarize':
1613 $commands .= ' -solarize ' . MathUtility::forceIntegerInRange((int)$value, 0, 99);
1614 break;
1615 case 'swirl':
1616 $commands .= ' -swirl ' . MathUtility::forceIntegerInRange((int)$value, 0, 1000);
1617 break;
1618 case 'wave':
1619 $params = GeneralUtility::intExplode(',', $value);
1620 $commands .= ' -wave ' . MathUtility::forceIntegerInRange($params[0], 0, 99) . 'x' . MathUtility::forceIntegerInRange($params[1], 0, 99);
1621 break;
1622 case 'charcoal':
1623 $commands .= ' -charcoal ' . MathUtility::forceIntegerInRange((int)$value, 0, 100);
1624 break;
1625 case 'gray':
1626 $commands .= ' -colorspace GRAY';
1627 break;
1628 case 'edge':
1629 $commands .= ' -edge ' . MathUtility::forceIntegerInRange((int)$value, 0, 99);
1630 break;
1631 case 'emboss':
1632 $commands .= ' -emboss';
1633 break;
1634 case 'flip':
1635 $commands .= ' -flip';
1636 break;
1637 case 'flop':
1638 $commands .= ' -flop';
1639 break;
1640 case 'colors':
1641 $commands .= ' -colors ' . MathUtility::forceIntegerInRange((int)$value, 2, 255);
1642 break;
1643 case 'shear':
1644 $commands .= ' -shear ' . MathUtility::forceIntegerInRange((int)$value, -90, 90);
1645 break;
1646 case 'invert':
1647 $commands .= ' -negate';
1648 break;
1649 }
1650 }
1651 return $commands;
1652 }
1653
1654 /**
1655 * Implements the "ADJUST" GIFBUILDER object
1656 *
1657 * @param resource $im GDlib image pointer
1658 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1659 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1660 * @see autoLevels()
1661 * @see outputLevels()
1662 * @see inputLevels()
1663 */
1664 public function adjust(&$im, $conf)
1665 {
1666 $setup = $conf['value'];
1667 if (!trim($setup)) {
1668 return;
1669 }
1670 $effects = explode('|', $setup);
1671 foreach ($effects as $val) {
1672 $pairs = explode('=', $val, 2);
1673 $value = trim($pairs[1]);
1674 $effect = strtolower(trim($pairs[0]));
1675 switch ($effect) {
1676 case 'inputlevels':
1677 // low,high
1678 $params = GeneralUtility::intExplode(',', $value);
1679 $this->inputLevels($im, $params[0], $params[1]);
1680 break;
1681 case 'outputlevels':
1682 $params = GeneralUtility::intExplode(',', $value);
1683 $this->outputLevels($im, $params[0], $params[1]);
1684 break;
1685 case 'autolevels':
1686 $this->autolevels($im);
1687 break;
1688 }
1689 }
1690 }
1691
1692 /**
1693 * Implements the "CROP" GIFBUILDER object
1694 *
1695 * @param resource $im GDlib image pointer
1696 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1697 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1698 */
1699 public function crop(&$im, $conf)
1700 {
1701 // Clears workArea to total image
1702 $this->setWorkArea('');
1703 $cords = GeneralUtility::intExplode(',', $conf['crop'] . ',,,');
1704 $conf['offset'] = $cords[0] . ',' . $cords[1];
1705 $cords = $this->objPosition($conf, $this->workArea, [$cords[2], $cords[3]]);
1706 $newIm = imagecreatetruecolor($cords[2], $cords[3]);
1707 $cols = $this->convertColor(!empty($conf['backColor']) ? $conf['backColor'] : $this->setup['backColor']);
1708 $Bcolor = imagecolorallocate($newIm, $cols[0], $cols[1], $cols[2]);
1709 imagefilledrectangle($newIm, 0, 0, $cords[2], $cords[3], $Bcolor);
1710 $newConf = [];
1711 $workArea = [0, 0, $cords[2], $cords[3]];
1712 if ($cords[0] < 0) {
1713 $workArea[0] = abs($cords[0]);
1714 } else {
1715 $newConf['offset'] = -$cords[0];
1716 }
1717 if ($cords[1] < 0) {
1718 $workArea[1] = abs($cords[1]);
1719 } else {
1720 $newConf['offset'] .= ',' . -$cords[1];
1721 }
1722 $this->copyGifOntoGif($newIm, $im, $newConf, $workArea);
1723 $im = $newIm;
1724 $this->w = imagesx($im);
1725 $this->h = imagesy($im);
1726 // Clears workArea to total image
1727 $this->setWorkArea('');
1728 }
1729
1730 /**
1731 * Implements the "SCALE" GIFBUILDER object
1732 *
1733 * @param resource $im GDlib image pointer
1734 * @param array $conf TypoScript array with configuration for the GIFBUILDER object.
1735 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1736 */
1737 public function scale(&$im, $conf)
1738 {
1739 if ($conf['width'] || $conf['height'] || $conf['params']) {
1740 $tmpStr = $this->randomName();
1741 $theFile = $tmpStr . '.' . $this->gifExtension;
1742 $this->ImageWrite($im, $theFile);
1743 $theNewFile = $this->imageMagickConvert($theFile, $this->gifExtension, $conf['width'] ?? '', $conf['height'] ?? '', $conf['params'] ?? '');
1744 $tmpImg = $this->imageCreateFromFile($theNewFile[3]);
1745 if ($tmpImg) {
1746 imagedestroy($im);
1747 $im = $tmpImg;
1748 $this->w = imagesx($im);
1749 $this->h = imagesy($im);
1750 // Clears workArea to total image
1751 $this->setWorkArea('');
1752 }
1753 unlink($theFile);
1754 if ($theNewFile[3] && $theNewFile[3] != $theFile) {
1755 unlink($theNewFile[3]);
1756 }
1757 }
1758 }
1759
1760 /**
1761 * Implements the "WORKAREA" GIFBUILDER object when setting it
1762 * Setting internal working area boundaries (->workArea)
1763 *
1764 * @param string $workArea Working area dimensions, comma separated
1765 * @internal
1766 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::make()
1767 */
1768 public function setWorkArea($workArea)
1769 {
1770 $this->workArea = GeneralUtility::intExplode(',', $workArea);
1771 $this->workArea = $this->applyOffset($this->workArea, $this->OFFSET);
1772 if (!($this->workArea[2] ?? false)) {
1773 $this->workArea[2] = $this->w;
1774 }
1775 if (!($this->workArea[3] ?? false)) {
1776 $this->workArea[3] = $this->h;
1777 }
1778 }
1779
1780 /*************************
1781 *
1782 * Adjustment functions
1783 *
1784 ************************/
1785 /**
1786 * Apply auto-levels to input image pointer
1787 *
1788 * @param resource $im GDlib Image Pointer
1789 */
1790 public function autolevels(&$im)
1791 {
1792 $totalCols = imagecolorstotal($im);
1793 $grayArr = [];
1794 for ($c = 0; $c < $totalCols; $c++) {
1795 $cols = imagecolorsforindex($im, $c);
1796 $grayArr[] = round(($cols['red'] + $cols['green'] + $cols['blue']) / 3);
1797 }
1798 $min = min($grayArr);
1799 $max = max($grayArr);
1800 $delta = $max - $min;
1801 if ($delta) {
1802 for ($c = 0; $c < $totalCols; $c++) {
1803 $cols = imagecolorsforindex($im, $c);
1804 $cols['red'] = floor(($cols['red'] - $min) / $delta * 255);
1805 $cols['green'] = floor(($cols['green'] - $min) / $delta * 255);
1806 $cols['blue'] = floor(($cols['blue'] - $min) / $delta * 255);
1807 imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1808 }
1809 }
1810 }
1811
1812 /**
1813 * Apply output levels to input image pointer (decreasing contrast)
1814 *
1815 * @param resource $im GDlib Image Pointer
1816 * @param int $low The "low" value (close to 0)
1817 * @param int $high The "high" value (close to 255)
1818 * @param bool $swap If swap, then low and high are swapped. (Useful for negated masks...)
1819 */
1820 public function outputLevels(&$im, $low, $high, $swap = false)
1821 {
1822 if ($low < $high) {
1823 $low = MathUtility::forceIntegerInRange($low, 0, 255);
1824 $high = MathUtility::forceIntegerInRange($high, 0, 255);
1825 if ($swap) {
1826 $temp = $low;
1827 $low = 255 - $high;
1828 $high = 255 - $temp;
1829 }
1830 $delta = $high - $low;
1831 $totalCols = imagecolorstotal($im);
1832 for ($c = 0; $c < $totalCols; $c++) {
1833 $cols = imagecolorsforindex($im, $c);
1834 $cols['red'] = $low + floor($cols['red'] / 255 * $delta);
1835 $cols['green'] = $low + floor($cols['green'] / 255 * $delta);
1836 $cols['blue'] = $low + floor($cols['blue'] / 255 * $delta);
1837 imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1838 }
1839 }
1840 }
1841
1842 /**
1843 * Apply input levels to input image pointer (increasing contrast)
1844 *
1845 * @param resource $im GDlib Image Pointer
1846 * @param int $low The "low" value (close to 0)
1847 * @param int $high The "high" value (close to 255)
1848 */
1849 public function inputLevels(&$im, $low, $high)
1850 {
1851 if ($low < $high) {
1852 $low = MathUtility::forceIntegerInRange($low, 0, 255);
1853 $high = MathUtility::forceIntegerInRange($high, 0, 255);
1854 $delta = $high - $low;
1855 $totalCols = imagecolorstotal($im);
1856 for ($c = 0; $c < $totalCols; $c++) {
1857 $cols = imagecolorsforindex($im, $c);
1858 $cols['red'] = MathUtility::forceIntegerInRange((int)(($cols['red'] - $low) / $delta * 255), 0, 255);
1859 $cols['green'] = MathUtility::forceIntegerInRange((int)(($cols['green'] - $low) / $delta * 255), 0, 255);
1860 $cols['blue'] = MathUtility::forceIntegerInRange((int)(($cols['blue'] - $low) / $delta * 255), 0, 255);
1861 imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1862 }
1863 }
1864 }
1865
1866 /**
1867 * Reduce colors in image using IM and create a palette based image if possible (<=256 colors)
1868 *
1869 * @param string $file Image file to reduce
1870 * @param int $cols Number of colors to reduce the image to.
1871 * @return string Reduced file
1872 */
1873 public function IMreduceColors($file, $cols)
1874 {
1875 $fI = GeneralUtility::split_fileref($file);
1876 $ext = strtolower($fI['fileext']);
1877 $result = $this->randomName() . '.' . $ext;
1878 $reduce = MathUtility::forceIntegerInRange($cols, 0, $ext === 'gif' ? 256 : $this->truecolorColors, 0);
1879 if ($reduce > 0) {
1880 $params = ' -colors ' . $reduce;
1881 if ($reduce <= 256) {
1882 $params .= ' -type Palette';
1883 }
1884 $prefix = $ext === 'png' && $reduce <= 256 ? 'png8:' : '';
1885 $this->imageMagickExec($file, $prefix . $result, $params);
1886 if ($result) {
1887 return $result;
1888 }
1889 }
1890 return '';
1891 }
1892
1893 /*********************************
1894 *
1895 * GIFBUILDER Helper functions
1896 *
1897 *********************************/
1898 /**
1899 * Returns the IM command for sharpening with ImageMagick 5
1900 * Uses $this->im5fx_sharpenSteps for translation of the factor to an actual command.
1901 *
1902 * @param int $factor The sharpening factor, 0-100 (effectively in 10 steps)
1903 * @return string The sharpening command, eg. " -sharpen 3x4
1904 * @see makeText()
1905 * @see IMparams()
1906 * @see v5_blur()
1907 */
1908 public function v5_sharpen($factor)
1909 {
1910 $factor = MathUtility::forceIntegerInRange((int)ceil($factor / 10), 0, 10);
1911 $sharpenArr = explode(',', ',' . $this->im5fx_sharpenSteps);
1912 $sharpenF = trim($sharpenArr[$factor]);
1913 if ($sharpenF) {
1914 return ' -sharpen ' . $sharpenF;
1915 }
1916 return '';
1917 }
1918
1919 /**
1920 * Returns the IM command for blurring with ImageMagick 5.
1921 * Uses $this->im5fx_blurSteps for translation of the factor to an actual command.
1922 *
1923 * @param int $factor The blurring factor, 0-100 (effectively in 10 steps)
1924 * @return string The blurring command, eg. " -blur 3x4
1925 * @see makeText()
1926 * @see IMparams()
1927 * @see v5_sharpen()
1928 */
1929 public function v5_blur($factor)
1930 {
1931 $factor = MathUtility::forceIntegerInRange((int)ceil($factor / 10), 0, 10);
1932 $blurArr = explode(',', ',' . $this->im5fx_blurSteps);
1933 $blurF = trim($blurArr[$factor]);
1934 if ($blurF) {
1935 return ' -blur ' . $blurF;
1936 }
1937 return '';
1938 }
1939
1940 /**
1941 * Returns a random filename prefixed with "temp_" and then 32 char md5 hash (without extension).
1942 * Used by functions in this class to create truly temporary files for the on-the-fly processing. These files will most likely be deleted right away.
1943 *
1944 * @return string
1945 */
1946 public function randomName()
1947 {
1948 GeneralUtility::mkdir_deep(Environment::getVarPath() . '/transient/');
1949 return Environment::getVarPath() . '/transient/' . md5(StringUtility::getUniqueId());
1950 }
1951
1952 /**
1953 * Applies offset value to coordinated in $cords.
1954 * Basically the value of key 0/1 of $OFFSET is added to keys 0/1 of $cords
1955 *
1956 * @param array $cords Integer coordinates in key 0/1
1957 * @param array $OFFSET Offset values in key 0/1
1958 * @return array Modified $cords array
1959 */
1960 public function applyOffset($cords, $OFFSET)
1961 {
1962 $cords[0] = (int)$cords[0] + (int)$OFFSET[0];
1963 $cords[1] = (int)($cords[1] ?? 0) + (int)($OFFSET[1] ?? 0);
1964 return $cords;
1965 }
1966
1967 /**
1968 * Converts a "HTML-color" TypoScript datatype to RGB-values.
1969 * Default is 0,0,0
1970 *
1971 * @param string $string "HTML-color" data type string, eg. 'red', '#ffeedd' or '255,0,255'. You can also add a modifying operator afterwards. There are two options: "255,0,255 : 20" - will add 20 to values, result is "255,20,255". Or "255,0,255 : *1.23" which will multiply all RGB values with 1.23
1972 * @return array RGB values in key 0/1/2 of the array
1973 */
1974 public function convertColor($string)
1975 {
1976 $col = [];
1977 $cParts = explode(':', $string, 2);
1978 // Finding the RGB definitions of the color:
1979 $string = $cParts[0];
1980 if (str_contains($string, '#')) {
1981 $string = preg_replace('/[^A-Fa-f0-9]*/', '', $string) ?? '';
1982 $col[] = hexdec(substr($string, 0, 2));
1983 $col[] = hexdec(substr($string, 2, 2));
1984 $col[] = hexdec(substr($string, 4, 2));
1985 } elseif (str_contains($string, ',')) {
1986 $string = preg_replace('/[^,0-9]*/', '', $string) ?? '';
1987 $strArr = explode(',', $string);
1988 $col[] = (int)$strArr[0];
1989 $col[] = (int)$strArr[1];
1990 $col[] = (int)$strArr[2];
1991 } else {
1992 $string = strtolower(trim($string));
1993 if ($this->colMap[$string] ?? false) {
1994 $col = $this->colMap[$string];
1995 } else {
1996 $col = [0, 0, 0];
1997 }
1998 }
1999 // ... and possibly recalculating the value
2000 if (trim($cParts[1] ?? '')) {
2001 $cParts[1] = trim($cParts[1]);
2002 if ($cParts[1][0] === '*') {
2003 $val = (float)substr($cParts[1], 1);
2004 $col[0] = MathUtility::forceIntegerInRange((int)($col[0] * $val), 0, 255);
2005 $col[1] = MathUtility::forceIntegerInRange((int)($col[1] * $val), 0, 255);
2006 $col[2] = MathUtility::forceIntegerInRange((int)($col[2] * $val), 0, 255);
2007 } else {
2008 $val = (int)$cParts[1];
2009 $col[0] = MathUtility::forceIntegerInRange((int)($col[0] + $val), 0, 255);
2010 $col[1] = MathUtility::forceIntegerInRange((int)($col[1] + $val), 0, 255);
2011 $col[2] = MathUtility::forceIntegerInRange((int)($col[2] + $val), 0, 255);
2012 }
2013 }
2014 return $col;
2015 }
2016
2017 /**
2018 * Create an array with object position/boundaries based on input TypoScript configuration (such as the "align" property is used), the work area definition and $BB array
2019 *
2020 * @param array $conf TypoScript configuration for a GIFBUILDER object
2021 * @param array $workArea Workarea definition
2022 * @param array $BB BB (Bounding box) array. Not just used for TEXT objects but also for others
2023 * @return array [0]=x, [1]=y, [2]=w, [3]=h
2024 * @internal
2025 * @see copyGifOntoGif()
2026 * @see makeBox()
2027 * @see crop()
2028 */
2029 public function objPosition($conf, $workArea, $BB)
2030 {
2031 // offset, align, valign, workarea
2032 $result = [];
2033 $result[2] = $BB[0];
2034 $result[3] = $BB[1];
2035 $w = $workArea[2];
2036 $h = $workArea[3];
2037 $align = explode(',', $conf['align'] ?? ',');
2038 $align[0] = strtolower(substr(trim($align[0]), 0, 1));
2039 $align[1] = strtolower(substr(trim($align[1]), 0, 1));
2040 switch ($align[0]) {
2041 case 'r':
2042 $result[0] = $w - $result[2];
2043 break;
2044 case 'c':
2045 $result[0] = round(($w - $result[2]) / 2);
2046 break;
2047 default:
2048 $result[0] = 0;
2049 }
2050 switch ($align[1]) {
2051 case 'b':
2052 // y pos
2053 $result[1] = $h - $result[3];
2054 break;
2055 case 'c':
2056 $result[1] = round(($h - $result[3]) / 2);
2057 break;
2058 default:
2059 $result[1] = 0;
2060 }
2061 $result = $this->applyOffset($result, GeneralUtility::intExplode(',', $conf['offset'] ?? ''));
2062 $result = $this->applyOffset($result, $workArea);
2063 return $result;
2064 }
2065
2066 /***********************************
2067 *
2068 * Scaling, Dimensions of images
2069 *
2070 ***********************************/
2071 /**
2072 * Converts $imagefile to another file in temp-dir of type $newExt (extension).
2073 *
2074 * @param string $imagefile The image filepath
2075 * @param string $newExt New extension, eg. "gif", "png", "jpg", "tif". If $newExt is NOT set, the new imagefile will be of the original format. If newExt = 'WEB' then one of the web-formats is applied.
2076 * @param string $w Width. $w / $h is optional. If only one is given the image is scaled proportionally. If an 'm' exists in the $w or $h and if both are present the $w and $h is regarded as the Maximum w/h and the proportions will be kept
2077 * @param string $h Height. See $w
2078 * @param string $params Additional ImageMagick parameters.
2079 * @param string $frame Refers to which frame-number to select in the image. '' or 0 will select the first frame, 1 will select the next and so on...
2080 * @param array $options An array with options passed to getImageScale (see this function).
2081 * @param bool $mustCreate If set, then another image than the input imagefile MUST be returned. Otherwise you can risk that the input image is good enough regarding measures etc and is of course not rendered to a new, temporary file in typo3temp/. But this option will force it to.
2082 * @return array|null [0]/[1] is w/h, [2] is file extension and [3] is the filename.
2083 * @see getImageScale()
2084 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getImgResource()
2085 * @see maskImageOntoImage()
2086 * @see copyImageOntoImage()
2087 * @see scale()
2088 */
2089 public function imageMagickConvert($imagefile, $newExt = '', $w = '', $h = '', $params = '', $frame = '', $options = [], $mustCreate = false)
2090 {
2091 if (!$this->processorEnabled) {
2092 // Returning file info right away
2093 return $this->getImageDimensions($imagefile);
2094 }
2095 $info = $this->getImageDimensions($imagefile);
2096 if (!$info) {
2097 return null;
2098 }
2099
2100 $newExt = strtolower(trim($newExt));
2101 // If no extension is given the original extension is used
2102 if (!$newExt) {
2103 $newExt = $info[2];
2104 }
2105 if ($newExt === 'web') {
2106 if (in_array($info[2], $this->webImageExt, true)) {
2107 $newExt = $info[2];
2108 } else {
2109 $newExt = $this->gif_or_jpg($info[2], $info[0], $info[1]);
2110 if (!$params) {
2111 $params = $this->cmds[$newExt];
2112 }
2113 }
2114 }
2115 if (!in_array($newExt, $this->imageFileExt, true)) {
2116 return null;
2117 }
2118
2119 $data = $this->getImageScale($info, $w, $h, $options);
2120 $w = $data['origW'];
2121 $h = $data['origH'];
2122 // If no conversion should be performed
2123 // this flag is TRUE if the width / height does NOT dictate
2124 // the image to be scaled!! (that is if no width / height is
2125 // given or if the destination w/h matches the original image
2126 // dimensions or if the option to not scale the image is set)
2127 $noScale = !$w && !$h || $data[0] == $info[0] && $data[1] == $info[1] || !empty($options['noScale']);
2128 if ($noScale && !$data['crs'] && !$params && !$frame && $newExt == $info[2] && !$mustCreate) {
2129 // Set the new width and height before returning,
2130 // if the noScale option is set
2131 if (!empty($options['noScale'])) {
2132 $info[0] = $data[0];
2133 $info[1] = $data[1];
2134 }
2135 $info[3] = $imagefile;
2136 return $info;
2137 }
2138 $info[0] = $data[0];
2139 $info[1] = $data[1];
2140 $frame = $this->addFrameSelection ? (int)$frame : 0;
2141 if (!$params) {
2142 $params = $this->cmds[$newExt] ?? '';
2143 }
2144 // Cropscaling:
2145 if ($data['crs']) {
2146 if (!$data['origW']) {
2147 $data['origW'] = $data[0];
2148 }
2149 if (!$data['origH']) {
2150 $data['origH'] = $data[1];
2151 }
2152 $offsetX = (int)(($data[0] - $data['origW']) * ($data['cropH'] + 100) / 200);
2153 $offsetY = (int)(($data[1] - $data['origH']) * ($data['cropV'] + 100) / 200);
2154 $params .= ' -crop ' . $data['origW'] . 'x' . $data['origH'] . '+' . $offsetX . '+' . $offsetY . '! +repage';
2155 }
2156 $command = $this->scalecmd . ' ' . $info[0] . 'x' . $info[1] . '! ' . $params . ' ';
2157 // re-apply colorspace-setting for the resulting image so colors don't appear to dark (sRGB instead of RGB)
2158 $command .= ' -colorspace ' . $this->colorspace;
2159 $cropscale = $data['crs'] ? 'crs-V' . $data['cropV'] . 'H' . $data['cropH'] : '';
2160 if ($this->alternativeOutputKey) {
2161 $theOutputName = md5($command . $cropscale . PathUtility::basename($imagefile) . $this->alternativeOutputKey . '[' . $frame . ']');
2162 } else {
2163 $theOutputName = md5($command . $cropscale . $imagefile . filemtime($imagefile) . '[' . $frame . ']');
2164 }
2165 if ($this->imageMagickConvert_forceFileNameBody) {
2166 $theOutputName = $this->imageMagickConvert_forceFileNameBody;
2167 $this->imageMagickConvert_forceFileNameBody = '';
2168 }
2169 // Making the temporary filename
2170 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/typo3temp/assets/images/');
2171 $output = Environment::getPublicPath() . '/typo3temp/assets/images/' . $this->filenamePrefix . $theOutputName . '.' . $newExt;
2172 if ($this->dontCheckForExistingTempFile || !file_exists($output)) {
2173 $this->imageMagickExec($imagefile, $output, $command, $frame);
2174 }
2175 if (file_exists($output)) {
2176 $info[3] = $output;
2177 $info[2] = $newExt;
2178 // params might change some image data!
2179 if ($params) {
2180 $info = $this->getImageDimensions($info[3]);
2181 }
2182 if ($info[2] == $this->gifExtension && !$this->dontCompress) {
2183 // Compress with IM (lzw) or GD (rle) (Workaround for the absence of lzw-compression in GD)
2184 self::gifCompress($info[3], '');
2185 }
2186 return $info;
2187 }
2188 return null;
2189 }
2190
2191 /**
2192 * Gets the input image dimensions.
2193 *
2194 * @param string $imageFile The image filepath
2195 * @return array|null Returns an array where [0]/[1] is w/h, [2] is extension and [3] is the filename.
2196 * @see imageMagickConvert()
2197 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getImgResource()
2198 */
2199 public function getImageDimensions($imageFile)
2200 {
2201 $returnArr = null;
2202 preg_match('/([^\\.]*)$/', $imageFile, $reg);
2203 if (file_exists($imageFile) && in_array(strtolower($reg[0]), $this->imageFileExt, true)) {
2204 $returnArr = $this->getCachedImageDimensions($imageFile);
2205 if (!$returnArr) {
2206 $imageInfoObject = GeneralUtility::makeInstance(ImageInfo::class, $imageFile);
2207 if ($imageInfoObject->getWidth()) {
2208 $returnArr = [
2209 $imageInfoObject->getWidth(),
2210 $imageInfoObject->getHeight(),
2211 strtolower($reg[0]),
2212 $imageFile,
2213 ];
2214 $this->cacheImageDimensions($returnArr);
2215 }
2216 }
2217 }
2218 return $returnArr;
2219 }
2220
2221 /**
2222 * Caches the result of the getImageDimensions function into the database. Does not check if the file exists.
2223 *
2224 * @param array $identifyResult Result of the getImageDimensions function
2225 *
2226 * @return bool always TRUE
2227 */
2228 public function cacheImageDimensions(array $identifyResult)
2229 {
2230 $filePath = $identifyResult[3];
2231 $statusHash = $this->generateStatusHashForImageFile($filePath);
2232 $identifier = $this->generateCacheKeyForImageFile($filePath);
2233
2234 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache */
2235 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('imagesizes');
2236 $imageDimensions = [
2237 'hash' => $statusHash,
2238 'imagewidth' => $identifyResult[0],
2239 'imageheight' => $identifyResult[1],
2240 ];
2241 $cache->set($identifier, $imageDimensions);
2242
2243 return true;
2244 }
2245
2246 /**
2247 * Fetches the cached image dimensions from the cache. Does not check if the image file exists.
2248 *
2249 * @param string $filePath Image file path, relative to public web path
2250 *
2251 * @return array|bool an array where [0]/[1] is w/h, [2] is extension and [3] is the file name,
2252 * or FALSE for a cache miss
2253 */
2254 public function getCachedImageDimensions($filePath)
2255 {
2256 $statusHash = $this->generateStatusHashForImageFile($filePath);
2257 $identifier = $this->generateCacheKeyForImageFile($filePath);
2258 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache */
2259 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('imagesizes');
2260 $cachedImageDimensions = $cache->get($identifier);
2261 if (!isset($cachedImageDimensions['hash'])) {
2262 return false;
2263 }
2264
2265 if ($cachedImageDimensions['hash'] !== $statusHash) {
2266 // The file has changed. Delete the cache entry.
2267 $cache->remove($identifier);
2268 $result = false;
2269 } else {
2270 preg_match('/([^\\.]*)$/', $filePath, $imageExtension);
2271 $result = [
2272 (int)$cachedImageDimensions['imagewidth'],
2273 (int)$cachedImageDimensions['imageheight'],
2274 strtolower($imageExtension[0]),
2275 $filePath,
2276 ];
2277 }
2278
2279 return $result;
2280 }
2281
2282 /**
2283 * Creates the key for the image dimensions cache for an image file.
2284 *
2285 * This method does not check if the image file actually exists.
2286 *
2287 * @param string $filePath Image file path, relative to public web path
2288 *
2289 * @return string the hash key (an SHA1 hash), will not be empty
2290 */
2291 protected function generateCacheKeyForImageFile($filePath)
2292 {
2293 return sha1($filePath);
2294 }
2295
2296 /**
2297 * Creates the status hash to check whether a file has been changed.
2298 *
2299 * @param string $filePath Image file path, relative to public web path
2300 *
2301 * @return string the status hash (an SHA1 hash)
2302 */
2303 protected function generateStatusHashForImageFile($filePath)
2304 {
2305 $fileStatus = stat($filePath);
2306
2307 return sha1($fileStatus['mtime'] . $fileStatus['size']);
2308 }
2309
2310 /**
2311 * Get numbers for scaling the image based on input
2312 *
2313 * @param array $info Current image information: Width, Height etc.
2314 * @param string $w "required" width
2315 * @param string $h "required" height
2316 * @param array $options Options: Keys are like "maxW", "maxH", "minW", "minH
2317 * @return array
2318 * @internal
2319 * @see imageMagickConvert()
2320 */
2321 public function getImageScale($info, $w, $h, $options)
2322 {
2323 $out = [];
2324 $max = str_contains($w . $h, 'm') ? 1 : 0;
2325 if (str_contains($w . $h, 'c')) {
2326 $out['cropH'] = (int)substr((string)strstr($w, 'c'), 1);
2327 $out['cropV'] = (int)substr((string)strstr($h, 'c'), 1);
2328 $crs = true;
2329 } else {
2330 $crs = false;
2331 }
2332 $out['crs'] = $crs;
2333 $w = (int)$w;
2334 $h = (int)$h;
2335 // If there are max-values...
2336 if (!empty($options['maxW'])) {
2337 // If width is given...
2338 if ($w) {
2339 if ($w > $options['maxW']) {
2340 $w = $options['maxW'];
2341 // Height should follow
2342 $max = 1;
2343 }
2344 } else {
2345 if ($info[0] > $options['maxW']) {
2346 $w = $options['maxW'];
2347 // Height should follow
2348 $max = 1;
2349 }
2350 }
2351 }
2352 if (!empty($options['maxH'])) {
2353 // If height is given...
2354 if ($h) {
2355 if ($h > $options['maxH']) {
2356 $h = $options['maxH'];
2357 // Height should follow
2358 $max = 1;
2359 }
2360 } else {
2361 // Changed [0] to [1] 290801
2362 if ($info[1] > $options['maxH']) {
2363 $h = $options['maxH'];
2364 // Height should follow
2365 $max = 1;
2366 }
2367 }
2368 }
2369 $out['origW'] = $w;
2370 $out['origH'] = $h;
2371 $out['max'] = $max;
2372 if (!$this->mayScaleUp) {
2373 if ($w > $info[0]) {
2374 $w = $info[0];
2375 }
2376 if ($h > $info[1]) {
2377 $h = $info[1];
2378 }
2379 }
2380 // If scaling should be performed. Check that input "info" array will not cause division-by-zero
2381 if (($w || $h) && $info[0] && $info[1]) {
2382 if ($w && !$h) {
2383 $info[1] = ceil($info[1] * ($w / $info[0]));
2384 $info[0] = $w;
2385 }
2386 if (!$w && $h) {
2387 $info[0] = ceil($info[0] * ($h / $info[1]));
2388 $info[1] = $h;
2389 }
2390 if ($w && $h) {
2391 if ($max) {
2392 $ratio = $info[0] / $info[1];
2393 if ($h * $ratio > $w) {
2394 $h = round($w / $ratio);
2395 } else {
2396 $w = round($h * $ratio);
2397 }
2398 }
2399 if ($crs) {
2400 $ratio = $info[0] / $info[1];
2401 if ($h * $ratio < $w) {
2402 $h = round($w / $ratio);
2403 } else {
2404 $w = round($h * $ratio);
2405 }
2406 }
2407 $info[0] = $w;
2408 $info[1] = $h;
2409 }
2410 }
2411 $out[0] = $info[0];
2412 $out[1] = $info[1];
2413 // Set minimum-measures!
2414 if (isset($options['minW']) && $out[0] < $options['minW']) {
2415 if (($max || $crs) && $out[0]) {
2416 $out[1] = round($out[1] * $options['minW'] / $out[0]);
2417 }
2418 $out[0] = $options['minW'];
2419 }
2420 if (isset($options['minH']) && $out[1] < $options['minH']) {
2421 if (($max || $crs) && $out[1]) {
2422 $out[0] = round($out[0] * $options['minH'] / $out[1]);
2423 }
2424 $out[1] = $options['minH'];
2425 }
2426 return $out;
2427 }
2428
2429 /***********************************
2430 *
2431 * ImageMagick API functions
2432 *
2433 ***********************************/
2434 /**
2435 * Call the identify command
2436 *
2437 * @param string $imagefile The relative to public web path image filepath
2438 * @return array|null Returns an array where [0]/[1] is w/h, [2] is extension, [3] is the filename and [4] the real image type identified by ImageMagick.
2439 */
2440 public function imageMagickIdentify($imagefile)
2441 {
2442 if (!$this->processorEnabled) {
2443 return null;
2444 }
2445
2446 $result = $this->executeIdentifyCommandForImageFile($imagefile);
2447 if ($result) {
2448 [$width, $height, $fileExtension, $fileType] = explode(' ', $result);
2449 if ((int)$width && (int)$height) {
2450 return [$width, $height, strtolower($fileExtension), $imagefile, strtolower($fileType)];
2451 }
2452 }
2453 return null;
2454 }
2455
2456 /**
2457 * Internal function to execute an IM command fetching information on an image
2458 *
2459 * @param string $imageFile the absolute path to the image
2460 * @return string|null the raw result of the identify command.
2461 */
2462 protected function executeIdentifyCommandForImageFile(string $imageFile): ?string
2463 {
2464 $frame = $this->addFrameSelection ? 0 : null;
2465 $cmd = CommandUtility::imageMagickCommand(
2466 'identify',
2467 '-format "%w %h %e %m" ' . ImageMagickFile::fromFilePath($imageFile, $frame)
2468 );
2469 $returnVal = [];
2470 CommandUtility::exec($cmd, $returnVal);
2471 $result = array_pop($returnVal);
2472 $this->IM_commands[] = ['identify', $cmd, $result];
2473 return $result;
2474 }
2475
2476 /**
2477 * Executes an ImageMagick "convert" on two filenames, $input and $output using $params before them.
2478 * Can be used for many things, mostly scaling and effects.
2479 *
2480 * @param string $input The relative to public web path image filepath, input file (read from)
2481 * @param string $output The relative to public web path image filepath, output filename (written to)
2482 * @param string $params ImageMagick parameters
2483 * @param int $frame Optional, refers to which frame-number to select in the image. '' or 0
2484 * @return string The result of a call to PHP function "exec()
2485 */
2486 public function imageMagickExec($input, $output, $params, $frame = 0)
2487 {
2488 if (!$this->processorEnabled) {
2489 return '';
2490 }
2491 // If addFrameSelection is set in the Install Tool, a frame number is added to
2492 // select a specific page of the image (by default this will be the first page)
2493 $frame = $this->addFrameSelection ? (int)$frame : null;
2494 $cmd = CommandUtility::imageMagickCommand(
2495 'convert',
2496 $params
2497 . ' ' . ImageMagickFile::fromFilePath($input, $frame)
2498 . ' ' . CommandUtility::escapeShellArgument($output)
2499 );
2500 $this->IM_commands[] = [$output, $cmd];
2501 $ret = CommandUtility::exec($cmd);
2502 // Change the permissions of the file
2503 GeneralUtility::fixPermissions($output);
2504 return $ret;
2505 }
2506
2507 /**
2508 * Executes an ImageMagick "combine" (or composite in newer times) on four filenames - $input, $overlay and $mask as input files and $output as the output filename (written to)
2509 * Can be used for many things, mostly scaling and effects.
2510 *
2511 * @param string $input The relative to public web path image filepath, bottom file
2512 * @param string $overlay The relative to public web path image filepath, overlay file (top)
2513 * @param string $mask The relative to public web path image filepath, the mask file (grayscale)
2514 * @param string $output The relative to public web path image filepath, output filename (written to)
2515 * @return string
2516 */
2517 public function combineExec($input, $overlay, $mask, $output)
2518 {
2519 if (!$this->processorEnabled) {
2520 return '';
2521 }
2522 $theMask = $this->randomName() . '.' . $this->gifExtension;
2523 // +matte = no alpha layer in output
2524 $this->imageMagickExec($mask, $theMask, '-colorspace GRAY +matte');
2525
2526 $parameters = '-compose over'
2527 . ' -quality ' . $this->jpegQuality
2528 . ' +matte '
2529 . ImageMagickFile::fromFilePath($input) . ' '
2530 . ImageMagickFile::fromFilePath($overlay) . ' '
2531 . ImageMagickFile::fromFilePath($theMask) . ' '
2532 . CommandUtility::escapeShellArgument($output);
2533 $cmd = CommandUtility::imageMagickCommand('combine', $parameters);
2534 $this->IM_commands[] = [$output, $cmd];
2535 $ret = CommandUtility::exec($cmd);
2536 // Change the permissions of the file
2537 GeneralUtility::fixPermissions($output);
2538 if (is_file($theMask)) {
2539 @unlink($theMask);
2540 }
2541 return $ret;
2542 }
2543
2544 /**
2545 * Compressing a GIF file if not already LZW compressed.
2546 * This function is a workaround for the fact that ImageMagick and/or GD does not compress GIF-files to their minimum size (that is RLE or no compression used)
2547 *
2548 * The function takes a file-reference, $theFile, and saves it again through GD or ImageMagick in order to compress the file
2549 * GIF:
2550 * If $type is not set, the compression is done with ImageMagick (provided that $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path_lzw'] is pointing to the path of a lzw-enabled version of 'convert') else with GD (should be RLE-enabled!)
2551 * If $type is set to either 'IM' or 'GD' the compression is done with ImageMagick and GD respectively
2552 * PNG:
2553 * No changes.
2554 *
2555 * $theFile is expected to be a valid GIF-file!
2556 * The function returns a code for the operation.
2557 *
2558 * @param string $theFile Filepath
2559 * @param string $type See description of function
2560 * @return string Returns "GD" if GD was used, otherwise "IM" if ImageMagick was used. If nothing done at all, it returns empty string.
2561 */
2562 public static function gifCompress($theFile, $type)
2563 {
2564 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
2565 if (!$gfxConf['gif_compress'] || strtolower(substr($theFile, -4, 4)) !== '.gif') {
2566 return '';
2567 }
2568
2569 if (($type === 'IM' || !$type) && $gfxConf['processor_enabled'] && $gfxConf['processor_path_lzw']) {
2570 // Use temporary file to prevent problems with read and write lock on same file on network file systems
2571 $temporaryName = PathUtility::dirname($theFile) . '/' . md5(StringUtility::getUniqueId()) . '.gif';
2572 // Rename could fail, if a simultaneous thread is currently working on the same thing
2573 if (@rename($theFile, $temporaryName)) {
2574 $cmd = CommandUtility::imageMagickCommand(
2575 'convert',
2576 ImageMagickFile::fromFilePath($temporaryName) . ' ' . CommandUtility::escapeShellArgument($theFile),
2577 $gfxConf['processor_path_lzw']
2578 );
2579 CommandUtility::exec($cmd);
2580 unlink($temporaryName);
2581 }
2582 $returnCode = 'IM';
2583 if (@is_file($theFile)) {
2584 GeneralUtility::fixPermissions($theFile);
2585 }
2586 } elseif (($type === 'GD' || !$type) && $gfxConf['gdlib'] && !$gfxConf['gdlib_png']) {
2587 $tempImage = imagecreatefromgif($theFile);
2588 imagegif($tempImage, $theFile);
2589 imagedestroy($tempImage);
2590 $returnCode = 'GD';
2591 if (@is_file($theFile)) {
2592 GeneralUtility::fixPermissions($theFile);
2593 }
2594 } else {
2595 $returnCode = '';
2596 }
2597
2598 return $returnCode;
2599 }
2600
2601 /**
2602 * Returns filename of the png/gif version of the input file (which can be png or gif).
2603 * If input file type does not match the wanted output type a conversion is made and temp-filename returned.
2604 *
2605 * @param string $theFile Filepath of image file
2606 * @param bool $output_png If TRUE, then input file is converted to PNG, otherwise to GIF
2607 * @return string|null If the new image file exists, its filepath is returned
2608 */
2609 public static function readPngGif($theFile, $output_png = false)
2610 {
2611 if (!$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'] || !@is_file($theFile)) {
2612 return null;
2613 }
2614
2615 $ext = strtolower(substr($theFile, -4, 4));
2616 if ((string)$ext === '.png' && $output_png || (string)$ext === '.gif' && !$output_png) {
2617 return $theFile;
2618 }
2619
2620 if (!@is_dir(Environment::getPublicPath() . '/typo3temp/assets/images/')) {
2621 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/typo3temp/assets/images/');
2622 }
2623 $newFile = Environment::getPublicPath() . '/typo3temp/assets/images/' . md5($theFile . '|' . filemtime($theFile)) . ($output_png ? '.png' : '.gif');
2624 $cmd = CommandUtility::imageMagickCommand(
2625 'convert',
2626 ImageMagickFile::fromFilePath($theFile)
2627 . ' ' . CommandUtility::escapeShellArgument($newFile),
2628 $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']
2629 );
2630 CommandUtility::exec($cmd);
2631 if (@is_file($newFile)) {
2632 GeneralUtility::fixPermissions($newFile);
2633 return $newFile;
2634 }
2635 return null;
2636 }
2637
2638 /***********************************
2639 *
2640 * Various IO functions
2641 *
2642 ***********************************/
2643
2644 /**
2645 * Applies an ImageMagick parameter to a GDlib image pointer resource by writing the resource to file, performing an IM operation upon it and reading back the result into the ImagePointer.
2646 *
2647 * @param resource $im The image pointer (reference)
2648 * @param string $command The ImageMagick parameters. Like effects, scaling etc.
2649 */
2650 public function applyImageMagickToPHPGif(&$im, $command)
2651 {
2652 $tmpStr = $this->randomName();
2653 $theFile = $tmpStr . '.' . $this->gifExtension;
2654 $this->ImageWrite($im, $theFile);
2655 $this->imageMagickExec($theFile, $theFile, $command);
2656 $tmpImg = $this->imageCreateFromFile($theFile);
2657 if ($tmpImg) {
2658 imagedestroy($im);
2659 $im = $tmpImg;
2660 $this->w = imagesx($im);
2661 $this->h = imagesy($im);
2662 }
2663 unlink($theFile);
2664 }
2665
2666 /**
2667 * Returns an image extension for an output image based on the number of pixels of the output and the file extension of the original file.
2668 * For example: If the number of pixels exceeds $this->pixelLimitGif (normally 10000) then it will be a "jpg" string in return.
2669 *
2670 * @param string $type The file extension, lowercase.
2671 * @param int $w The width of the output image.
2672 * @param int $h The height of the output image.
2673 * @return string The filename, either "jpg" or "gif"/"png" (whatever $this->gifExtension is set to.)
2674 */
2675 public function gif_or_jpg($type, $w, $h)
2676 {
2677 if ($type === 'ai' || $w * $h < $this->pixelLimitGif) {
2678 return $this->gifExtension;
2679 }
2680 return 'jpg';
2681 }
2682
2683 /**
2684 * Writing the internal image pointer, $this->im, to file based on the extension of the input filename
2685 * Used in GIFBUILDER
2686 * Uses $this->setup['reduceColors'] for gif/png images and $this->setup['quality'] for jpg images to reduce size/quality if needed.
2687 *
2688 * @param string $file The filename to write to.
2689 * @return string Returns input filename
2690 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::gifBuild()
2691 */
2692 public function output($file)
2693 {
2694 if ($file) {
2695 $reg = [];
2696 preg_match('/([^\\.]*)$/', $file, $reg);
2697 $ext = strtolower($reg[0]);
2698 switch ($ext) {
2699 case 'gif':
2700 case 'png':
2701 if ($this->ImageWrite($this->im, $file)) {
2702 // ImageMagick operations
2703 if ($this->setup['reduceColors'] ?? false) {
2704 $reduced = $this->IMreduceColors($file, MathUtility::forceIntegerInRange($this->setup['reduceColors'], 256, $this->truecolorColors, 256));
2705 if ($reduced) {
2706 @copy($reduced, $file);
2707 @unlink($reduced);
2708 }
2709 }
2710 // Compress with IM! (adds extra compression, LZW from ImageMagick)
2711 // (Workaround for the absence of lzw-compression in GD)
2712 self::gifCompress($file, 'IM');
2713 }
2714 break;
2715 case 'jpg':
2716 case 'jpeg':
2717 // Use the default
2718 $quality = 0;
2719 if ($this->setup['quality'] ?? false) {
2720 $quality = MathUtility::forceIntegerInRange($this->setup['quality'], 10, 100);
2721 }
2722 $this->ImageWrite($this->im, $file, $quality);
2723 break;
2724 }
2725 }
2726 return $file;
2727 }
2728
2729 /**
2730 * Destroy internal image pointer, $this->im
2731 *
2732 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::gifBuild()
2733 */
2734 public function destroy()
2735 {
2736 imagedestroy($this->im);
2737 }
2738
2739 /**
2740 * Returns Image Tag for input image information array.
2741 *
2742 * @param array $imgInfo Image information array, key 0/1 is width/height and key 3 is the src value
2743 * @return string Image tag for the input image information array.
2744 */
2745 public function imgTag($imgInfo)
2746 {
2747 return '<img src="' . $imgInfo[3] . '" width="' . $imgInfo[0] . '" height="' . $imgInfo[1] . '" border="0" alt="" />';
2748 }
2749
2750 /**
2751 * Writes the input GDlib image pointer to file
2752 *
2753 * @param resource $destImg The GDlib image resource pointer
2754 * @param string $theImage The filename to write to
2755 * @param int $quality The image quality (for JPEGs)
2756 * @return bool The output of either imageGif, imagePng or imageJpeg based on the filename to write
2757 * @see maskImageOntoImage()
2758 * @see scale()
2759 * @see output()
2760 */
2761 public function ImageWrite($destImg, $theImage, $quality = 0)
2762 {
2763 imageinterlace($destImg, 0);
2764 $ext = strtolower(substr($theImage, (int)strrpos($theImage, '.') + 1));
2765 $result = false;
2766 switch ($ext) {
2767 case 'jpg':
2768 case 'jpeg':
2769 if (function_exists('imagejpeg')) {
2770 if ($quality === 0) {
2771 $quality = $this->jpegQuality;
2772 }
2773 $result = imagejpeg($destImg, $theImage, $quality);
2774 }
2775 break;
2776 case 'gif':
2777 if (function_exists('imagegif')) {
2778 imagetruecolortopalette($destImg, true, 256);
2779 $result = imagegif($destImg, $theImage);
2780 }
2781 break;
2782 case 'png':
2783 if (function_exists('imagepng')) {
2784 $result = imagepng($destImg, $theImage);
2785 }
2786 break;
2787 }
2788 if ($result) {
2789 GeneralUtility::fixPermissions($theImage);
2790 }
2791 return $result;
2792 }
2793
2794 /**
2795 * Creates a new GDlib image resource based on the input image filename.
2796 * If it fails creating an image from the input file a blank gray image with the dimensions of the input image will be created instead.
2797 *
2798 * @param string $sourceImg Image filename
2799 * @return resource Image Resource pointer
2800 */
2801 public function imageCreateFromFile($sourceImg)
2802 {
2803 $imgInf = pathinfo($sourceImg);
2804 $ext = strtolower($imgInf['extension']);
2805 switch ($ext) {
2806 case 'gif':
2807 if (function_exists('imagecreatefromgif')) {
2808 return imagecreatefromgif($sourceImg);
2809 }
2810 break;
2811 case 'png':
2812 if (function_exists('imagecreatefrompng')) {
2813 $imageHandle = imagecreatefrompng($sourceImg);
2814 if ($this->saveAlphaLayer) {
2815 imagesavealpha($imageHandle, true);
2816 }
2817 return $imageHandle;
2818 }
2819 break;
2820 case 'jpg':
2821 case 'jpeg':
2822 if (function_exists('imagecreatefromjpeg')) {
2823 return imagecreatefromjpeg($sourceImg);
2824 }
2825 break;
2826 }
2827 // If non of the above:
2828 $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $sourceImg);
2829 $im = imagecreatetruecolor($imageInfo->getWidth(), $imageInfo->getHeight());
2830 $Bcolor = imagecolorallocate($im, 128, 128, 128);
2831 imagefilledrectangle($im, 0, 0, $imageInfo->getWidth(), $imageInfo->getHeight(), $Bcolor);
2832 return $im;
2833 }
2834
2835 /**
2836 * Returns the HEX color value for an RGB color array
2837 *
2838 * @param array $color RGB color array
2839 * @return string HEX color value
2840 */
2841 public function hexColor($color)
2842 {
2843 $r = dechex($color[0]);
2844 if (strlen($r) < 2) {
2845 $r = '0' . $r;
2846 }
2847 $g = dechex($color[1]);
2848 if (strlen($g) < 2) {
2849 $g = '0' . $g;
2850 }
2851 $b = dechex($color[2]);
2852 if (strlen($b) < 2) {
2853 $b = '0' . $b;
2854 }
2855 return '#' . $r . $g . $b;
2856 }
2857
2858 /**
2859 * Unifies all colors given in the colArr color array to the first color in the array.
2860 *
2861 * @param resource $img Image resource
2862 * @param array $colArr Array containing RGB color arrays
2863 * @param bool $closest
2864 * @return int The index of the unified color
2865 */
2866 public function unifyColors(&$img, $colArr, $closest = false)
2867 {
2868 $retCol = -1;
2869 if (is_array($colArr) && !empty($colArr) && function_exists('imagepng') && function_exists('imagecreatefrompng')) {
2870 $firstCol = array_shift($colArr);
2871 $firstColArr = $this->convertColor($firstCol);
2872 $origName = $preName = $this->randomName() . '.png';
2873 $postName = $this->randomName() . '.png';
2874 $tmpImg = null;
2875 if (count($colArr) > 1) {
2876 $this->ImageWrite($img, $preName);
2877 $firstCol = $this->hexColor($firstColArr);
2878 foreach ($colArr as $transparentColor) {
2879 $transparentColor = $this->convertColor($transparentColor);
2880 $transparentColor = $this->hexColor($transparentColor);
2881 $cmd = '-fill "' . $firstCol . '" -opaque "' . $transparentColor . '"';
2882 $this->imageMagickExec($preName, $postName, $cmd);
2883 $preName = $postName;
2884 }
2885 $this->imageMagickExec($postName, $origName, '');
2886 if (@is_file($origName)) {
2887 $tmpImg = $this->imageCreateFromFile($origName);
2888 }
2889 } else {
2890 $tmpImg = $img;
2891 }
2892 if ($tmpImg) {
2893 $img = $tmpImg;
2894 if ($closest) {
2895 $retCol = imagecolorclosest($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
2896 } else {
2897 $retCol = imagecolorexact($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
2898 }
2899 }
2900 // Unlink files from process
2901 if ($origName) {
2902 @unlink($origName);
2903 }
2904 if ($postName) {
2905 @unlink($postName);
2906 }
2907 }
2908 return $retCol;
2909 }
2910
2911 /**
2912 * Creates error image based on gfx/notfound_thumb.png
2913 * Requires GD lib enabled, otherwise it will exit with the three
2914 * textstrings outputted as text. Outputs the image stream to browser and exits!
2915 *
2916 * @param string $filename Name of the file
2917 * @param string $textline1 Text line 1
2918 * @param string $textline2 Text line 2
2919 * @param string $textline3 Text line 3
2920 * @throws \RuntimeException
2921 */
2922 public function getTemporaryImageWithText($filename, $textline1, $textline2, $textline3)
2923 {
2924 if (empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib'])) {
2925 throw new \RuntimeException('TYPO3 Fatal Error: No gdlib. ' . $textline1 . ' ' . $textline2 . ' ' . $textline3, 1270853952);
2926 }
2927 // Creates the basis for the error image
2928 $basePath = ExtensionManagementUtility::extPath('core') . 'Resources/Public/Images/';
2929 if (!empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'])) {
2930 $im = imagecreatefrompng($basePath . 'NotFound.png');
2931 } else {
2932 $im = imagecreatefromgif($basePath . 'NotFound.gif');
2933 }
2934 // Sets background color and print color.
2935 $white = imagecolorallocate($im, 255, 255, 255);
2936 $black = imagecolorallocate($im, 0, 0, 0);
2937 // Prints the text strings with the build-in font functions of GD
2938 $x = 0;
2939 $font = 0;
2940 if ($textline1) {
2941 imagefilledrectangle($im, $x, 9, 56, 16, $white);
2942 imagestring($im, $font, $x, 9, $textline1, $black);
2943 }
2944 if ($textline2) {
2945 imagefilledrectangle($im, $x, 19, 56, 26, $white);
2946 imagestring($im, $font, $x, 19, $textline2, $black);
2947 }
2948 if ($textline3) {
2949 imagefilledrectangle($im, $x, 29, 56, 36, $white);
2950 imagestring($im, $font, $x, 29, substr($textline3, -14), $black);
2951 }
2952 // Outputting the image stream and exit
2953 if (!empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'])) {
2954 imagepng($im, $filename);
2955 } else {
2956 imagegif($im, $filename);
2957 }
2958 }
2959
2960 /**
2961 * Function to compensate for DPI resolution.
2962 * FreeType 2 always has 96 dpi, so it is hard-coded at this place.
2963 *
2964 * @param float $fontSize font size for freetype function call
2965 * @return float compensated font size based on 96 dpi
2966 */
2967 protected function compensateFontSizeiBasedOnFreetypeDpi($fontSize)
2968 {
2969 return $fontSize / 96.0 * 72;
2970 }
2971 }