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