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