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