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