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