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