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