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