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