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