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