TYPO3 CMS  TYPO3_8-7
GraphicalFunctions.php
Go to the documentation of this file.
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 
25 
33 {
39  public $addFrameSelection = true;
40 
46  public $gifExtension = 'gif';
47 
53  public $gdlibExtensions = '';
54 
60  public $png_truecolor = false;
61 
67  protected $colorspace = 'RGB';
68 
75  'CMY',
76  'CMYK',
77  'Gray',
78  'HCL',
79  'HSB',
80  'HSL',
81  'HWB',
82  'Lab',
83  'LCH',
84  'LMS',
85  'Log',
86  'Luv',
87  'OHTA',
88  'Rec601Luma',
89  'Rec601YCbCr',
90  'Rec709Luma',
91  'Rec709YCbCr',
92  'RGB',
93  'sRGB',
94  'Transparent',
95  'XYZ',
96  'YCbCr',
97  'YCC',
98  'YIQ',
99  'YCbCr',
100  'YUV'
101  ];
102 
108  public $truecolorColors = 16777215;
109 
116  public $imageFileExt = 'gif,jpg,jpeg,png,tif,bmp,tga,pcx,ai,pdf';
117 
123  public $webImageExt = 'gif,jpg,jpeg,png';
124 
128  public $NO_IM_EFFECTS = '';
129 
133  public $cmds = [
134  'jpg' => '',
135  'jpeg' => '',
136  'gif' => '',
137  'png' => '-colors 64'
138  ];
139 
143  public $NO_IMAGE_MAGICK = '';
144 
148  public $mayScaleUp = 1;
149 
155  public $filenamePrefix = '';
156 
163 
170 
176  public $dontCompress = 0;
177 
184 
193 
199  public $IM_commands = [];
200 
204  public $workArea = [];
205 
211  protected $saveAlphaLayer = false;
212 
219  public $tempPath = 'typo3temp/';
220 
226  public $absPrefix = '';
227 
233  public $scalecmd = '-geometry';
234 
240  public $im5fx_blurSteps = '1x2,2x2,3x2,4x3,5x3,5x4,6x4,7x5,8x5,9x5';
241 
247  public $im5fx_sharpenSteps = '1x2,2x2,3x2,2x3,3x3,4x3,3x4,4x4,4x5,5x5';
248 
254  public $pixelLimitGif = 10000;
255 
261  public $colMap = [
262  'aqua' => [0, 255, 255],
263  'black' => [0, 0, 0],
264  'blue' => [0, 0, 255],
265  'fuchsia' => [255, 0, 255],
266  'gray' => [128, 128, 128],
267  'green' => [0, 128, 0],
268  'lime' => [0, 255, 0],
269  'maroon' => [128, 0, 0],
270  'navy' => [0, 0, 128],
271  'olive' => [128, 128, 0],
272  'purple' => [128, 0, 128],
273  'red' => [255, 0, 0],
274  'silver' => [192, 192, 192],
275  'teal' => [0, 128, 128],
276  'yellow' => [255, 255, 0],
277  'white' => [255, 255, 255]
278  ];
279 
285  public $csConvObj;
286 
290  public $jpegQuality = 75;
291 
295  public $map = '';
296 
303  public $setup = [];
304 
308  public $w = 0;
309 
313  public $h = 0;
314 
318  public $OFFSET;
319 
323  protected $im;
324 
329  public function init()
330  {
331  $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
332  if (function_exists('imagecreatefromjpeg') && function_exists('imagejpeg')) {
333  $this->gdlibExtensions .= ',jpg,jpeg';
334  }
335  if (function_exists('imagecreatefrompng') && function_exists('imagepng')) {
336  $this->gdlibExtensions .= ',png';
337  }
338  if (function_exists('imagecreatefromgif') && function_exists('imagegif')) {
339  $this->gdlibExtensions .= ',gif';
340  }
341  if ($gfxConf['png_truecolor']) {
342  $this->png_truecolor = true;
343  }
344 
345  if ($gfxConf['processor_colorspace'] && in_array($gfxConf['processor_colorspace'], $this->allowedColorSpaceNames, true)) {
346  $this->colorspace = $gfxConf['processor_colorspace'];
347  }
348 
349  if (!$gfxConf['processor_enabled']) {
350  $this->NO_IMAGE_MAGICK = 1;
351  }
352  // When GIFBUILDER gets used in truecolor mode
353  // No colors parameter if we generate truecolor images.
354  if ($this->png_truecolor) {
355  $this->cmds['png'] = '';
356  }
357  // Setting default JPG parameters:
358  $this->jpegQuality = MathUtility::forceIntegerInRange($gfxConf['jpg_quality'], 10, 100, 75);
359  $this->addFrameSelection = (bool)$gfxConf['processor_allowFrameSelection'];
360  if ($gfxConf['gdlib_png']) {
361  $this->gifExtension = 'png';
362  }
363  $this->imageFileExt = $gfxConf['imagefile_ext'];
364 
365  // Boolean. This is necessary if using ImageMagick 5+.
366  // Effects in Imagemagick 5+ tends to render very slowly!!
367  // - therefore must be disabled in order not to perform sharpen, blurring and such.
368  $this->NO_IM_EFFECTS = 1;
369  $this->cmds['jpg'] = $this->cmds['jpeg'] = '-colorspace ' . $this->colorspace . ' -quality ' . $this->jpegQuality;
370 
371  // ... but if 'processor_effects' is set, enable effects
372  if ((int)$gfxConf['processor_effects'] === 1) {
373  $this->NO_IM_EFFECTS = 0;
374  $this->cmds['jpg'] .= $this->v5_sharpen(10);
375  $this->cmds['jpeg'] .= $this->v5_sharpen(10);
376  }
377  // Secures that images are not scaled up.
378  if (!$gfxConf['processor_allowUpscaling']) {
379  $this->mayScaleUp = 0;
380  }
381  $this->csConvObj = GeneralUtility::makeInstance(CharsetConverter::class);
382  }
383 
384  /*************************************************
385  *
386  * Layering images / "IMAGE" GIFBUILDER object
387  *
388  *************************************************/
399  public function maskImageOntoImage(&$im, $conf, $workArea)
400  {
401  if ($conf['file'] && $conf['mask']) {
402  $imgInf = pathinfo($conf['file']);
403  $imgExt = strtolower($imgInf['extension']);
404  if (!GeneralUtility::inList($this->gdlibExtensions, $imgExt)) {
405  $BBimage = $this->imageMagickConvert($conf['file'], $this->gifExtension);
406  } else {
407  $BBimage = $this->getImageDimensions($conf['file']);
408  }
409  $maskInf = pathinfo($conf['mask']);
410  $maskExt = strtolower($maskInf['extension']);
411  if (!GeneralUtility::inList($this->gdlibExtensions, $maskExt)) {
412  $BBmask = $this->imageMagickConvert($conf['mask'], $this->gifExtension);
413  } else {
414  $BBmask = $this->getImageDimensions($conf['mask']);
415  }
416  if ($BBimage && $BBmask) {
417  $w = imagesx($im);
418  $h = imagesy($im);
419  $tmpStr = $this->randomName();
420  $theImage = $tmpStr . '_img.' . $this->gifExtension;
421  $theDest = $tmpStr . '_dest.' . $this->gifExtension;
422  $theMask = $tmpStr . '_mask.' . $this->gifExtension;
423  // Prepare overlay image
424  $cpImg = $this->imageCreateFromFile($BBimage[3]);
425  $destImg = imagecreatetruecolor($w, $h);
426  // Preserve alpha transparency
427  if ($this->saveAlphaLayer) {
428  imagesavealpha($destImg, true);
429  $Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
430  imagefill($destImg, 0, 0, $Bcolor);
431  } else {
432  $Bcolor = imagecolorallocate($destImg, 0, 0, 0);
433  imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
434  }
435  $this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
436  $this->ImageWrite($destImg, $theImage);
437  imagedestroy($cpImg);
438  imagedestroy($destImg);
439  // Prepare mask image
440  $cpImg = $this->imageCreateFromFile($BBmask[3]);
441  $destImg = imagecreatetruecolor($w, $h);
442  if ($this->saveAlphaLayer) {
443  imagesavealpha($destImg, true);
444  $Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
445  imagefill($destImg, 0, 0, $Bcolor);
446  } else {
447  $Bcolor = imagecolorallocate($destImg, 0, 0, 0);
448  imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
449  }
450  $this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
451  $this->ImageWrite($destImg, $theMask);
452  imagedestroy($cpImg);
453  imagedestroy($destImg);
454  // Mask the images
455  $this->ImageWrite($im, $theDest);
456  // Let combineExec handle maskNegation
457  $this->combineExec($theDest, $theImage, $theMask, $theDest);
458  // The main image is loaded again...
459  $backIm = $this->imageCreateFromFile($theDest);
460  // ... and if nothing went wrong we load it onto the old one.
461  if ($backIm) {
462  if (!$this->saveAlphaLayer) {
463  imagecolortransparent($backIm, -1);
464  }
465  $im = $backIm;
466  }
467  // Unlink files from process
468  if (!$this->dontUnlinkTempFiles) {
469  unlink($theDest);
470  unlink($theImage);
471  unlink($theMask);
472  }
473  }
474  }
475  }
476 
485  public function copyImageOntoImage(&$im, $conf, $workArea)
486  {
487  if ($conf['file']) {
488  if (!GeneralUtility::inList($this->gdlibExtensions, $conf['BBOX'][2])) {
489  $conf['BBOX'] = $this->imageMagickConvert($conf['BBOX'][3], $this->gifExtension);
490  $conf['file'] = $conf['BBOX'][3];
491  }
492  $cpImg = $this->imageCreateFromFile($conf['file']);
493  $this->copyGifOntoGif($im, $cpImg, $conf, $workArea);
494  imagedestroy($cpImg);
495  }
496  }
497 
507  public function copyGifOntoGif(&$im, $cpImg, $conf, $workArea)
508  {
509  $cpW = imagesx($cpImg);
510  $cpH = imagesy($cpImg);
511  $tile = GeneralUtility::intExplode(',', $conf['tile']);
512  $tile[0] = MathUtility::forceIntegerInRange($tile[0], 1, 20);
513  $tile[1] = MathUtility::forceIntegerInRange($tile[1], 1, 20);
514  $cpOff = $this->objPosition($conf, $workArea, [$cpW * $tile[0], $cpH * $tile[1]]);
515  for ($xt = 0; $xt < $tile[0]; $xt++) {
516  $Xstart = $cpOff[0] + $cpW * $xt;
517  // If this image is inside of the workArea, then go on
518  if ($Xstart + $cpW > $workArea[0]) {
519  // X:
520  if ($Xstart < $workArea[0]) {
521  $cpImgCutX = $workArea[0] - $Xstart;
522  $Xstart = $workArea[0];
523  } else {
524  $cpImgCutX = 0;
525  }
526  $w = $cpW - $cpImgCutX;
527  if ($Xstart > $workArea[0] + $workArea[2] - $w) {
528  $w = $workArea[0] + $workArea[2] - $Xstart;
529  }
530  // If this image is inside of the workArea, then go on
531  if ($Xstart < $workArea[0] + $workArea[2]) {
532  // Y:
533  for ($yt = 0; $yt < $tile[1]; $yt++) {
534  $Ystart = $cpOff[1] + $cpH * $yt;
535  // If this image is inside of the workArea, then go on
536  if ($Ystart + $cpH > $workArea[1]) {
537  if ($Ystart < $workArea[1]) {
538  $cpImgCutY = $workArea[1] - $Ystart;
539  $Ystart = $workArea[1];
540  } else {
541  $cpImgCutY = 0;
542  }
543  $h = $cpH - $cpImgCutY;
544  if ($Ystart > $workArea[1] + $workArea[3] - $h) {
545  $h = $workArea[1] + $workArea[3] - $Ystart;
546  }
547  // If this image is inside of the workArea, then go on
548  if ($Ystart < $workArea[1] + $workArea[3]) {
549  $this->imagecopyresized($im, $cpImg, $Xstart, $Ystart, $cpImgCutX, $cpImgCutY, $w, $h, $w, $h);
550  }
551  }
552  }
553  }
554  }
555  }
556  }
557 
586  public function imagecopyresized(&$dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight)
587  {
588  if (!$this->saveAlphaLayer) {
589  // Make true color image
590  $tmpImg = imagecreatetruecolor(imagesx($dstImg), imagesy($dstImg));
591  // Copy the source image onto that
592  imagecopyresized($tmpImg, $dstImg, 0, 0, 0, 0, imagesx($dstImg), imagesy($dstImg), imagesx($dstImg), imagesy($dstImg));
593  // Then copy the source image onto that (the actual operation!)
594  imagecopyresized($tmpImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
595  // Set the destination image
596  $dstImg = $tmpImg;
597  } else {
598  imagecopyresized($dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
599  }
600  }
601 
602  /********************************
603  *
604  * Text / "TEXT" GIFBUILDER object
605  *
606  ********************************/
615  public function makeText(&$im, $conf, $workArea)
616  {
617  // Spacing
618  list($spacing, $wordSpacing) = $this->calcWordSpacing($conf);
619  // Position
620  $txtPos = $this->txtPosition($conf, $workArea, $conf['BBOX']);
621  $theText = $conf['text'];
622  if ($conf['imgMap'] && is_array($conf['imgMap.'])) {
623  $this->addToMap($this->calcTextCordsForMap($conf['BBOX'][2], $txtPos, $conf['imgMap.']), $conf['imgMap.']);
624  }
625  if (!$conf['hideButCreateMap']) {
626  // Font Color:
627  $cols = $this->convertColor($conf['fontColor']);
628  // NiceText is calculated
629  if (!$conf['niceText']) {
630  $Fcolor = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
631  // antiAliasing is setup:
632  $Fcolor = $conf['antiAlias'] ? $Fcolor : -$Fcolor;
633  for ($a = 0; $a < $conf['iterations']; $a++) {
634  // If any kind of spacing applys, we use this function:
635  if ($spacing || $wordSpacing) {
636  $this->SpacedImageTTFText($im, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.']);
637  } else {
638  $this->renderTTFText($im, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'], $conf);
639  }
640  }
641  } else {
642  // NICETEXT::
643  // options anti_aliased and iterations is NOT available when doing this!!
644  $w = imagesx($im);
645  $h = imagesy($im);
646  $tmpStr = $this->randomName();
647  $fileMenu = $tmpStr . '_menuNT.' . $this->gifExtension;
648  $fileColor = $tmpStr . '_colorNT.' . $this->gifExtension;
649  $fileMask = $tmpStr . '_maskNT.' . $this->gifExtension;
650  // Scalefactor
651  $sF = MathUtility::forceIntegerInRange($conf['niceText.']['scaleFactor'], 2, 5);
652  $newW = ceil($sF * imagesx($im));
653  $newH = ceil($sF * imagesy($im));
654  // Make mask
655  $maskImg = imagecreatetruecolor($newW, $newH);
656  $Bcolor = imagecolorallocate($maskImg, 255, 255, 255);
657  imagefilledrectangle($maskImg, 0, 0, $newW, $newH, $Bcolor);
658  $Fcolor = imagecolorallocate($maskImg, 0, 0, 0);
659  // If any kind of spacing applies, we use this function:
660  if ($spacing || $wordSpacing) {
661  $this->SpacedImageTTFText($maskImg, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.'], $sF);
662  } else {
663  $this->renderTTFText($maskImg, $conf['fontSize'], $conf['angle'], $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'], $conf, $sF);
664  }
665  $this->ImageWrite($maskImg, $fileMask);
666  imagedestroy($maskImg);
667  // Downscales the mask
668  if ($this->NO_IM_EFFECTS) {
669  $command = trim($this->scalecmd . ' ' . $w . 'x' . $h . '! -negate');
670  } else {
671  $command = trim($conf['niceText.']['before'] . ' ' . $this->scalecmd . ' ' . $w . 'x' . $h . '! ' . $conf['niceText.']['after'] . ' -negate');
672  if ($conf['niceText.']['sharpen']) {
673  $command .= $this->v5_sharpen($conf['niceText.']['sharpen']);
674  }
675  }
676  $this->imageMagickExec($fileMask, $fileMask, $command);
677  // Make the color-file
678  $colorImg = imagecreatetruecolor($w, $h);
679  $Ccolor = imagecolorallocate($colorImg, $cols[0], $cols[1], $cols[2]);
680  imagefilledrectangle($colorImg, 0, 0, $w, $h, $Ccolor);
681  $this->ImageWrite($colorImg, $fileColor);
682  imagedestroy($colorImg);
683  // The mask is applied
684  // The main pictures is saved temporarily
685  $this->ImageWrite($im, $fileMenu);
686  $this->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
687  // The main image is loaded again...
688  $backIm = $this->imageCreateFromFile($fileMenu);
689  // ... and if nothing went wrong we load it onto the old one.
690  if ($backIm) {
691  if (!$this->saveAlphaLayer) {
692  imagecolortransparent($backIm, -1);
693  }
694  $im = $backIm;
695  }
696  // Deleting temporary files;
697  if (!$this->dontUnlinkTempFiles) {
698  unlink($fileMenu);
699  unlink($fileColor);
700  unlink($fileMask);
701  }
702  }
703  }
704  }
705 
716  public function txtPosition($conf, $workArea, $BB)
717  {
718  $angle = (int)$conf['angle'] / 180 * pi();
719  $conf['angle'] = 0;
720  $straightBB = $this->calcBBox($conf);
721  // offset, align, valign, workarea
722  // [0]=x, [1]=y, [2]=w, [3]=h
723  $result = [];
724  $result[2] = $BB[0];
725  $result[3] = $BB[1];
726  $w = $workArea[2];
727  switch ($conf['align']) {
728  case 'right':
729 
730  case 'center':
731  $factor = abs(cos($angle));
732  $sign = cos($angle) < 0 ? -1 : 1;
733  $len1 = $sign * $factor * $straightBB[0];
734  $len2 = $sign * $BB[0];
735  $result[0] = $w - ceil(($len2 * $factor + (1 - $factor) * $len1));
736  $factor = abs(sin($angle));
737  $sign = sin($angle) < 0 ? -1 : 1;
738  $len1 = $sign * $factor * $straightBB[0];
739  $len2 = $sign * $BB[1];
740  $result[1] = ceil($len2 * $factor + (1 - $factor) * $len1);
741  break;
742  }
743  switch ($conf['align']) {
744  case 'right':
745  break;
746  case 'center':
747  $result[0] = round($result[0] / 2);
748  $result[1] = round($result[1] / 2);
749  break;
750  default:
751  $result[0] = 0;
752  $result[1] = 0;
753  }
754  $result = $this->applyOffset($result, GeneralUtility::intExplode(',', $conf['offset']));
755  $result = $this->applyOffset($result, $workArea);
756  return $result;
757  }
758 
767  public function calcBBox($conf)
768  {
769  $sF = $this->getTextScalFactor($conf);
770  list($spacing, $wordSpacing) = $this->calcWordSpacing($conf, $sF);
771  $theText = $conf['text'];
772  $charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $theText, $conf['splitRendering.'], $sF);
773  $theBBoxInfo = $charInf;
774  if ($conf['angle']) {
775  $xArr = [$charInf[0], $charInf[2], $charInf[4], $charInf[6]];
776  $yArr = [$charInf[1], $charInf[3], $charInf[5], $charInf[7]];
777  $x = max($xArr) - min($xArr);
778  $y = max($yArr) - min($yArr);
779  } else {
780  $x = $charInf[2] - $charInf[0];
781  $y = $charInf[1] - $charInf[7];
782  }
783  // Set original lineHeight (used by line breaks):
784  $theBBoxInfo['lineHeight'] = $y;
785  if (!empty($conf['lineHeight'])) {
786  $theBBoxInfo['lineHeight'] = (int)$conf['lineHeight'];
787  }
788 
789  // If any kind of spacing applys, we use this function:
790  if ($spacing || $wordSpacing) {
791  $x = 0;
792  if (!$spacing && $wordSpacing) {
793  $bits = explode(' ', $theText);
794  foreach ($bits as $word) {
795  $word .= ' ';
796  $wordInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $word, $conf['splitRendering.'], $sF);
797  $wordW = $wordInf[2] - $wordInf[0];
798  $x += $wordW + $wordSpacing;
799  }
800  } else {
801  $utf8Chars = $this->csConvObj->utf8_to_numberarray($theText);
802  // For each UTF-8 char, do:
803  foreach ($utf8Chars as $char) {
804  $charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $char, $conf['splitRendering.'], $sF);
805  $charW = $charInf[2] - $charInf[0];
806  $x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
807  }
808  }
809  } elseif (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($conf['text'], $conf) > $conf['breakWidth']) {
810  $maxWidth = 0;
811  $currentWidth = 0;
812  $breakWidth = $conf['breakWidth'];
813  $breakSpace = $this->getBreakSpace($conf, $theBBoxInfo);
814  $wordPairs = $this->getWordPairsForLineBreak($conf['text']);
815  // Iterate through all word pairs:
816  foreach ($wordPairs as $index => $wordPair) {
817  $wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
818  if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
819  $currentWidth += $wordWidth;
820  } else {
821  $maxWidth = max($maxWidth, $currentWidth);
822  $y += $breakSpace;
823  // Restart:
824  $currentWidth = $wordWidth;
825  }
826  }
827  $x = max($maxWidth, $currentWidth) * $sF;
828  }
829  if ($sF > 1) {
830  $x = ceil($x / $sF);
831  $y = ceil($y / $sF);
832  if (is_array($theBBoxInfo)) {
833  foreach ($theBBoxInfo as &$value) {
834  $value = ceil($value / $sF);
835  }
836  unset($value);
837  }
838  }
839  return [$x, $y, $theBBoxInfo];
840  }
841 
850  public function addToMap($cords, $conf)
851  {
852  $this->map .= '<area' . ' shape="poly"' . ' coords="' . implode(',', $cords) . '"'
853  . ' href="' . htmlspecialchars($conf['url']) . '"'
854  . ($conf['target'] ? ' target="' . htmlspecialchars($conf['target']) . '"' : '')
855  . ((string)$conf['titleText'] !== '' ? ' title="' . htmlspecialchars($conf['titleText']) . '"' : '')
856  . ' alt="' . htmlspecialchars($conf['altText']) . '" />';
857  }
858 
869  public function calcTextCordsForMap($cords, $offset, $conf)
870  {
871  $pars = GeneralUtility::intExplode(',', $conf['explode'] . ',');
872  $newCords[0] = $cords[0] + $offset[0] - $pars[0];
873  $newCords[1] = $cords[1] + $offset[1] + $pars[1];
874  $newCords[2] = $cords[2] + $offset[0] + $pars[0];
875  $newCords[3] = $cords[3] + $offset[1] + $pars[1];
876  $newCords[4] = $cords[4] + $offset[0] + $pars[0];
877  $newCords[5] = $cords[5] + $offset[1] - $pars[1];
878  $newCords[6] = $cords[6] + $offset[0] - $pars[0];
879  $newCords[7] = $cords[7] + $offset[1] - $pars[1];
880  return $newCords;
881  }
882 
902  public function SpacedImageTTFText(&$im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $text, $spacing, $wordSpacing, $splitRenderingConf, $sF = 1)
903  {
904  $spacing *= $sF;
905  $wordSpacing *= $sF;
906  if (!$spacing && $wordSpacing) {
907  $bits = explode(' ', $text);
908  foreach ($bits as $word) {
909  $word .= ' ';
910  $wordInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $word, $splitRenderingConf, $sF);
911  $wordW = $wordInf[2] - $wordInf[0];
912  $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $word, $splitRenderingConf, $sF);
913  $x += $wordW + $wordSpacing;
914  }
915  } else {
916  $utf8Chars = $this->csConvObj->utf8_to_numberarray($text);
917  // For each UTF-8 char, do:
918  foreach ($utf8Chars as $char) {
919  $charInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $char, $splitRenderingConf, $sF);
920  $charW = $charInf[2] - $charInf[0];
921  $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $char, $splitRenderingConf, $sF);
922  $x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
923  }
924  }
925  }
926 
935  public function fontResize($conf)
936  {
937  // 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!!!!
938  $maxWidth = (int)$conf['maxWidth'];
939  list($spacing, $wordSpacing) = $this->calcWordSpacing($conf);
940  if ($maxWidth) {
941  // If any kind of spacing applys, we use this function:
942  if ($spacing || $wordSpacing) {
943  return $conf['fontSize'];
944  }
945  do {
946  // Determine bounding box.
947  $bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $conf['text'], $conf['splitRendering.']);
948  if ($conf['angle'] < 0) {
949  $pixelWidth = abs($bounds[4] - $bounds[0]);
950  } elseif ($conf['angle'] > 0) {
951  $pixelWidth = abs($bounds[2] - $bounds[6]);
952  } else {
953  $pixelWidth = abs($bounds[4] - $bounds[6]);
954  }
955  // Size is fine, exit:
956  if ($pixelWidth <= $maxWidth) {
957  break;
958  }
959  $conf['fontSize']--;
960  } while ($conf['fontSize'] > 1);
961  }
962  return $conf['fontSize'];
963  }
964 
976  public function ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $string, $splitRendering, $sF = 1)
977  {
978  // Initialize:
979  $offsetInfo = [];
980  $stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
981  // Traverse string parts:
982  foreach ($stringParts as $strCfg) {
983  $fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
984  if (is_readable($fontFile)) {
992  $try = 0;
993  do {
994  $calc = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $fontFile, $strCfg['str']);
995  } while ($calc[2] < 0 && $try++ < 10);
996  // Calculate offsets:
997  if (empty($offsetInfo)) {
998  // First run, just copy over.
999  $offsetInfo = $calc;
1000  } else {
1001  $offsetInfo[2] += $calc[2] - $calc[0] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
1002  $offsetInfo[3] += $calc[3] - $calc[1] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
1003  $offsetInfo[4] += $calc[4] - $calc[6] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
1004  $offsetInfo[5] += $calc[5] - $calc[7] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
1005  }
1006  } else {
1007  debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFBBoxWrapper()');
1008  }
1009  }
1010  return $offsetInfo;
1011  }
1012 
1027  public function ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF = 1)
1028  {
1029  // Initialize:
1030  $stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
1031  $x = ceil($sF * $x);
1032  $y = ceil($sF * $y);
1033  // Traverse string parts:
1034  foreach ($stringParts as $i => $strCfg) {
1035  // Initialize:
1036  $colorIndex = $color;
1037  // Set custom color if any (only when niceText is off):
1038  if ($strCfg['color'] && $sF == 1) {
1039  $cols = $this->convertColor($strCfg['color']);
1040  $colorIndex = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
1041  $colorIndex = $color >= 0 ? $colorIndex : -$colorIndex;
1042  }
1043  // Setting xSpaceBefore
1044  if ($i) {
1045  $x += (int)$strCfg['xSpaceBefore'];
1046  $y -= (int)$strCfg['ySpaceBefore'];
1047  }
1048  $fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
1049  if (is_readable($fontFile)) {
1050  // Render part:
1051  imagettftext($im, $this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $x, $y, $colorIndex, $fontFile, $strCfg['str']);
1052  // Calculate offset to apply:
1053  $wordInf = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, GeneralUtility::getFileAbsFileName($strCfg['fontFile']), $strCfg['str']);
1054  $x += $wordInf[2] - $wordInf[0] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceAfter'];
1055  $y += $wordInf[5] - $wordInf[7] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceAfter'];
1056  } else {
1057  debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFTextWrapper()');
1058  }
1059  }
1060  }
1061 
1071  public function splitString($string, $splitRendering, $fontSize, $fontFile)
1072  {
1073  // Initialize by setting the whole string and default configuration as the first entry.
1074  $result = [];
1075  $result[] = [
1076  'str' => $string,
1077  'fontSize' => $fontSize,
1078  'fontFile' => $fontFile
1079  ];
1080  // Traverse the split-rendering configuration:
1081  // Splitting will create more entries in $result with individual configurations.
1082  if (is_array($splitRendering)) {
1083  $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($splitRendering);
1084  // Traverse configured options:
1085  foreach ($sKeyArray as $key) {
1086  $cfg = $splitRendering[$key . '.'];
1087  // Process each type of split rendering keyword:
1088  switch ((string)$splitRendering[$key]) {
1089  case 'highlightWord':
1090  if ((string)$cfg['value'] !== '') {
1091  $newResult = [];
1092  // Traverse the current parts of the result array:
1093  foreach ($result as $part) {
1094  // Explode the string value by the word value to highlight:
1095  $explodedParts = explode($cfg['value'], $part['str']);
1096  foreach ($explodedParts as $c => $expValue) {
1097  if ((string)$expValue !== '') {
1098  $newResult[] = array_merge($part, ['str' => $expValue]);
1099  }
1100  if ($c + 1 < count($explodedParts)) {
1101  $newResult[] = [
1102  'str' => $cfg['value'],
1103  'fontSize' => $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1104  'fontFile' => $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1105  'color' => $cfg['color'],
1106  'xSpaceBefore' => $cfg['xSpaceBefore'],
1107  'xSpaceAfter' => $cfg['xSpaceAfter'],
1108  'ySpaceBefore' => $cfg['ySpaceBefore'],
1109  'ySpaceAfter' => $cfg['ySpaceAfter']
1110  ];
1111  }
1112  }
1113  }
1114  // Set the new result as result array:
1115  if (!empty($newResult)) {
1116  $result = $newResult;
1117  }
1118  }
1119  break;
1120  case 'charRange':
1121  if ((string)$cfg['value'] !== '') {
1122  // Initialize range:
1123  $ranges = GeneralUtility::trimExplode(',', $cfg['value'], true);
1124  foreach ($ranges as $i => $rangeDef) {
1125  $ranges[$i] = GeneralUtility::intExplode('-', $ranges[$i]);
1126  if (!isset($ranges[$i][1])) {
1127  $ranges[$i][1] = $ranges[$i][0];
1128  }
1129  }
1130  $newResult = [];
1131  // Traverse the current parts of the result array:
1132  foreach ($result as $part) {
1133  // Initialize:
1134  $currentState = -1;
1135  $bankAccum = '';
1136  // Explode the string value by the word value to highlight:
1137  $utf8Chars = $this->csConvObj->utf8_to_numberarray($part['str']);
1138  foreach ($utf8Chars as $utfChar) {
1139  // Find number and evaluate position:
1140  $uNumber = (int)$this->csConvObj->utf8CharToUnumber($utfChar);
1141  $inRange = 0;
1142  foreach ($ranges as $rangeDef) {
1143  if ($uNumber >= $rangeDef[0] && (!$rangeDef[1] || $uNumber <= $rangeDef[1])) {
1144  $inRange = 1;
1145  break;
1146  }
1147  }
1148  if ($currentState == -1) {
1149  $currentState = $inRange;
1150  }
1151  // Initialize first char
1152  // Switch bank:
1153  if ($inRange != $currentState && $uNumber !== 9 && $uNumber !== 10 && $uNumber !== 13 && $uNumber !== 32) {
1154  // Set result:
1155  if ($bankAccum !== '') {
1156  $newResult[] = [
1157  'str' => $bankAccum,
1158  'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1159  'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1160  'color' => $currentState ? $cfg['color'] : '',
1161  'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
1162  'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
1163  'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
1164  'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : ''
1165  ];
1166  }
1167  // Initialize new settings:
1168  $currentState = $inRange;
1169  $bankAccum = '';
1170  }
1171  // Add char to bank:
1172  $bankAccum .= $utfChar;
1173  }
1174  // Set result for FINAL part:
1175  if ($bankAccum !== '') {
1176  $newResult[] = [
1177  'str' => $bankAccum,
1178  'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
1179  'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
1180  'color' => $currentState ? $cfg['color'] : '',
1181  'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
1182  'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
1183  'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
1184  'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : ''
1185  ];
1186  }
1187  }
1188  // Set the new result as result array:
1189  if (!empty($newResult)) {
1190  $result = $newResult;
1191  }
1192  }
1193  break;
1194  }
1195  }
1196  }
1197  return $result;
1198  }
1199 
1209  public function calcWordSpacing($conf, $scaleFactor = 1)
1210  {
1211  $spacing = (int)$conf['spacing'];
1212  $wordSpacing = (int)$conf['wordSpacing'];
1213  $wordSpacing = $wordSpacing ?: $spacing * 2;
1214  $spacing *= $scaleFactor;
1215  $wordSpacing *= $scaleFactor;
1216  return [$spacing, $wordSpacing];
1217  }
1218 
1226  public function getTextScalFactor($conf)
1227  {
1228  if (!$conf['niceText']) {
1229  $sF = 1;
1230  } else {
1231  // NICETEXT::
1232  $sF = MathUtility::forceIntegerInRange($conf['niceText.']['scaleFactor'], 2, 5);
1233  }
1234  return $sF;
1235  }
1236 
1252  protected function renderTTFText(&$im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $conf, $sF = 1)
1253  {
1254  if (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($string, $conf) > $conf['breakWidth']) {
1255  $phrase = '';
1256  $currentWidth = 0;
1257  $breakWidth = $conf['breakWidth'];
1258  $breakSpace = $this->getBreakSpace($conf);
1259  $wordPairs = $this->getWordPairsForLineBreak($string);
1260  // Iterate through all word pairs:
1261  foreach ($wordPairs as $index => $wordPair) {
1262  $wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
1263  if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
1264  $currentWidth += $wordWidth;
1265  $phrase .= $wordPair;
1266  } else {
1267  // Render the current phrase that is below breakWidth:
1268  $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
1269  // Calculate the news height offset:
1270  $y += $breakSpace;
1271  // Restart the phrase:
1272  $currentWidth = $wordWidth;
1273  $phrase = $wordPair;
1274  }
1275  }
1276  // Render the remaining phrase:
1277  if ($currentWidth) {
1278  $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
1279  }
1280  } else {
1281  $this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF);
1282  }
1283  }
1284 
1291  protected function getWordPairsForLineBreak($string)
1292  {
1293  $wordPairs = [];
1294  $wordsArray = preg_split('#([- .,!:]+)#', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
1295  $wordsCount = count($wordsArray);
1296  for ($index = 0; $index < $wordsCount; $index += 2) {
1297  $wordPairs[] = $wordsArray[$index] . $wordsArray[$index + 1];
1298  }
1299  return $wordPairs;
1300  }
1301 
1309  protected function getRenderedTextWidth($text, $conf)
1310  {
1311  $bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $text, $conf['splitRendering.']);
1312  if ($conf['angle'] < 0) {
1313  $pixelWidth = abs($bounds[4] - $bounds[0]);
1314  } elseif ($conf['angle'] > 0) {
1315  $pixelWidth = abs($bounds[2] - $bounds[6]);
1316  } else {
1317  $pixelWidth = abs($bounds[4] - $bounds[6]);
1318  }
1319  return $pixelWidth;
1320  }
1321 
1329  protected function getBreakSpace($conf, array $boundingBox = null)
1330  {
1331  if (!isset($boundingBox)) {
1332  $boundingBox = $this->calcBBox($conf);
1333  $boundingBox = $boundingBox[2];
1334  }
1335  if (isset($conf['breakSpace']) && $conf['breakSpace']) {
1336  $breakSpace = $boundingBox['lineHeight'] * $conf['breakSpace'];
1337  } else {
1338  $breakSpace = $boundingBox['lineHeight'];
1339  }
1340  return $breakSpace;
1341  }
1342 
1343  /*********************************************
1344  *
1345  * Other GIFBUILDER objects related to TEXT
1346  *
1347  *********************************************/
1357  public function makeOutline(&$im, $conf, $workArea, $txtConf)
1358  {
1359  $thickness = (int)$conf['thickness'];
1360  if ($thickness) {
1361  $txtConf['fontColor'] = $conf['color'];
1362  $outLineDist = MathUtility::forceIntegerInRange($thickness, 1, 2);
1363  for ($b = 1; $b <= $outLineDist; $b++) {
1364  if ($b == 1) {
1365  $it = 8;
1366  } else {
1367  $it = 16;
1368  }
1369  $outL = $this->circleOffset($b, $it);
1370  for ($a = 0; $a < $it; $a++) {
1371  $this->makeText($im, $txtConf, $this->applyOffset($workArea, $outL[$a]));
1372  }
1373  }
1374  }
1375  }
1376 
1387  public function circleOffset($distance, $iterations)
1388  {
1389  $res = [];
1390  if ($distance && $iterations) {
1391  for ($a = 0; $a < $iterations; $a++) {
1392  $yOff = round(sin((2 * pi() / $iterations * ($a + 1))) * 100 * $distance);
1393  if ($yOff) {
1394  $yOff = (int)(ceil(abs(($yOff / 100))) * ($yOff / abs($yOff)));
1395  }
1396  $xOff = round(cos((2 * pi() / $iterations * ($a + 1))) * 100 * $distance);
1397  if ($xOff) {
1398  $xOff = (int)(ceil(abs(($xOff / 100))) * ($xOff / abs($xOff)));
1399  }
1400  $res[$a] = [$xOff, $yOff];
1401  }
1402  }
1403  return $res;
1404  }
1405 
1415  public function makeEmboss(&$im, $conf, $workArea, $txtConf)
1416  {
1417  $conf['color'] = $conf['highColor'];
1418  $this->makeShadow($im, $conf, $workArea, $txtConf);
1419  $newOffset = GeneralUtility::intExplode(',', $conf['offset']);
1420  $newOffset[0] *= -1;
1421  $newOffset[1] *= -1;
1422  $conf['offset'] = implode(',', $newOffset);
1423  $conf['color'] = $conf['lowColor'];
1424  $this->makeShadow($im, $conf, $workArea, $txtConf);
1425  }
1426 
1437  public function makeShadow(&$im, $conf, $workArea, $txtConf)
1438  {
1439  $workArea = $this->applyOffset($workArea, GeneralUtility::intExplode(',', $conf['offset']));
1440  $blurRate = MathUtility::forceIntegerInRange((int)$conf['blur'], 0, 99);
1441  // No effects if ImageMagick ver. 5+
1442  if (!$blurRate || $this->NO_IM_EFFECTS) {
1443  $txtConf['fontColor'] = $conf['color'];
1444  $this->makeText($im, $txtConf, $workArea);
1445  } else {
1446  $w = imagesx($im);
1447  $h = imagesy($im);
1448  // Area around the blur used for cropping something
1449  $blurBorder = 3;
1450  $tmpStr = $this->randomName();
1451  $fileMenu = $tmpStr . '_menu.' . $this->gifExtension;
1452  $fileColor = $tmpStr . '_color.' . $this->gifExtension;
1453  $fileMask = $tmpStr . '_mask.' . $this->gifExtension;
1454  // BlurColor Image laves
1455  $blurColImg = imagecreatetruecolor($w, $h);
1456  $bcols = $this->convertColor($conf['color']);
1457  $Bcolor = imagecolorallocate($blurColImg, $bcols[0], $bcols[1], $bcols[2]);
1458  imagefilledrectangle($blurColImg, 0, 0, $w, $h, $Bcolor);
1459  $this->ImageWrite($blurColImg, $fileColor);
1460  imagedestroy($blurColImg);
1461  // The mask is made: BlurTextImage
1462  $blurTextImg = imagecreatetruecolor($w + $blurBorder * 2, $h + $blurBorder * 2);
1463  // Black background
1464  $Bcolor = imagecolorallocate($blurTextImg, 0, 0, 0);
1465  imagefilledrectangle($blurTextImg, 0, 0, $w + $blurBorder * 2, $h + $blurBorder * 2, $Bcolor);
1466  $txtConf['fontColor'] = 'white';
1467  $blurBordArr = [$blurBorder, $blurBorder];
1468  $this->makeText($blurTextImg, $txtConf, $this->applyOffset($workArea, $blurBordArr));
1469  // Dump to temporary file
1470  $this->ImageWrite($blurTextImg, $fileMask);
1471  // Destroy
1472  imagedestroy($blurTextImg);
1473  $command = $this->v5_blur($blurRate + 1);
1474  $this->imageMagickExec($fileMask, $fileMask, $command . ' +matte');
1475  // The mask is loaded again
1476  $blurTextImg_tmp = $this->imageCreateFromFile($fileMask);
1477  // If nothing went wrong we continue with the blurred mask
1478  if ($blurTextImg_tmp) {
1479  // Cropping the border from the mask
1480  $blurTextImg = imagecreatetruecolor($w, $h);
1481  $this->imagecopyresized($blurTextImg, $blurTextImg_tmp, 0, 0, $blurBorder, $blurBorder, $w, $h, $w, $h);
1482  // Destroy the temporary mask
1483  imagedestroy($blurTextImg_tmp);
1484  // Adjust the mask
1485  $intensity = 40;
1486  if ($conf['intensity']) {
1487  $intensity = MathUtility::forceIntegerInRange($conf['intensity'], 0, 100);
1488  }
1489  $intensity = ceil(255 - $intensity / 100 * 255);
1490  $this->inputLevels($blurTextImg, 0, $intensity);
1491  $opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 0, 100);
1492  if ($opacity && $opacity < 100) {
1493  $high = ceil(255 * $opacity / 100);
1494  // Reducing levels as the opacity demands
1495  $this->outputLevels($blurTextImg, 0, $high);
1496  }
1497  // Dump the mask again
1498  $this->ImageWrite($blurTextImg, $fileMask);
1499  // Destroy the mask
1500  imagedestroy($blurTextImg);
1501  // The pictures are combined
1502  // The main pictures is saved temporarily
1503  $this->ImageWrite($im, $fileMenu);
1504  $this->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
1505  // The main image is loaded again...
1506  $backIm = $this->imageCreateFromFile($fileMenu);
1507  // ... and if nothing went wrong we load it onto the old one.
1508  if ($backIm) {
1509  if (!$this->saveAlphaLayer) {
1510  imagecolortransparent($backIm, -1);
1511  }
1512  $im = $backIm;
1513  }
1514  }
1515  // Deleting temporary files;
1516  if (!$this->dontUnlinkTempFiles) {
1517  unlink($fileMenu);
1518  unlink($fileColor);
1519  unlink($fileMask);
1520  }
1521  }
1522  }
1523 
1524  /****************************
1525  *
1526  * Other GIFBUILDER objects
1527  *
1528  ****************************/
1537  public function makeBox(&$im, $conf, $workArea)
1538  {
1539  $cords = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
1540  $conf['offset'] = $cords[0] . ',' . $cords[1];
1541  $cords = $this->objPosition($conf, $workArea, [$cords[2], $cords[3]]);
1542  $cols = $this->convertColor($conf['color']);
1543  $opacity = 0;
1544  if (isset($conf['opacity'])) {
1545  // conversion:
1546  // PHP 0 = opaque, 127 = transparent
1547  // TYPO3 100 = opaque, 0 = transparent
1548  $opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 1, 100, 1);
1549  $opacity = abs($opacity - 100);
1550  $opacity = round(127 * $opacity / 100);
1551  }
1552  $tmpColor = imagecolorallocatealpha($im, $cols[0], $cols[1], $cols[2], $opacity);
1553  imagefilledrectangle($im, $cords[0], $cords[1], $cords[0] + $cords[2] - 1, $cords[1] + $cords[3] - 1, $tmpColor);
1554  }
1555 
1576  public function makeEllipse(&$im, array $conf, array $workArea)
1577  {
1578  $ellipseConfiguration = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
1579  // Ellipse offset inside workArea (x/y)
1580  $conf['offset'] = $ellipseConfiguration[0] . ',' . $ellipseConfiguration[1];
1581  // @see objPosition
1582  $imageCoordinates = $this->objPosition($conf, $workArea, [$ellipseConfiguration[2], $ellipseConfiguration[3]]);
1583  $color = $this->convertColor($conf['color']);
1584  $fillingColor = imagecolorallocate($im, $color[0], $color[1], $color[2]);
1585  imagefilledellipse($im, $imageCoordinates[0], $imageCoordinates[1], $imageCoordinates[2], $imageCoordinates[3], $fillingColor);
1586  }
1587 
1596  public function makeEffect(&$im, $conf)
1597  {
1598  $commands = $this->IMparams($conf['value']);
1599  if ($commands) {
1600  $this->applyImageMagickToPHPGif($im, $commands);
1601  }
1602  }
1603 
1612  public function IMparams($setup)
1613  {
1614  if (!trim($setup)) {
1615  return '';
1616  }
1617  $effects = explode('|', $setup);
1618  $commands = '';
1619  foreach ($effects as $val) {
1620  $pairs = explode('=', $val, 2);
1621  $value = trim($pairs[1]);
1622  $effect = strtolower(trim($pairs[0]));
1623  switch ($effect) {
1624  case 'gamma':
1625  $commands .= ' -gamma ' . (float)$value;
1626  break;
1627  case 'blur':
1628  if (!$this->NO_IM_EFFECTS) {
1629  $commands .= $this->v5_blur($value);
1630  }
1631  break;
1632  case 'sharpen':
1633  if (!$this->NO_IM_EFFECTS) {
1634  $commands .= $this->v5_sharpen($value);
1635  }
1636  break;
1637  case 'rotate':
1638  $commands .= ' -rotate ' . MathUtility::forceIntegerInRange($value, 0, 360);
1639  break;
1640  case 'solarize':
1641  $commands .= ' -solarize ' . MathUtility::forceIntegerInRange($value, 0, 99);
1642  break;
1643  case 'swirl':
1644  $commands .= ' -swirl ' . MathUtility::forceIntegerInRange($value, 0, 1000);
1645  break;
1646  case 'wave':
1647  $params = GeneralUtility::intExplode(',', $value);
1648  $commands .= ' -wave ' . MathUtility::forceIntegerInRange($params[0], 0, 99) . 'x' . MathUtility::forceIntegerInRange($params[1], 0, 99);
1649  break;
1650  case 'charcoal':
1651  $commands .= ' -charcoal ' . MathUtility::forceIntegerInRange($value, 0, 100);
1652  break;
1653  case 'gray':
1654  $commands .= ' -colorspace GRAY';
1655  break;
1656  case 'edge':
1657  $commands .= ' -edge ' . MathUtility::forceIntegerInRange($value, 0, 99);
1658  break;
1659  case 'emboss':
1660  $commands .= ' -emboss';
1661  break;
1662  case 'flip':
1663  $commands .= ' -flip';
1664  break;
1665  case 'flop':
1666  $commands .= ' -flop';
1667  break;
1668  case 'colors':
1669  $commands .= ' -colors ' . MathUtility::forceIntegerInRange($value, 2, 255);
1670  break;
1671  case 'shear':
1672  $commands .= ' -shear ' . MathUtility::forceIntegerInRange($value, -90, 90);
1673  break;
1674  case 'invert':
1675  $commands .= ' -negate';
1676  break;
1677  }
1678  }
1679  return $commands;
1680  }
1681 
1689  public function adjust(&$im, $conf)
1690  {
1691  $setup = $conf['value'];
1692  if (!trim($setup)) {
1693  return;
1694  }
1695  $effects = explode('|', $setup);
1696  foreach ($effects as $val) {
1697  $pairs = explode('=', $val, 2);
1698  $value = trim($pairs[1]);
1699  $effect = strtolower(trim($pairs[0]));
1700  switch ($effect) {
1701  case 'inputlevels':
1702  // low,high
1703  $params = GeneralUtility::intExplode(',', $value);
1704  $this->inputLevels($im, $params[0], $params[1]);
1705  break;
1706  case 'outputlevels':
1707  $params = GeneralUtility::intExplode(',', $value);
1708  $this->outputLevels($im, $params[0], $params[1]);
1709  break;
1710  case 'autolevels':
1711  $this->autolevels($im);
1712  break;
1713  }
1714  }
1715  }
1716 
1724  public function crop(&$im, $conf)
1725  {
1726  // Clears workArea to total image
1727  $this->setWorkArea('');
1728  $cords = GeneralUtility::intExplode(',', $conf['crop'] . ',,,');
1729  $conf['offset'] = $cords[0] . ',' . $cords[1];
1730  $cords = $this->objPosition($conf, $this->workArea, [$cords[2], $cords[3]]);
1731  $newIm = imagecreatetruecolor($cords[2], $cords[3]);
1732  $cols = $this->convertColor($conf['backColor'] ? $conf['backColor'] : $this->setup['backColor']);
1733  $Bcolor = imagecolorallocate($newIm, $cols[0], $cols[1], $cols[2]);
1734  imagefilledrectangle($newIm, 0, 0, $cords[2], $cords[3], $Bcolor);
1735  $newConf = [];
1736  $workArea = [0, 0, $cords[2], $cords[3]];
1737  if ($cords[0] < 0) {
1738  $workArea[0] = abs($cords[0]);
1739  } else {
1740  $newConf['offset'] = -$cords[0];
1741  }
1742  if ($cords[1] < 0) {
1743  $workArea[1] = abs($cords[1]);
1744  } else {
1745  $newConf['offset'] .= ',' . -$cords[1];
1746  }
1747  $this->copyGifOntoGif($newIm, $im, $newConf, $workArea);
1748  $im = $newIm;
1749  $this->w = imagesx($im);
1750  $this->h = imagesy($im);
1751  // Clears workArea to total image
1752  $this->setWorkArea('');
1753  }
1754 
1762  public function scale(&$im, $conf)
1763  {
1764  if ($conf['width'] || $conf['height'] || $conf['params']) {
1765  $tmpStr = $this->randomName();
1766  $theFile = $tmpStr . '.' . $this->gifExtension;
1767  $this->ImageWrite($im, $theFile);
1768  $theNewFile = $this->imageMagickConvert($theFile, $this->gifExtension, $conf['width'], $conf['height'], $conf['params']);
1769  $tmpImg = $this->imageCreateFromFile($theNewFile[3]);
1770  if ($tmpImg) {
1771  imagedestroy($im);
1772  $im = $tmpImg;
1773  $this->w = imagesx($im);
1774  $this->h = imagesy($im);
1775  // Clears workArea to total image
1776  $this->setWorkArea('');
1777  }
1778  if (!$this->dontUnlinkTempFiles) {
1779  unlink($theFile);
1780  if ($theNewFile[3] && $theNewFile[3] != $theFile) {
1781  unlink($theNewFile[3]);
1782  }
1783  }
1784  }
1785  }
1786 
1795  public function setWorkArea($workArea)
1796  {
1797  $this->workArea = GeneralUtility::intExplode(',', $workArea);
1798  $this->workArea = $this->applyOffset($this->workArea, $this->OFFSET);
1799  if (!$this->workArea[2]) {
1800  $this->workArea[2] = $this->w;
1801  }
1802  if (!$this->workArea[3]) {
1803  $this->workArea[3] = $this->h;
1804  }
1805  }
1806 
1807  /*************************
1808  *
1809  * Adjustment functions
1810  *
1811  ************************/
1817  public function autolevels(&$im)
1818  {
1819  $totalCols = imagecolorstotal($im);
1820  $grayArr = [];
1821  for ($c = 0; $c < $totalCols; $c++) {
1822  $cols = imagecolorsforindex($im, $c);
1823  $grayArr[] = round(($cols['red'] + $cols['green'] + $cols['blue']) / 3);
1824  }
1825  $min = min($grayArr);
1826  $max = max($grayArr);
1827  $delta = $max - $min;
1828  if ($delta) {
1829  for ($c = 0; $c < $totalCols; $c++) {
1830  $cols = imagecolorsforindex($im, $c);
1831  $cols['red'] = floor(($cols['red'] - $min) / $delta * 255);
1832  $cols['green'] = floor(($cols['green'] - $min) / $delta * 255);
1833  $cols['blue'] = floor(($cols['blue'] - $min) / $delta * 255);
1834  imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1835  }
1836  }
1837  }
1838 
1847  public function outputLevels(&$im, $low, $high, $swap = false)
1848  {
1849  if ($low < $high) {
1850  $low = MathUtility::forceIntegerInRange($low, 0, 255);
1851  $high = MathUtility::forceIntegerInRange($high, 0, 255);
1852  if ($swap) {
1853  $temp = $low;
1854  $low = 255 - $high;
1855  $high = 255 - $temp;
1856  }
1857  $delta = $high - $low;
1858  $totalCols = imagecolorstotal($im);
1859  for ($c = 0; $c < $totalCols; $c++) {
1860  $cols = imagecolorsforindex($im, $c);
1861  $cols['red'] = $low + floor($cols['red'] / 255 * $delta);
1862  $cols['green'] = $low + floor($cols['green'] / 255 * $delta);
1863  $cols['blue'] = $low + floor($cols['blue'] / 255 * $delta);
1864  imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1865  }
1866  }
1867  }
1868 
1876  public function inputLevels(&$im, $low, $high)
1877  {
1878  if ($low < $high) {
1879  $low = MathUtility::forceIntegerInRange($low, 0, 255);
1880  $high = MathUtility::forceIntegerInRange($high, 0, 255);
1881  $delta = $high - $low;
1882  $totalCols = imagecolorstotal($im);
1883  for ($c = 0; $c < $totalCols; $c++) {
1884  $cols = imagecolorsforindex($im, $c);
1885  $cols['red'] = MathUtility::forceIntegerInRange(($cols['red'] - $low) / $delta * 255, 0, 255);
1886  $cols['green'] = MathUtility::forceIntegerInRange(($cols['green'] - $low) / $delta * 255, 0, 255);
1887  $cols['blue'] = MathUtility::forceIntegerInRange(($cols['blue'] - $low) / $delta * 255, 0, 255);
1888  imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
1889  }
1890  }
1891  }
1892 
1900  public function IMreduceColors($file, $cols)
1901  {
1902  $fI = GeneralUtility::split_fileref($file);
1903  $ext = strtolower($fI['fileext']);
1904  $result = $this->randomName() . '.' . $ext;
1905  $reduce = MathUtility::forceIntegerInRange($cols, 0, $ext === 'gif' ? 256 : $this->truecolorColors, 0);
1906  if ($reduce > 0) {
1907  $params = ' -colors ' . $reduce;
1908  if ($reduce <= 256) {
1909  $params .= ' -type Palette';
1910  }
1911  $prefix = $ext === 'png' && $reduce <= 256 ? 'png8:' : '';
1912  $this->imageMagickExec($file, $prefix . $result, $params);
1913  if ($result) {
1914  return $result;
1915  }
1916  }
1917  return '';
1918  }
1919 
1920  /*********************************
1921  *
1922  * GIFBUILDER Helper functions
1923  *
1924  *********************************/
1933  public function prependAbsolutePath($fontFile)
1934  {
1936  return GeneralUtility::isAbsPath($fontFile) ? $fontFile : PATH_site . $fontFile;
1937  }
1938 
1947  public function v5_sharpen($factor)
1948  {
1949  $factor = MathUtility::forceIntegerInRange(ceil($factor / 10), 0, 10);
1950  $sharpenArr = explode(',', ',' . $this->im5fx_sharpenSteps);
1951  $sharpenF = trim($sharpenArr[$factor]);
1952  if ($sharpenF) {
1953  return ' -sharpen ' . $sharpenF;
1954  }
1955  return '';
1956  }
1957 
1966  public function v5_blur($factor)
1967  {
1968  $factor = MathUtility::forceIntegerInRange(ceil($factor / 10), 0, 10);
1969  $blurArr = explode(',', ',' . $this->im5fx_blurSteps);
1970  $blurF = trim($blurArr[$factor]);
1971  if ($blurF) {
1972  return ' -blur ' . $blurF;
1973  }
1974  return '';
1975  }
1976 
1983  public function randomName()
1984  {
1985  GeneralUtility::mkdir_deep(PATH_site . 'typo3temp/var/transient/');
1986  return PATH_site . 'typo3temp/var/transient/' . md5(uniqid('', true));
1987  }
1988 
1997  public function applyOffset($cords, $OFFSET)
1998  {
1999  $cords[0] = (int)$cords[0] + (int)$OFFSET[0];
2000  $cords[1] = (int)$cords[1] + (int)$OFFSET[1];
2001  return $cords;
2002  }
2003 
2011  public function convertColor($string)
2012  {
2013  $col = [];
2014  $cParts = explode(':', $string, 2);
2015  // Finding the RGB definitions of the color:
2016  $string = $cParts[0];
2017  if (strstr($string, '#')) {
2018  $string = preg_replace('/[^A-Fa-f0-9]*/', '', $string);
2019  $col[] = hexdec(substr($string, 0, 2));
2020  $col[] = hexdec(substr($string, 2, 2));
2021  $col[] = hexdec(substr($string, 4, 2));
2022  } elseif (strstr($string, ',')) {
2023  $string = preg_replace('/[^,0-9]*/', '', $string);
2024  $strArr = explode(',', $string);
2025  $col[] = (int)$strArr[0];
2026  $col[] = (int)$strArr[1];
2027  $col[] = (int)$strArr[2];
2028  } else {
2029  $string = strtolower(trim($string));
2030  if ($this->colMap[$string]) {
2031  $col = $this->colMap[$string];
2032  } else {
2033  $col = [0, 0, 0];
2034  }
2035  }
2036  // ... and possibly recalculating the value
2037  if (trim($cParts[1])) {
2038  $cParts[1] = trim($cParts[1]);
2039  if ($cParts[1][0] === '*') {
2040  $val = (float)substr($cParts[1], 1);
2041  $col[0] = MathUtility::forceIntegerInRange($col[0] * $val, 0, 255);
2042  $col[1] = MathUtility::forceIntegerInRange($col[1] * $val, 0, 255);
2043  $col[2] = MathUtility::forceIntegerInRange($col[2] * $val, 0, 255);
2044  } else {
2045  $val = (int)$cParts[1];
2046  $col[0] = MathUtility::forceIntegerInRange($col[0] + $val, 0, 255);
2047  $col[1] = MathUtility::forceIntegerInRange($col[1] + $val, 0, 255);
2048  $col[2] = MathUtility::forceIntegerInRange($col[2] + $val, 0, 255);
2049  }
2050  }
2051  return $col;
2052  }
2053 
2064  public function objPosition($conf, $workArea, $BB)
2065  {
2066  // offset, align, valign, workarea
2067  $result = [];
2068  $result[2] = $BB[0];
2069  $result[3] = $BB[1];
2070  $w = $workArea[2];
2071  $h = $workArea[3];
2072  $align = explode(',', $conf['align']);
2073  $align[0] = strtolower(substr(trim($align[0]), 0, 1));
2074  $align[1] = strtolower(substr(trim($align[1]), 0, 1));
2075  switch ($align[0]) {
2076  case 'r':
2077  $result[0] = $w - $result[2];
2078  break;
2079  case 'c':
2080  $result[0] = round(($w - $result[2]) / 2);
2081  break;
2082  default:
2083  $result[0] = 0;
2084  }
2085  switch ($align[1]) {
2086  case 'b':
2087  // y pos
2088  $result[1] = $h - $result[3];
2089  break;
2090  case 'c':
2091  $result[1] = round(($h - $result[3]) / 2);
2092  break;
2093  default:
2094  $result[1] = 0;
2095  }
2096  $result = $this->applyOffset($result, GeneralUtility::intExplode(',', $conf['offset']));
2097  $result = $this->applyOffset($result, $workArea);
2098  return $result;
2099  }
2100 
2101  /***********************************
2102  *
2103  * Scaling, Dimensions of images
2104  *
2105  ***********************************/
2120  public function imageMagickConvert($imagefile, $newExt = '', $w = '', $h = '', $params = '', $frame = '', $options = [], $mustCreate = false)
2121  {
2122  if ($this->NO_IMAGE_MAGICK) {
2123  // Returning file info right away
2124  return $this->getImageDimensions($imagefile);
2125  }
2126  $info = $this->getImageDimensions($imagefile);
2127  if (!$info) {
2128  return null;
2129  }
2130 
2131  $newExt = strtolower(trim($newExt));
2132  // If no extension is given the original extension is used
2133  if (!$newExt) {
2134  $newExt = $info[2];
2135  }
2136  if ($newExt === 'web') {
2137  if (GeneralUtility::inList($this->webImageExt, $info[2])) {
2138  $newExt = $info[2];
2139  } else {
2140  $newExt = $this->gif_or_jpg($info[2], $info[0], $info[1]);
2141  if (!$params) {
2142  $params = $this->cmds[$newExt];
2143  }
2144  }
2145  }
2146  if (!GeneralUtility::inList($this->imageFileExt, $newExt)) {
2147  return null;
2148  }
2149 
2150  $data = $this->getImageScale($info, $w, $h, $options);
2151  $w = $data['origW'];
2152  $h = $data['origH'];
2153  // If no conversion should be performed
2154  // this flag is TRUE if the width / height does NOT dictate
2155  // the image to be scaled!! (that is if no width / height is
2156  // given or if the destination w/h matches the original image
2157  // dimensions or if the option to not scale the image is set)
2158  $noScale = !$w && !$h || $data[0] == $info[0] && $data[1] == $info[1] || !empty($options['noScale']);
2159  if ($noScale && !$data['crs'] && !$params && !$frame && $newExt == $info[2] && !$mustCreate) {
2160  // Set the new width and height before returning,
2161  // if the noScale option is set
2162  if (!empty($options['noScale'])) {
2163  $info[0] = $data[0];
2164  $info[1] = $data[1];
2165  }
2166  $info[3] = $imagefile;
2167  return $info;
2168  }
2169  $info[0] = $data[0];
2170  $info[1] = $data[1];
2171  $frame = $this->addFrameSelection ? (int)$frame : '';
2172  if (!$params) {
2173  $params = $this->cmds[$newExt];
2174  }
2175  // Cropscaling:
2176  if ($data['crs']) {
2177  if (!$data['origW']) {
2178  $data['origW'] = $data[0];
2179  }
2180  if (!$data['origH']) {
2181  $data['origH'] = $data[1];
2182  }
2183  $offsetX = (int)(($data[0] - $data['origW']) * ($data['cropH'] + 100) / 200);
2184  $offsetY = (int)(($data[1] - $data['origH']) * ($data['cropV'] + 100) / 200);
2185  $params .= ' -crop ' . $data['origW'] . 'x' . $data['origH'] . '+' . $offsetX . '+' . $offsetY . '! +repage';
2186  }
2187  $command = $this->scalecmd . ' ' . $info[0] . 'x' . $info[1] . '! ' . $params . ' ';
2188  // re-apply colorspace-setting for the resulting image so colors don't appear to dark (sRGB instead of RGB)
2189  $command .= ' -colorspace ' . $this->colorspace;
2190  $cropscale = $data['crs'] ? 'crs-V' . $data['cropV'] . 'H' . $data['cropH'] : '';
2191  if ($this->alternativeOutputKey) {
2192  $theOutputName = GeneralUtility::shortMD5($command . $cropscale . basename($imagefile) . $this->alternativeOutputKey . '[' . $frame . ']');
2193  } else {
2194  $theOutputName = GeneralUtility::shortMD5($command . $cropscale . $imagefile . filemtime($imagefile) . '[' . $frame . ']');
2195  }
2196  if ($this->imageMagickConvert_forceFileNameBody) {
2198  $this->imageMagickConvert_forceFileNameBody = '';
2199  }
2200  // Making the temporary filename:
2201  GeneralUtility::mkdir_deep(PATH_site . 'typo3temp/assets/images/');
2202  $output = $this->absPrefix . 'typo3temp/assets/images/' . $this->filenamePrefix . $theOutputName . '.' . $newExt;
2203  if ($this->dontCheckForExistingTempFile || !file_exists($output)) {
2204  $this->imageMagickExec($imagefile, $output, $command, $frame);
2205  }
2206  if (file_exists($output)) {
2207  $info[3] = $output;
2208  $info[2] = $newExt;
2209  // params might change some image data!
2210  if ($params) {
2211  $info = $this->getImageDimensions($info[3]);
2212  }
2213  if ($info[2] == $this->gifExtension && !$this->dontCompress) {
2214  // Compress with IM (lzw) or GD (rle) (Workaround for the absence of lzw-compression in GD)
2215  self::gifCompress($info[3], '');
2216  }
2217  return $info;
2218  }
2219  return null;
2220  }
2221 
2229  public function getImageDimensions($imageFile)
2230  {
2231  $returnArr = null;
2232  preg_match('/([^\\.]*)$/', $imageFile, $reg);
2233  if (file_exists($imageFile) && GeneralUtility::inList($this->imageFileExt, strtolower($reg[0]))) {
2234  $returnArr = $this->getCachedImageDimensions($imageFile);
2235  if (!$returnArr) {
2236  $imageInfoObject = GeneralUtility::makeInstance(ImageInfo::class, $imageFile);
2237  if ($imageInfoObject->getWidth()) {
2238  $returnArr = [
2239  $imageInfoObject->getWidth(),
2240  $imageInfoObject->getHeight(),
2241  strtolower($reg[0]),
2242  $imageFile
2243  ];
2244  $this->cacheImageDimensions($returnArr);
2245  }
2246  }
2247  }
2248  return $returnArr;
2249  }
2250 
2258  public function cacheImageDimensions(array $identifyResult)
2259  {
2260  $filePath = $identifyResult[3];
2261  $statusHash = $this->generateStatusHashForImageFile($filePath);
2262  $identifier = $this->generateCacheKeyForImageFile($filePath);
2263 
2265  $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_imagesizes');
2266  $imageDimensions = [
2267  'hash' => $statusHash,
2268  'imagewidth' => $identifyResult[0],
2269  'imageheight' => $identifyResult[1],
2270  ];
2271  $cache->set($identifier, $imageDimensions);
2272 
2273  return true;
2274  }
2275 
2284  public function getCachedImageDimensions($filePath)
2285  {
2286  $statusHash = $this->generateStatusHashForImageFile($filePath);
2287  $identifier = $this->generateCacheKeyForImageFile($filePath);
2289  $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_imagesizes');
2290  $cachedImageDimensions = $cache->get($identifier);
2291  if (!isset($cachedImageDimensions['hash'])) {
2292  return false;
2293  }
2294 
2295  if ($cachedImageDimensions['hash'] !== $statusHash) {
2296  // The file has changed. Delete the cache entry.
2297  $cache->remove($identifier);
2298  $result = false;
2299  } else {
2300  preg_match('/([^\\.]*)$/', $filePath, $imageExtension);
2301  $result = [
2302  (int)$cachedImageDimensions['imagewidth'],
2303  (int)$cachedImageDimensions['imageheight'],
2304  strtolower($imageExtension[0]),
2305  $filePath
2306  ];
2307  }
2308 
2309  return $result;
2310  }
2311 
2321  protected function generateCacheKeyForImageFile($filePath)
2322  {
2323  return sha1($filePath);
2324  }
2325 
2333  protected function generateStatusHashForImageFile($filePath)
2334  {
2335  $fileStatus = stat($filePath);
2336 
2337  return sha1($fileStatus['mtime'] . $fileStatus['size']);
2338  }
2339 
2351  public function getImageScale($info, $w, $h, $options)
2352  {
2353  if (strstr($w . $h, 'm')) {
2354  $max = 1;
2355  } else {
2356  $max = 0;
2357  }
2358  if (strstr($w . $h, 'c')) {
2359  $out['cropH'] = (int)substr(strstr($w, 'c'), 1);
2360  $out['cropV'] = (int)substr(strstr($h, 'c'), 1);
2361  $crs = true;
2362  } else {
2363  $crs = false;
2364  }
2365  $out['crs'] = $crs;
2366  $w = (int)$w;
2367  $h = (int)$h;
2368  // If there are max-values...
2369  if (!empty($options['maxW'])) {
2370  // If width is given...
2371  if ($w) {
2372  if ($w > $options['maxW']) {
2373  $w = $options['maxW'];
2374  // Height should follow
2375  $max = 1;
2376  }
2377  } else {
2378  if ($info[0] > $options['maxW']) {
2379  $w = $options['maxW'];
2380  // Height should follow
2381  $max = 1;
2382  }
2383  }
2384  }
2385  if (!empty($options['maxH'])) {
2386  // If height is given...
2387  if ($h) {
2388  if ($h > $options['maxH']) {
2389  $h = $options['maxH'];
2390  // Height should follow
2391  $max = 1;
2392  }
2393  } else {
2394  // Changed [0] to [1] 290801
2395  if ($info[1] > $options['maxH']) {
2396  $h = $options['maxH'];
2397  // Height should follow
2398  $max = 1;
2399  }
2400  }
2401  }
2402  $out['origW'] = $w;
2403  $out['origH'] = $h;
2404  $out['max'] = $max;
2405  if (!$this->mayScaleUp) {
2406  if ($w > $info[0]) {
2407  $w = $info[0];
2408  }
2409  if ($h > $info[1]) {
2410  $h = $info[1];
2411  }
2412  }
2413  // If scaling should be performed. Check that input "info" array will not cause division-by-zero
2414  if (($w || $h) && $info[0] && $info[1]) {
2415  if ($w && !$h) {
2416  $info[1] = ceil($info[1] * ($w / $info[0]));
2417  $info[0] = $w;
2418  }
2419  if (!$w && $h) {
2420  $info[0] = ceil($info[0] * ($h / $info[1]));
2421  $info[1] = $h;
2422  }
2423  if ($w && $h) {
2424  if ($max) {
2425  $ratio = $info[0] / $info[1];
2426  if ($h * $ratio > $w) {
2427  $h = round($w / $ratio);
2428  } else {
2429  $w = round($h * $ratio);
2430  }
2431  }
2432  if ($crs) {
2433  $ratio = $info[0] / $info[1];
2434  if ($h * $ratio < $w) {
2435  $h = round($w / $ratio);
2436  } else {
2437  $w = round($h * $ratio);
2438  }
2439  }
2440  $info[0] = $w;
2441  $info[1] = $h;
2442  }
2443  }
2444  $out[0] = $info[0];
2445  $out[1] = $info[1];
2446  // Set minimum-measures!
2447  if (isset($options['minW']) && $out[0] < $options['minW']) {
2448  if (($max || $crs) && $out[0]) {
2449  $out[1] = round($out[1] * $options['minW'] / $out[0]);
2450  }
2451  $out[0] = $options['minW'];
2452  }
2453  if (isset($options['minH']) && $out[1] < $options['minH']) {
2454  if (($max || $crs) && $out[1]) {
2455  $out[0] = round($out[0] * $options['minH'] / $out[1]);
2456  }
2457  $out[1] = $options['minH'];
2458  }
2459  return $out;
2460  }
2461 
2462  /***********************************
2463  *
2464  * ImageMagick API functions
2465  *
2466  ***********************************/
2473  public function imageMagickIdentify($imagefile)
2474  {
2475  if ($this->NO_IMAGE_MAGICK) {
2476  return null;
2477  }
2478 
2479  $frame = $this->addFrameSelection ? 0 : null;
2481  'identify',
2482  ImageMagickFile::fromFilePath($imagefile, $frame)
2483  );
2484  $returnVal = [];
2485  CommandUtility::exec($cmd, $returnVal);
2486  $splitstring = array_pop($returnVal);
2487  $this->IM_commands[] = ['identify', $cmd, $splitstring];
2488  if ($splitstring) {
2489  preg_match('/([^\\.]*)$/', $imagefile, $reg);
2490  $splitinfo = explode(' ', $splitstring);
2491  $dim = false;
2492  foreach ($splitinfo as $key => $val) {
2493  $temp = '';
2494  if ($val) {
2495  $temp = explode('x', $val);
2496  }
2497  if ((int)$temp[0] && (int)$temp[1]) {
2498  $dim = $temp;
2499  break;
2500  }
2501  }
2502  if (!empty($dim[0]) && !empty($dim[1])) {
2503  return [$dim[0], $dim[1], strtolower($reg[0]), $imagefile];
2504  }
2505  }
2506  return null;
2507  }
2508 
2519  public function imageMagickExec($input, $output, $params, $frame = 0)
2520  {
2521  if ($this->NO_IMAGE_MAGICK) {
2522  return '';
2523  }
2524  // If addFrameSelection is set in the Install Tool, a frame number is added to
2525  // select a specific page of the image (by default this will be the first page)
2526  $frame = $this->addFrameSelection ? (int)$frame : null;
2528  'convert',
2529  $params
2530  . ' ' . ImageMagickFile::fromFilePath($input, $frame)
2531  . ' ' . CommandUtility::escapeShellArgument($output)
2532  );
2533  $this->IM_commands[] = [$output, $cmd];
2534  $ret = CommandUtility::exec($cmd);
2535  // Change the permissions of the file
2537  return $ret;
2538  }
2539 
2550  public function combineExec($input, $overlay, $mask, $output)
2551  {
2552  if ($this->NO_IMAGE_MAGICK) {
2553  return '';
2554  }
2555  $theMask = $this->randomName() . '.' . $this->gifExtension;
2556  // +matte = no alpha layer in output
2557  $this->imageMagickExec($mask, $theMask, '-colorspace GRAY +matte');
2558 
2559  $parameters = '-compose over +matte '
2560  . ImageMagickFile::fromFilePath($input) . ' '
2561  . ImageMagickFile::fromFilePath($overlay) . ' '
2562  . ImageMagickFile::fromFilePath($theMask) . ' '
2564  $cmd = CommandUtility::imageMagickCommand('combine', $parameters);
2565  $this->IM_commands[] = [$output, $cmd];
2566  $ret = CommandUtility::exec($cmd);
2567  // Change the permissions of the file
2569  if (is_file($theMask)) {
2570  @unlink($theMask);
2571  }
2572  return $ret;
2573  }
2574 
2593  public static function gifCompress($theFile, $type)
2594  {
2595  $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
2596  if (!$gfxConf['gif_compress'] || strtolower(substr($theFile, -4, 4)) !== '.gif') {
2597  return '';
2598  }
2599 
2600  if (($type === 'IM' || !$type) && $gfxConf['processor_enabled'] && $gfxConf['processor_path_lzw']) {
2601  // Use temporary file to prevent problems with read and write lock on same file on network file systems
2602  $temporaryName = dirname($theFile) . '/' . md5(uniqid('', true)) . '.gif';
2603  // Rename could fail, if a simultaneous thread is currently working on the same thing
2604  if (@rename($theFile, $temporaryName)) {
2606  'convert',
2607  ImageMagickFile::fromFilePath($temporaryName) . ' ' . CommandUtility::escapeShellArgument($theFile),
2608  $gfxConf['processor_path_lzw']
2609  );
2610  CommandUtility::exec($cmd);
2611  unlink($temporaryName);
2612  }
2613  $returnCode = 'IM';
2614  if (@is_file($theFile)) {
2616  }
2617  } elseif (($type === 'GD' || !$type) && $gfxConf['gdlib'] && !$gfxConf['gdlib_png']) {
2618  $tempImage = imagecreatefromgif($theFile);
2619  imagegif($tempImage, $theFile);
2620  imagedestroy($tempImage);
2621  $returnCode = 'GD';
2622  if (@is_file($theFile)) {
2624  }
2625  } else {
2626  $returnCode = '';
2627  }
2628 
2629  return $returnCode;
2630  }
2631 
2640  public static function readPngGif($theFile, $output_png = false)
2641  {
2642  if (!$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'] || !@is_file($theFile)) {
2643  return null;
2644  }
2645 
2646  $ext = strtolower(substr($theFile, -4, 4));
2647  if ((string)$ext === '.png' && $output_png || (string)$ext === '.gif' && !$output_png) {
2648  return $theFile;
2649  }
2650 
2651  if (!@is_dir(PATH_site . 'typo3temp/assets/images/')) {
2652  GeneralUtility::mkdir_deep(PATH_site . 'typo3temp/assets/images/');
2653  }
2654  $newFile = PATH_site . 'typo3temp/assets/images/' . md5($theFile . '|' . filemtime($theFile)) . ($output_png ? '.png' : '.gif');
2656  'convert',
2658  $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']
2659  );
2660  CommandUtility::exec($cmd);
2661  if (@is_file($newFile)) {
2663  return $newFile;
2664  }
2665  return null;
2666  }
2667 
2668  /***********************************
2669  *
2670  * Various IO functions
2671  *
2672  ***********************************/
2673 
2681  public function createTempSubDir($dirName)
2682  {
2684  // Checking if the this->tempPath is already prefixed with PATH_site and if not, prefix it with that constant.
2685  if (GeneralUtility::isFirstPartOfStr($this->tempPath, PATH_site)) {
2686  $tmpPath = $this->tempPath;
2687  } else {
2688  $tmpPath = PATH_site . $this->tempPath;
2689  }
2690  // Making the temporary filename:
2691  if (!@is_dir($tmpPath . $dirName)) {
2692  GeneralUtility::mkdir_deep($tmpPath . $dirName);
2693  return @is_dir($tmpPath . $dirName);
2694  }
2695  return false;
2696  }
2697 
2704  public function applyImageMagickToPHPGif(&$im, $command)
2705  {
2706  $tmpStr = $this->randomName();
2707  $theFile = $tmpStr . '.' . $this->gifExtension;
2708  $this->ImageWrite($im, $theFile);
2709  $this->imageMagickExec($theFile, $theFile, $command);
2710  $tmpImg = $this->imageCreateFromFile($theFile);
2711  if ($tmpImg) {
2712  imagedestroy($im);
2713  $im = $tmpImg;
2714  $this->w = imagesx($im);
2715  $this->h = imagesy($im);
2716  }
2717  if (!$this->dontUnlinkTempFiles) {
2718  unlink($theFile);
2719  }
2720  }
2721 
2731  public function gif_or_jpg($type, $w, $h)
2732  {
2733  if ($type === 'ai' || $w * $h < $this->pixelLimitGif) {
2734  return $this->gifExtension;
2735  }
2736  return 'jpg';
2737  }
2738 
2748  public function output($file)
2749  {
2750  if ($file) {
2751  $reg = [];
2752  preg_match('/([^\\.]*)$/', $file, $reg);
2753  $ext = strtolower($reg[0]);
2754  switch ($ext) {
2755  case 'gif':
2756 
2757  case 'png':
2758  if ($this->ImageWrite($this->im, $file)) {
2759  // ImageMagick operations
2760  if ($this->setup['reduceColors'] || !$this->png_truecolor) {
2761  $reduced = $this->IMreduceColors($file, MathUtility::forceIntegerInRange($this->setup['reduceColors'], 256, $this->truecolorColors, 256));
2762  if ($reduced) {
2763  @copy($reduced, $file);
2764  @unlink($reduced);
2765  }
2766  }
2767  // Compress with IM! (adds extra compression, LZW from ImageMagick)
2768  // (Workaround for the absence of lzw-compression in GD)
2769  self::gifCompress($file, 'IM');
2770  }
2771  break;
2772  case 'jpg':
2773 
2774  case 'jpeg':
2775  // Use the default
2776  $quality = 0;
2777  if ($this->setup['quality']) {
2778  $quality = MathUtility::forceIntegerInRange($this->setup['quality'], 10, 100);
2779  }
2780  $this->ImageWrite($this->im, $file, $quality);
2781  break;
2782  }
2783  }
2784  return $file;
2785  }
2786 
2792  public function destroy()
2793  {
2794  imagedestroy($this->im);
2795  }
2796 
2803  public function imgTag($imgInfo)
2804  {
2805  return '<img src="' . $imgInfo[3] . '" width="' . $imgInfo[0] . '" height="' . $imgInfo[1] . '" border="0" alt="" />';
2806  }
2807 
2817  public function ImageWrite($destImg, $theImage, $quality = 0)
2818  {
2819  imageinterlace($destImg, 0);
2820  $ext = strtolower(substr($theImage, strrpos($theImage, '.') + 1));
2821  $result = false;
2822  switch ($ext) {
2823  case 'jpg':
2824 
2825  case 'jpeg':
2826  if (function_exists('imagejpeg')) {
2827  if ($quality == 0) {
2828  $quality = $this->jpegQuality;
2829  }
2830  $result = imagejpeg($destImg, $theImage, $quality);
2831  }
2832  break;
2833  case 'gif':
2834  if (function_exists('imagegif')) {
2835  imagetruecolortopalette($destImg, true, 256);
2836  $result = imagegif($destImg, $theImage);
2837  }
2838  break;
2839  case 'png':
2840  if (function_exists('imagepng')) {
2841  $result = imagepng($destImg, $theImage);
2842  }
2843  break;
2844  }
2845  if ($result) {
2846  GeneralUtility::fixPermissions($theImage);
2847  }
2848  return $result;
2849  }
2850 
2858  public function imageCreateFromFile($sourceImg)
2859  {
2860  $imgInf = pathinfo($sourceImg);
2861  $ext = strtolower($imgInf['extension']);
2862  switch ($ext) {
2863  case 'gif':
2864  if (function_exists('imagecreatefromgif')) {
2865  return imagecreatefromgif($sourceImg);
2866  }
2867  break;
2868  case 'png':
2869  if (function_exists('imagecreatefrompng')) {
2870  $imageHandle = imagecreatefrompng($sourceImg);
2871  if ($this->saveAlphaLayer) {
2872  imagesavealpha($imageHandle, true);
2873  }
2874  return $imageHandle;
2875  }
2876  break;
2877  case 'jpg':
2878 
2879  case 'jpeg':
2880  if (function_exists('imagecreatefromjpeg')) {
2881  return imagecreatefromjpeg($sourceImg);
2882  }
2883  break;
2884  }
2885  // If non of the above:
2886  $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $sourceImg);
2887  $im = imagecreatetruecolor($imageInfo->getWidth(), $imageInfo->getHeight());
2888  $Bcolor = imagecolorallocate($im, 128, 128, 128);
2889  imagefilledrectangle($im, 0, 0, $imageInfo->getWidth(), $imageInfo->getHeight(), $Bcolor);
2890  return $im;
2891  }
2892 
2899  public function hexColor($color)
2900  {
2901  $r = dechex($color[0]);
2902  if (strlen($r) < 2) {
2903  $r = '0' . $r;
2904  }
2905  $g = dechex($color[1]);
2906  if (strlen($g) < 2) {
2907  $g = '0' . $g;
2908  }
2909  $b = dechex($color[2]);
2910  if (strlen($b) < 2) {
2911  $b = '0' . $b;
2912  }
2913  return '#' . $r . $g . $b;
2914  }
2915 
2924  public function unifyColors(&$img, $colArr, $closest = false)
2925  {
2926  $retCol = -1;
2927  if (is_array($colArr) && !empty($colArr) && function_exists('imagepng') && function_exists('imagecreatefrompng')) {
2928  $firstCol = array_shift($colArr);
2929  $firstColArr = $this->convertColor($firstCol);
2930  $origName = $preName = $this->randomName() . '.png';
2931  $postName = $this->randomName() . '.png';
2932  $tmpImg = null;
2933  if (count($colArr) > 1) {
2934  $this->ImageWrite($img, $preName);
2935  $firstCol = $this->hexColor($firstColArr);
2936  foreach ($colArr as $transparentColor) {
2937  $transparentColor = $this->convertColor($transparentColor);
2938  $transparentColor = $this->hexColor($transparentColor);
2939  $cmd = '-fill "' . $firstCol . '" -opaque "' . $transparentColor . '"';
2940  $this->imageMagickExec($preName, $postName, $cmd);
2941  $preName = $postName;
2942  }
2943  $this->imageMagickExec($postName, $origName, '');
2944  if (@is_file($origName)) {
2945  $tmpImg = $this->imageCreateFromFile($origName);
2946  }
2947  } else {
2948  $tmpImg = $img;
2949  }
2950  if ($tmpImg) {
2951  $img = $tmpImg;
2952  if ($closest) {
2953  $retCol = imagecolorclosest($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
2954  } else {
2955  $retCol = imagecolorexact($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
2956  }
2957  }
2958  // Unlink files from process
2959  if (!$this->dontUnlinkTempFiles) {
2960  if ($origName) {
2961  @unlink($origName);
2962  }
2963  if ($postName) {
2964  @unlink($postName);
2965  }
2966  }
2967  }
2968  return $retCol;
2969  }
2970 
2982  public function getTemporaryImageWithText($filename, $textline1, $textline2, $textline3)
2983  {
2984  if (empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib'])) {
2985  throw new \RuntimeException('TYPO3 Fatal Error: No gdlib. ' . $textline1 . ' ' . $textline2 . ' ' . $textline3, 1270853952);
2986  }
2987  // Creates the basis for the error image
2988  $basePath = ExtensionManagementUtility::extPath('core') . 'Resources/Public/Images/';
2989  if (!empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'])) {
2990  $im = imagecreatefrompng($basePath . 'NotFound.png');
2991  } else {
2992  $im = imagecreatefromgif($basePath . 'NotFound.gif');
2993  }
2994  // Sets background color and print color.
2995  $white = imagecolorallocate($im, 255, 255, 255);
2996  $black = imagecolorallocate($im, 0, 0, 0);
2997  // Prints the text strings with the build-in font functions of GD
2998  $x = 0;
2999  $font = 0;
3000  if ($textline1) {
3001  imagefilledrectangle($im, $x, 9, 56, 16, $white);
3002  imagestring($im, $font, $x, 9, $textline1, $black);
3003  }
3004  if ($textline2) {
3005  imagefilledrectangle($im, $x, 19, 56, 26, $white);
3006  imagestring($im, $font, $x, 19, $textline2, $black);
3007  }
3008  if ($textline3) {
3009  imagefilledrectangle($im, $x, 29, 56, 36, $white);
3010  imagestring($im, $font, $x, 29, substr($textline3, -14), $black);
3011  }
3012  // Outputting the image stream and exit
3013  if (!empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'])) {
3014  imagepng($im, $filename);
3015  } else {
3016  imagegif($im, $filename);
3017  }
3018  }
3019 
3027  protected function compensateFontSizeiBasedOnFreetypeDpi($fontSize)
3028  {
3029  return $fontSize / 96.0 * 72;
3030  }
3031 }
combineExec($input, $overlay, $mask, $output)
copyGifOntoGif(&$im, $cpImg, $conf, $workArea)
outputLevels(&$im, $low, $high, $swap=false)
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
static mkdir_deep($directory, $deepDirectory='')
unifyColors(&$img, $colArr, $closest=false)
makeEmboss(&$im, $conf, $workArea, $txtConf)
imagecopyresized(&$dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight)
SpacedImageTTFText(&$im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $text, $spacing, $wordSpacing, $splitRenderingConf, $sF=1)
debug($variable='', $name=' *variable *', $line=' *line *', $file=' *file *', $recursiveDepth=3, $debugLevel='E_DEBUG')
static isFirstPartOfStr($str, $partStr)
static forceIntegerInRange($theInt, $min, $max=2000000000, $defaultValue=0)
Definition: MathUtility.php:31
makeEllipse(&$im, array $conf, array $workArea)
getBreakSpace($conf, array $boundingBox=null)
imageMagickExec($input, $output, $params, $frame=0)
static exec($command, &$output=null, &$returnValue=0)
static getFileAbsFileName($filename, $_=null, $_2=null)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF=1)
static imageMagickCommand($command, $parameters, $path='')
static split_fileref($fileNameWithPath)
makeShadow(&$im, $conf, $workArea, $txtConf)
makeOutline(&$im, $conf, $workArea, $txtConf)
static fixPermissions($path, $recursive=false)
ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $string, $splitRendering, $sF=1)
static fromFilePath(string $filePath, int $frame=null)
static filterAndSortByNumericKeys($setupArr, $acceptAnyKeys=false)
getTemporaryImageWithText($filename, $textline1, $textline2, $textline3)
static readPngGif($theFile, $output_png=false)
renderTTFText(&$im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $conf, $sF=1)
ImageWrite($destImg, $theImage, $quality=0)
imageMagickConvert($imagefile, $newExt='', $w='', $h='', $params='', $frame='', $options=[], $mustCreate=false)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
splitString($string, $splitRendering, $fontSize, $fontFile)