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