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