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