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