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