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