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