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