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